kumi 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +7 -231
  4. data/README.md +1 -1
  5. data/docs/VECTOR_SEMANTICS.md +286 -0
  6. data/docs/features/hierarchical-broadcasting.md +1 -1
  7. data/docs/features/s-expression-printer.md +2 -2
  8. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  9. data/lib/kumi/analyzer.rb +34 -12
  10. data/lib/kumi/compiler.rb +2 -12
  11. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  13. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  14. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -101
  15. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  16. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  17. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  18. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  19. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
  20. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  21. data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
  22. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  23. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  24. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
  25. data/lib/kumi/core/analyzer/plans.rb +52 -0
  26. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  27. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  28. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  29. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  30. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  31. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  32. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  33. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  34. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  35. data/lib/kumi/core/compiler_base.rb +2 -2
  36. data/lib/kumi/core/error_reporter.rb +6 -5
  37. data/lib/kumi/core/errors.rb +4 -0
  38. data/lib/kumi/core/explain.rb +157 -205
  39. data/lib/kumi/core/export/node_builders.rb +2 -2
  40. data/lib/kumi/core/export/node_serializers.rb +1 -1
  41. data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
  42. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  43. data/lib/kumi/core/function_registry/function_builder.rb +142 -55
  44. data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
  45. data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
  46. data/lib/kumi/core/function_registry.rb +126 -108
  47. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  48. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  49. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  50. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  51. data/lib/kumi/core/ir.rb +58 -0
  52. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  53. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  54. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
  55. data/lib/kumi/core/ruby_parser/input_builder.rb +5 -5
  56. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  57. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  58. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  59. data/lib/kumi/registry.rb +14 -79
  60. data/lib/kumi/runtime/executable.rb +213 -0
  61. data/lib/kumi/schema.rb +14 -3
  62. data/lib/kumi/schema_metadata.rb +2 -2
  63. data/lib/kumi/support/ir_dump.rb +491 -0
  64. data/lib/kumi/support/s_expression_printer.rb +1 -1
  65. data/lib/kumi/syntax/location.rb +5 -0
  66. data/lib/kumi/syntax/node.rb +0 -1
  67. data/lib/kumi/syntax/root.rb +2 -2
  68. data/lib/kumi/version.rb +1 -1
  69. data/lib/kumi.rb +6 -15
  70. metadata +26 -15
  71. data/lib/kumi/core/cascade_executor_builder.rb +0 -132
  72. data/lib/kumi/core/compiled_schema.rb +0 -43
  73. data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
  74. data/lib/kumi/core/compiler/function_invoker.rb +0 -55
  75. data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
  76. data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
  77. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  78. data/lib/kumi/core/nested_structure_utils.rb +0 -78
  79. data/lib/kumi/core/schema_instance.rb +0 -115
  80. data/lib/kumi/core/vectorized_function_builder.rb +0 -88
  81. data/lib/kumi/js/compiler.rb +0 -878
  82. data/lib/kumi/js/function_registry.rb +0 -333
  83. data/migrate_to_core_iterative.rb +0 -938
