kumi 0.0.10 → 0.0.12

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +23 -0
  4. data/CLAUDE.md +7 -231
  5. data/README.md +5 -5
  6. data/docs/SYNTAX.md +66 -0
  7. data/docs/VECTOR_SEMANTICS.md +286 -0
  8. data/docs/features/hierarchical-broadcasting.md +67 -1
  9. data/docs/features/input-declaration-system.md +16 -0
  10. data/docs/features/s-expression-printer.md +2 -2
  11. data/lib/kumi/analyzer.rb +34 -12
  12. data/lib/kumi/compiler.rb +2 -12
  13. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
  14. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  15. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  16. data/lib/kumi/core/analyzer/passes/input_collector.rb +123 -101
  17. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  18. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  19. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  20. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  21. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
  22. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  23. data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
  24. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  25. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  26. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
  27. data/lib/kumi/core/analyzer/plans.rb +52 -0
  28. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  29. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  30. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  31. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  32. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  33. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  34. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  35. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  36. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  37. data/lib/kumi/core/compiler_base.rb +2 -2
  38. data/lib/kumi/core/error_reporter.rb +6 -5
  39. data/lib/kumi/core/errors.rb +4 -0
  40. data/lib/kumi/core/explain.rb +157 -205
  41. data/lib/kumi/core/export/node_builders.rb +2 -2
  42. data/lib/kumi/core/export/node_serializers.rb +1 -1
  43. data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
  44. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  45. data/lib/kumi/core/function_registry/function_builder.rb +142 -55
  46. data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
  47. data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
  48. data/lib/kumi/core/function_registry.rb +126 -108
  49. data/lib/kumi/core/input/validator.rb +1 -1
  50. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  51. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  52. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  53. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  54. data/lib/kumi/core/ir.rb +58 -0
  55. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  56. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  57. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
  58. data/lib/kumi/core/ruby_parser/input_builder.rb +30 -9
  59. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  60. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  61. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  62. data/lib/kumi/core/types/validator.rb +1 -1
  63. data/lib/kumi/registry.rb +14 -79
  64. data/lib/kumi/runtime/executable.rb +213 -0
  65. data/lib/kumi/schema.rb +14 -3
  66. data/lib/kumi/schema_metadata.rb +2 -2
  67. data/lib/kumi/support/ir_dump.rb +491 -0
  68. data/lib/kumi/support/s_expression_printer.rb +1 -1
  69. data/lib/kumi/syntax/location.rb +5 -0
  70. data/lib/kumi/syntax/node.rb +0 -1
  71. data/lib/kumi/syntax/root.rb +2 -2
  72. data/lib/kumi/version.rb +1 -1
  73. data/lib/kumi.rb +6 -15
  74. metadata +37 -19
  75. data/lib/kumi/core/cascade_executor_builder.rb +0 -132
  76. data/lib/kumi/core/compiled_schema.rb +0 -43
  77. data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
  78. data/lib/kumi/core/compiler/function_invoker.rb +0 -55
  79. data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
  80. data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
  81. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  82. data/lib/kumi/core/nested_structure_utils.rb +0 -78
  83. data/lib/kumi/core/schema_instance.rb +0 -115
  84. data/lib/kumi/core/vectorized_function_builder.rb +0 -88
  85. data/lib/kumi/js/compiler.rb +0 -878
  86. data/lib/kumi/js/function_registry.rb +0 -333
  87. data/migrate_to_core_iterative.rb +0 -938
@@ -4,291 +4,243 @@ module Kumi
4
4
  module Core
5
5
  module Explain
6
6
  class ExplanationGenerator
7
- def initialize(syntax_tree, analyzer_result, inputs)
8
- @analyzer_result = analyzer_result
9
- @inputs = EvaluationWrapper.new(inputs)
10
- @definitions = analyzer_result.definitions
11
- @compiled_schema = Kumi::Compiler.compile(syntax_tree, analyzer: analyzer_result)
7
+ def initialize(syntax_tree, analysis_state, inputs, registry: Kumi::Registry)
8
+ @syntax_tree = syntax_tree
9
+ @state = analysis_state
10
+ @inputs = inputs
11
+ @definitions = analysis_state[:declarations] || {}
12
+ @registry = registry
12
13
 
