kumi 0.0.5 → 0.0.7

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +76 -174
  3. data/README.md +205 -52
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/SYNTAX.md +95 -8
  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/docs/schema_metadata/broadcasts.md +53 -0
  14. data/docs/schema_metadata/cascades.md +45 -0
  15. data/docs/schema_metadata/declarations.md +54 -0
  16. data/docs/schema_metadata/dependencies.md +57 -0
  17. data/docs/schema_metadata/evaluation_order.md +29 -0
  18. data/docs/schema_metadata/examples.md +95 -0
  19. data/docs/schema_metadata/inferred_types.md +46 -0
  20. data/docs/schema_metadata/inputs.md +86 -0
  21. data/docs/schema_metadata.md +108 -0
  22. data/examples/federal_tax_calculator_2024.rb +11 -6
  23. data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
  24. data/lib/kumi/analyzer/passes/broadcast_detector.rb +246 -0
  25. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
  26. data/lib/kumi/analyzer/passes/dependency_resolver.rb +78 -38
  27. data/lib/kumi/analyzer/passes/input_collector.rb +91 -30
  28. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  29. data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
  30. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +24 -25
  31. data/lib/kumi/analyzer/passes/toposorter.rb +44 -8
  32. data/lib/kumi/analyzer/passes/type_checker.rb +34 -14
  33. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
  34. data/lib/kumi/analyzer/passes/type_inferencer.rb +130 -21
  35. data/lib/kumi/analyzer/passes/unsat_detector.rb +134 -56
  36. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
  37. data/lib/kumi/analyzer.rb +16 -17
  38. data/lib/kumi/compiler.rb +188 -16
  39. data/lib/kumi/constraint_relationship_solver.rb +6 -6
  40. data/lib/kumi/domain/validator.rb +0 -4
  41. data/lib/kumi/error_reporting.rb +1 -1
  42. data/lib/kumi/explain.rb +32 -20
  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 +14 -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/json_schema/generator.rb +63 -0
  51. data/lib/kumi/json_schema/validator.rb +25 -0
  52. data/lib/kumi/json_schema.rb +14 -0
  53. data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
  54. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +36 -0
  55. data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
  56. data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +5 -5
  57. data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +20 -20
  58. data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
  59. data/lib/kumi/{parser → ruby_parser}/input_builder.rb +41 -10
  60. data/lib/kumi/ruby_parser/input_field_proxy.rb +46 -0
  61. data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +4 -4
  62. data/lib/kumi/ruby_parser/nested_input.rb +15 -0
  63. data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
  64. data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +11 -10
  65. data/lib/kumi/{parser → ruby_parser}/sugar.rb +62 -10
  66. data/lib/kumi/ruby_parser.rb +10 -0
  67. data/lib/kumi/schema.rb +10 -4
  68. data/lib/kumi/schema_instance.rb +6 -6
  69. data/lib/kumi/schema_metadata.rb +524 -0
  70. data/lib/kumi/syntax/array_expression.rb +15 -0
  71. data/lib/kumi/syntax/call_expression.rb +11 -0
  72. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  73. data/lib/kumi/syntax/case_expression.rb +11 -0
  74. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  75. data/lib/kumi/syntax/hash_expression.rb +11 -0
  76. data/lib/kumi/syntax/input_declaration.rb +12 -0
  77. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  78. data/lib/kumi/syntax/input_reference.rb +12 -0
  79. data/lib/kumi/syntax/literal.rb +11 -0
  80. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  81. data/lib/kumi/syntax/value_declaration.rb +11 -0
  82. data/lib/kumi/vectorization_metadata.rb +108 -0
  83. data/lib/kumi/version.rb +1 -1
  84. data/lib/kumi.rb +14 -0
  85. metadata +55 -25
  86. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
  87. data/lib/kumi/domain.rb +0 -8
  88. data/lib/kumi/input.rb +0 -8
  89. data/lib/kumi/syntax/declarations.rb +0 -26
  90. data/lib/kumi/syntax/expressions.rb +0 -34
  91. data/lib/kumi/syntax/terminal_expressions.rb +0 -30
  92. data/lib/kumi/syntax.rb +0 -9
  93. /data/{documents → docs}/DSL.md +0 -0
  94. /data/{documents → docs}/FUNCTIONS.md +0 -0
data/lib/kumi/compiler.rb CHANGED
@@ -14,9 +14,26 @@ 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
+
17
30
  def compile_binding_node(expr)
18
- fn = @bindings[expr.name].last
19
- ->(ctx) { fn.call(ctx) }
31
+ name = expr.name
32
+ # Handle forward references in cycles by deferring binding lookup to runtime
33
+ lambda do |ctx|
34
+ fn = @bindings[name].last
35
+ fn.call(ctx)
36
+ end
20
37
  end
21
38
 
22
39
  def compile_list(expr)
@@ -27,28 +44,101 @@ module Kumi
27
44
  def compile_call(expr)