data/lib/kumi/analyzer.rb CHANGED
@@ -7,15 +7,19 @@ module Kumi
7
7
  DEFAULT_PASSES = [
8
8
  Core::Analyzer::Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
9
9
  Core::Analyzer::Passes::InputCollector, # 2. Collects field metadata from input declarations.
10
- Core::Analyzer::Passes::DeclarationValidator, # 3. Checks the basic structure of each rule.
11
- Core::Analyzer::Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
12
- Core::Analyzer::Passes::DependencyResolver, # 5. Builds the dependency graph with conditional dependencies.
13
- Core::Analyzer::Passes::UnsatDetector, # 6. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
14
- Core::Analyzer::Passes::Toposorter, # 7. Creates the final evaluation order, allowing safe cycles.
15
- Core::Analyzer::Passes::BroadcastDetector, # 8. Detects which operations should be broadcast over arrays (must run before type inference).
16
- Core::Analyzer::Passes::TypeInferencer, # 9. Infers types for all declarations (uses vectorization metadata).
17
- Core::Analyzer::Passes::TypeConsistencyChecker, # 10. Validates declared vs inferred type consistency.
18
- Core::Analyzer::Passes::TypeChecker # 11. Validates types using inferred information.
10
+ Core::Analyzer::Passes::DeclarationValidator, # 4. Checks the basic structure of each rule.
11
+ Core::Analyzer::Passes::SemanticConstraintValidator, # 5. Validates DSL semantic constraints at AST level.
12
+ Core::Analyzer::Passes::DependencyResolver, # 6. Builds the dependency graph with conditional dependencies.
13
+ Core::Analyzer::Passes::UnsatDetector, # 7. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
14
+ Core::Analyzer::Passes::Toposorter, # 8. Creates the final evaluation order, allowing safe cycles.
15
+ Core::Analyzer::Passes::BroadcastDetector, # 9. Detects which operations should be broadcast over arrays.
16
+ Core::Analyzer::Passes::TypeInferencerPass, # 10. Infers types for all declarations (uses vectorization metadata).
17
+ Core::Analyzer::Passes::TypeConsistencyChecker, # 11. Validates declared vs inferred type consistency.
18
+ Core::Analyzer::Passes::TypeChecker, # 12. Validates types using inferred information.
19
+ Core::Analyzer::Passes::InputAccessPlannerPass, # 13. Plans access strategies for input fields.
20
+ Core::Analyzer::Passes::ScopeResolutionPass, # 14. Plans execution scope and lifting needs for declarations.
21
+ Core::Analyzer::Passes::JoinReducePlanningPass, # 15. Plans join/reduce operations (Generates IR Structs)
22
+ Core::Analyzer::Passes::LowerToIRPass # 16. Lowers the schema to IR (Generates IR Structs)
19
23
  ].freeze
20
24
 
21
25
  def self.analyze!(schema, passes: DEFAULT_PASSES, **opts)
@@ -33,7 +37,13 @@ module Kumi
33
37
  begin
34
38
  state = pass_instance.run(errors)
35
39
  rescue StandardError => e
36
- errors << Core::ErrorReporter.create_error(e.message, location: nil, type: :semantic)
40
+ # TODO: - GREATLY improve this, need to capture the context of the error
41
+ # and the pass that failed and line number if relevant
42
+ pass_name = pass_class.name.split("::").last
43
+ message = "Error in Analysis Pass(#{pass_name}): #{e.message}"
44
+ errors << Core::ErrorReporter.create_error(message, location: nil, type: :semantic, backtrace: e.backtrace)
45
+
46
+ raise
37
47
  end
38
48
  end
39
49
  state
@@ -41,11 +51,14 @@ module Kumi
41
51
 
42
52
  def self.handle_analysis_errors(errors)
43
53
  type_errors = errors.select { |e| e.type == :type }
54
+ semantic_errors = errors.select { |e| e.type == :semantic }
44
55
  first_error_location = errors.first.location
45
56
 
46
57
  raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
47
58
 
48
- raise Errors::SemanticError.new(format_errors(errors), first_error_location)
59
+ raise Errors::SemanticError.new(format_errors(errors), first_error_location) if first_error_location || semantic_errors
60
+
61
+ raise Errors::AnalysisError.new(format_errors(errors))
49
62
  end
50
63
 
51
64
  def self.create_analysis_result(state)
@@ -63,7 +76,16 @@ module Kumi
63
76
  def self.format_errors(errors)
64
77
  return "" if errors.empty?
65
78
 
66
- errors.map(&:to_s).join("\n")
79
+ backtrace = errors.first.backtrace
80
+
81
+ message = errors.map(&:to_s).join("\n")
82
+
83
+ message.tap do |msg|
84
+ if backtrace && !backtrace.empty?
85
+ msg << "\n\nBacktrace:\n"
86
+ msg << backtrace[0..10].join("\n") # Limit to first 10 lines for readability
87
+ end
88
+ end
67
89
  end