13
- # TODO: REFACTOR QUICK!
14
- # Set up compiler once for expression evaluation
15
- @compiler = Kumi::Compiler.new(syntax_tree, analyzer_result)
16
- @compiler.send(:build_index)
17
-
18
- # Populate bindings from the compiled schema
19
- @compiled_schema.bindings.each do |name, (type, fn)|
20
- @compiler.instance_variable_get(:@bindings)[name] = [type, fn]
21
- end
14
+ @program = Kumi::Runtime::Executable.from_analysis(@state, registry: nil)
15
+ @session = @program.read(@inputs, mode: :ruby)
22
16
  end
23
17
 
24
18
  def explain(target_name)
25
- declaration = @definitions[target_name]
26
- raise ArgumentError, "Unknown declaration: #{target_name}" unless declaration
27
-
28
- expression = declaration.expression
29
- result_value = @compiled_schema.evaluate_binding(target_name, @inputs)
19
+ decl = @definitions[target_name] or raise ArgumentError, "Unknown declaration: #{target_name}"
20
+ expr = decl.expression
21
+ value = @session.get(target_name)
30
22
 
31
23
  prefix = "#{target_name} = "
32
- expression_str = format_expression(expression, indent_context: prefix.length)
24
+ expr_str = format_expression(expr, indent_context: prefix.length)
33
25
 
34
- "#{prefix}#{expression_str} => #{format_value(result_value)}"
26
+ "#{prefix}#{expr_str} => #{format_value(value)}"
35
27
  end
36
28
 
37
29
  private
38
30
 
31
+ # ---------- formatting ----------
32
+
39
33
  def format_expression(expr, indent_context: 0, nested: false)
40
34
  case expr
41
35
  when Kumi::Syntax::InputReference
42
36
  "input.#{expr.name}"
37
+ when Kumi::Syntax::InputElementReference
38
+ "input.#{expr.path.join('.')}"
43
39
  when Kumi::Syntax::DeclarationReference
44
40
  expr.name.to_s
45
41
  when Kumi::Syntax::Literal
46
42
  format_value(expr.value)
47
- when Kumi::Syntax::CallExpression
48
- format_call_expression(expr, indent_context: indent_context, nested: nested)
49
43
  when Kumi::Syntax::ArrayExpression
50
- "[#{expr.elements.map { |e| format_expression(e, indent_context: indent_context, nested: nested) }.join(', ')}]"
44
+ "[" + expr.elements.map { |e| format_expression(e, indent_context:, nested:) }.join(", ") + "]"
51
45
  when Kumi::Syntax::CascadeExpression
52
- format_cascade_expression(expr, indent_context: indent_context)
46
+ format_cascade(expr, indent_context:)
47
+ when Kumi::Syntax::CallExpression
48
+ format_call(expr, indent_context:, nested:)
53
49
  else
54
50
  expr.class.name.split("::").last
55
51
  end
56
52
  end
57
53
 
58
- def format_call_expression(expr, indent_context: 0, nested: false)
59
- if pretty_printable?(expr.fn_name)
60
- format_pretty_function(expr, expr.fn_name, indent_context, nested: nested)
54
+ def format_call(expr, indent_context:, nested:)
55
+ fn = expr.fn_name
56
+ if pretty_print?(fn)
57
+ format_pretty(expr, fn, indent_context:, nested:)
61
58
  else
62
- format_generic_function(expr, indent_context)
59
+ format_generic(expr, indent_context:)
63
60
  end
64
61
  end
65
62
 
66
- def format_pretty_function(expr, fn_name, _indent_context, nested: false)
67
- if needs_evaluation?(expr.args) && !nested
68
- # For top-level expressions, show the flattened symbolic form and evaluation
69
- if chain_of_same_operator?(expr, fn_name)
70
- # For chains like a + b + c, flatten to show all operands
71
- all_operands = flatten_operator_chain(expr, fn_name)
72
- symbolic_operands = all_operands.map { |op| format_expression(op, indent_context: 0, nested: true) }
73
- symbolic_format = symbolic_operands.join(" #{get_operator_symbol(fn_name)} ")
74
-
75
- evaluated_operands = all_operands.map do |op|
76
- if op.is_a?(Kumi::Syntax::Literal)
77
- format_expression(op, indent_context: 0, nested: true)
78
- else
79
- arg_value = format_value(evaluate_expression(op))
80
- if op.is_a?(Kumi::Syntax::DeclarationReference) && all_operands.length > 1
81
- "(#{format_expression(op, indent_context: 0, nested: true)} = #{arg_value})"
82
- else
83
- arg_value
84
- end
85
- end
86
- end
87
- evaluated_format = evaluated_operands.join(" #{get_operator_symbol(fn_name)} ")
63
+ def pretty_print?(fn)
64
+ %i[add subtract multiply divide == != > < >= <= and or not].include?(fn)
65
+ end
88
66
 
