kumi 0.0.5 → 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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +51 -6
  3. data/README.md +173 -51
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/SYNTAX.md +93 -1
  6. data/docs/features/README.md +45 -0
  7. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  8. data/docs/features/analysis-type-inference.md +42 -0
  9. data/docs/features/analysis-unsat-detection.md +71 -0
  10. data/docs/features/array-broadcasting.md +170 -0
  11. data/docs/features/input-declaration-system.md +42 -0
  12. data/docs/features/performance.md +16 -0
  13. data/examples/federal_tax_calculator_2024.rb +11 -6
  14. data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
  15. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  16. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
  17. data/lib/kumi/analyzer/passes/dependency_resolver.rb +72 -32
  18. data/lib/kumi/analyzer/passes/input_collector.rb +90 -29
  19. data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
  20. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +9 -9
  21. data/lib/kumi/analyzer/passes/toposorter.rb +42 -6
  22. data/lib/kumi/analyzer/passes/type_checker.rb +32 -10
  23. data/lib/kumi/analyzer/passes/type_inferencer.rb +126 -17
  24. data/lib/kumi/analyzer/passes/unsat_detector.rb +133 -53
  25. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
  26. data/lib/kumi/analyzer.rb +11 -12
  27. data/lib/kumi/compiler.rb +194 -16
  28. data/lib/kumi/constraint_relationship_solver.rb +6 -6
  29. data/lib/kumi/domain/validator.rb +0 -4
  30. data/lib/kumi/explain.rb +20 -20
  31. data/lib/kumi/export/node_registry.rb +26 -12
  32. data/lib/kumi/export/node_serializers.rb +1 -1
  33. data/lib/kumi/function_registry/collection_functions.rb +14 -9
  34. data/lib/kumi/function_registry/function_builder.rb +4 -3
  35. data/lib/kumi/function_registry.rb +8 -2
  36. data/lib/kumi/input/type_matcher.rb +3 -0
  37. data/lib/kumi/input/validator.rb +0 -3
  38. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  39. data/lib/kumi/parser/dsl_cascade_builder.rb +3 -3
  40. data/lib/kumi/parser/expression_converter.rb +6 -6
  41. data/lib/kumi/parser/input_builder.rb +40 -9
  42. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  43. data/lib/kumi/parser/input_proxy.rb +3 -3
  44. data/lib/kumi/parser/nested_input.rb +15 -0
  45. data/lib/kumi/parser/schema_builder.rb +10 -9
  46. data/lib/kumi/parser/sugar.rb +61 -9
  47. data/lib/kumi/syntax/array_expression.rb +15 -0
  48. data/lib/kumi/syntax/call_expression.rb +11 -0
  49. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  50. data/lib/kumi/syntax/case_expression.rb +11 -0
  51. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  52. data/lib/kumi/syntax/hash_expression.rb +11 -0
  53. data/lib/kumi/syntax/input_declaration.rb +12 -0
  54. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  55. data/lib/kumi/syntax/input_reference.rb +12 -0
  56. data/lib/kumi/syntax/literal.rb +11 -0
  57. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  58. data/lib/kumi/syntax/value_declaration.rb +11 -0
  59. data/lib/kumi/vectorization_metadata.rb +108 -0
  60. data/lib/kumi/version.rb +1 -1
  61. metadata +31 -14
  62. data/lib/kumi/domain.rb +0 -8
  63. data/lib/kumi/input.rb +0 -8
  64. data/lib/kumi/syntax/declarations.rb +0 -26
  65. data/lib/kumi/syntax/expressions.rb +0 -34
  66. data/lib/kumi/syntax/terminal_expressions.rb +0 -30
  67. data/lib/kumi/syntax.rb +0 -9
  68. /data/{documents → docs}/DSL.md +0 -0
  69. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -20,7 +20,7 @@ module Kumi
20
20
 
21
21
  # Helper to visit each declaration's expression tree
22
22
  # @param errors [Array] Error accumulator
23
- # @yield [Syntax::Node, Syntax::Declarations::Base] Each node and its containing declaration
23
+ # @yield [Syntax::Node, Syntax::Base] Each node and its containing declaration
24
24
  def visit_all_expressions(errors)
25
25
  each_decl do |decl|
26
26
  visit(decl.expression) { |node| yield(node, decl, errors) }
@@ -30,7 +30,7 @@ module Kumi
30
30
  # Helper to visit only specific node types
31
31
  # @param node_types [Array<Class>] Node types to match
32
32
  # @param errors [Array] Error accumulator
33
- # @yield [Syntax::Node, Syntax::Declarations::Base] Matching nodes and their declarations
33
+ # @yield [Syntax::Node, Syntax::Base] Matching nodes and their declarations
34
34
  def visit_nodes_of_type(*node_types, errors:)
