kumi 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -3,20 +3,36 @@
3
3
  module Kumi
4
4
  EvaluationWrapper = Struct.new(:ctx) do
5
5
  def initialize(ctx)
6
- @ctx = ctx
6
+ super
7
7
  @__schema_cache__ = {} # memoization cache for bindings
8
8
  end
9
9
 
10
10
  def [](key)
11
- @ctx[key]
11
+ ctx[key]
12
+ end
13
+
14
+ def []=(key, value)
15
+ ctx[key] = value
12
16
  end
13
17
 
14
18
  def keys
15
- @ctx.keys
19
+ ctx.keys
16
20
  end
17
21
 
18
22
  def key?(key)
19
- @ctx.key?(key)
23
+ ctx.key?(key)
24
+ end
25
+
26
+ def clear
27
+ @__schema_cache__.clear
28
+ end
29
+
30
+ def clear_cache(*keys)
31
+ if keys.empty?
32
+ @__schema_cache__.clear
33
+ else
34
+ keys.each { |key| @__schema_cache__.delete(key) }
35
+ end
20
36
  end
21
37
  end
22
38
  end
data/lib/kumi/explain.rb CHANGED
@@ -37,17 +37,17 @@ module Kumi
37
37
 
38
38
  def format_expression(expr, indent_context: 0, nested: false)
39
39
  case expr
40
- when Syntax::TerminalExpressions::FieldRef
40
+ when Kumi::Syntax::InputReference
41
41
  "input.#{expr.name}"
42
- when Syntax::TerminalExpressions::Binding
42
+ when Kumi::Syntax::DeclarationReference
43
43
  expr.name.to_s
44
- when Syntax::TerminalExpressions::Literal
44
+ when Kumi::Syntax::Literal
45
45
  format_value(expr.value)
46
- when Syntax::Expressions::CallExpression
46
+ when Kumi::Syntax::CallExpression
47
47
  format_call_expression(expr, indent_context: indent_context, nested: nested)
48
- when Syntax::Expressions::ListExpression
48
+ when Kumi::Syntax::ArrayExpression
49
49
  "[#{expr.elements.map { |e| format_expression(e, indent_context: indent_context, nested: nested) }.join(', ')}]"
50
- when Syntax::Expressions::CascadeExpression
50
+ when Kumi::Syntax::CascadeExpression
51
51
  format_cascade_expression(expr, indent_context: indent_context)
52
52
  else
53
53
  expr.class.name.split("::").last
@@ -56,27 +56,27 @@ module Kumi
56
56
 
57
57
  def format_call_expression(expr, indent_context: 0, nested: false)
58
58
  if pretty_printable?(expr.fn_name)
59
- format_pretty_function(expr, expr.fn_name, indent_context, nested)
59
+ format_pretty_function(expr, expr.fn_name, indent_context, nested: nested)
60
60
  else
61
61
  format_generic_function(expr, indent_context)
62
62
  end
63
63
  end
64
64
 
65
- def format_pretty_function(expr, fn_name, _indent_context, nested = false)
65
+ def format_pretty_function(expr, fn_name, _indent_context, nested: false)
66
66
  if needs_evaluation?(expr.args) && !nested
67
67
  # For top-level expressions, show the flattened symbolic form and evaluation
68
- if is_chain_of_same_operator?(expr, fn_name)
68
+ if chain_of_same_operator?(expr, fn_name)
69
69
  # For chains like a + b + c, flatten to show all operands
70
70
  all_operands = flatten_operator_chain(expr, fn_name)
71
71
  symbolic_operands = all_operands.map { |op| format_expression(op, indent_context: 0, nested: true) }
72
72
  symbolic_format = symbolic_operands.join(" #{get_operator_symbol(fn_name)} ")
73
73
 
74
74
  evaluated_operands = all_operands.map do |op|
75
- if op.is_a?(Syntax::TerminalExpressions::Literal)
75
+ if op.is_a?(Kumi::Syntax::Literal)
76
76
  format_expression(op, indent_context: 0, nested: true)
77
77
  else
78
78
  arg_value = format_value(evaluate_expression(op))
79
- if op.is_a?(Syntax::TerminalExpressions::Binding) && all_operands.length > 1
79
+ if op.is_a?(Kumi::Syntax::DeclarationReference) && all_operands.length > 1
80
80
  "(#{format_expression(op, indent_context: 0, nested: true)} = #{arg_value})"
81
81
  else