67
+ def format_pretty(expr, fn, indent_context:, nested:)
68
+ if needs_eval?(expr.args) && !nested
69
+ if chain_of_same_op?(expr, fn)
70
+ ops = flatten_chain(expr, fn)
71
+ sym = op_symbol(fn)
72
+ sym_args = ops.map { |a| format_expression(a, indent_context:, nested: true) }
73
+ eval_args = ops.map { |a| eval_arg_for_display(a) }
74
+ "#{sym_args.join(" #{sym} ")} = #{eval_args.join(" #{sym} ")}"
89
75
  else
90
- # Regular pretty formatting for non-chain expressions
91
- symbolic_args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
92
- symbolic_format = display_format(fn_name, symbolic_args)
93
-
94
- evaluated_args = expr.args.map do |arg|
95
- if arg.is_a?(Kumi::Syntax::Literal)
96
- format_expression(arg, indent_context: 0, nested: true)
97
- else
98
- arg_value = format_value(evaluate_expression(arg))
99
- if arg.is_a?(Kumi::Syntax::DeclarationReference) &&
100
- expr.args.count { |a| !a.is_a?(Kumi::Syntax::Literal) } > 1
101
- "(#{format_expression(arg, indent_context: 0, nested: true)} = #{arg_value})"
102
- else
103
- arg_value
104
- end
105
- end
106
- end
107
- evaluated_format = display_format(fn_name, evaluated_args)
108
-
76
+ sym_args = expr.args.map { |a| format_expression(a, indent_context:, nested: true) }
77
+ eval_args = expr.args.map { |a| eval_arg_for_display(a) }
78
+ display_fmt(fn, sym_args) + " = " + display_fmt(fn, eval_args)
109
79
  end
110
- "#{symbolic_format} = #{evaluated_format}"
111
80
  else
112
- # For nested expressions, just show the symbolic form without evaluation details
113
- args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
114
- display_format(fn_name, args)
81
+ display_fmt(fn, expr.args.map { |a| format_expression(a, indent_context:, nested: true) })
115
82
  end
116
83
  end
117
84
 
118
- def chain_of_same_operator?(expr, fn_name)
119
- return false unless %i[add subtract multiply divide].include?(fn_name)
120
-
121
- # Check if any argument is the same operator
122
- expr.args.any? do |arg|
123
- arg.is_a?(Kumi::Syntax::CallExpression) && arg.fn_name == fn_name
85
+ def format_generic(expr, indent_context:)
86
+ parts = expr.args.map do |a|
87
+ desc = format_expression(a, indent_context:)
88
+ if literalish?(a)
89
+ desc
90
+ else
91
+ val = evaluate(a)
92
+ "#{desc} = #{format_value(val)}"
93
+ end
94
+ end
95
+ if parts.length > 1
96
+ indent = " " * (indent_context + expr.fn_name.to_s.length + 1)
97
+ "#{expr.fn_name}(#{parts.join(",\n#{indent}")})"
98
+ else
99
+ "#{expr.fn_name}(#{parts.join(', ')})"
124
100
  end
125
101
  end
126
102
 
127
- def flatten_operator_chain(expr, operator)
128
- operands = []
129
-
130
- expr.args.each do |arg|
131
- if arg.is_a?(Kumi::Syntax::CallExpression) && arg.fn_name == operator
132
- # Recursively flatten nested operations of the same type
133
- operands.concat(flatten_operator_chain(arg, operator))
134
- else
135
- operands << arg
136
- end
103
+ def format_cascade(expr, indent_context:)
104
+ lines = []
105
+ expr.cases.each do |c|
106
+ cond_val = evaluate(c.condition)
107
+ cond_desc = format_expression(c.condition, indent_context:)
108
+ res_desc = format_expression(c.result, indent_context:)
109
+ lines << " #{cond_val ? '✓' : '✗'} on #{cond_desc}, #{res_desc}"
110
+ break if cond_val
137
111
  end
