kumi 0.0.9 → 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 +18 -258
- data/README.md +188 -121
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/features/s-expression-printer.md +2 -2
- data/docs/schema_metadata.md +7 -7
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +34 -14
- data/lib/kumi/compiler.rb +4 -283
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
- 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 -99
- 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 +28 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
- 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 +92 -48
- 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 +137 -0
- 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 +100 -6
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -53
- data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +138 -98
- 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 +37 -16
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- 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/errors.rb +2 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +17 -22
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +15 -4
- 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 +17 -16
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +33 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +36 -9
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/schema_instance.rb +0 -111
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
- 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 = 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,11 +104,31 @@ 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
|
-
description: "Flatten nested arrays into a single array"
|
111
|
+
description: "Flatten nested arrays into a single array",
|
112
|
+
structure_function: true,
|
113
|
+
reducer: true
|
114
|
+
),
|
115
|
+
|
116
|
+
flatten_one: FunctionBuilder::Entry.new(
|
117
|
+
fn: ->(array) { array.flatten(1) },
|
118
|
+
arity: 1,
|
119
|
+
param_types: [Kumi::Core::Types.array(:any)],
|
120
|
+
return_type: Kumi::Core::Types.array(:any),
|
121
|
+
description: "Flatten nested arrays by one level only",
|
122
|
+
structure_function: true
|
123
|
+
),
|
124
|
+
|
125
|
+
to_array: FunctionBuilder::Entry.new(
|
126
|
+
fn: ->(vals) { vals },
|
127
|
+
arity: 1,
|
128
|
+
param_types: [Kumi::Core::Types.array(:any)],
|
129
|
+
return_type: Kumi::Core::Types.array(:any),
|
130
|
+
description: "Collect vector rows into a Ruby array",
|
131
|
+
reducer: true
|
102
132
|
),
|
103
133
|
|
104
134
|
# Mathematical transformation functions
|
@@ -193,6 +223,70 @@ module Kumi
|
|
193
223
|
param_types: [Kumi::Core::Types.array(:any)],
|
194
224
|
return_type: Kumi::Core::Types.array(:integer),
|
195
225
|
description: "Generate array of indices for the collection"
|
226
|
+
),
|
227
|
+
|
228
|
+
# Conditional aggregation functions
|
229
|
+
count_if: FunctionBuilder::Entry.new(
|
230
|
+
fn: ->(condition_array) { condition_array.count(true) },
|
231
|
+
arity: 1,
|
232
|
+
param_types: [Kumi::Core::Types.array(:boolean)],
|
233
|
+
return_type: :integer,
|
234
|
+
description: "Count number of true values in boolean array",
|
235
|
+
reducer: true
|
236
|
+
),
|
237
|
+
|
238
|
+
sum_if: FunctionBuilder::Entry.new(
|
239
|
+
fn: lambda { |value_array, condition_array|
|
240
|
+
value_array.zip(condition_array).sum { |value, condition| condition ? value : 0 }
|
241
|
+
},
|
242
|
+
arity: 2,
|
243
|
+
param_types: [Kumi::Core::Types.array(:float), Kumi::Core::Types.array(:boolean)],
|
244
|
+
return_type: :float,
|
245
|
+
description: "Sum values where corresponding condition is true",
|
246
|
+
reducer: true
|
247
|
+
),
|
248
|
+
|
249
|
+
avg_if: FunctionBuilder::Entry.new(
|
250
|
+
fn: lambda { |value_array, condition_array|
|
251
|
+
pairs = value_array.zip(condition_array)
|
252
|
+
true_values = pairs.filter_map { |value, condition| value if condition }
|
253
|
+
return 0.0 if true_values.empty?
|
254
|
+
|
255
|
+
true_values.sum.to_f / true_values.size
|
256
|
+
},
|
257
|
+
arity: 2,
|
258
|
+
param_types: [Kumi::Core::Types.array(:float), Kumi::Core::Types.array(:boolean)],
|
259
|
+
return_type: :float,
|
260
|
+
description: "Average values where corresponding condition is true",
|
261
|
+
reducer: true
|
262
|
+
),
|
263
|
+
|
264
|
+
# Flattening utilities for hierarchical data
|
265
|
+
any_across: FunctionBuilder::Entry.new(
|
266
|
+
fn: ->(nested_array) { nested_array.flatten.any? },
|
267
|
+
arity: 1,
|
268
|
+
param_types: [Kumi::Core::Types.array(:any)],
|
269
|
+
return_type: :boolean,
|
270
|
+
description: "Check if any element is truthy across all nested levels",
|
271
|
+
reducer: true
|
272
|
+
),
|
273
|
+
|
274
|
+
all_across: FunctionBuilder::Entry.new(
|
275
|
+
fn: ->(nested_array) { nested_array.flatten.all? },
|
276
|
+
arity: 1,
|
277
|
+
param_types: [Kumi::Core::Types.array(:any)],
|
278
|
+
return_type: :boolean,
|
279
|
+
description: "Check if all elements are truthy across all nested levels",
|
280
|
+
reducer: true
|
281
|
+
),
|
282
|
+
|
283
|
+
count_across: FunctionBuilder::Entry.new(
|
284
|
+
fn: ->(nested_array) { nested_array.flatten.size },
|
285
|
+
arity: 1,
|
286
|
+
param_types: [Kumi::Core::Types.array(:any)],
|
287
|
+
return_type: :integer,
|
288
|
+
description: "Count total elements across all nested levels",
|
289
|
+
reducer: true
|
196
290
|
)
|
197
291
|
}
|
198
292
|
end
|
@@ -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
|
}
|