82
82
  arg_value
@@ -88,38 +88,38 @@ module Kumi
88
88
  else
89
89
  # Regular pretty formatting for non-chain expressions
90
90
  symbolic_args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
91
- symbolic_format = get_display_format(fn_name, symbolic_args)
91
+ symbolic_format = display_format(fn_name, symbolic_args)
92
92
 
93
93
  evaluated_args = expr.args.map do |arg|
94
- if arg.is_a?(Syntax::TerminalExpressions::Literal)
94
+ if arg.is_a?(Kumi::Syntax::Literal)
95
95
  format_expression(arg, indent_context: 0, nested: true)
96
96
  else
97
97
  arg_value = format_value(evaluate_expression(arg))
98
- if arg.is_a?(Syntax::TerminalExpressions::Binding) &&
99
- expr.args.count { |a| !a.is_a?(Syntax::TerminalExpressions::Literal) } > 1
98
+ if arg.is_a?(Kumi::Syntax::DeclarationReference) &&
99
+ expr.args.count { |a| !a.is_a?(Kumi::Syntax::Literal) } > 1
100
100
  "(#{format_expression(arg, indent_context: 0, nested: true)} = #{arg_value})"
101
101
  else
102
102
  arg_value
103
103
  end
104
104
  end
105
105
  end
106
- evaluated_format = get_display_format(fn_name, evaluated_args)
106
+ evaluated_format = display_format(fn_name, evaluated_args)
107
107
 
108
108
  end
109
109
  "#{symbolic_format} = #{evaluated_format}"
110
110
  else
111
111
  # For nested expressions, just show the symbolic form without evaluation details
112
112
  args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
113
- get_display_format(fn_name, args)
113
+ display_format(fn_name, args)
114
114
  end
115
115
  end
116
116
 
117
- def is_chain_of_same_operator?(expr, fn_name)
117
+ def chain_of_same_operator?(expr, fn_name)
118
118
  return false unless %i[add subtract multiply divide].include?(fn_name)
119
119
 
120
120
  # Check if any argument is the same operator
121
121
  expr.args.any? do |arg|
122
- arg.is_a?(Syntax::Expressions::CallExpression) && arg.fn_name == fn_name
122
+ arg.is_a?(Kumi::Syntax::CallExpression) && arg.fn_name == fn_name
123
123
  end
124
124
  end
125
125
 
@@ -127,7 +127,7 @@ module Kumi
127
127
  operands = []
128
128
 
129
129
  expr.args.each do |arg|
130
- if arg.is_a?(Syntax::Expressions::CallExpression) && arg.fn_name == operator
130
+ if arg.is_a?(Kumi::Syntax::CallExpression) && arg.fn_name == operator
131
131
  # Recursively flatten nested operations of the same type
132
132
  operands.concat(flatten_operator_chain(arg, operator))
133
133
  else
@@ -152,7 +152,7 @@ module Kumi
152
152
  %i[add subtract multiply divide == != > < >= <= and or not].include?(fn_name)
153
153
  end
154
154
 
155
- def get_display_format(fn_name, args)
155
+ def display_format(fn_name, args)
156
156
  case fn_name
157
157
  when :add then args.join(" + ")
158
158
  when :subtract then args.join(" - ")
@@ -176,8 +176,8 @@ module Kumi
176
176
  arg_desc = format_expression(arg, indent_context: indent_context)
177
177
 
178
178
  # For literals and literal lists, just show the value, no need for "100 = 100"
179
- if arg.is_a?(Syntax::TerminalExpressions::Literal) ||
180
- (arg.is_a?(Syntax::Expressions::ListExpression) && arg.elements.all?(Syntax::TerminalExpressions::Literal))
179
+ if arg.is_a?(Kumi::Syntax::Literal) ||
180
+ (arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::Literal))
181
181
  arg_desc
182
182
  else
183
183
  arg_value = evaluate_expression(arg)
@@ -197,8 +197,8 @@ module Kumi
197
197
 
198
198
  def needs_evaluation?(args)
199
199
  args.any? do |arg|
200
- !arg.is_a?(Syntax::TerminalExpressions::Literal) &&
201
- !(arg.is_a?(Syntax::Expressions::ListExpression) && arg.elements.all?(Syntax::TerminalExpressions::Literal))
200
+ !arg.is_a?(Kumi::Syntax::Literal) &&
201
+ !(arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::Literal))
202
202
  end
203
203
  end
204
204
 