112
+ "\n" + lines.join("\n")
113
+ end
138
114
 
139
- operands
115
+ def literalish?(expr)
116
+ expr.is_a?(Kumi::Syntax::Literal) ||
117
+ (expr.is_a?(Kumi::Syntax::ArrayExpression) && expr.elements.all?(Kumi::Syntax::Literal))
140
118
  end
141
119
 
142
- def get_operator_symbol(fn_name)
143
- case fn_name
144
- when :add then "+"
145
- when :subtract then "-"
146
- when :multiply then "×"
147
- when :divide then "÷"
148
- else fn_name.to_s
149
- end
120
+ def needs_eval?(args)
121
+ args.any? { |a| !literalish?(a) }
122
+ end
123
+
124
+ def chain_of_same_op?(expr, fn) = expr.args.any? { |a| a.is_a?(Kumi::Syntax::CallExpression) && a.fn_name == fn }
125
+ def flatten_chain(expr, fn)
126
+ expr.args.flat_map { |a|
127
+ a.is_a?(Kumi::Syntax::CallExpression) && a.fn_name == fn ? flatten_chain(a, fn) : [a]
128
+ }
150
129
  end
151
130
 
152
- def pretty_printable?(fn_name)
153
- %i[add subtract multiply divide == != > < >= <= and or not].include?(fn_name)
131
+ def op_symbol(fn)
132
+ { add: "+", subtract: "-", multiply: "×", divide: "÷" }[fn] || fn.to_s
154
133
  end
155
134
 
156
- def display_format(fn_name, args)
157
- case fn_name
158
- when :add then args.join(" + ")
135
+ def display_fmt(fn, args)
136
+ case fn
137
+ when :add then args.join(" + ")
159
138
  when :subtract then args.join(" - ")
160
139
  when :multiply then args.join(" × ")
161
- when :divide then args.join(" ÷ ")
162
- when :== then "#{args[0]} == #{args[1]}"
163
- when :!= then "#{args[0]} != #{args[1]}"
164
- when :> then "#{args[0]} > #{args[1]}"
165
- when :< then "#{args[0]} < #{args[1]}"
166
- when :>= then "#{args[0]} >= #{args[1]}"
167
- when :<= then "#{args[0]} <= #{args[1]}"
168
- when :and then args.join(" && ")
169
- when :or then args.join(" || ")
170
- when :not then "!#{args[0]}"
171
- else "#{fn_name}(#{args.join(', ')})"
140
+ when :divide then args.join(" ÷ ")
141
+ when :== then "#{args[0]} == #{args[1]}"
142
+ when :!= then "#{args[0]} != #{args[1]}"
143
+ when :> then "#{args[0]} > #{args[1]}"
144
+ when :< then "#{args[0]} < #{args[1]}"
145
+ when :>= then "#{args[0]} >= #{args[1]}"
146
+ when :<= then "#{args[0]} <= #{args[1]}"
147
+ when :and then args.join(" && ")
148
+ when :or then args.join(" || ")
149
+ when :not then "!#{args[0]}"
150
+ else "#{fn}(#{args.join(', ')})"
172
151
  end
173
152
  end
174
153
 
175
- def format_generic_function(expr, indent_context)
176
- args = expr.args.map do |arg|
177
- arg_desc = format_expression(arg, indent_context: indent_context)
178
-
179
- # For literals and literal lists, just show the value, no need for "100 = 100"
180
- if arg.is_a?(Kumi::Syntax::Literal) ||
181
- (arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::Literal))
182
- arg_desc
183
- else
184
- arg_value = evaluate_expression(arg)
185
- "#{arg_desc} = #{format_value(arg_value)}"
186
- end
187
- end
188
-
189
- if args.length > 1
190
- # Align with opening parenthesis, accounting for the full context
191
- function_indent = indent_context + expr.fn_name.to_s.length + 1
192
- indent = " " * function_indent
193
- "#{expr.fn_name}(#{args.join(",\n#{indent}")})"
154
+ def eval_arg_for_display(arg)
155
+ return format_expression(arg, indent_context: 0, nested: true) if literalish?(arg)
156
+ val = evaluate(arg)
157
+ if arg.is_a?(Kumi::Syntax::DeclarationReference)
158
+ "(#{format_expression(arg, indent_context: 0, nested: true)} = #{format_value(val)})"
194
159
  else