68
90
  end
69
91
  end
data/lib/kumi/compiler.rb CHANGED
@@ -3,11 +3,6 @@
3
3
  module Kumi
4
4
  # Compiles an analyzed schema into executable lambdas
5
5
  class Compiler < Core::CompilerBase
6
- include Kumi::Core::Compiler::ReferenceCompiler
7
- include Kumi::Core::Compiler::PathTraversalCompiler
8
- include Kumi::Core::Compiler::ExpressionCompiler
9
- include Kumi::Core::Compiler::FunctionInvoker
10
-
11
6
  def self.compile(schema, analyzer:)
12
7
  new(schema, analyzer).compile
13
8
  end
@@ -18,13 +13,8 @@ module Kumi
18
13
  end
19
14
 
20
15
  def compile
21
- build_index
22
- @analysis.topo_order.each do |name|
23
- decl = @index[name] or raise("Unknown binding #{name}")
24
- compile_declaration(decl)
25
- end
26
-
27
- Core::CompiledSchema.new(@bindings.freeze)
16
+ # Switch to LIR: Use the analysis state instead of old compilation
17
+ Runtime::Executable.from_analysis(@analysis.state)
28
18
  end
29
19
  end
30
20
  end
@@ -5,11 +5,11 @@ module Kumi
5
5
  module Analyzer
6
6
  module Passes
7
7
  # Detects which operations should be broadcast over arrays
8
- # DEPENDENCIES: :inputs, :declarations
8
+ # DEPENDENCIES: :input_metadata, :declarations
9
9
  # PRODUCES: :broadcasts
10
10
  class BroadcastDetector < PassBase
11
11
  def run(errors)
12
- input_meta = get_state(:inputs) || {}
12
+ input_meta = get_state(:input_metadata) || {}
13
13
  definitions = get_state(:declarations) || {}
14
14
 
15
15
  # Find array fields with their element types
@@ -40,6 +40,10 @@ module Kumi
40
40
  result = analyze_value_vectorization(name, decl.expression, array_fields, nested_paths, vectorized_values, errors,
41
41
  definitions)
42
42
 
43
+ if ENV["DEBUG_BROADCAST_CLEAN"]
44
+ puts "#{name}: #{result[:type]} #{format_broadcast_info(result)}"
45
+ end
46
+
43
47
  case result[:type]
44
48
  when :vectorized
45
49
  compiler_metadata[:vectorized_operations][name] = result[:info]
@@ -70,6 +74,43 @@ module Kumi
70
74
 
71
75
  private
72
76
 
77
+ def infer_argument_scope(arg, array_fields, nested_paths)
78
+ case arg
79
+ when Kumi::Syntax::InputElementReference
80
+ if nested_paths.key?(arg.path)
81
+ # Extract scope from path - each array dimension in the path
82
+ arg.path.select.with_index { |_seg, i| nested_paths[arg.path[0..i]] }
83
+ else
84
+ arg.path.select { |seg| array_fields.key?(seg) }
85
+ end
86
+ when Kumi::Syntax::CallExpression
87
+ # For nested calls, find the deepest input reference
88
+ deepest_scope = []
89
+ arg.args.each do |nested_arg|
90
+ scope = infer_argument_scope(nested_arg, array_fields, nested_paths)
91
+ deepest_scope = scope if scope.length > deepest_scope.length
92
+ end
93
+ deepest_scope
94
+ else
95
+ []
96
+ end
97
+ end
98
+
99
+ def format_broadcast_info(result)
100
+ case result[:type]
101
+ when :vectorized
102
+ info = result[:info]
103
+ "→ #{info[:source]} (path: #{info[:path]&.join('.')})"
104
+ when :reduction
105
+ info = result[:info]
106
+ "→ fn:#{info[:function]} (arg: #{info[:argument]&.class&.name&.split('::')&.last})"
107
+ when :scalar
108
+ "→ scalar"
109
+ else
110
+ "→ #{result[:info]}"
111
+ end
112
+ end
113
+
73
114
  def compute_compilation_metadata(name, _decl, compiler_metadata, _vectorized_values, _array_fields)