@@ -252,11 +252,11 @@ module Kumi
252
252
 
253
253
  def evaluate_expression(expr)
254
254
  case expr
255
- when Syntax::TerminalExpressions::Binding
255
+ when Kumi::Syntax::DeclarationReference
256
256
  @compiled_schema.evaluate_binding(expr.name, @inputs)
257
- when Syntax::TerminalExpressions::FieldRef
257
+ when Kumi::Syntax::InputReference
258
258
  @inputs[expr.name]
259
- when Syntax::TerminalExpressions::Literal
259
+ when Kumi::Syntax::Literal
260
260
  expr.value
261
261
  else
262
262
  # For complex expressions, compile and evaluate using existing compiler
@@ -6,20 +6,34 @@ module Kumi
6
6
  # Maps AST classes to JSON type names
7
7
  SERIALIZATION_MAP = {
8
8
  "Kumi::Syntax::Root" => "root",
9
- "Kumi::Syntax::Declarations::FieldDecl" => "field_declaration",
10
- "Kumi::Syntax::Declarations::Attribute" => "attribute_declaration",
11
- "Kumi::Syntax::Declarations::Trait" => "trait_declaration",
12
- "Kumi::Syntax::Expressions::CallExpression" => "call_expression",
13
- "Kumi::Syntax::TerminalExpressions::Literal" => "literal",
14
- "Kumi::Syntax::TerminalExpressions::FieldRef" => "field_reference",
15
- "Kumi::Syntax::TerminalExpressions::Binding" => "binding_reference",
16
- "Kumi::Syntax::Expressions::ListExpression" => "list_expression",
17
- "Kumi::Syntax::Expressions::CascadeExpression" => "cascade_expression",
18
- "Kumi::Syntax::Expressions::WhenCaseExpression" => "when_case_expression"
9
+ "Kumi::Syntax::InputDeclaration" => "field_declaration",
10
+ "Kumi::Syntax::ValueDeclaration" => "attribute_declaration",
11
+ "Kumi::Syntax::TraitDeclaration" => "trait_declaration",
12
+ "Kumi::Syntax::CallExpression" => "call_expression",
13
+ "Kumi::Syntax::ArrayExpression" => "list_expression",
14
+ "Kumi::Syntax::HashExpression" => "hash_expression",
15
+ "Kumi::Syntax::CascadeExpression" => "cascade_expression",
16
+ "Kumi::Syntax::CaseExpression" => "when_case_expression",
17
+ "Kumi::Syntax::Literal" => "literal",
18
+ "Kumi::Syntax::InputReference" => "field_reference",
19
+ "Kumi::Syntax::DeclarationReference" => "binding_reference"
19
20
  }.freeze
20
21
 
21
- # Maps JSON type names back to AST classes
22
- DESERIALIZATION_MAP = SERIALIZATION_MAP.invert.freeze
22
+ # Maps JSON type names back to AST classes (using new canonical class names)
23
+ DESERIALIZATION_MAP = {
24
+ "root" => "Kumi::Syntax::Root",
25
+ "field_declaration" => "Kumi::Syntax::InputDeclaration",
26
+ "attribute_declaration" => "Kumi::Syntax::ValueDeclaration",
27
+ "trait_declaration" => "Kumi::Syntax::TraitDeclaration",
28
+ "call_expression" => "Kumi::Syntax::CallExpression",
29
+ "list_expression" => "Kumi::Syntax::ArrayExpression",
30
+ "hash_expression" => "Kumi::Syntax::HashExpression",
31
+ "cascade_expression" => "Kumi::Syntax::CascadeExpression",
32
+ "when_case_expression" => "Kumi::Syntax::CaseExpression",
33
+ "literal" => "Kumi::Syntax::Literal",
34
+ "field_reference" => "Kumi::Syntax::InputReference",
35
+ "binding_reference" => "Kumi::Syntax::DeclarationReference"
36
+ }.freeze
23
37
 
24
38
  def self.type_name_for(node)
25
39
  SERIALIZATION_MAP[node.class.name] or
@@ -66,7 +66,7 @@ module Kumi
66
66
  }
67
67
  end
68
68
 
69
- # Binding Reference: critical for dependency resolution
69
+ # DeclarationReference Reference: critical for dependency resolution
70
70
  def serialize_binding_reference(node)