195
- "#{expr.fn_name}(#{args.join(', ')})"
160
+ format_value(val)
196
161
  end
197
162
  end
198
163
 
199
- def needs_evaluation?(args)
200
- args.any? do |arg|
201
- !arg.is_a?(Kumi::Syntax::Literal) &&
202
- !(arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::Literal))
164
+ def format_value(v)
165
+ case v
166
+ when Float, Integer then format_number(v)
167
+ when String then "\"#{v}\""
168
+ when Array then v.length <= 4 ? "[#{v.map { |x| format_value(x) }.join(', ')}]" :
169
+ "[#{v.take(4).map { |x| format_value(x) }.join(', ')}, …]"
170
+ else v.to_s
203
171
  end
204
172
  end
205
173
 
206
- def format_cascade_expression(expr, indent_context: 0)
207
- lines = []
208
- expr.cases.each do |case_expr|
209
- condition_result = evaluate_expression(case_expr.condition)
210
- condition_desc = format_expression(case_expr.condition, indent_context: indent_context)
211
- result_desc = format_expression(case_expr.result, indent_context: indent_context)
174
+ def format_number(n)
175
+ return n.to_s unless n.is_a?(Numeric)
176
+ i = (n.is_a?(Integer) || n == n.to_i) ? n.to_i : nil
177
+ return n.to_s unless i
178
+ i.abs >= 1000 ? i.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1 ').reverse : i.to_s
179
+ end
212
180
 
213
- status = condition_result ? "✓" : "✗"
214
- lines << " #{status} on #{condition_desc}, #{result_desc}"
181
+ # ---------- evaluation (Program + Registry) ----------
215
182
 
216
- break if condition_result
183
+ def evaluate(expr)
184
+ case expr
185
+ when Kumi::Syntax::DeclarationReference
186
+ @session.get(expr.name)
187
+ when Kumi::Syntax::InputReference
188
+ fetch_indifferent(@inputs, expr.name)
189
+ when Kumi::Syntax::InputElementReference
190
+ dig_path(@inputs, expr.path)
191
+ when Kumi::Syntax::Literal
192
+ expr.value
193
+ when Kumi::Syntax::ArrayExpression
194
+ expr.elements.map { |e| evaluate(e) }
195
+ when Kumi::Syntax::CascadeExpression
196
+ evaluate_cascade(expr)
197
+ when Kumi::Syntax::CallExpression
198
+ eval_call(expr)
199
+ else
200
+ raise "Unsupported expression: #{expr.class}"
217
201
  end
202
+ end
218
203
 
219
- "\n#{lines.join("\n")}"
204
+ def eval_call(expr)
205
+ entry = @registry.entry(expr.fn_name) or raise "Unknown function: #{expr.fn_name}"
206
+ fn = entry.fn
207
+ args = expr.args.map { |a| evaluate(a) }
208
+ fn.call(*args)
220
209
  end
221
210
 
222
- def format_value(value)
223
- case value
224
- when Float, Integer
225
- format_number(value)
226
- when String
227
- "\"#{value}\""
228
- when Array
229
- if value.length <= 4
230
- "[#{value.map { |v| format_value(v) }.join(', ')}]"
231
- else
232
- "[#{value.take(4).map { |v| format_value(v) }.join(', ')}, …]"
233
- end
234
- else
235
- value.to_s
211
+ def evaluate_cascade(expr)
212
+ expr.cases.each do |c|
213
+ return evaluate(c.result) if evaluate(c.condition)
236
214
  end
215
+ nil
237
216
  end
238
217
 
239
- def format_number(num)
240
- return num.to_s unless num.is_a?(Numeric)
218
+ def fetch_indifferent(h, k)
219
+ h[k] || h[k.to_s] || h[k.to_sym]
220
+ end
241
221
 