35
35
  visit_all_expressions(errors) do |node, decl, errs|
36
36
  yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
data/lib/kumi/analyzer.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "analyzer/analysis_state"
4
-
5
3
  module Kumi
6
4
  module Analyzer
7
5
  Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
@@ -9,16 +7,17 @@ module Kumi
9
7
  module_function
10
8
 
11
9
  DEFAULT_PASSES = [
12
- Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
13
- Passes::InputCollector, # 2. Collects field metadata from input declarations.
14
- Passes::DefinitionValidator, # 3. Checks the basic structure of each rule.
15
- Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
16
- Passes::DependencyResolver, # 5. Builds the dependency graph.
17
- Passes::UnsatDetector, # 6. Detects unsatisfiable constraints in rules.
18
- Passes::Toposorter, # 7. Creates the final evaluation order.
19
- Passes::TypeInferencer, # 8. Infers types for all declarations (pure annotation).
20
- Passes::TypeConsistencyChecker, # 9. Validates declared vs inferred type consistency.
21
- Passes::TypeChecker # 10. Validates types using inferred information.
10
+ Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
11
+ Passes::InputCollector, # 2. Collects field metadata from input declarations.
12
+ Passes::DeclarationValidator, # 3. Checks the basic structure of each rule.
13
+ Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
14
+ Passes::DependencyResolver, # 5. Builds the dependency graph with conditional dependencies.
15
+ Passes::UnsatDetector, # 6. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
16
+ Passes::Toposorter, # 7. Creates the final evaluation order, allowing safe cycles.
17
+ Passes::BroadcastDetector, # 8. Detects which operations should be broadcast over arrays (must run before type inference).
18
+ Passes::TypeInferencer, # 9. Infers types for all declarations (uses vectorization metadata).
19
+ Passes::TypeConsistencyChecker, # 10. Validates declared vs inferred type consistency.
20
+ Passes::TypeChecker # 11. Validates types using inferred information.
22
21
  ].freeze
23
22
 
24
23
  def analyze!(schema, passes: DEFAULT_PASSES, **opts)
data/lib/kumi/compiler.rb CHANGED
@@ -14,9 +14,27 @@ module Kumi
14
14
  compile_field(expr)
15
15
  end
16
16
 
17
+ def compile_element_field_reference(expr)
18
+ path = expr.path
19
+
20
+ lambda do |ctx|
21
+ # Start with the top-level collection from the context.
22
+ collection = ctx[path.first]
23
+
24
+ # Recursively map over the nested collections.
25
+ # The `dig_and_map` helper will handle any level of nesting.
26
+ dig_and_map(collection, path[1..])
27
+ end
28
+ end
29
+
30
+
17
31
  def compile_binding_node(expr)
18
- fn = @bindings[expr.name].last
19
- ->(ctx) { fn.call(ctx) }
32
+ name = expr.name
33
+ # Handle forward references in cycles by deferring binding lookup to runtime
34
+ lambda do |ctx|
35
+ fn = @bindings[name].last
36
+ fn.call(ctx)
37
+ end
20
38
  end
21
39
 
22
40
  def compile_list(expr)
@@ -27,28 +45,102 @@ module Kumi
27
45
  def compile_call(expr)
28
46
  fn_name = expr.fn_name
29
47
  arg_fns = expr.args.map { |a| compile_expr(a) }
30
- ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
48
+
49
+ # Check if this is a vectorized operation
50
+ if vectorized_operation?(expr)
51
+ ->(ctx) { invoke_vectorized_function(fn_name, arg_fns, ctx, expr.loc) }
52
+ else
53
+ ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
54
+ end
31
55
  end
32
56
 
33
57
  def compile_cascade(expr)
