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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +28 -44
  3. data/README.md +187 -120
  4. data/docs/AST.md +1 -1
  5. data/docs/FUNCTIONS.md +52 -8
  6. data/docs/compiler_design_principles.md +86 -0
  7. data/docs/features/README.md +15 -2
  8. data/docs/features/hierarchical-broadcasting.md +349 -0
  9. data/docs/features/javascript-transpiler.md +148 -0
  10. data/docs/features/performance.md +1 -3
  11. data/docs/schema_metadata.md +7 -7
  12. data/examples/game_of_life.rb +2 -4
  13. data/lib/kumi/analyzer.rb +0 -2
  14. data/lib/kumi/compiler.rb +6 -275
  15. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
  16. data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
  17. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
  18. data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
  19. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
  20. data/lib/kumi/core/cascade_executor_builder.rb +132 -0
  21. data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
  22. data/lib/kumi/core/compiler/function_invoker.rb +55 -0
  23. data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
  24. data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
  25. data/lib/kumi/core/compiler_base.rb +137 -0
  26. data/lib/kumi/core/explain.rb +2 -2
  27. data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
  28. data/lib/kumi/core/function_registry/function_builder.rb +5 -3
  29. data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
  30. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  31. data/lib/kumi/core/function_registry.rb +32 -10
  32. data/lib/kumi/core/nested_structure_utils.rb +78 -0
  33. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
  34. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  35. data/lib/kumi/core/schema_instance.rb +4 -0
  36. data/lib/kumi/core/vectorized_function_builder.rb +88 -0
  37. data/lib/kumi/errors.rb +2 -0
  38. data/lib/kumi/js/compiler.rb +878 -0
  39. data/lib/kumi/js/function_registry.rb +333 -0
  40. data/lib/kumi/js.rb +23 -0
  41. data/lib/kumi/registry.rb +61 -1
  42. data/lib/kumi/schema.rb +1 -1
  43. data/lib/kumi/support/s_expression_printer.rb +16 -15
  44. data/lib/kumi/syntax/array_expression.rb +6 -6
  45. data/lib/kumi/syntax/call_expression.rb +4 -4
  46. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  47. data/lib/kumi/syntax/case_expression.rb +4 -4
  48. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  49. data/lib/kumi/syntax/hash_expression.rb +4 -4
  50. data/lib/kumi/syntax/input_declaration.rb +6 -5
  51. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  52. data/lib/kumi/syntax/input_reference.rb +5 -5
  53. data/lib/kumi/syntax/literal.rb +4 -4
  54. data/lib/kumi/syntax/node.rb +34 -34
  55. data/lib/kumi/syntax/root.rb +6 -6
  56. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  57. data/lib/kumi/syntax/value_declaration.rb +4 -4
  58. data/lib/kumi/version.rb +1 -1
  59. data/lib/kumi.rb +1 -1
  60. data/scripts/analyze_broadcast_methods.rb +68 -0
  61. data/scripts/analyze_cascade_methods.rb +74 -0
  62. data/scripts/check_broadcasting_coverage.rb +51 -0
  63. data/scripts/find_dead_code.rb +114 -0
  64. metadata +20 -4
  65. data/docs/features/array-broadcasting.md +0 -170
  66. data/lib/kumi/cli.rb +0 -449
  67. 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: None (validation only)
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
- state
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
- impossible = if definitions && !definitions.empty?
45
- Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
46
- else
47
- Kumi::Core::AtomUnsatSolver.unsat?(atoms)
48
- end
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 == :all?
85
+ next unless when_case.condition.fn_name == :cascade_and
67
86
 
68
87
  when_case.condition.args.each do |arg|
69
- next unless arg.is_a?(ArrayExpression)
70
-
71
- arg.elements.each do |element|
72
- next unless element.is_a?(DeclarationReference)
73
-
74
- trait_name = element.name
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 == :all?
187
- # For all? function, add all trait arguments to the stack
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
- decl.expression.cases.each_with_index do |when_case, _index|
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 == :all?
224
- # Handle both ArrayExpression (old format) and multiple args (new format)
225
- if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
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
- # Only flag if this individual condition is impossible
242
- # if !condition_atoms.empty?
243
- # is_unsat = Kumi::Core::AtomUnsatSolver.unsat?(condition_atoms)
244
- # puts " Is unsat? #{is_unsat}"
245
- # end
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
- impossible = if definitions && !definitions.empty?
248
- Kumi::Core::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
249
- else
250
- Kumi::Core::AtomUnsatSolver.unsat?(condition_atoms)
251
- end
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 == :all?
256
- # Handle both ArrayExpression (old format) and multiple args (new format)
257
- trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
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