242
- if num.is_a?(Integer) || (num.is_a?(Float) && num == num.to_i)
243
- int_val = num.to_i
244
- if int_val.abs >= 1000
245
- int_val.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1 ').reverse
222
+ def dig_path(h, path)
223
+ node = h
224
+ path.each do |seg|
225
+ if node.is_a?(Hash)
226
+ node = fetch_indifferent(node, seg)
246
227
  else
247
- int_val.to_s
228
+ # if arrays are in path, interpret seg as index when Integer-like
229
+ node = seg.is_a?(Integer) ? node[seg] : nil
248
230
  end
249
- else
250
- num.to_s
251
- end
252
- end
253
-
254
- def evaluate_expression(expr)
255
- case expr
256
- when Kumi::Syntax::DeclarationReference
257
- @compiled_schema.evaluate_binding(expr.name, @inputs)
258
- when Kumi::Syntax::InputReference
259
- @inputs[expr.name]
260
- when Kumi::Syntax::Literal
261
- expr.value
262
- else
263
- # For complex expressions, compile and evaluate using existing compiler
264
- compiled_fn = @compiler.send(:compile_expr, expr)
265
- compiled_fn.call(@inputs)
266
231
  end
232
+ node
267
233
  end
268
234
  end
269
235
 
270
236
  module_function
271
237
 
272
238
  def call(schema_class, target_name, inputs:)
273
- syntax_tree = schema_class.instance_variable_get(:@__syntax_tree__)
274
- analyzer_result = schema_class.instance_variable_get(:@__analyzer_result__)
275
-
276
- raise ArgumentError, "Schema not found or not compiled" unless syntax_tree && analyzer_result
277
-
278
- metadata = analyzer_result.state
279
-
280
- # Create a minimal analyzer result structure for compatibility
281
- analyzer_result = OpenStruct.new(
282
- definitions: metadata[:declarations] || {},
283
- dependency_graph: metadata[:dependencies] || {},
284
- leaf_map: metadata[:leaves] || {},
285
- topo_order: metadata[:evaluation_order] || [],
286
- decl_types: metadata[:inferred_types] || {},
287
- state: metadata
288
- )
239
+ syntax_tree = schema_class.instance_variable_get(:@__syntax_tree__)
240
+ analysis_state = schema_class.instance_variable_get(:@__analyzer_result__)&.state
241
+ raise ArgumentError, "Schema not found or not compiled" unless syntax_tree && analysis_state
289
242
 
290
- generator = ExplanationGenerator.new(syntax_tree, analyzer_result, inputs)
291
- generator.explain(target_name)
243
+ ExplanationGenerator.new(syntax_tree, analysis_state, inputs).explain(target_name)
292
244
  end
293
245
  end
294
246
  end
@@ -6,10 +6,10 @@ module Kumi
6
6
  module NodeBuilders
7
7
  def build_root(data, node_class)
8
8
  inputs = data[:inputs].map { |input_data| build_node(input_data) }
9
- attributes = data[:attributes].map { |attr_data| build_node(attr_data) }
9
+ values = data[:values].map { |attr_data| build_node(attr_data) }
10
10
  traits = data[:traits].map { |trait_data| build_node(trait_data) }
11
11
 
12
- node_class.new(inputs, attributes, traits)
12
+ node_class.new(inputs, values, traits)
13
13
  end
14
14
 
15
15
  def build_field_declaration(data, node_class)
@@ -9,7 +9,7 @@ module Kumi
9
9
  {
10
10
  type: "root",
11
11
  inputs: node.inputs.map { |input| serialize_node(input) },
12
- attributes: node.attributes.map { |attr| serialize_node(attr) },
12
+ values: node.values.map { |attr| serialize_node(attr) },
13
13
  traits: node.traits.map { |trait| serialize_node(trait) }
14
14
  }
15
15
  end
@@ -8,9 +8,18 @@ module Kumi
8
8
  def self.definitions