71
71
  {
72
72
  binding_name: node.name.to_s,
@@ -6,10 +6,10 @@ module Kumi
6
6
  module CollectionFunctions
7
7
  def self.definitions
8
8
  {
9
- # Collection queries
10
- empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty?),
11
- size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer),
12
- length: FunctionBuilder.collection_unary(:length, "Get collection length", :length, return_type: :integer),
9
+ # Collection queries (these are reducers - they reduce arrays to scalars)
10
+ empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty?, reducer: true),
11
+ size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer, reducer: true),
12
+ length: FunctionBuilder.collection_unary(:length, "Get collection length", :length, return_type: :integer, reducer: true),
13
13
 
14
14
  # Element access
15
15
  first: FunctionBuilder::Entry.new(
@@ -17,7 +17,8 @@ module Kumi
17
17
  arity: 1,
18
18
  param_types: [Kumi::Types.array(:any)],
19
19
  return_type: :any,
20
- description: "Get first element of collection"
20
+ description: "Get first element of collection",
21
+ reducer: true
21
22
  ),
22
23
 
23
24
  last: FunctionBuilder::Entry.new(
@@ -25,7 +26,8 @@ module Kumi
25
26
  arity: 1,
26
27
  param_types: [Kumi::Types.array(:any)],
27
28
  return_type: :any,
28
- description: "Get last element of collection"
29
+ description: "Get last element of collection",
30
+ reducer: true
29
31
  ),
30
32
 
31
33
  # Mathematical operations on collections
@@ -34,7 +36,8 @@ module Kumi
34
36
  arity: 1,
35
37
  param_types: [Kumi::Types.array(:float)],
36
38
  return_type: :float,
37
- description: "Sum all numeric elements in collection"
39
+ description: "Sum all numeric elements in collection",
40
+ reducer: true
38
41
  ),
39
42
 
40
43
  min: FunctionBuilder::Entry.new(
@@ -42,7 +45,8 @@ module Kumi
42
45
  arity: 1,
43
46
  param_types: [Kumi::Types.array(:float)],
44
47
  return_type: :float,
45
- description: "Find minimum value in numeric collection"
48
+ description: "Find minimum value in numeric collection",
49
+ reducer: true
46
50
  ),
47
51
 
48
52
  max: FunctionBuilder::Entry.new(
@@ -50,7 +54,8 @@ module Kumi
50
54
  arity: 1,
51
55
  param_types: [Kumi::Types.array(:float)],
52
56
  return_type: :float,
53
- description: "Find maximum value in numeric collection"
57
+ description: "Find maximum value in numeric collection",
58
+ reducer: true
54
59
  ),
55
60
 
56
61
  # Collection operations
@@ -84,6 +89,109 @@ module Kumi
84
89
  param_types: [Kumi::Types.array(:any)],
85
90
  return_type: Kumi::Types.array(:any),
86
91
  description: "Remove duplicate elements from collection"
