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.
- checksums.yaml +4 -4
- data/CLAUDE.md +76 -174
- data/README.md +205 -52
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/SYNTAX.md +95 -8
- data/docs/features/README.md +45 -0
- data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
- data/docs/features/analysis-type-inference.md +42 -0
- data/docs/features/analysis-unsat-detection.md +71 -0
- data/docs/features/array-broadcasting.md +170 -0
- data/docs/features/input-declaration-system.md +42 -0
- data/docs/features/performance.md +16 -0
- data/docs/schema_metadata/broadcasts.md +53 -0
- data/docs/schema_metadata/cascades.md +45 -0
- data/docs/schema_metadata/declarations.md +54 -0
- data/docs/schema_metadata/dependencies.md +57 -0
- data/docs/schema_metadata/evaluation_order.md +29 -0
- data/docs/schema_metadata/examples.md +95 -0
- data/docs/schema_metadata/inferred_types.md +46 -0
- data/docs/schema_metadata/inputs.md +86 -0
- data/docs/schema_metadata.md +108 -0
- data/examples/federal_tax_calculator_2024.rb +11 -6
- data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +246 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +78 -38
- data/lib/kumi/analyzer/passes/input_collector.rb +91 -30
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +24 -25
- data/lib/kumi/analyzer/passes/toposorter.rb +44 -8
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -14
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/analyzer/passes/type_inferencer.rb +130 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +134 -56
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
- data/lib/kumi/analyzer.rb +16 -17
- data/lib/kumi/compiler.rb +188 -16
- data/lib/kumi/constraint_relationship_solver.rb +6 -6
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporting.rb +1 -1
- data/lib/kumi/explain.rb +32 -20
- data/lib/kumi/export/node_registry.rb +26 -12
- data/lib/kumi/export/node_serializers.rb +1 -1
- data/lib/kumi/function_registry/collection_functions.rb +14 -9
- data/lib/kumi/function_registry/function_builder.rb +4 -3
- data/lib/kumi/function_registry.rb +8 -2
- data/lib/kumi/input/type_matcher.rb +3 -0
- data/lib/kumi/input/validator.rb +0 -3
- data/lib/kumi/json_schema/generator.rb +63 -0
- data/lib/kumi/json_schema/validator.rb +25 -0
- data/lib/kumi/json_schema.rb +14 -0
- data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
- data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +5 -5
- data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +20 -20
- data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_builder.rb +41 -10
- data/lib/kumi/ruby_parser/input_field_proxy.rb +46 -0
- data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +4 -4
- data/lib/kumi/ruby_parser/nested_input.rb +15 -0
- data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/sugar.rb +62 -10
- data/lib/kumi/ruby_parser.rb +10 -0
- data/lib/kumi/schema.rb +10 -4
- data/lib/kumi/schema_instance.rb +6 -6
- data/lib/kumi/schema_metadata.rb +524 -0
- data/lib/kumi/syntax/array_expression.rb +15 -0
- data/lib/kumi/syntax/call_expression.rb +11 -0
- data/lib/kumi/syntax/cascade_expression.rb +11 -0
- data/lib/kumi/syntax/case_expression.rb +11 -0
- data/lib/kumi/syntax/declaration_reference.rb +11 -0
- data/lib/kumi/syntax/hash_expression.rb +11 -0
- data/lib/kumi/syntax/input_declaration.rb +12 -0
- data/lib/kumi/syntax/input_element_reference.rb +12 -0
- data/lib/kumi/syntax/input_reference.rb +12 -0
- data/lib/kumi/syntax/literal.rb +11 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +14 -0
- metadata +55 -25
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -26
- data/lib/kumi/syntax/expressions.rb +0 -34
- data/lib/kumi/syntax/terminal_expressions.rb +0 -30
- data/lib/kumi/syntax.rb +0 -9
- /data/{documents → docs}/DSL.md +0 -0
- /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
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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::
|
47
|
-
Syntax::
|
48
|
-
Syntax::
|
49
|
-
Syntax::
|
50
|
-
Syntax::
|
51
|
-
Syntax::
|
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
|
-
|
84
|
-
|
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::
|
101
|
+
when Kumi::Syntax::CallExpression
|
102
102
|
extract_call_relationship(target, expression)
|
103
|
-
when Kumi::Syntax::
|
103
|
+
when Kumi::Syntax::DeclarationReference
|
104
104
|
# Simple alias: target = other_variable
|
105
105
|
Relationship.new(target, :identity, [expression.name])
|
106
|
-
when Kumi::Syntax::
|
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::
|
155
|
+
when Kumi::Syntax::DeclarationReference
|
156
156
|
arg.name
|
157
|
-
when Kumi::Syntax::
|
157
|
+
when Kumi::Syntax::Literal
|
158
158
|
arg.value
|
159
|
-
when Kumi::Syntax::
|
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
|
data/lib/kumi/error_reporting.rb
CHANGED
@@ -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::
|
40
|
+
when Kumi::Syntax::InputReference
|
41
41
|
"input.#{expr.name}"
|
42
|
-
when Syntax::
|
42
|
+
when Kumi::Syntax::DeclarationReference
|
43
43
|
expr.name.to_s
|
44
|
-
when Syntax::
|
44
|
+
when Kumi::Syntax::Literal
|
45
45
|
format_value(expr.value)
|
46
|
-
when Syntax::
|
46
|
+
when Kumi::Syntax::CallExpression
|
47
47
|
format_call_expression(expr, indent_context: indent_context, nested: nested)
|
48
|
-
when Syntax::
|
48
|
+
when Kumi::Syntax::ArrayExpression
|
49
49
|
"[#{expr.elements.map { |e| format_expression(e, indent_context: indent_context, nested: nested) }.join(', ')}]"
|
50
|
-
when Syntax::
|
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::
|
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::
|
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::
|
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::
|
99
|
-
expr.args.count { |a| !a.is_a?(Syntax::
|
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::
|
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::
|
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::
|
180
|
-
(arg.is_a?(Syntax::
|
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::
|
201
|
-
!(arg.is_a?(Syntax::
|
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::
|
255
|
+
when Kumi::Syntax::DeclarationReference
|
256
256
|
@compiled_schema.evaluate_binding(expr.name, @inputs)
|
257
|
-
when Syntax::
|
257
|
+
when Kumi::Syntax::InputReference
|
258
258
|
@inputs[expr.name]
|
259
|
-
when Syntax::
|
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::
|
10
|
-
"Kumi::Syntax::
|
11
|
-
"Kumi::Syntax::
|
12
|
-
"Kumi::Syntax::
|
13
|
-
"Kumi::Syntax::
|
14
|
-
"Kumi::Syntax::
|
15
|
-
"Kumi::Syntax::
|
16
|
-
"Kumi::Syntax::
|
17
|
-
"Kumi::Syntax::
|
18
|
-
"Kumi::Syntax::
|
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 =
|
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
|
@@ -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
|