kumi 0.0.4 → 0.0.6

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
data/lib/kumi/cli.rb ADDED
@@ -0,0 +1,449 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "optparse"
6
+ require "irb"
7
+
8
+ module Kumi
9
+ module CLI
10
+ class Application
11
+ def initialize
12
+ @options = {
13
+ interactive: false,
14
+ schema_file: nil,
15
+ input_file: nil,
16
+ output_format: :pretty,
17
+ keys: [],
18
+ explain: false
19
+ }
20
+ end
21
+
22
+ def run(args = ARGV)
23
+ parse_options(args)
24
+
25
+ if @options[:interactive]
26
+ start_repl
27
+ elsif @options[:schema_file]
28
+ execute_schema_file
29
+ else
30
+ show_help_and_exit
31
+ end
32
+ rescue StandardError => e
33
+ puts "Error: #{e.message}"
34
+ exit 1
35
+ end
36
+
37
+ private
38
+
39
+ def parse_options(args)
40
+ parser = OptionParser.new do |opts|
41
+ opts.banner = "Usage: kumi [options]"
42
+ opts.separator ""
43
+ opts.separator "Options:"
44
+
45
+ opts.on("-i", "--interactive", "Start interactive REPL mode") do
46
+ @options[:interactive] = true
47
+ end
48
+
49
+ opts.on("-f", "--file FILE", "Load schema from Ruby file") do |file|
50
+ @options[:schema_file] = file
51
+ end
52
+
53
+ opts.on("-d", "--data FILE", "Load input data from JSON/YAML file") do |file|
54
+ @options[:input_file] = file
55
+ end
56
+
57
+ opts.on("-k", "--keys KEY1,KEY2", Array, "Extract specific keys (comma-separated)") do |keys|
58
+ @options[:keys] = keys.map(&:to_sym)
59
+ end
60
+
61
+ opts.on("-e", "--explain KEY", "Explain how a specific key is computed") do |key|
62
+ @options[:explain] = key.to_sym
63
+ end
64
+
65
+ opts.on("-o", "--format FORMAT", %i[pretty json yaml], "Output format: pretty, json, yaml") do |format|
66
+ @options[:output_format] = format
67
+ end
68
+
69
+ opts.on("-h", "--help", "Show this help message") do
70
+ puts opts
71
+ exit
72
+ end
73
+ end
74
+
75
+ parser.parse!(args)
76
+ end
77
+
78
+ def show_help_and_exit
79
+ puts <<~HELP
80
+ Kumi CLI - Declarative decision modeling for Ruby
81
+
82
+ Usage:
83
+ kumi -i # Start interactive mode
84
+ kumi -f schema.rb -d data.json # Execute schema with data
85
+ kumi -f schema.rb -k key1,key2 # Extract specific keys
86
+ kumi -f schema.rb -e key_name # Explain computation
87
+
88
+ Examples:
89
+ # Interactive mode for rapid testing
90
+ kumi -i
91
+
92
+ # Execute schema file with JSON data
93
+ kumi -f my_schema.rb -d input.json
94
+
95
+ # Get specific values in JSON format
96
+ kumi -f my_schema.rb -d input.yaml -k salary,bonus -o json
97
+
98
+ # Debug a specific computation
99
+ kumi -f my_schema.rb -d input.json -e total_compensation
100
+
101
+ For more information, see: https://github.com/amuta/kumi
102
+ HELP
103
+ exit
104
+ end
105
+
106
+ def start_repl
107
+ puts "🚀 Kumi Interactive REPL"
108
+ puts "Type 'help' for commands, 'exit' to quit"
109
+ puts
110
+
111
+ repl = InteractiveREPL.new
112
+ repl.start
113
+ end
114
+
115
+ def execute_schema_file
116
+ schema_module = load_schema_file(@options[:schema_file])
117
+ input_data = load_input_data(@options[:input_file])
118
+
119
+ runner = schema_module.from(input_data)
120
+
121
+ if @options[:explain]
122
+ result = schema_module.explain(input_data, @options[:explain])
123
+ puts result
124
+ elsif @options[:keys].any?
125
+ result = runner.slice(*@options[:keys])
126
+ output_result(result)
127
+ else
128
+ # Show available keys if no specific keys requested
129
+ puts "Schema loaded successfully!"
130
+ available_bindings = schema_module.__compiled_schema__.bindings.keys
131
+ puts "Available keys: #{available_bindings.join(', ')}"
132
+ puts "Use -k to extract specific keys or -e to explain computations"
133
+ end
134
+ end
135
+
136
+ def load_schema_file(file_path)
137
+ raise "Schema file not found: #{file_path}" unless File.exist?(file_path)
138
+
139
+ # Load the file and extract the module
140
+ require_relative File.expand_path(file_path)
141
+
142
+ # Find the module name from the file
143
+ module_name = extract_module_name_from_file(file_path)
144
+
145
+ raise "Could not find module extending Kumi::Schema in #{file_path}" unless module_name
146
+
147
+ # Get the module constant
148
+ schema_module = Object.const_get(module_name)
149
+
150
+ raise "Module #{module_name} does not have a compiled schema" unless schema_module.__compiled_schema__
151
+
152
+ schema_module
153
+ end
154
+
155
+ def extract_module_name_from_file(file_path)
156
+ content = File.read(file_path)
157
+
158
+ # Look for "module ModuleName" pattern
159
+ if (match = content.match(/^\s*module\s+(\w+)/))
160
+ match[1]
161
+ end
162
+ end
163
+
164
+ def load_input_data(file_path)
165
+ return {} unless file_path
166
+
167
+ raise "Input file not found: #{file_path}" unless File.exist?(file_path)
168
+
169
+ case File.extname(file_path).downcase
170
+ when ".json"
171
+ JSON.parse(File.read(file_path), symbolize_names: true)
172
+ when ".yml", ".yaml"
173
+ YAML.safe_load_file(file_path, symbolize_names: true)
174
+ else
175
+ raise "Unsupported input file format. Use .json or .yaml"
176
+ end
177
+ end
178
+
179
+ def output_result(result)
180
+ case @options[:output_format]
181
+ when :json
182
+ puts JSON.pretty_generate(result)
183
+ when :yaml
184
+ puts result.to_yaml
185
+ else
186
+ output_pretty(result)
187
+ end
188
+ end
189
+
190
+ def output_pretty(result)
191
+ case result
192
+ when Hash
193
+ result.each do |key, value|
194
+ puts "#{key}: #{format_value(value)}"
195
+ end
196
+ when Kumi::Explain::Result
197
+ puts "Explanation for: #{result.key}"
198
+ puts "Value: #{format_value(result.value)}"
199
+ puts
200
+ puts "Computation trace:"
201
+ result.trace.each do |step|
202
+ puts " #{step[:operation]} -> #{format_value(step[:result])}"
203
+ end
204
+ else
205
+ puts format_value(result)
206
+ end
207
+ end
208
+
209
+ def format_value(value)
210
+ case value
211
+ when String
212
+ value.inspect
213
+ when Numeric
214
+ value.is_a?(Float) ? value.round(2) : value
215
+ when Array, Hash
216
+ value.inspect
217
+ else
218
+ value.to_s
219
+ end
220
+ end
221
+ end
222
+
223
+ class InteractiveREPL
224
+ def initialize
225
+ @schema_module = nil
226
+ @runner = nil
227
+ @input_data = {}
228
+ end
229
+
230
+ def start
231
+ loop do
232
+ print "kumi> "
233
+ input = gets&.chomp
234
+ break if input.nil? || input == "exit"
235
+
236
+ execute_command(input)
237
+ end
238
+ puts "Goodbye!"
239
+ end
240
+
241
+ private
242
+
243
+ def execute_command(input)
244
+ case input.strip
245
+ when "help"
246
+ show_help
247
+ when /^schema\s+(.+)/
248
+ load_schema_command(::Regexp.last_match(1))
249
+ when /^data\s+(.+)/
250
+ load_data_command(::Regexp.last_match(1))
251
+ when /^set\s+(\w+)\s+(.+)/
252
+ set_data_command(::Regexp.last_match(1), ::Regexp.last_match(2))
253
+ when /^get\s+(.+)/
254
+ get_value_command(::Regexp.last_match(1))
255
+ when /^explain\s+(.+)/
256
+ explain_command(::Regexp.last_match(1))
257
+ when /^slice\s+(.+)/
258
+ slice_command(::Regexp.last_match(1))
259
+ when "keys"
260
+ show_keys
261
+ when "clear"
262
+ clear_data
263
+ when ""
264
+ # ignore empty input
265
+ else
266
+ puts "Unknown command. Type 'help' for available commands."
267
+ end
268
+ rescue StandardError => e
269
+ puts "Error: #{e.message}"
270
+ puts e.backtrace.first if ENV["DEBUG"]
271
+ end
272
+
273
+ def show_help
274
+ puts <<~HELP
275
+ Available commands:
276
+
277
+ Schema management:
278
+ schema <file> Load schema from Ruby file
279
+ schema { ... } Define schema inline (experimental)
280
+
281
+ Data management:
282
+ data <file> Load input data from JSON/YAML file
283
+ set <key> <value> Set individual input value
284
+ clear Clear all input data
285
+
286
+ Evaluation:
287
+ get <key> Get computed value for key
288
+ explain <key> Show detailed computation trace
289
+ slice <key1,key2> Get multiple values
290
+ keys Show available keys
291
+
292
+ General:
293
+ help Show this help
294
+ exit Exit REPL
295
+
296
+ Examples:
297
+ schema examples/tax_2024.rb
298
+ data test_input.json
299
+ get total_tax
300
+ explain effective_rate
301
+ slice income,deductions,total_tax
302
+ HELP
303
+ end
304
+
305
+ def load_schema_command(file_path)
306
+ file_path = file_path.strip.gsub(/^["']|["']$/, "") # Remove quotes
307
+
308
+ unless File.exist?(file_path)
309
+ puts "Schema file not found: #{file_path}"
310
+ return
311
+ end
312
+
313
+ @schema_module = Module.new
314
+ @schema_module.extend(Kumi::Schema)
315
+
316
+ schema_content = File.read(file_path)
317
+ @schema_module.module_eval(schema_content, file_path)
318
+
319
+ puts "✅ Schema loaded from #{file_path}"
320
+ refresh_runner
321
+ rescue StandardError => e
322
+ puts "❌ Failed to load schema: #{e.message}"
323
+ end
324
+
325
+ def load_data_command(file_path)
326
+ file_path = file_path.strip.gsub(/^["']|["']$/, "") # Remove quotes
327
+
328
+ unless File.exist?(file_path)
329
+ puts "Data file not found: #{file_path}"
330
+ return
331
+ end
332
+
333
+ case File.extname(file_path).downcase
334
+ when ".json"
335
+ @input_data = JSON.parse(File.read(file_path), symbolize_names: true)
336
+ when ".yml", ".yaml"
337
+ @input_data = YAML.safe_load_file(file_path, symbolize_names: true)
338
+ else
339
+ puts "Unsupported file format. Use .json or .yaml"
340
+ return
341
+ end
342
+
343
+ puts "✅ Data loaded from #{file_path}"
344
+ puts "Keys: #{@input_data.keys.join(', ')}"
345
+ refresh_runner
346
+ rescue StandardError => e
347
+ puts "❌ Failed to load data: #{e.message}"
348
+ end
349
+
350
+ def set_data_command(key, value)
351
+ # Try to parse value as JSON first, then as literal
352
+ parsed_value = begin
353
+ JSON.parse(value)
354
+ rescue JSON::ParserError
355
+ # If not valid JSON, treat as string unless it looks like a number/boolean
356
+ case value
357
+ when /^\d+$/ then value.to_i
358
+ when /^\d+\.\d+$/ then value.to_f
359
+ when "true" then true
360
+ when "false" then false
361
+ else value
362
+ end
363
+ end
364
+
365
+ @input_data[key.to_sym] = parsed_value
366
+ puts "✅ Set #{key} = #{parsed_value.inspect}"
367
+ refresh_runner
368
+ end
369
+
370
+ def get_value_command(key)
371
+ ensure_runner_ready
372
+
373
+ key_sym = key.strip.to_sym
374
+ result = @runner[key_sym]
375
+ puts "#{key_sym}: #{format_value(result)}"
376
+ rescue StandardError => e
377
+ puts "❌ Error getting #{key}: #{e.message}"
378
+ end
379
+
380
+ def explain_command(key)
381
+ ensure_runner_ready
382
+
383
+ key_sym = key.strip.to_sym
384
+ puts @schema_module.explain(@input_data, key_sym)
385
+ rescue StandardError => e
386
+ puts "❌ Error explaining #{key}: #{e.message}"
387
+ end
388
+
389
+ def slice_command(keys_str)
390
+ ensure_runner_ready
391
+
392
+ keys = keys_str.split(",").map { |k| k.strip.to_sym }
393
+ result = @runner.slice(*keys)
394
+
395
+ result.each do |key, value|
396
+ puts "#{key}: #{format_value(value)}"
397
+ end
398
+ rescue StandardError => e
399
+ puts "❌ Error getting slice: #{e.message}"
400
+ end
401
+
402
+ def show_keys
403
+ if @schema_module
404
+ available_bindings = @schema_module.__compiled_schema__.bindings.keys
405
+ puts "Available keys: #{available_bindings.join(', ')}"
406
+ else
407
+ puts "No schema loaded. Use 'schema <file>' to load a schema."
408
+ end
409
+ end
410
+
411
+ def clear_data
412
+ @input_data = {}
413
+ @runner = nil
414
+ puts "✅ Input data cleared"
415
+ end
416
+
417
+ def ensure_runner_ready
418
+ raise "No schema loaded. Use 'schema <file>' to load a schema." unless @schema_module
419
+
420
+ return if @runner
421
+
422
+ raise "No runner available. Load data with 'data <file>' or set values with 'set <key> <value>'"
423
+ end
424
+
425
+ def refresh_runner
426
+ return unless @schema_module
427
+
428
+ @runner = @schema_module.from(@input_data)
429
+ puts "✅ Runner refreshed with current data"
430
+ rescue StandardError => e
431
+ puts "⚠️ Runner refresh failed: #{e.message}"
432
+ @runner = nil
433
+ end
434
+
435
+ def format_value(value)
436
+ case value
437
+ when String
438
+ value.inspect
439
+ when Numeric
440
+ value.is_a?(Float) ? value.round(2) : value
441
+ when Array, Hash
442
+ value.inspect
443
+ else
444
+ value.to_s
445
+ end
446
+ end
447
+ end
448
+ end
449
+ end
data/lib/kumi/compiler.rb CHANGED
@@ -14,9 +14,27 @@ 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
+
30
+
17
31
  def compile_binding_node(expr)
18
- fn = @bindings[expr.name].last
19
- ->(ctx) { fn.call(ctx) }
32
+ name = expr.name
33
+ # Handle forward references in cycles by deferring binding lookup to runtime
34
+ lambda do |ctx|
35
+ fn = @bindings[name].last
36
+ fn.call(ctx)
37
+ end
20
38
  end
21
39
 
22
40
  def compile_list(expr)
@@ -27,28 +45,102 @@ module Kumi
27
45
  def compile_call(expr)
28
46
  fn_name = expr.fn_name
29
47
  arg_fns = expr.args.map { |a| compile_expr(a) }
30
- ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
48
+
49
+ # Check if this is a vectorized operation
50
+ if vectorized_operation?(expr)
51
+ ->(ctx) { invoke_vectorized_function(fn_name, arg_fns, ctx, expr.loc) }
52
+ else
53
+ ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
54
+ end
31
55
  end
32
56
 
33
57
  def compile_cascade(expr)
34
- pairs = expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
35
- lambda do |ctx|
36
- pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
37
- nil
58
+ # Check if current declaration is vectorized
59
+ broadcast_meta = @analysis.state[:broadcast_metadata]
60
+ is_vectorized = @current_declaration && broadcast_meta&.dig(:vectorized_operations, @current_declaration)
61
+
62
+
63
+ # For vectorized cascades, we need to transform conditions that use all?
64
+ if is_vectorized
65
+ pairs = expr.cases.map do |c|
66
+ condition_fn = transform_vectorized_condition(c.condition)
67
+ result_fn = compile_expr(c.result)
68
+ [condition_fn, result_fn]
69
+ end
70
+ else
71
+ pairs = expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
72
+ end
73
+
74
+ if is_vectorized
75
+ lambda do |ctx|
76
+ # This cascade can be vectorized - check if we actually need to at runtime
77
+ # Evaluate all conditions and results to check for arrays
78
+ cond_results = pairs.map { |cond, _res| cond.call(ctx) }
79
+ res_results = pairs.map { |_cond, res| res.call(ctx) }
80
+
81
+ # Check if any conditions or results are arrays (vectorized)
82
+ has_vectorized_data = (cond_results + res_results).any? { |v| v.is_a?(Array) }
83
+
84
+ if has_vectorized_data
85
+ # Apply element-wise cascade evaluation
86
+ array_length = cond_results.find { |v| v.is_a?(Array) }&.length ||
87
+ res_results.find { |v| v.is_a?(Array) }&.length || 1
88
+
89
+ (0...array_length).map do |i|
90
+ pairs.each_with_index do |(cond, res), pair_idx|
91
+ cond_val = cond_results[pair_idx].is_a?(Array) ? cond_results[pair_idx][i] : cond_results[pair_idx]
92
+
93
+ if cond_val
94
+ res_val = res_results[pair_idx].is_a?(Array) ? res_results[pair_idx][i] : res_results[pair_idx]
95
+ break res_val
96
+ end
97
+ end || nil
98
+ end
99
+ else
100
+ # All data is scalar - use regular cascade evaluation
101
+ pairs.each_with_index do |(cond, res), pair_idx|
102
+ return res_results[pair_idx] if cond_results[pair_idx]
103
+ end
104
+ nil
105
+ end
106
+ end
107
+ else
108
+ lambda do |ctx|
109
+ pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
110
+ nil
111
+ end
38
112
  end
39
113
  end
114
+
115
+ def transform_vectorized_condition(condition_expr)
116
+ # If this is fn(:all?, [trait_ref]), extract the trait_ref for vectorized cascades
117
+ if condition_expr.is_a?(Kumi::Syntax::CallExpression) &&
118
+ condition_expr.fn_name == :all? &&
119
+ condition_expr.args.length == 1
120
+
121
+ arg = condition_expr.args.first
122
+ if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
123
+ trait_ref = arg.elements.first
124
+ return compile_expr(trait_ref)
125
+ end
126
+ end
127
+
128
+ # Otherwise compile normally
129
+ compile_expr(condition_expr)
130
+ end
40
131
  end
41
132
 
42
133
  include ExprCompilers
43
134
 
44
135
  # Map node classes to compiler methods
45
136
  DISPATCH = {
46
- Syntax::TerminalExpressions::Literal => :compile_literal,
47
- Syntax::TerminalExpressions::FieldRef => :compile_field_node,
48
- Syntax::TerminalExpressions::Binding => :compile_binding_node,
49
- Syntax::Expressions::ListExpression => :compile_list,
50
- Syntax::Expressions::CallExpression => :compile_call,
51
- Syntax::Expressions::CascadeExpression => :compile_cascade
137
+ Kumi::Syntax::Literal => :compile_literal,
138
+ Kumi::Syntax::InputReference => :compile_field_node,
139
+ Kumi::Syntax::InputElementReference => :compile_element_field_reference,
140
+ Kumi::Syntax::DeclarationReference => :compile_binding_node,
141
+ Kumi::Syntax::ArrayExpression => :compile_list,
142
+ Kumi::Syntax::CallExpression => :compile_call,
143
+ Kumi::Syntax::CascadeExpression => :compile_cascade
52
144
  }.freeze
53
145
 
54
146
  def self.compile(schema, analyzer:)
@@ -79,10 +171,30 @@ module Kumi
79
171
  @schema.traits.each { |t| @index[t.name] = t }
80
172
  end
81
173
 
174
+ def dig_and_map(collection, path_segments)
175
+ return collection unless collection.is_a?(Array)
176
+
177
+ current_segment = path_segments.first
178
+ remaining_segments = path_segments[1..]
179
+
180
+ collection.map do |element|
181
+ value = element[current_segment]
182
+
183
+ # If there are more segments, recurse. Otherwise, return the value.
184
+ if remaining_segments.empty?
185
+ value
186
+ else
187
+ dig_and_map(value, remaining_segments)
188
+ end
189
+ end
190
+ end
191
+
82
192
  def compile_declaration(decl)
83
- kind = decl.is_a?(Syntax::Declarations::Trait) ? :trait : :attr
84
- fn = compile_expr(decl.expression)
193
+ @current_declaration = decl.name
194
+ kind = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? :trait : :attr
195
+ fn = compile_expr(decl.expression)
85
196
  @bindings[decl.name] = [kind, fn]
197
+ @current_declaration = nil
86
198
  end
87
199
 
88
200
  # Dispatch to the appropriate compile_* method
@@ -91,7 +203,6 @@ module Kumi
91
203
  send(method, expr)
92
204
  end
93
205
 
94
- # Existing helpers unchanged
95
206
  def compile_field(node)
96
207
  name = node.name
97
208
  loc = node.loc
@@ -103,6 +214,73 @@ module Kumi
103
214
  end
104
215
  end
105
216
 
217
+ def vectorized_operation?(expr)
218
+ # Check if this operation uses vectorized inputs
219
+ broadcast_meta = @analysis.state[:broadcast_metadata]
220
+ return false unless broadcast_meta
221
+
222
+ # Reduction functions are NOT vectorized operations - they consume arrays
223
+ if FunctionRegistry.reducer?(expr.fn_name)
224
+ return false
225
+ end
226
+
227
+ expr.args.any? do |arg|
228
+ case arg
229
+ when Kumi::Syntax::InputElementReference
230
+ broadcast_meta[:array_fields]&.key?(arg.path.first)
231
+ when Kumi::Syntax::DeclarationReference
232
+ broadcast_meta[:vectorized_operations]&.key?(arg.name)
233
+ else
234
+ false
235
+ end
236
+ end
237
+ end
238
+
239
+
240
+ def invoke_vectorized_function(name, arg_fns, ctx, loc)
241
+ # Evaluate arguments
242
+ values = arg_fns.map { |fn| fn.call(ctx) }
243
+
244
+ # Check if any argument is vectorized (array)
245
+ has_vectorized_args = values.any? { |v| v.is_a?(Array) }
246
+
247
+ if has_vectorized_args
248
+ # Apply function with broadcasting to all vectorized arguments
249
+ vectorized_function_call(name, values)
250
+ else
251
+ # All arguments are scalars - regular function call
252
+ fn = FunctionRegistry.fetch(name)
253
+ fn.call(*values)
254
+ end
255
+ rescue StandardError => e
256
+ enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
257
+ runtime_error = Errors::RuntimeError.new(enhanced_message)
258
+ runtime_error.set_backtrace(e.backtrace)
259
+ runtime_error.define_singleton_method(:cause) { e }
260
+ raise runtime_error
261
+ end
262
+
263
+ def vectorized_function_call(fn_name, values)
264
+ # Get the function from registry
265
+ fn = FunctionRegistry.fetch(fn_name)
266
+
267
+ # Find array dimensions for broadcasting
268
+ array_values = values.select { |v| v.is_a?(Array) }
269
+ return fn.call(*values) if array_values.empty?
270
+
271
+ # All arrays should have the same length (validation could be added)
272
+ array_length = array_values.first.size
273
+
274
+ # Broadcast and apply function element-wise
275
+ (0...array_length).map do |i|
276
+ element_args = values.map do |v|
277
+ v.is_a?(Array) ? v[i] : v # Broadcast scalars
278
+ end
279
+ fn.call(*element_args)
280
+ end
281
+ end
282
+
283
+
106
284
  def invoke_function(name, arg_fns, ctx, loc)
107
285
  fn = FunctionRegistry.fetch(name)
108
286
  values = arg_fns.map { |fn| fn.call(ctx) }