kumi 0.0.10 → 0.0.11
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +7 -231
- data/README.md +1 -1
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/features/hierarchical-broadcasting.md +1 -1
- data/docs/features/s-expression-printer.md +2 -2
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/lib/kumi/analyzer.rb +34 -12
- data/lib/kumi/compiler.rb +2 -12
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -101
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +2 -2
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -55
- data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
- data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
- data/lib/kumi/core/function_registry.rb +126 -108
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
- data/lib/kumi/core/ruby_parser/input_builder.rb +5 -5
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/registry.rb +14 -79
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +14 -3
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +1 -1
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +0 -1
- data/lib/kumi/syntax/root.rb +2 -2
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- metadata +26 -15
- data/lib/kumi/core/cascade_executor_builder.rb +0 -132
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
- data/lib/kumi/core/compiler/function_invoker.rb +0 -55
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
- data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/nested_structure_utils.rb +0 -78
- data/lib/kumi/core/schema_instance.rb +0 -115
- data/lib/kumi/core/vectorized_function_builder.rb +0 -88
- data/lib/kumi/js/compiler.rb +0 -878
- data/lib/kumi/js/function_registry.rb +0 -333
- data/migrate_to_core_iterative.rb +0 -938
data/lib/kumi/core/explain.rb
CHANGED
@@ -4,291 +4,243 @@ module Kumi
|
|
4
4
|
module Core
|
5
5
|
module Explain
|
6
6
|
class ExplanationGenerator
|
7
|
-
def initialize(syntax_tree,
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
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
|
-
|
14
|
-
|
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
|
-
|
26
|
-
|
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
|
-
|
24
|
+
expr_str = format_expression(expr, indent_context: prefix.length)
|
33
25
|
|
34
|
-
"#{prefix}#{
|
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
|
-
"[
|
44
|
+
"[" + expr.elements.map { |e| format_expression(e, indent_context:, nested:) }.join(", ") + "]"
|
51
45
|
when Kumi::Syntax::CascadeExpression
|
52
|
-
|
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
|
59
|
-
|
60
|
-
|
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
|
-
|
59
|
+
format_generic(expr, indent_context:)
|
63
60
|
end
|
64
61
|
end
|
65
62
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
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
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
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
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
153
|
-
|
131
|
+
def op_symbol(fn)
|
132
|
+
{ add: "+", subtract: "-", multiply: "×", divide: "÷" }[fn] || fn.to_s
|
154
133
|
end
|
155
134
|
|
156
|
-
def
|
157
|
-
case
|
158
|
-
when :add
|
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
|
162
|
-
when :==
|
163
|
-
when :!=
|
164
|
-
when :>
|
165
|
-
when :<
|
166
|
-
when :>=
|
167
|
-
when :<=
|
168
|
-
when :and
|
169
|
-
when :or
|
170
|
-
when :not
|
171
|
-
else
|
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
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
#
|
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
|
-
|
160
|
+
format_value(val)
|
196
161
|
end
|
197
162
|
end
|
198
163
|
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
214
|
-
lines << " #{status} on #{condition_desc}, #{result_desc}"
|
181
|
+
# ---------- evaluation (Program + Registry) ----------
|
215
182
|
|
216
|
-
|
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
|
-
|
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
|
223
|
-
|
224
|
-
|
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
|
240
|
-
|
218
|
+
def fetch_indifferent(h, k)
|
219
|
+
h[k] || h[k.to_s] || h[k.to_sym]
|
220
|
+
end
|
241
221
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
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
|
274
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
13
|
-
|
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:
|
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
|
-
|
115
|
-
fn:
|
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: "
|
120
|
-
|
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
|
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
|
-
|
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, #
|
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
|
}
|