74
115
  metadata = {
75
116
  operation_mode: :broadcast, # Default mode
@@ -169,7 +210,7 @@ module Kumi
169
210
  current_access_mode = parent_access_mode
170
211
  if current_meta[:type] == :array
171
212
  array_depth += 1
172
- current_access_mode = current_meta[:access_mode] || :object # Default to :object if not specified
213
+ current_access_mode = current_meta[:access_mode] || :field # Default to :field if not specified
173
214
  end
174
215
 
175
216
  # If this field has children, recurse into them
@@ -208,7 +249,6 @@ module Kumi
208
249
  # Check if this path exists in nested_paths metadata (supports nested arrays)
209
250
  if nested_paths.key?(expr.path)
210
251
  { type: :vectorized, info: { source: :nested_array_access, path: expr.path, nested_metadata: nested_paths[expr.path] } }
211
- # Fallback to old array_fields detection for backward compatibility
212
252
  elsif array_fields.key?(expr.path.first)
213
253
  { type: :vectorized, info: { source: :array_field_access, path: expr.path } }
214
254
  else
@@ -236,76 +276,106 @@ module Kumi
236
276
  end
237
277
 
238
278
  def analyze_call_vectorization(_name, expr, array_fields, nested_paths, vectorized_values, errors, definitions = nil)
239
- # Check if this is a reduction function using function registry metadata
240
- if Kumi::Registry.reducer?(expr.fn_name)
241
- # Only treat as reduction if the argument is actually vectorized
242
- arg_info = analyze_argument_vectorization(expr.args.first, array_fields, nested_paths, vectorized_values, definitions)
243
- if arg_info[:vectorized]
244
- # Pre-compute which argument indices need flattening
245
- flatten_indices = []
246
- expr.args.each_with_index do |arg, index|
247
- arg_vectorization = analyze_argument_vectorization(arg, array_fields, nested_paths, vectorized_values, definitions)
248
- flatten_indices << index if arg_vectorization[:vectorized]
249
- end
250
-
251
- { type: :reduction, info: {
252
- function: expr.fn_name,
253
- source: arg_info[:source],
254
- argument: expr.args.first,
255
- flatten_argument_indices: flatten_indices
256
- } }
257
- else
258
- # Not a vectorized reduction - just a regular function call
259
- { type: :scalar }
260
- end
279
+ entry = Kumi::Registry.entry(expr.fn_name)
280
+ is_reducer = entry&.reducer
281
+ is_structure = entry&.structure_function
261
282
 
262
- else
283
+ # 1) Analyze all args once
284
+ arg_infos = expr.args.map do |arg|
285
+ analyze_argument_vectorization(arg, array_fields, nested_paths, vectorized_values, definitions)
286
+ end
287
+ vec_idx = arg_infos.each_index.select { |i| arg_infos[i][:vectorized] }
288
+ vec_any = !vec_idx.empty?
263
289
 
264
- # Special case: cascade_and takes individual trait arguments
265
- if expr.fn_name == :cascade_and
266
- # Check if any of the individual arguments are vectorized traits
267
- vectorized_trait = expr.args.find do |arg|
268
- arg.is_a?(Kumi::Syntax::DeclarationReference) && vectorized_values[arg.name]&.[](:vectorized)
269
- end
270
- if vectorized_trait
271
- return { type: :vectorized, info: { source: :cascade_condition_with_vectorized_trait, trait: vectorized_trait.name } }
272
- end
290
+ # 2) Special form: cascade_and (vectorized if any trait arg is vectorized)
291
+ if expr.fn_name == :cascade_and
292
+ vectorized_trait = expr.args.find do |arg|
293
+ arg.is_a?(Kumi::Syntax::DeclarationReference) && vectorized_values[arg.name]&.[](:vectorized)
273
294
  end
274
-
275
- # Analyze arguments to determine function behavior
276
- arg_infos = expr.args.map do |arg|
277
- analyze_argument_vectorization(arg, array_fields, nested_paths, vectorized_values, definitions)
295
+ if vectorized_trait
296
+ return { type: :vectorized,
297
+ info: { source: :cascade_condition_with_vectorized_trait, trait: vectorized_trait&.name } }
278
298
  end
279
299
 
280
- if arg_infos.any? { |info| info[:vectorized] }
281
- # Check for dimension mismatches when multiple arguments are vectorized
282
- vectorized_sources = arg_infos.select { |info| info[:vectorized] }.filter_map { |info| info[:array_source] }.uniq
300
+ return { type: :scalar }
301
+ end
283
302
 
284
- if vectorized_sources.length > 1
285
- # Multiple different array sources - this is a dimension mismatch
286
- # Generate enhanced error message with type information
287
- enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, vectorized_sources)
303
+ # 3) Reducers: only reduce when the input is actually vectorized
304
+ if is_reducer
305
+ return { type: :scalar } unless vec_any
288
306
 