34
- pairs = expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
35
- lambda do |ctx|
36
- pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
37
- nil
58
+ # Check if current declaration is vectorized
59
+ broadcast_meta = @analysis.state[:broadcast_metadata]
60
+ is_vectorized = @current_declaration && broadcast_meta&.dig(:vectorized_operations, @current_declaration)
61
+
62
+
63
+ # For vectorized cascades, we need to transform conditions that use all?
64
+ if is_vectorized
65
+ pairs = expr.cases.map do |c|
66
+ condition_fn = transform_vectorized_condition(c.condition)
67
+ result_fn = compile_expr(c.result)
68
+ [condition_fn, result_fn]
69
+ end
70
+ else
71
+ pairs = expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
72
+ end
73
+
74
+ if is_vectorized
75
+ lambda do |ctx|
76
+ # This cascade can be vectorized - check if we actually need to at runtime
77
+ # Evaluate all conditions and results to check for arrays
78
+ cond_results = pairs.map { |cond, _res| cond.call(ctx) }
79
+ res_results = pairs.map { |_cond, res| res.call(ctx) }
80
+
81
+ # Check if any conditions or results are arrays (vectorized)
82
+ has_vectorized_data = (cond_results + res_results).any? { |v| v.is_a?(Array) }
83
+
84
+ if has_vectorized_data
85
+ # Apply element-wise cascade evaluation
86
+ array_length = cond_results.find { |v| v.is_a?(Array) }&.length ||
87
+ res_results.find { |v| v.is_a?(Array) }&.length || 1
88
+
89
+ (0...array_length).map do |i|
90
+ pairs.each_with_index do |(cond, res), pair_idx|
91
+ cond_val = cond_results[pair_idx].is_a?(Array) ? cond_results[pair_idx][i] : cond_results[pair_idx]
92
+
93
+ if cond_val
94
+ res_val = res_results[pair_idx].is_a?(Array) ? res_results[pair_idx][i] : res_results[pair_idx]
95
+ break res_val
96
+ end
97
+ end || nil
98
+ end
99
+ else
100
+ # All data is scalar - use regular cascade evaluation
101
+ pairs.each_with_index do |(cond, res), pair_idx|
102
+ return res_results[pair_idx] if cond_results[pair_idx]
103
+ end
104
+ nil
105
+ end
106
+ end
107
+ else
108
+ lambda do |ctx|
109
+ pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
110
+ nil
111
+ end
38
112
  end
39
113
  end
114
+
115
+ def transform_vectorized_condition(condition_expr)
116
+ # If this is fn(:all?, [trait_ref]), extract the trait_ref for vectorized cascades
117
+ if condition_expr.is_a?(Kumi::Syntax::CallExpression) &&
118
+ condition_expr.fn_name == :all? &&
119
+ condition_expr.args.length == 1
120
+
121
+ arg = condition_expr.args.first
122
+ if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
123
+ trait_ref = arg.elements.first
124
+ return compile_expr(trait_ref)
125
+ end
126
+ end
127
+
128
+ # Otherwise compile normally
129
+ compile_expr(condition_expr)
130
+ end
40
131
  end
41
132
 
42
133
  include ExprCompilers
43
134
 
44
135
  # Map node classes to compiler methods
45
136
  DISPATCH = {
46
- Syntax::TerminalExpressions::Literal => :compile_literal,
47
- Syntax::TerminalExpressions::FieldRef => :compile_field_node,
48
- Syntax::TerminalExpressions::Binding => :compile_binding_node,
49
- Syntax::Expressions::ListExpression => :compile_list,
50
- Syntax::Expressions::CallExpression => :compile_call,
51
- Syntax::Expressions::CascadeExpression => :compile_cascade
137
+ Kumi::Syntax::Literal => :compile_literal,
138
+ Kumi::Syntax::InputReference => :compile_field_node,
139
+ Kumi::Syntax::InputElementReference => :compile_element_field_reference,
140
+ Kumi::Syntax::DeclarationReference => :compile_binding_node,
141
+ Kumi::Syntax::ArrayExpression => :compile_list,
142
+ Kumi::Syntax::CallExpression => :compile_call,
143
+ Kumi::Syntax::CascadeExpression => :compile_cascade
52
144
  }.freeze
53
145
 
54
146
  def self.compile(schema, analyzer:)
@@ -79,10 +171,30 @@ module Kumi
79
171
  @schema.traits.each { |t| @index[t.name] = t }
80
172
  end
81
173
 
174
+ def dig_and_map(collection, path_segments)
175
+ return collection unless collection.is_a?(Array)
176
+
177
+ current_segment = path_segments.first
178
+ remaining_segments = path_segments[1..]
179
+
180
+ collection.map do |element|
181
+ value = element[current_segment]
182
+
183
+ # If there are more segments, recurse. Otherwise, return the value.
184
+ if remaining_segments.empty?
185
+ value
186
+ else
187
+ dig_and_map(value, remaining_segments)
188
+ end
189
+ end
190
+ end
191
+
82
192
  def compile_declaration(decl)
83
- kind = decl.is_a?(Syntax::Declarations::Trait) ? :trait : :attr
84
- fn = compile_expr(decl.expression)
193
+ @current_declaration = decl.name
194
+ kind = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? :trait : :attr
195
+ fn = compile_expr(decl.expression)
85
196
  @bindings[decl.name] = [kind, fn]
197
+ @current_declaration = nil
86
198
  end
87
199
 
88
200
  # Dispatch to the appropriate compile_* method
@@ -91,7 +203,6 @@ module Kumi
91
203
  send(method, expr)
92
204
  end
93
205
 
94
- # Existing helpers unchanged
95
206
  def compile_field(node)
96
207
  name = node.name
97
208
  loc = node.loc
@@ -103,6 +214,73 @@ module Kumi
103
214
  end