9
9
  {
10
10
  # Collection queries (these are reducers - they reduce arrays to scalars)
11
- empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty?, reducer: true),
12
- size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer, reducer: false,
13
- structure_function: true),
11
+ empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty?, reducer: true,
12
+ structure_function: true),
13
+ size: FunctionBuilder::Entry.new(
14
+ fn: ->(collection) { collection.size },
15
+ arity: 1,
16
+ param_types: [:any],
17
+ return_type: :integer,
18
+ description: "Get size of collection",
19
+ param_modes: { fixed: [:elem] }, # take a vector argument elementwise
20
+ reducer: true,
21
+ structure_function: true
22
+ ),
14
23
 
15
24
  # Element access
16
25
  first: FunctionBuilder::Entry.new(
@@ -56,7 +65,8 @@ module Kumi
56
65
  param_types: [Kumi::Core::Types.array(:float)],
57
66
  return_type: :float,
58
67
  description: "Find maximum value in numeric collection",
59
- reducer: true
68
+ reducer: true,
69
+ param_modes: { fixed: [:elem] } # first param is the vector being reduced
60
70
  ),
61
71
 
62
72
  # Collection operations
@@ -94,12 +104,13 @@ module Kumi
94
104
 
95
105
  # Array transformation functions
96
106
  flatten: FunctionBuilder::Entry.new(
97
- fn: lambda(&:flatten),
107
+ fn: ->(array) { array.flatten },
98
108
  arity: 1,
99
109
  param_types: [Kumi::Core::Types.array(:any)],
100
110
  return_type: Kumi::Core::Types.array(:any),
101
111
  description: "Flatten nested arrays into a single array",
102
- structure_function: true
112
+ structure_function: true,
113
+ reducer: true
103
114
  ),
104
115
 
105
116
  flatten_one: FunctionBuilder::Entry.new(
@@ -111,13 +122,13 @@ module Kumi
111
122
  structure_function: true
112
123
  ),
113
124
 
114
- flatten_deep: FunctionBuilder::Entry.new(
115
- fn: lambda(&:flatten),
125
+ to_array: FunctionBuilder::Entry.new(
126
+ fn: ->(vals) { vals },
116
127
  arity: 1,
117
128
  param_types: [Kumi::Core::Types.array(:any)],
118
129
  return_type: Kumi::Core::Types.array(:any),
119
- description: "Recursively flatten all nested arrays (alias for flatten)",
120
- structure_function: true
130
+ description: "Collect vector rows into a Ruby array",
131
+ reducer: true
121
132
  ),
122
133
 
123
134
  # Mathematical transformation functions
@@ -3,31 +3,41 @@
3
3
  module Kumi
4
4
  module Core
5
5
  module FunctionRegistry
6
- # Conditional and control flow functions
7
6
  module ConditionalFunctions
8
7
  def self.definitions
9
8
  {
9
+ # a ? b : c
10
10
  conditional: FunctionBuilder::Entry.new(
11
11
  fn: ->(condition, true_value, false_value) { condition ? true_value : false_value },
12
12
  arity: 3,
13
13
  param_types: %i[boolean any any],
14
14
  return_type: :any,
15
+ # all three are element-wise (scalars auto-broadcast)
16
+ param_modes: { fixed: %i[elem elem elem] },
15
17
  description: "Ternary conditional operator"
16
18
  ),
17
19
 
20
+ # if(cond, then, else=nil)
18
21
  if: FunctionBuilder::Entry.new(
19
22
  fn: ->(condition, true_value, false_value = nil) { condition ? true_value : false_value },
20
- arity: -1, # Variable arity (2 or 3)
23
+ # keep arity=3; the last arg is optional at call time
24
+ arity: 3,
21
25
  param_types: %i[boolean any any],
22
26
  return_type: :any,
23
- description: "If-then-else conditional"
27
+ param_modes: { fixed: %i[elem elem elem] },
28
+ description: "If-then-else conditional",
29
+ reducer: false,
30
+ structure_function: false
24
31
  ),
25
32
 
33
+ # coalesce(a, b, c, ...)
26
34
  coalesce: FunctionBuilder::Entry.new(
27
35
  fn: ->(*values) { values.find { |v| !v.nil? } },
28
- arity: -1, # Variable arity
36
+ arity: -1, # variadic
29
37
  param_types: [:any],
30
38
  return_type: :any,
39
+ # every variadic arg participates element-wise
40
+ param_modes: { fixed: [], variadic: :elem },
31
41
  description: "Return first non-nil value"
32
42
  )
33
43
  }