92
+ ),
93
+
94
+ # Array transformation functions
95
+ flatten: FunctionBuilder::Entry.new(
96
+ fn: lambda(&:flatten),
97
+ arity: 1,
98
+ param_types: [Kumi::Types.array(:any)],
99
+ return_type: Kumi::Types.array(:any),
100
+ description: "Flatten nested arrays into a single array"
101
+ ),
102
+
103
+ # Mathematical transformation functions
104
+ map_multiply: FunctionBuilder::Entry.new(
105
+ fn: ->(collection, factor) { collection.map { |x| x * factor } },
106
+ arity: 2,
107
+ param_types: [Kumi::Types.array(:float), :float],
108
+ return_type: Kumi::Types.array(:float),
109
+ description: "Multiply each element by factor"
110
+ ),
111
+
112
+ map_add: FunctionBuilder::Entry.new(
113
+ fn: ->(collection, value) { collection.map { |x| x + value } },
114
+ arity: 2,
115
+ param_types: [Kumi::Types.array(:float), :float],
116
+ return_type: Kumi::Types.array(:float),
117
+ description: "Add value to each element"
118
+ ),
119
+
120
+ # Conditional transformation functions
121
+ map_conditional: FunctionBuilder::Entry.new(
122
+ fn: lambda { |collection, condition_value, true_value, false_value|
123
+ collection.map { |x| x == condition_value ? true_value : false_value }
124
+ },
125
+ arity: 4,
126
+ param_types: %i[array any any any],
127
+ return_type: :array,
128
+ description: "Transform elements based on condition: if element == condition_value then true_value else false_value"
129
+ ),
130
+
131
+ # Range/index functions for grid operations
132
+ build_array: FunctionBuilder::Entry.new(
133
+ fn: lambda { |size, &generator|
134
+ (0...size).map { |i| generator ? generator.call(i) : i }
135
+ },
136
+ arity: 1,
137
+ param_types: [:integer],
138
+ return_type: Kumi::Types.array(:any),
139
+ description: "Build array of given size with index values"
140
+ ),
141
+
142
+ range: FunctionBuilder::Entry.new(
143
+ fn: ->(start, finish) { (start...finish).to_a },
144
+ arity: 2,
145
+ param_types: %i[integer integer],
146
+ return_type: Kumi::Types.array(:integer),
147
+ description: "Generate range of integers from start to finish (exclusive)"
148
+ ),
149
+
150
+ # Array slicing and grouping for rendering
151
+ each_slice: FunctionBuilder::Entry.new(
152
+ fn: ->(array, size) { array.each_slice(size).to_a },
153
+ arity: 2,
154
+ param_types: %i[array integer],
155
+ return_type: Kumi::Types.array(:array),
156
+ description: "Group array elements into subarrays of given size"
157
+ ),
158
+
159
+ join: FunctionBuilder::Entry.new(
160
+ fn: lambda { |array, separator = ""|
161
+ array.map(&:to_s).join(separator.to_s)
162
+ },
163
+ arity: 2,
164
+ param_types: %i[array string],
165
+ return_type: :string,
166
+ description: "Join array elements into string with separator"
167
+ ),
168
+
169
+ # Transform each subarray to string and join the results
170
+ map_join_rows: FunctionBuilder::Entry.new(
171
+ fn: lambda { |array_of_arrays, row_separator = "", column_separator = "\n"|
172
+ array_of_arrays.map { |row| row.join(row_separator.to_s) }.join(column_separator.to_s)
173
+ },
174
+ arity: 3,
175
+ param_types: [Kumi::Types.array(:array), :string, :string],
176
+ return_type: :string,
177
+ description: "Join 2D array into string with row and column separators"
178
+ ),
179
+
180
+ # Higher-order collection functions (limited to common patterns)
181
+ map_with_index: FunctionBuilder::Entry.new(
182
+ fn: ->(collection) { collection.map.with_index.to_a },
183
+ arity: 1,
184
+ param_types: [Kumi::Types.array(:any)],
185
+ return_type: Kumi::Types.array(:any),
186
+ description: "Map collection elements to [element, index] pairs"
187
+ ),
188
+
189
+ indices: FunctionBuilder::Entry.new(
190
+ fn: ->(collection) { (0...collection.size).to_a },
191
+ arity: 1,
192
+ param_types: [Kumi::Types.array(:any)],
193
+ return_type: Kumi::Types.array(:integer),
194
+ description: "Generate array of indices for the collection"
87
195
  )
88
196
  }
89
197
  end
@@ -4,7 +4,7 @@ module Kumi
4
4
  module FunctionRegistry
5
5
  # Utility class to reduce repetition in function definitions
6
6
  class FunctionBuilder
7
- Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, keyword_init: true)
7
+ Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, :reducer, keyword_init: true)
8
8
 
9
9
  def self.comparison(_name, description, operation)
10
10
  Entry.new(
@@ -78,13 +78,14 @@ module Kumi
78
78
  )
79
79
  end
80
80
 
81
- def self.collection_unary(_name, description, operation, return_type: :boolean)
81
+ def self.collection_unary(_name, description, operation, return_type: :boolean, reducer: false)
82
82
  Entry.new(
83
83
  fn: proc(&operation),
84
84
  arity: 1,
85
85
  param_types: [Kumi::Types.array(:any)],
86
86
  return_type: return_type,
87
- description: description
87
+ description: description,
88
+ reducer: reducer
88
89
  )
89
90
  end
90
91
  end
@@ -36,7 +36,7 @@ module Kumi
36
36
 
37
37
  # Register with custom metadata
38
38
  def register_with_metadata(name, fn_lambda, arity:, param_types: [:any], return_type: :any, description: nil,
39
- inverse: nil)
39
+ inverse: nil, reducer: false)
40
40
  raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
41
41
 
42
42
  @functions[name] = Entry.new(
@@ -45,7 +45,8 @@ module Kumi
45
45
  param_types: param_types,
46
46
  return_type: return_type,
47
47
  description: description,
48
- inverse: inverse
48
+ inverse: inverse,
49
+ reducer: reducer
49
50
  )
