kumi 0.0.9 → 0.0.10
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 +28 -44
- data/README.md +187 -120
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- 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/schema_metadata.md +7 -7
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +0 -2
- data/lib/kumi/compiler.rb +6 -275
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
- data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
- data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
- data/lib/kumi/core/cascade_executor_builder.rb +132 -0
- data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
- data/lib/kumi/core/compiler/function_invoker.rb +55 -0
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
- data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
- data/lib/kumi/core/compiler_base.rb +137 -0
- data/lib/kumi/core/explain.rb +2 -2
- data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
- data/lib/kumi/core/function_registry/function_builder.rb +5 -3
- data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +32 -10
- data/lib/kumi/core/nested_structure_utils.rb +78 -0
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- data/lib/kumi/core/schema_instance.rb +4 -0
- data/lib/kumi/core/vectorized_function_builder.rb +88 -0
- data/lib/kumi/errors.rb +2 -0
- data/lib/kumi/js/compiler.rb +878 -0
- data/lib/kumi/js/function_registry.rb +333 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +61 -1
- data/lib/kumi/schema.rb +1 -1
- data/lib/kumi/support/s_expression_printer.rb +16 -15
- 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/node.rb +34 -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 +1 -1
- 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 +20 -4
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
@@ -41,7 +41,8 @@ module Kumi
|
|
41
41
|
|
42
42
|
metadata = {
|
43
43
|
type: field_decl.type,
|
44
|
-
domain: field_decl.domain
|
44
|
+
domain: field_decl.domain,
|
45
|
+
access_mode: field_decl.access_mode
|
45
46
|
}
|
46
47
|
|
47
48
|
# Process children if present
|
@@ -83,7 +84,8 @@ module Kumi
|
|
83
84
|
# Merge metadata (later declarations override nil values)
|
84
85
|
merged = {
|
85
86
|
type: field_decl.type || existing[:type],
|
86
|
-
domain: field_decl.domain || existing[:domain]
|
87
|
+
domain: field_decl.domain || existing[:domain],
|
88
|
+
access_mode: field_decl.access_mode || existing[:access_mode]
|
87
89
|
}
|
88
90
|
|
89
91
|
# Merge children if present
|
@@ -17,9 +17,15 @@ module Kumi
|
|
17
17
|
# 4. Expression types are valid for their context
|
18
18
|
class SemanticConstraintValidator < VisitorPass
|
19
19
|
def run(errors)
|
20
|
+
# Visit value and trait declarations
|
20
21
|
each_decl do |decl|
|
21
22
|
visit(decl) { |node| validate_semantic_constraints(node, decl, errors) }
|
22
23
|
end
|
24
|
+
|
25
|
+
# Visit input declarations
|
26
|
+
schema.inputs.each do |input_decl|
|
27
|
+
visit(input_decl) { |node| validate_semantic_constraints(node, input_decl, errors) }
|
28
|
+
end
|
23
29
|
state
|
24
30
|
end
|
25
31
|
|
@@ -33,6 +39,8 @@ module Kumi
|
|
33
39
|
validate_cascade_condition(node, errors)
|
34
40
|
when Kumi::Syntax::CallExpression
|
35
41
|
validate_function_call(node, errors)
|
42
|
+
when Kumi::Syntax::InputDeclaration
|
43
|
+
validate_input_declaration(node, errors)
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
@@ -90,6 +98,25 @@ module Kumi
|
|
90
98
|
)
|
91
99
|
end
|
92
100
|
|
101
|
+
def validate_input_declaration(input_decl, errors)
|
102
|
+
return unless input_decl.type == :array && input_decl.children.any?
|
103
|
+
|
104
|
+
# Validate that access_mode is consistent with children structure
|
105
|
+
if input_decl.access_mode == :element
|
106
|
+
# Element mode arrays can only have exactly one direct child
|
107
|
+
if input_decl.children.size > 1
|
108
|
+
error_msg = "array with access_mode :element can only have one direct child element, " \
|
109
|
+
"but found #{input_decl.children.size} children"
|
110
|
+
report_error(errors, error_msg, location: input_decl.loc, type: :semantic)
|
111
|
+
end
|
112
|
+
elsif input_decl.access_mode == :object
|
113
|
+
# Object mode allows multiple children
|
114
|
+
end
|
115
|
+
|
116
|
+
# Recursively validate children
|
117
|
+
input_decl.children.each { |child| validate_input_declaration(child, errors) }
|
118
|
+
end
|
119
|
+
|
93
120
|
def boolean_trait_composition?(call_expr)
|
94
121
|
# Allow boolean composition functions that operate on trait collections
|
95
122
|
%i[all? any? none?].include?(call_expr.fn_name)
|
@@ -6,14 +6,18 @@ module Kumi
|
|
6
6
|
module Passes
|
7
7
|
# RESPONSIBILITY: Validate function call arity and argument types against FunctionRegistry
|
8
8
|
# DEPENDENCIES: :inferred_types from TypeInferencer
|
9
|
-
# PRODUCES:
|
9
|
+
# PRODUCES: :functions_required - Set of function names used in the schema
|
10
10
|
# INTERFACE: new(schema, state).run(errors)
|
11
11
|
class TypeChecker < VisitorPass
|
12
12
|
def run(errors)
|
13
|
+
functions_required = Set.new
|
14
|
+
|
13
15
|
visit_nodes_of_type(Kumi::Syntax::CallExpression, errors: errors) do |node, _decl, errs|
|
14
16
|
validate_function_call(node, errs)
|
17
|
+
functions_required.add(node.fn_name)
|
15
18
|
end
|
16
|
-
|
19
|
+
|
20
|
+
state.with(:functions_required, functions_required)
|
17
21
|
end
|
18
22
|
|
19
23
|
private
|
@@ -40,12 +40,31 @@ module Kumi
|
|
40
40
|
atoms = gather_atoms(decl.expression, definitions, Set.new)
|
41
41
|
next if atoms.empty?
|
42
42
|
|
43
|
+
# DEBUG: Add detailed logging for hierarchical broadcasting debugging
|
44
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
45
|
+
puts "DEBUG UNSAT: Checking declaration '#{decl.name}' at #{decl.loc}"
|
46
|
+
puts " Expression: #{decl.expression.inspect}"
|
47
|
+
puts " Gathered atoms: #{atoms.map(&:inspect)}"
|
48
|
+
puts " Input meta: #{@input_meta.keys.inspect}" if @input_meta
|
49
|
+
end
|
50
|
+
|
43
51
|
# Use enhanced solver that can detect cross-variable mathematical constraints
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
52
|
+
if definitions && !definitions.empty?
|
53
|
+
result = Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
|
54
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
55
|
+
puts " Enhanced solver result: #{result}"
|
56
|
+
end
|
57
|
+
else
|
58
|
+
result = Kumi::Core::AtomUnsatSolver.unsat?(atoms)
|
59
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
60
|
+
puts " Basic solver result: #{result}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
impossible = result
|
64
|
+
|
65
|
+
if impossible && (ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257"))
|
66
|
+
puts " -> FLAGGING AS IMPOSSIBLE: #{decl.name}"
|
67
|
+
end
|
49
68
|
|
50
69
|
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
51
70
|
end
|
@@ -63,15 +82,24 @@ module Kumi
|
|
63
82
|
decl.expression.cases[0...-1].each do |when_case|
|
64
83
|
next unless when_case.condition
|
65
84
|
|
66
|
-
next unless when_case.condition.fn_name == :
|
85
|
+
next unless when_case.condition.fn_name == :cascade_and
|
67
86
|
|
68
87
|
when_case.condition.args.each do |arg|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
88
|
+
if arg.is_a?(ArrayExpression)
|
89
|
+
# Handle array elements (for array broadcasting)
|
90
|
+
arg.elements.each do |element|
|
91
|
+
next unless element.is_a?(DeclarationReference)
|
92
|
+
|
93
|
+
trait_name = element.name
|
94
|
+
trait = definitions[trait_name]
|
95
|
+
if trait
|
96
|
+
conditions << trait.expression
|
97
|
+
condition_traits << trait_name
|
98
|
+
end
|
99
|
+
end
|
100
|
+
elsif arg.is_a?(DeclarationReference)
|
101
|
+
# Handle direct trait references (simple case)
|
102
|
+
trait_name = arg.name
|
75
103
|
trait = definitions[trait_name]
|
76
104
|
if trait
|
77
105
|
conditions << trait.expression
|
@@ -183,8 +211,8 @@ module Kumi
|
|
183
211
|
# We should NOT add OR children to the stack as they would be treated as AND
|
184
212
|
# OR expressions need separate analysis in the main run() method
|
185
213
|
next
|
186
|
-
elsif current.is_a?(CallExpression) && current.fn_name == :
|
187
|
-
#
|
214
|
+
elsif current.is_a?(CallExpression) && current.fn_name == :cascade_and
|
215
|
+
# cascade_and takes individual arguments (not wrapped in array)
|
188
216
|
current.args.each { |arg| stack << arg }
|
189
217
|
elsif current.is_a?(ArrayExpression)
|
190
218
|
# For ArrayExpression, add all elements to the stack
|
@@ -212,7 +240,18 @@ module Kumi
|
|
212
240
|
# This is the correct behavior: each 'on' condition should be checked separately
|
213
241
|
# since only ONE will be evaluated at runtime (they're mutually exclusive by design)
|
214
242
|
|
215
|
-
|
243
|
+
# DEBUG: Add detailed logging for hierarchical broadcasting debugging
|
244
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
245
|
+
puts "DEBUG UNSAT CASCADE: Checking cascade '#{decl.name}' at #{decl.loc}"
|
246
|
+
puts " Total cases: #{decl.expression.cases.length}"
|
247
|
+
end
|
248
|
+
|
249
|
+
decl.expression.cases.each_with_index do |when_case, index|
|
250
|
+
# DEBUG: Log each case
|
251
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
252
|
+
puts " Case #{index}: condition=#{when_case.condition.inspect}"
|
253
|
+
end
|
254
|
+
|
216
255
|
# Skip the base case (it's typically a literal true condition)
|
217
256
|
next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
|
218
257
|
|
@@ -220,52 +259,51 @@ module Kumi
|
|
220
259
|
next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
|
221
260
|
|
222
261
|
# Skip single-trait 'on' branches: trait-level unsat detection covers these
|
223
|
-
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :
|
224
|
-
#
|
225
|
-
|
226
|
-
list = when_case.condition.args.first
|
227
|
-
next if list.elements.size == 1
|
228
|
-
elsif when_case.condition.args.size == 1
|
229
|
-
# Multiple args format
|
230
|
-
next
|
231
|
-
end
|
262
|
+
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and && (when_case.condition.args.size == 1)
|
263
|
+
# cascade_and uses individual arguments - skip if only one trait
|
264
|
+
next
|
232
265
|
end
|
266
|
+
|
233
267
|
# Gather atoms from this individual condition only
|
234
268
|
condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
|
235
|
-
# DEBUG
|
236
|
-
# if when_case.condition.is_a?(CallExpression) && [:all?, :any?, :none?].include?(when_case.condition.fn_name)
|
237
|
-
# puts " Args: #{when_case.condition.args.inspect}"
|
238
|
-
# puts " Atoms found: #{condition_atoms.inspect}"
|
239
|
-
# end
|
240
269
|
|
241
|
-
#
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
270
|
+
# DEBUG: Add detailed logging for hierarchical broadcasting debugging
|
271
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
272
|
+
puts " Condition atoms: #{condition_atoms.map(&:inspect)}"
|
273
|
+
end
|
274
|
+
|
246
275
|
# Use enhanced solver for cascade conditions too
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
276
|
+
if definitions && !definitions.empty?
|
277
|
+
result = Kumi::Core::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
|
278
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
279
|
+
puts " Enhanced solver result: #{result}"
|
280
|
+
end
|
281
|
+
else
|
282
|
+
result = Kumi::Core::AtomUnsatSolver.unsat?(condition_atoms)
|
283
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
284
|
+
puts " Basic solver result: #{result}"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
impossible = result
|
252
288
|
next unless !condition_atoms.empty? && impossible
|
253
289
|
|
254
290
|
# For multi-trait on-clauses, report the trait names rather than the value name
|
255
|
-
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :
|
256
|
-
#
|
257
|
-
trait_bindings =
|
258
|
-
when_case.condition.args.first.elements
|
259
|
-
else
|
260
|
-
when_case.condition.args
|
261
|
-
end
|
291
|
+
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and
|
292
|
+
# cascade_and uses individual arguments
|
293
|
+
trait_bindings = when_case.condition.args
|
262
294
|
|
263
295
|
if trait_bindings.all?(DeclarationReference)
|
264
296
|
traits = trait_bindings.map(&:name).join(" AND ")
|
297
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
298
|
+
puts " -> FLAGGING AS IMPOSSIBLE CASCADE CONDITION: #{traits}"
|
299
|
+
end
|
265
300
|
report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
|
266
301
|
next
|
267
302
|
end
|
268
303
|
end
|
304
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
305
|
+
puts " -> FLAGGING AS IMPOSSIBLE CASCADE: #{decl.name}"
|
306
|
+
end
|
269
307
|
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc)
|
270
308
|
end
|
271
309
|
end
|
@@ -275,6 +313,12 @@ module Kumi
|
|
275
313
|
when InputReference, DeclarationReference
|
276
314
|
val = @evaluator.evaluate(node)
|
277
315
|
val == :unknown ? node.name : val
|
316
|
+
when InputElementReference
|
317
|
+
# For hierarchical paths like input.companies.regions.offices.teams.department,
|
318
|
+
# create a unique identifier that represents the specific path
|
319
|
+
# This prevents false positives where different paths are treated as the same :unknown
|
320
|
+
path_identifier = node.path.join(".").to_s
|
321
|
+
path_identifier.to_sym
|
278
322
|
when Literal
|
279
323
|
node.value
|
280
324
|
else
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
# Builds cascade execution lambdas from analysis metadata
|
6
|
+
class CascadeExecutorBuilder
|
7
|
+
include NestedStructureUtils
|
8
|
+
|
9
|
+
def self.build_executor(strategy, analysis_state)
|
10
|
+
new(strategy, analysis_state).build
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(strategy, analysis_state)
|
14
|
+
@strategy = strategy
|
15
|
+
@analysis_state = analysis_state
|
16
|
+
end
|
17
|
+
|
18
|
+
def build
|
19
|
+
case @strategy[:mode]
|
20
|
+
when :hierarchical
|
21
|
+
build_hierarchical_executor
|
22
|
+
when :nested_array, :deep_nested_array
|
23
|
+
build_nested_array_executor
|
24
|
+
when :simple_array
|
25
|
+
build_simple_array_executor
|
26
|
+
else
|
27
|
+
build_scalar_executor
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def build_hierarchical_executor
|
34
|
+
lambda do |cond_results, res_results, base_result, pairs|
|
35
|
+
# Find the result structure to use as template (deepest structure)
|
36
|
+
all_values = (res_results + cond_results + [base_result].compact).select { |v| v.is_a?(Array) }
|
37
|
+
result_template = all_values.max_by { |v| calculate_array_depth(v) }
|
38
|
+
|
39
|
+
return execute_scalar_cascade(cond_results, res_results, base_result, pairs) unless result_template
|
40
|
+
|
41
|
+
# Apply hierarchical cascade logic using the result structure as template
|
42
|
+
map_nested_structure(result_template) do |*indices|
|
43
|
+
result = nil
|
44
|
+
|
45
|
+
# Check conditional cases first with hierarchical broadcasting for conditions
|
46
|
+
pairs.each_with_index do |(_cond, _res), pair_idx|
|
47
|
+
cond_val = navigate_with_hierarchical_broadcasting(cond_results[pair_idx], indices, result_template)
|
48
|
+
next unless cond_val
|
49
|
+
|
50
|
+
res_val = navigate_nested_indices(res_results[pair_idx], indices)
|
51
|
+
result = res_val
|
52
|
+
break
|
53
|
+
end
|
54
|
+
|
55
|
+
# If no conditional case matched, use base case
|
56
|
+
result = navigate_nested_indices(base_result, indices) if result.nil? && base_result
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_nested_array_executor
|
64
|
+
lambda do |cond_results, res_results, base_result, pairs|
|
65
|
+
# For nested arrays, we need to find the structure template
|
66
|
+
structure_template = find_structure_template(cond_results + res_results + [base_result].compact)
|
67
|
+
return execute_scalar_cascade(cond_results, res_results, base_result, pairs) unless structure_template
|
68
|
+
|
69
|
+
# Apply cascade logic recursively through the nested structure
|
70
|
+
map_nested_structure(structure_template) do |*indices|
|
71
|
+
result = nil
|
72
|
+
|
73
|
+
# Check conditional cases first
|
74
|
+
pairs.each_with_index do |(_cond, _res), pair_idx|
|
75
|
+
cond_val = navigate_nested_indices(cond_results[pair_idx], indices)
|
76
|
+
next unless cond_val
|
77
|
+
|
78
|
+
res_val = navigate_nested_indices(res_results[pair_idx], indices)
|
79
|
+
result = res_val
|
80
|
+
break
|
81
|
+
end
|
82
|
+
|
83
|
+
# If no conditional case matched, use base case
|
84
|
+
result = navigate_nested_indices(base_result, indices) if result.nil? && base_result
|
85
|
+
|
86
|
+
result
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_simple_array_executor
|
92
|
+
lambda do |cond_results, res_results, base_result, pairs|
|
93
|
+
array_length = determine_array_length(cond_results + res_results + [base_result].compact)
|
94
|
+
|
95
|
+
(0...array_length).map do |i|
|
96
|
+
result = nil
|
97
|
+
# Check conditional cases first
|
98
|
+
pairs.each_with_index do |(_cond, _res), pair_idx|
|
99
|
+
cond_val = extract_element_at_index(cond_results[pair_idx], i)
|
100
|
+
next unless cond_val
|
101
|
+
|
102
|
+
res_val = extract_element_at_index(res_results[pair_idx], i)
|
103
|
+
result = res_val
|
104
|
+
break
|
105
|
+
end
|
106
|
+
|
107
|
+
# If no conditional case matched, use base case
|
108
|
+
result = extract_element_at_index(base_result, i) if result.nil? && base_result
|
109
|
+
|
110
|
+
result
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_scalar_executor
|
116
|
+
lambda do |cond_results, res_results, base_result, pairs|
|
117
|
+
pairs.each_with_index do |(_cond, _res), pair_idx|
|
118
|
+
return res_results[pair_idx] if cond_results[pair_idx]
|
119
|
+
end
|
120
|
+
base_result
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def execute_scalar_cascade(cond_results, res_results, base_result, pairs)
|
125
|
+
pairs.each_with_index do |(_cond, _res), pair_idx|
|
126
|
+
return res_results[pair_idx] if cond_results[pair_idx]
|
127
|
+
end
|
128
|
+
base_result
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Kumi
|
2
|
+
module Core
|
3
|
+
module Compiler
|
4
|
+
module ExpressionCompiler
|
5
|
+
private
|
6
|
+
|
7
|
+
def compile_list(expr)
|
8
|
+
fns = expr.elements.map { |e| compile_expr(e) }
|
9
|
+
->(ctx) { fns.map { |fn| fn.call(ctx) } }
|
10
|
+
end
|
11
|
+
|
12
|
+
def compile_call(expr)
|
13
|
+
fn_name = expr.fn_name
|
14
|
+
arg_fns = expr.args.map { |a| compile_expr(a) }
|
15
|
+
|
16
|
+
# Get compilation metadata once
|
17
|
+
compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, @current_declaration)
|
18
|
+
|
19
|
+
# Check if this is a vectorized operation
|
20
|
+
if vectorized_operation?(expr)
|
21
|
+
# Build vectorized executor at COMPILATION time
|
22
|
+
executor = Core::VectorizedFunctionBuilder.build_executor(fn_name, compilation_meta, @analysis.state)
|
23
|
+
|
24
|
+
lambda do |ctx|
|
25
|
+
# Evaluate arguments and use pre-built executor at RUNTIME
|
26
|
+
values = arg_fns.map { |fn| fn.call(ctx) }
|
27
|
+
executor.call(values, expr.loc)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
# Use pre-computed function call strategy
|
31
|
+
function_strategy = compilation_meta&.dig(:function_call_strategy) || {}
|
32
|
+
|
33
|
+
if function_strategy[:flattening_required]
|
34
|
+
flattening_info = @analysis.state[:broadcasts][:flattening_declarations][@current_declaration]
|
35
|
+
->(ctx) { invoke_function_with_flattening(fn_name, arg_fns, ctx, expr.loc, expr.args, flattening_info) }
|
36
|
+
else
|
37
|
+
->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def compile_cascade(expr)
|
43
|
+
# Use metadata to determine if this cascade is vectorized
|
44
|
+
broadcast_meta = @analysis.state[:broadcasts]
|
45
|
+
cascade_info = @current_declaration && broadcast_meta&.dig(:vectorized_operations, @current_declaration)
|
46
|
+
is_vectorized = cascade_info && cascade_info[:source] == :cascade_with_vectorized_conditions_or_results
|
47
|
+
|
48
|
+
# Separate conditional cases from base case
|
49
|
+
conditional_cases = expr.cases.select(&:condition)
|
50
|
+
base_case = expr.cases.find { |c| c.condition.nil? }
|
51
|
+
|
52
|
+
# Compile conditional pairs
|
53
|
+
pairs = conditional_cases.map do |c|
|
54
|
+
condition_fn = if is_vectorized
|
55
|
+
transform_vectorized_condition(c.condition)
|
56
|
+
else
|
57
|
+
compile_expr(c.condition)
|
58
|
+
end
|
59
|
+
result_fn = compile_expr(c.result)
|
60
|
+
[condition_fn, result_fn]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Compile base case
|
64
|
+
base_fn = base_case ? compile_expr(base_case.result) : nil
|
65
|
+
|
66
|
+
if is_vectorized
|
67
|
+
# Capture the current declaration name in the closure
|
68
|
+
current_decl_name = @current_declaration
|
69
|
+
|
70
|
+
# Get pre-computed cascade strategy
|
71
|
+
compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, current_decl_name)
|
72
|
+
cascade_info = compilation_meta&.dig(:cascade_info) || {}
|
73
|
+
|
74
|
+
# Build executor at COMPILATION time (outside the lambda)
|
75
|
+
strategy = @analysis.state[:broadcasts][:cascade_strategies][current_decl_name]
|
76
|
+
executor = strategy ? Core::CascadeExecutorBuilder.build_executor(strategy, @analysis.state) : nil
|
77
|
+
|
78
|
+
# Metadata-driven vectorized cascade evaluation
|
79
|
+
lambda do |ctx|
|
80
|
+
# Evaluate all conditions and results
|
81
|
+
cond_results = pairs.map { |cond, _res| cond.call(ctx) }
|
82
|
+
res_results = pairs.map { |_cond, res| res.call(ctx) }
|
83
|
+
base_result = base_fn&.call(ctx)
|
84
|
+
|
85
|
+
if ENV["DEBUG_CASCADE"]
|
86
|
+
puts "DEBUG: Vectorized cascade evaluation for #{current_decl_name}:"
|
87
|
+
cond_results.each_with_index { |cr, i| puts " cond_results[#{i}]: #{cr.inspect}" }
|
88
|
+
res_results.each_with_index { |rr, i| puts " res_results[#{i}]: #{rr.inspect}" }
|
89
|
+
puts " base_result: #{base_result.inspect}"
|
90
|
+
puts " Pre-computed cascade_info: #{cascade_info.inspect}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# Use pre-built executor at RUNTIME
|
94
|
+
if executor
|
95
|
+
executor.call(cond_results, res_results, base_result, pairs)
|
96
|
+
else
|
97
|
+
# Fallback for cases without strategy
|
98
|
+
pairs.each_with_index do |(_cond, _res), pair_idx|
|
99
|
+
return res_results[pair_idx] if cond_results[pair_idx]
|
100
|
+
end
|
101
|
+
base_result
|
102
|
+
end
|
103
|
+
end
|
104
|
+
else
|
105
|
+
# Non-vectorized cascade - standard evaluation
|
106
|
+
lambda do |ctx|
|
107
|
+
pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
|
108
|
+
# If no conditional case matched, return base case
|
109
|
+
base_fn&.call(ctx)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def transform_vectorized_condition(condition_expr)
|
115
|
+
if condition_expr.is_a?(Kumi::Syntax::CallExpression) &&
|
116
|
+
condition_expr.fn_name == :cascade_and
|
117
|
+
|
118
|
+
puts " transform_vectorized_condition: handling cascade_and with #{condition_expr.args.length} args" if ENV["DEBUG_CASCADE"]
|
119
|
+
|
120
|
+
# For cascade_and in vectorized contexts, we need to compile it as a structure-level operation
|
121
|
+
# rather than element-wise operation
|
122
|
+
return compile_cascade_and_for_hierarchical_broadcasting(condition_expr)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Otherwise compile normally
|
126
|
+
compile_expr(condition_expr)
|
127
|
+
end
|
128
|
+
|
129
|
+
def compile_cascade_and_for_hierarchical_broadcasting(condition_expr)
|
130
|
+
# Compile individual trait references
|
131
|
+
trait_fns = condition_expr.args.map { |arg| compile_expr(arg) }
|
132
|
+
|
133
|
+
lambda do |ctx|
|
134
|
+
# Evaluate all traits to get their array structures
|
135
|
+
trait_values = trait_fns.map { |fn| fn.call(ctx) }
|
136
|
+
|
137
|
+
fn = Kumi::Registry.fetch(:cascade_and)
|
138
|
+
result = fn.call(*trait_values)
|
139
|
+
|
140
|
+
result
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Kumi
|
2
|
+
module Core
|
3
|
+
module Compiler
|
4
|
+
module FunctionInvoker
|
5
|
+
private
|
6
|
+
|
7
|
+
def invoke_function(name, arg_fns, ctx, loc)
|
8
|
+
fn = Kumi::Registry.fetch(name)
|
9
|
+
values = arg_fns.map { |fn| fn.call(ctx) }
|
10
|
+
|
11
|
+
# REMOVED AUTO-FLATTENING: Let operations work on the structure they receive
|
12
|
+
# If flattening is needed, it should be handled by explicit operation modes
|
13
|
+
# in the InputElementReference compilation, not here.
|
14
|
+
fn.call(*values)
|
15
|
+
rescue StandardError => e
|
16
|
+
# Preserve original error class and backtrace while adding context
|
17
|
+
enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
|
18
|
+
|
19
|
+
if e.is_a?(Kumi::Core::Errors::Error)
|
20
|
+
# Re-raise Kumi errors with enhanced message but preserve type
|
21
|
+
e.define_singleton_method(:message) { enhanced_message }
|
22
|
+
raise e
|
23
|
+
else
|
24
|
+
# For non-Kumi errors, wrap in RuntimeError but preserve original error info
|
25
|
+
runtime_error = Errors::RuntimeError.new(enhanced_message)
|
26
|
+
runtime_error.set_backtrace(e.backtrace)
|
27
|
+
runtime_error.define_singleton_method(:cause) { e }
|
28
|
+
raise runtime_error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def invoke_function_with_flattening(name, arg_fns, ctx, loc, _original_args, _flattening_info)
|
33
|
+
fn = Kumi::Registry.fetch(name)
|
34
|
+
|
35
|
+
# Use pre-computed flattening indices from analysis
|
36
|
+
compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, @current_declaration)
|
37
|
+
flatten_indices = compilation_meta&.dig(:function_call_strategy, :flatten_argument_indices) || []
|
38
|
+
|
39
|
+
values = arg_fns.map.with_index do |arg_fn, index|
|
40
|
+
value = arg_fn.call(ctx)
|
41
|
+
flatten_indices.include?(index) ? flatten_completely(value) : value
|
42
|
+
end
|
43
|
+
|
44
|
+
fn.call(*values)
|
45
|
+
rescue StandardError => e
|
46
|
+
enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
|
47
|
+
runtime_error = Errors::RuntimeError.new(enhanced_message)
|
48
|
+
runtime_error.set_backtrace(e.backtrace)
|
49
|
+
runtime_error.define_singleton_method(:cause) { e }
|
50
|
+
raise runtime_error
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|