28
45
  fn_name = expr.fn_name
29
46
  arg_fns = expr.args.map { |a| compile_expr(a) }
30
- ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
47
+
48
+ # Check if this is a vectorized operation
49
+ if vectorized_operation?(expr)
50
+ ->(ctx) { invoke_vectorized_function(fn_name, arg_fns, ctx, expr.loc) }
51
+ else
52
+ ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
53
+ end
31
54
  end
32
55
 
33
56
  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
57
+ # Check if current declaration is vectorized
58
+ broadcast_meta = @analysis.state[:broadcasts]
59
+ is_vectorized = @current_declaration && broadcast_meta&.dig(:vectorized_operations, @current_declaration)
60
+
61
+ # For vectorized cascades, we need to transform conditions that use all?
62
+ pairs = if is_vectorized
63
+ expr.cases.map do |c|
64
+ condition_fn = transform_vectorized_condition(c.condition)
65
+ result_fn = compile_expr(c.result)
66
+ [condition_fn, result_fn]
67
+ end
68
+ else
69
+ expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
70
+ end
71
+
72
+ if is_vectorized
73
+ lambda do |ctx|
74
+ # This cascade can be vectorized - check if we actually need to at runtime
75
+ # Evaluate all conditions and results to check for arrays
76
+ cond_results = pairs.map { |cond, _res| cond.call(ctx) }
77
+ res_results = pairs.map { |_cond, res| res.call(ctx) }
78
+
79
+ # Check if any conditions or results are arrays (vectorized)
80
+ has_vectorized_data = (cond_results + res_results).any?(Array)
81
+
82
+ if has_vectorized_data
83
+ # Apply element-wise cascade evaluation
84
+ array_length = cond_results.find { |v| v.is_a?(Array) }&.length ||
85
+ res_results.find { |v| v.is_a?(Array) }&.length || 1
86
+
87
+ (0...array_length).map do |i|
88
+ pairs.each_with_index do |(_cond, _res), pair_idx|
89
+ cond_val = cond_results[pair_idx].is_a?(Array) ? cond_results[pair_idx][i] : cond_results[pair_idx]
90
+
91
+ if cond_val
92
+ res_val = res_results[pair_idx].is_a?(Array) ? res_results[pair_idx][i] : res_results[pair_idx]
93
+ break res_val
94
+ end
95
+ end || nil
96
+ end
97
+ else
98
+ # All data is scalar - use regular cascade evaluation
99
+ pairs.each_with_index do |(_cond, _res), pair_idx|
100
+ return res_results[pair_idx] if cond_results[pair_idx]
101
+ end
102
+ nil
103
+ end
104
+ end
105
+ else
106
+ lambda do |ctx|
107
+ pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
108
+ nil
109
+ end
38
110
  end
39
111
  end
112
+
113
+ def transform_vectorized_condition(condition_expr)
114
+ # If this is fn(:all?, [trait_ref]), extract the trait_ref for vectorized cascades
115
+ if condition_expr.is_a?(Kumi::Syntax::CallExpression) &&
116
+ condition_expr.fn_name == :all? &&
117
+ condition_expr.args.length == 1
118
+
119
+ arg = condition_expr.args.first
120
+ if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
121
+ trait_ref = arg.elements.first
122
+ return compile_expr(trait_ref)
123
+ end
124
+ end
125
+
126
+ # Otherwise compile normally
127
+ compile_expr(condition_expr)
128
+ end
40
129
  end
41
130
 
42
131
  include ExprCompilers
43
132
 
44
133
  # Map node classes to compiler methods
45
134
  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
135
+ Kumi::Syntax::Literal => :compile_literal,
136
+ Kumi::Syntax::InputReference => :compile_field_node,
137
+ Kumi::Syntax::InputElementReference => :compile_element_field_reference,
138
+ Kumi::Syntax::DeclarationReference => :compile_binding_node,
139
+ Kumi::Syntax::ArrayExpression => :compile_list,
140
+ Kumi::Syntax::CallExpression => :compile_call,
141
+ Kumi::Syntax::CascadeExpression => :compile_cascade
52
142
  }.freeze
53
143
 
54
144
  def self.compile(schema, analyzer:)
@@ -79,10 +169,30 @@ module Kumi
79
169
  @schema.traits.each { |t| @index[t.name] = t }
80
170
  end
81
171
 
172
+ def dig_and_map(collection, path_segments)
173
+ return collection unless collection.is_a?(Array)
174
+
175
+ current_segment = path_segments.first
176
+ remaining_segments = path_segments[1..]
177
+
178
+ collection.map do |element|
179
+ value = element[current_segment]
180
+
181
+ # If there are more segments, recurse. Otherwise, return the value.
182
+ if remaining_segments.empty?
183
+ value
184
+ else
185
+ dig_and_map(value, remaining_segments)
186
+ end
187
+ end
188
+ end
189
+
82
190
  def compile_declaration(decl)