104
215
  end
105
216
 
217
+ def vectorized_operation?(expr)
218
+ # Check if this operation uses vectorized inputs
219
+ broadcast_meta = @analysis.state[:broadcast_metadata]
220
+ return false unless broadcast_meta
221
+
222
+ # Reduction functions are NOT vectorized operations - they consume arrays
223
+ if FunctionRegistry.reducer?(expr.fn_name)
224
+ return false
225
+ end
226
+
227
+ expr.args.any? do |arg|
228
+ case arg
229
+ when Kumi::Syntax::InputElementReference
230
+ broadcast_meta[:array_fields]&.key?(arg.path.first)
231
+ when Kumi::Syntax::DeclarationReference
232
+ broadcast_meta[:vectorized_operations]&.key?(arg.name)
233
+ else
234
+ false
235
+ end
236
+ end
237
+ end
238
+
239
+
240
+ def invoke_vectorized_function(name, arg_fns, ctx, loc)
241
+ # Evaluate arguments
242
+ values = arg_fns.map { |fn| fn.call(ctx) }
243
+
244
+ # Check if any argument is vectorized (array)
245
+ has_vectorized_args = values.any? { |v| v.is_a?(Array) }
246
+
247
+ if has_vectorized_args
248
+ # Apply function with broadcasting to all vectorized arguments
249
+ vectorized_function_call(name, values)
250
+ else
251
+ # All arguments are scalars - regular function call
252
+ fn = FunctionRegistry.fetch(name)
253
+ fn.call(*values)
254
+ end
255
+ rescue StandardError => e
256
+ enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
257
+ runtime_error = Errors::RuntimeError.new(enhanced_message)
258
+ runtime_error.set_backtrace(e.backtrace)
259
+ runtime_error.define_singleton_method(:cause) { e }
260
+ raise runtime_error
261
+ end
262
+
263
+ def vectorized_function_call(fn_name, values)
264
+ # Get the function from registry
265
+ fn = FunctionRegistry.fetch(fn_name)
266
+
267
+ # Find array dimensions for broadcasting
268
+ array_values = values.select { |v| v.is_a?(Array) }
269
+ return fn.call(*values) if array_values.empty?
270
+
271
+ # All arrays should have the same length (validation could be added)
272
+ array_length = array_values.first.size
273
+
274
+ # Broadcast and apply function element-wise
275
+ (0...array_length).map do |i|
276
+ element_args = values.map do |v|
277
+ v.is_a?(Array) ? v[i] : v # Broadcast scalars
278
+ end
279
+ fn.call(*element_args)
280
+ end
281
+ end
282
+
283
+
106
284
  def invoke_function(name, arg_fns, ctx, loc)
107
285
  fn = FunctionRegistry.fetch(name)
108
286
  values = arg_fns.map { |fn| fn.call(ctx) }
@@ -98,12 +98,12 @@ module Kumi
98
98
  # @return [Relationship, nil] the relationship or nil if not extractable
99
99
  def extract_relationship(target, expression)
100
100
  case expression
101
- when Kumi::Syntax::Expressions::CallExpression
101
+ when Kumi::Syntax::CallExpression
102
102
  extract_call_relationship(target, expression)
103
- when Kumi::Syntax::TerminalExpressions::Binding
103
+ when Kumi::Syntax::DeclarationReference
104
104
  # Simple alias: target = other_variable
105
105
  Relationship.new(target, :identity, [expression.name])
106
- when Kumi::Syntax::TerminalExpressions::FieldRef
106
+ when Kumi::Syntax::InputReference
107
107
  # Direct field reference: target = input.field
108
108
  # Create identity relationship so we can propagate constraints
109
109
  Relationship.new(target, :identity, [expression.name])
@@ -152,11 +152,11 @@ module Kumi
152
152
 
153
153
  args.map do |arg|
154
154
  case arg
155
- when Kumi::Syntax::TerminalExpressions::Binding
155
+ when Kumi::Syntax::DeclarationReference
156
156
  arg.name
157
- when Kumi::Syntax::TerminalExpressions::Literal
157
+ when Kumi::Syntax::Literal
158
158
  arg.value
159
- when Kumi::Syntax::TerminalExpressions::FieldRef
159
+ when Kumi::Syntax::InputReference
160
160
  # Use the field name directly to match how atoms represent input fields
161
161
  arg.name
162
162
  else
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "range_analyzer"
4
- require_relative "enum_analyzer"
5
- require_relative "violation_formatter"
6
-
7
3
  module Kumi
8
4
  module Domain
9
5
  class Validator
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
@@ -72,11 +72,11 @@ module Kumi
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
@@ -91,12 +91,12 @@ module Kumi
91
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
@@ -119,7 +119,7 @@ module Kumi
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
@@ -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
@@ -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