289
- report_error(errors, enhanced_message, location: expr.loc, type: :semantic)
290
- return { type: :scalar } # Treat as scalar to prevent further errors
291
- end
307
+ # which args were vectorized?
308
+ flatten_indices = vec_idx.dup
309
+ vectorized_arg_index = vec_idx.first
310
+ argument_ast = expr.args[vectorized_arg_index]
311
+
312
+ src_info = arg_infos[vectorized_arg_index]
313
+
314
+ return {
315
+ type: :reduction,
316
+ info: {
317
+ function: expr.fn_name,
318
+ source: src_info[:source],
319
+ argument: argument_ast, # << keep AST of the vectorized argument
320
+ flatten_argument_indices: flatten_indices
321
+ }
322
+ }
323
+ end
292
324
 
293
- # Check if this is a structure function that should work on the array as-is
294
- if structure_function?(expr.fn_name)
295
- # Structure functions like size should work on structure as-is (scalar)
296
- { type: :scalar }
325
+ # 4) Structure (non-reducer) functions like `size`
326
+ if is_structure
327
+ # If any arg is itself a PURE reducer call (e.g., size(sum(x))), the inner collapses first ⇒ outer is scalar
328
+ # But dual-nature functions (both reducer AND structure) should be treated as structure functions when nested
329
+ return { type: :scalar } if expr.args.any? do |a|
330
+ if a.is_a?(Kumi::Syntax::CallExpression)
331
+ arg_entry = Kumi::Registry.entry(a.fn_name)
332
+ arg_entry&.reducer && !arg_entry&.structure_function # Pure reducer only
297
333
  else
298
- # This is a vectorized operation - broadcast over elements
299
- { type: :vectorized, info: {
300
- operation: expr.fn_name,
301
- vectorized_args: arg_infos.map.with_index { |info, i| [i, info[:vectorized]] }.to_h
302
- } }
334
+ false
303
335
  end
304
- else
305
- # No vectorized arguments - regular scalar function
306
- { type: :scalar }
307
336
  end
337
+
338
+ # Structure fn over a vectorized element path ⇒ per-parent vectorization
339
+ return { type: :scalar } unless vec_any
340
+
341
+ src_info = arg_infos[vec_idx.first]
342
+ parent_scope = src_info[:parent_scope] || src_info[:source] # fallback if analyzer encodes parent separately
343
+ return {
344
+ type: :vectorized,
345
+ info: {
346
+ operation: expr.fn_name,
347
+ source: src_info[:source],
348
+ parent_scope: parent_scope,
349
+ vectorized_args: vec_idx.to_h { |i| [i, true] }
350
+ }
351
+ }
352
+
353
+ # Structure fn over a scalar/materialized container ⇒ scalar
354
+
308
355
  end