83
- kind = decl.is_a?(Syntax::Declarations::Trait) ? :trait : :attr
84
- fn = compile_expr(decl.expression)
191
+ @current_declaration = decl.name
192
+ kind = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? :trait : :attr
193
+ fn = compile_expr(decl.expression)
85
194
  @bindings[decl.name] = [kind, fn]
195
+ @current_declaration = nil
86
196
  end
87
197
 
88
198
  # Dispatch to the appropriate compile_* method
@@ -91,7 +201,6 @@ module Kumi
91
201
  send(method, expr)
92
202
  end
93
203
 
94
- # Existing helpers unchanged
95
204
  def compile_field(node)
96
205
  name = node.name
97
206
  loc = node.loc
@@ -103,6 +212,69 @@ module Kumi
103
212
  end
104
213
  end
105
214
 
215
+ def vectorized_operation?(expr)
216
+ # Check if this operation uses vectorized inputs
217
+ broadcast_meta = @analysis.state[:broadcasts]
218
+ return false unless broadcast_meta
219
+
220
+ # Reduction functions are NOT vectorized operations - they consume arrays
221
+ return false if FunctionRegistry.reducer?(expr.fn_name)
222
+
223
+ expr.args.any? do |arg|
224
+ case arg
225
+ when Kumi::Syntax::InputElementReference
226
+ broadcast_meta[:array_fields]&.key?(arg.path.first)
227
+ when Kumi::Syntax::DeclarationReference
228
+ broadcast_meta[:vectorized_operations]&.key?(arg.name)
229
+ else
230
+ false
231
+ end
232
+ end
233
+ end
234
+
235
+ def invoke_vectorized_function(name, arg_fns, ctx, loc)
236
+ # Evaluate arguments
237
+ values = arg_fns.map { |fn| fn.call(ctx) }
238
+
239
+ # Check if any argument is vectorized (array)
240
+ has_vectorized_args = values.any?(Array)
241
+
242
+ if has_vectorized_args
243
+ # Apply function with broadcasting to all vectorized arguments
244
+ vectorized_function_call(name, values)
245
+ else
246
+ # All arguments are scalars - regular function call
247
+ fn = FunctionRegistry.fetch(name)
248
+ fn.call(*values)
249
+ end
250
+ rescue StandardError => e
251
+ enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
252
+ runtime_error = Errors::RuntimeError.new(enhanced_message)
253
+ runtime_error.set_backtrace(e.backtrace)
254
+ runtime_error.define_singleton_method(:cause) { e }
255
+ raise runtime_error
256
+ end
257
+
258
+ def vectorized_function_call(fn_name, values)
259
+ # Get the function from registry
260
+ fn = FunctionRegistry.fetch(fn_name)
261
+
262
+ # Find array dimensions for broadcasting
263
+ array_values = values.select { |v| v.is_a?(Array) }
264
+ return fn.call(*values) if array_values.empty?
265
+
266
+ # All arrays should have the same length (validation could be added)
267
+ array_length = array_values.first.size
268
+
269
+ # Broadcast and apply function element-wise
270
+ (0...array_length).map do |i|
271
+ element_args = values.map do |v|
272
+ v.is_a?(Array) ? v[i] : v # Broadcast scalars
273
+ end
274
+ fn.call(*element_args)
275
+ end
276
+ end
277
+
106
278
  def invoke_function(name, arg_fns, ctx, loc)
107
279
  fn = FunctionRegistry.fetch(name)
108
280
  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
@@ -54,7 +54,7 @@ module Kumi
54
54
 
55
55
  # Immediately raise a syntax error
56
56
  def raise_syntax_error(message, location: nil, context: {})
57
- raise_localized_error(message, location: location, error_class: Errors::SyntaxError, type: :syntax, context: context)
57
+ raise_localized_error(message, location: location, error_class: Kumi::Errors::SyntaxError, type: :syntax, context: context)
58
58
  end
59
59
 
60
60
  # Immediately raise a type error
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
@@ -274,6 +274,18 @@ module Kumi
274
274
 
275
275
  raise ArgumentError, "Schema not found or not compiled" unless syntax_tree && analyzer_result
276
276
 
277
+ metadata = analyzer_result.state
278
+
279
+ # Create a minimal analyzer result structure for compatibility
280
+ analyzer_result = OpenStruct.new(
281
+ definitions: metadata[:declarations] || {},
282
+ dependency_graph: metadata[:dependencies] || {},
283
+ leaf_map: metadata[:leaves] || {},
284
+ topo_order: metadata[:evaluation_order] || [],
285
+ decl_types: metadata[:inferred_types] || {},
286
+ state: metadata
287
+ )
288
+
277
289
  generator = ExplanationGenerator.new(syntax_tree, analyzer_result, inputs)
278
290
  generator.explain(target_name)
279
291
  end
@@ -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