50
51
  end
51
52
 
@@ -91,6 +92,11 @@ module Kumi
91
92
  @functions.keys
92
93
  end
93
94
 
95
+ def reducer?(name)
96
+ entry = @functions.fetch(name) { return false }
97
+ entry.reducer || false
98
+ end
99
+
94
100
  # Alias for compatibility
95
101
  def all
96
102
  @functions.keys
@@ -15,6 +15,9 @@ module Kumi
15
15
  value.is_a?(TrueClass) || value.is_a?(FalseClass)
16
16
  when :symbol
17
17
  value.is_a?(Symbol)
18
+ when :array
19
+ # Simple :array type - just check if it's an Array
20
+ value.is_a?(Array)
18
21
  when :any
19
22
  true
20
23
  else
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "type_matcher"
4
- require_relative "violation_creator"
5
-
6
3
  module Kumi
7
4
  module Input
8
5
  class Validator
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ # DSL proxy for declaration references (traits and values)
6
+ # Handles references to declared items and field access on them
7
+ class DeclarationReferenceProxy
8
+ include Syntax
9
+
10
+ # Use shared operator methods
11
+ extend Sugar::ProxyRefinement
12
+
13
+ def initialize(name, context)
14
+ @name = name
15
+ @context = context
16
+ end
17
+
18
+ # Convert to DeclarationReference AST node
19
+ def to_ast_node
20
+ Kumi::Syntax::DeclarationReference.new(@name, loc: @context.current_location)
21
+ end
22
+
23
+ private
24
+
25
+ def method_missing(method_name, *args, &block)
26
+ # All operators are handled by ProxyRefinement methods
27
+ # Field access should use input.field.subfield syntax, not bare identifiers
28
+ super
29
+ end
30
+
31
+ def respond_to_missing?(_method_name, _include_private = false)
32
+ true
33
+ end
34
+ end
35
+ end
36
+ end
@@ -20,7 +20,8 @@ module Kumi
20
20
  trait_names = args[0..-2]
21
21
  expr = args.last
22
22
 
23
- condition = create_function_call(:all?, trait_names, on_loc)
23
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
24
+ condition = create_fn(:all?, trait_bindings)
24
25
  result = ensure_syntax(expr)
25
26
  add_case(condition, result)
26
27
  end
@@ -32,7 +33,8 @@ module Kumi
32
33
  trait_names = args[0..-2]
33
34
  expr = args.last
34
35
 
35
- condition = create_function_call(:any?, trait_names, on_loc)
36
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
37
+ condition = create_fn(:any?, trait_bindings)
36
38
  result = ensure_syntax(expr)
37
39
  add_case(condition, result)
38
40
  end
@@ -44,7 +46,8 @@ module Kumi
44
46
  trait_names = args[0..-2]
45
47
  expr = args.last
46
48
 
47
- condition = create_function_call(:none?, trait_names, on_loc)
49
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
50
+ condition = create_fn(:none?, trait_bindings)
48
51
  result = ensure_syntax(expr)
49
52
  add_case(condition, result)
50
53
  end
@@ -80,13 +83,21 @@ module Kumi
80
83
  raise_error("cascade '#{method_name}' requires an expression as the last argument", location)
81
84
  end
82
85
 
83
- def create_function_call(function_name, trait_names, location)
84
- trait_bindings = trait_names.map { |name| create_binding(name, location) }
85
- create_fn(function_name, trait_bindings)
86
+ def convert_trait_names_to_bindings(trait_names, location)
87
+ trait_names.map do |name|
88
+ case name
89
+ when Symbol
90
+ create_binding(name, location)
91
+ when DeclarationReference
92
+ name # Already a binding from method_missing
93
+ else
94
+ raise_error("trait reference must be a symbol or bare identifier, got #{name.class}", location)
95
+ end
96
+ end
86
97
  end
87
98
 
88
99
  def add_case(condition, result)
89
- @cases << WhenCaseExpression.new(condition, result)
100
+ @cases << Kumi::Syntax::CaseExpression.new(condition, result)
90
101
  end
91
102
 
92
103
  def ref(name)
@@ -118,7 +129,7 @@ module Kumi
118
129
  end
119
130
 
120
131
  def create_binding(name, location)
121
- Binding.new(name, loc: location)
132
+ Kumi::Syntax::DeclarationReference.new(name, loc: location)
122
133
  end
123
134
  end
124
135
  end