356
+
357
+ # 5) Generic vectorized map (non-structure, non-reducer)
358
+ if vec_any
359
+ # Dimension / source compatibility check
360
+ sources = vec_idx.map { |i| arg_infos[i][:array_source] }.compact.uniq
361
+ if sources.size > 1
362
+ enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, sources)
363
+ report_error(errors, enhanced_message, location: expr.loc, type: :semantic)
364
+ return { type: :scalar } # fail safe to prevent cascading errors
365
+ end
366
+
367
+ return {
368
+ type: :vectorized,
369
+ info: {
370
+ operation: expr.fn_name,
371
+ source: arg_infos[vec_idx.first][:source],
372
+ vectorized_args: vec_idx.to_h { |i| [i, true] }
373
+ }
374
+ }
375
+ end
376
+
377
+ # 6) Pure scalar
378
+ { type: :scalar }
309
379
  end
310
380
 
311
381
  def structure_function?(fn_name)
@@ -337,9 +407,32 @@ module Kumi
337
407
  end
338
408
 
339
409
  when Kumi::Syntax::CallExpression
340
- # Recursively check
410
+ # Recursively check nested call
341
411
  result = analyze_value_vectorization(nil, arg, array_fields, nested_paths, vectorized_values, [], definitions)
342
- { vectorized: result[:type] == :vectorized, source: :expression }
412
+ # Handle different result types appropriately
413
+ case result[:type]
414
+ when :reduction
415
+ # Reductions can produce vectors if they preserve some dimensions
416
+ # This aligns with lower_to_ir logic for grouped reductions
417
+ info = result[:info]
418
+ if info && info[:argument]
419
+ # Check if the reduction argument has array scope that would be preserved
420
+ arg_scope = infer_argument_scope(info[:argument], array_fields, nested_paths)
421
+ if arg_scope.length > 1
422
+ # Multi-dimensional reduction - likely preserves outer dimension (per-player)
423
+ { vectorized: true, source: :grouped_reduction, array_source: arg_scope.first }
424
+ else
425
+ # Single dimension or scalar reduction
426
+ { vectorized: false, source: :scalar_from_reduction }
427
+ end
428
+ else
429
+ { vectorized: false, source: :scalar_from_reduction }
430
+ end
431
+ when :vectorized
432
+ { vectorized: true, source: :expression }
433
+ else
434
+ { vectorized: false, source: :scalar }
435
+ end
343
436
 
344
437
  else
345
438
  { vectorized: false }
@@ -26,7 +26,7 @@ module Kumi
26
26
 
27
27
  def run(errors)
28
28
  definitions = get_state(:declarations)
29
- input_meta = get_state(:inputs)
29
+ input_meta = get_state(:input_metadata)
30
30
 
31
31
  dependency_graph = Hash.new { |h, k| h[k] = [] }
32
32
  reverse_dependencies = Hash.new { |h, k| h[k] = [] }
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ class InputAccessPlannerPass < PassBase
8
+ def run(errors)
9
+ input_metadata = get_state(:input_metadata)
10
+
11
+ options = {
12
+ on_missing: :error,
13
+ key_policy: :indifferent
14
+ }
15
+
16
+ # TODO : Allow by input definition on policies or at least general policy definition
17
+ plans = Kumi::Core::Compiler::AccessPlanner.plan(input_metadata, options)
18
+
19
+ # Quick validation
20
+ validate_plans!(plans, errors)
21
+
22
+ # Create new state with access plans
23
+ state.with(:access_plans, plans.freeze)
24
+ end
25
+
26
+ private
27
+
28
+ def validate_plans!(plans, errors)
29
+ plans.each do |path, plan_list|
30
+ add_error(errors, nil, "No access plans generated for path: #{path}") if plan_list.nil? || plan_list.empty?
31
+
32
+ plan_list&.each do |plan|
33
+ unless plan[:operations].is_a?(Array)
34
+ add_error(errors, nil, "Invalid operations for path #{path}: expected Array, got #{plan[:operations].class}")
35
+ end
36
+
37
+ unless plan[:mode].is_a?(Symbol)
38
+ add_error(errors, nil, "Invalid mode for path #{path}: expected Symbol, got #{plan[:mode].class}")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end