kumi 0.0.10 → 0.0.12

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +23 -0
  4. data/CLAUDE.md +7 -231
  5. data/README.md +5 -5
  6. data/docs/SYNTAX.md +66 -0
  7. data/docs/VECTOR_SEMANTICS.md +286 -0
  8. data/docs/features/hierarchical-broadcasting.md +67 -1
  9. data/docs/features/input-declaration-system.md +16 -0
  10. data/docs/features/s-expression-printer.md +2 -2
  11. data/lib/kumi/analyzer.rb +34 -12
  12. data/lib/kumi/compiler.rb +2 -12
  13. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
  14. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  15. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  16. data/lib/kumi/core/analyzer/passes/input_collector.rb +123 -101
  17. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  18. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  19. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  20. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  21. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
  22. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  23. data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
  24. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  25. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  26. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
  27. data/lib/kumi/core/analyzer/plans.rb +52 -0
  28. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  29. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  30. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  31. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  32. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  33. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  34. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  35. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  36. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  37. data/lib/kumi/core/compiler_base.rb +2 -2
  38. data/lib/kumi/core/error_reporter.rb +6 -5
  39. data/lib/kumi/core/errors.rb +4 -0
  40. data/lib/kumi/core/explain.rb +157 -205
  41. data/lib/kumi/core/export/node_builders.rb +2 -2
  42. data/lib/kumi/core/export/node_serializers.rb +1 -1
  43. data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
  44. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  45. data/lib/kumi/core/function_registry/function_builder.rb +142 -55
  46. data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
  47. data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
  48. data/lib/kumi/core/function_registry.rb +126 -108
  49. data/lib/kumi/core/input/validator.rb +1 -1
  50. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  51. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  52. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  53. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  54. data/lib/kumi/core/ir.rb +58 -0
  55. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  56. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  57. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
  58. data/lib/kumi/core/ruby_parser/input_builder.rb +30 -9
  59. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  60. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  61. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  62. data/lib/kumi/core/types/validator.rb +1 -1
  63. data/lib/kumi/registry.rb +14 -79
  64. data/lib/kumi/runtime/executable.rb +213 -0
  65. data/lib/kumi/schema.rb +14 -3
  66. data/lib/kumi/schema_metadata.rb +2 -2
  67. data/lib/kumi/support/ir_dump.rb +491 -0
  68. data/lib/kumi/support/s_expression_printer.rb +1 -1
  69. data/lib/kumi/syntax/location.rb +5 -0
  70. data/lib/kumi/syntax/node.rb +0 -1
  71. data/lib/kumi/syntax/root.rb +2 -2
  72. data/lib/kumi/version.rb +1 -1
  73. data/lib/kumi.rb +6 -15
  74. metadata +37 -19
  75. data/lib/kumi/core/cascade_executor_builder.rb +0 -132
  76. data/lib/kumi/core/compiled_schema.rb +0 -43
  77. data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
  78. data/lib/kumi/core/compiler/function_invoker.rb +0 -55
  79. data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
  80. data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
  81. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  82. data/lib/kumi/core/nested_structure_utils.rb +0 -78
  83. data/lib/kumi/core/schema_instance.rb +0 -115
  84. data/lib/kumi/core/vectorized_function_builder.rb +0 -88
  85. data/lib/kumi/js/compiler.rb +0 -878
  86. data/lib/kumi/js/function_registry.rb +0 -333
  87. data/migrate_to_core_iterative.rb +0 -938
@@ -27,10 +27,10 @@ module Kumi
27
27
 
28
28
  attr_reader :schema, :state
29
29
 
30
- # Iterate over all declarations (attributes and traits) in the schema
30
+ # Iterate over all declarations (values and traits) in the schema
31
31
  # @yield [Syntax::Attribute|Syntax::Trait] Each declaration
32
32
  def each_decl(&block)
33
- schema.attributes.each(&block)
33
+ schema.values.each(&block)
34
34
  schema.traits.each(&block)
35
35
  end
36
36
 
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # Plans per-declaration execution scope and join/lift needs.
8
+ # Determines the dimensional scope (array nesting level) for each declaration
9
+ # based on vectorization metadata and input paths.
10
+ #
11
+ # DEPENDENCIES: :declarations, :input_metadata, :broadcasts
12
+ # PRODUCES: :scope_plans, :decl_shapes
13
+ class ScopeResolutionPass < PassBase
14
+ include Kumi::Core::Analyzer::Plans
15
+
16
+ def run(_errors)
17
+ declarations = get_state(:declarations, required: true)
18
+ input_metadata = get_state(:input_metadata, required: true)
19
+ broadcasts = get_state(:broadcasts) || {}
20
+ dependencies = get_state(:dependencies) || {}
21
+
22
+ puts "Available dependencies: #{dependencies.keys.inspect}" if ENV["DEBUG_SCOPE_RESOLUTION"]
23
+
24
+ scope_plans = {}
25
+ decl_shapes = {}
26
+
27
+ initial_scopes = {}
28
+ declarations.each do |name, decl|
29
+ debug_output(name, decl) if ENV["DEBUG_SCOPE_RESOLUTION"]
30
+ target_scope = infer_target_scope(name, decl, broadcasts, input_metadata)
31
+ result_kind = determine_result_kind(name, target_scope, broadcasts)
32
+ initial_scopes[name] = target_scope
33
+ debug_result(target_scope, result_kind) if ENV["DEBUG_SCOPE_RESOLUTION"]
34
+ end
35
+
36
+ final_scopes = propagate_scope_constraints(initial_scopes, declarations, input_metadata)
37
+ final_scopes.each do |name, target_scope|
38
+ result_kind = determine_result_kind(name, target_scope, broadcasts)
39
+ plan = build_scope_plan(target_scope)
40
+ scope_plans[name] = plan
41
+ decl_shapes[name] = { scope: target_scope, result: result_kind }.freeze
42
+ end
43
+
44
+ # Return new state with scope information
45
+ state.with(:scope_plans, scope_plans.freeze)
46
+ .with(:decl_shapes, decl_shapes.freeze)
47
+ end
48
+
49
+ private
50
+
51
+ def propagate_scope_constraints(initial_scopes, declarations, input_metadata)
52
+ scopes = initial_scopes.dup
53
+ puts "\n=== Propagating scope constraints ===" if ENV["DEBUG_SCOPE_RESOLUTION"]
54
+
55
+ declarations.each do |name, decl|
56
+ case decl.expression
57
+ when Kumi::Syntax::ArrayExpression
58
+ propagate_from_array_expression(name, decl.expression, scopes, declarations, input_metadata)
59
+ when Kumi::Syntax::CascadeExpression
60
+ propagate_from_cascade_expression(name, decl.expression, scopes, declarations, input_metadata)
61
+ end
62
+ end
63
+
64
+ puts "Final propagated scopes: #{scopes.inspect}" if ENV["DEBUG_SCOPE_RESOLUTION"]
65
+ scopes
66
+ end
67
+
68
+ def propagate_from_array_expression(name, array_expr, scopes, declarations, input_metadata)
69
+ puts "Analyzing array expression in #{name}: #{array_expr.elements.map(&:class)}" if ENV["DEBUG_SCOPE_RESOLUTION"]
70
+
71
+ anchor_scope = nil
72
+ declaration_refs = []
73
+
74
+ array_expr.elements.each do |element|
75
+ case element
76
+ when Kumi::Syntax::InputElementReference
77
+ path_scope = dims_from_path(element.path, input_metadata)
78
+ puts "Found input anchor: #{element.path} -> scope #{path_scope}" if ENV["DEBUG_SCOPE_RESOLUTION"]
79
+ anchor_scope = path_scope if path_scope.length > (anchor_scope&.length || 0)
80
+ when Kumi::Syntax::DeclarationReference
81
+ declaration_refs << element.name
82
+ end
83
+ end
84
+
85
+ if anchor_scope && !anchor_scope.empty?
86
+ declaration_refs.each do |ref_name|
87
+ current_scope = scopes[ref_name] || []
88
+ if anchor_scope.length > current_scope.length
89
+ puts "Propagating scope #{anchor_scope} to #{ref_name} (was #{current_scope})" if ENV["DEBUG_SCOPE_RESOLUTION"]
90
+ scopes[ref_name] = anchor_scope
91
+ propagate_to_dependencies(ref_name, anchor_scope, scopes, declarations, input_metadata)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def propagate_from_cascade_expression(name, cascade_expr, scopes, declarations, input_metadata)
98
+ puts "Analyzing cascade expression in #{name}" if ENV["DEBUG_SCOPE_RESOLUTION"]
99
+
100
+ # Cascade should propagate its own scope to condition dependencies
101
+ cascade_scope = scopes[name] || []
102
+ return if cascade_scope.empty?
103
+
104
+ puts "Propagating cascade scope #{cascade_scope} to condition dependencies" if ENV["DEBUG_SCOPE_RESOLUTION"]
105
+
106
+ cascade_expr.cases.each do |case_expr|
107
+ find_declaration_references(case_expr.condition).each do |ref_name|
108
+ current_scope = scopes[ref_name] || []
109
+ if cascade_scope.length > current_scope.length
110
+ puts "Propagating scope #{cascade_scope} to cascade condition #{ref_name} (was #{current_scope})" if ENV["DEBUG_SCOPE_RESOLUTION"]
111
+ scopes[ref_name] = cascade_scope
112
+ propagate_to_dependencies(ref_name, cascade_scope, scopes, declarations, input_metadata)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def propagate_to_dependencies(decl_name, required_scope, scopes, declarations, input_metadata)
119
+ return unless declarations[decl_name]
120
+
121
+ decl = declarations[decl_name]
122
+ puts "Propagating #{required_scope} into dependencies of #{decl_name}" if ENV["DEBUG_SCOPE_RESOLUTION"]
123
+
124
+ case decl.expression
125
+ when Kumi::Syntax::CascadeExpression
126
+ decl.expression.cases.each do |case_expr|
127
+ find_declaration_references(case_expr.condition).each do |ref_name|
128
+ current_scope = scopes[ref_name] || []
129
+ if required_scope.length > current_scope.length
130
+ puts "Propagating scope #{required_scope} to trait dependency #{ref_name}" if ENV["DEBUG_SCOPE_RESOLUTION"]
131
+ scopes[ref_name] = required_scope
132
+ update_reduction_scope_if_needed(ref_name, required_scope, declarations, input_metadata)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def find_declaration_references(expr)
140
+ refs = []
141
+ case expr
142
+ when Kumi::Syntax::DeclarationReference
143
+ refs << expr.name
144
+ when Kumi::Syntax::CallExpression
145
+ expr.args.each { |arg| refs.concat(find_declaration_references(arg)) }
146
+ when Kumi::Syntax::ArrayExpression
147
+ expr.elements.each { |elem| refs.concat(find_declaration_references(elem)) }
148
+ end
149
+ refs
150
+ end
151
+
152
+ def update_reduction_scope_if_needed(decl_name, required_scope, declarations, input_metadata)
153
+ decl = declarations[decl_name]
154
+ return unless decl
155
+ puts "Checking if #{decl_name} needs reduction scope update for #{required_scope}" if ENV["DEBUG_SCOPE_RESOLUTION"]
156
+ end
157
+
158
+ def debug_output(name, decl)
159
+ puts "\n=== Resolving scope for #{name} ==="
160
+ puts "Declaration: #{decl.inspect}"
161
+ end
162
+
163
+ def debug_result(target_scope, result_kind)
164
+ puts "Target scope: #{target_scope.inspect}"
165
+ puts "Result kind: #{result_kind.inspect}"
166
+ end
167
+
168
+ def build_scope_plan(target_scope)
169
+ Scope.new(
170
+ scope: target_scope,
171
+ lifts: [], # Will be computed during IR lowering per call-site
172
+ join_hint: nil, # Will be set to :zip when multiple vectorized args exist
173
+ arg_shapes: {} # Optional: filled during lowering
174
+ )
175
+ end
176
+
177
+ def determine_result_kind(name, target_scope, broadcasts)
178
+ return :scalar if broadcasts.dig(:reduction_operations, name)
179
+ return :scalar if target_scope.empty?
180
+
181
+ { array: :dense }
182
+ end
183
+
184
+ # Derive scope from vectorization metadata or from deepest input path
185
+ def infer_target_scope(name, decl, broadcasts, input_metadata)
186
+ # First check vectorized operations
187
+ vec = broadcasts.dig(:vectorized_operations, name)
188
+ if vec
189
+ puts "Vectorization info: #{vec.inspect}" if ENV["DEBUG_SCOPE_RESOLUTION"]
190
+
191
+ case vec[:source]
192
+ when :nested_array_access, :array_field_access
193
+ path = vec[:path] || []
194
+ return dims_from_path(path, input_metadata)
195
+ when :cascade_with_vectorized_conditions_or_results,
196
+ :cascade_condition_with_vectorized_trait
197
+ # Fallback: derive from first input path seen in expression
198
+ path = find_first_input_path(decl.expression) || []
199
+ return dims_from_path(path, input_metadata)
200
+ else
201
+ return []
202
+ end
203
+ end
204
+
205
+ # Check if this is a reduction operation that should preserve some scope
206
+ red = broadcasts.dig(:reduction_operations, name)
207
+ if red
208
+ puts "Reduction info: #{red.inspect}" if ENV["DEBUG_SCOPE_RESOLUTION"]
209
+
210
+ # Infer the natural scope for this reduction
211
+ # For expressions like fn(:any?, input.players.score_matrices.session.points > 1000)
212
+ # we want to reduce over session dimension but preserve the players dimension
213
+ scope = infer_reduction_target_scope(decl.expression, input_metadata)
214
+ puts "Inferred reduction target scope: #{scope.inspect}" if ENV["DEBUG_SCOPE_RESOLUTION"]
215
+ return scope
216
+ end
217
+
218
+ return []
219
+ end
220
+
221
+ def infer_reduction_target_scope(expr, input_metadata)
222
+ # For reduction expressions, we need to analyze the argument to the reducer
223
+ # and determine which dimensions should be preserved vs reduced
224
+ case expr
225
+ when Kumi::Syntax::CallExpression
226
+ if reducer_function?(expr.fn_name)
227
+ # Find the argument being reduced
228
+ arg = expr.args.first
229
+ if arg
230
+ # Get the full scope from the argument
231
+ full_scope = infer_scope_from_argument(arg, input_metadata)
232
+
233
+ # For array reductions, we typically want to preserve
234
+ # the outermost dimension (e.g., keep :players, reduce :score_matrices/:session)
235
+ if full_scope.length > 1
236
+ return full_scope[0..0] # Keep only the first dimension
237
+ end
238
+ end
239
+ else
240
+ # Recursively check if any argument contains a reducer
241
+ # This handles cases like (fn(:sum, ...) >= 3500)
242
+ expr.args.each do |arg|
243
+ nested_scope = infer_reduction_target_scope(arg, input_metadata)
244
+ return nested_scope if !nested_scope.empty?
245
+ end
246
+ end
247
+ end
248
+ []
249
+ end
250
+
251
+ def reducer_function?(fn_name)
252
+ entry = Kumi::Registry.entry(fn_name)
253
+ entry&.reducer == true
254
+ end
255
+
256
+ def infer_scope_from_argument(arg, input_metadata)
257
+ case arg
258
+ when Kumi::Syntax::InputElementReference
259
+ dims_from_path(arg.path, input_metadata)
260
+ when Kumi::Syntax::InputReference
261
+ dims_from_path([arg.name], input_metadata)
262
+ when Kumi::Syntax::CallExpression
263
+ # For expressions like (input.players.score_matrices.session.points > 1000),
264
+ # we need to find the deepest input path
265
+ deepest_path = find_deepest_input_path(arg)
266
+ deepest_path ? dims_from_path(deepest_path, input_metadata) : []
267
+ else
268
+ []
269
+ end
270
+ end
271
+
272
+ def find_deepest_input_path(expr)
273
+ paths = collect_input_paths(expr)
274
+ paths.max_by(&:length)
275
+ end
276
+
277
+ def collect_input_paths(expr)
278
+ paths = []
279
+ case expr
280
+ when Kumi::Syntax::InputElementReference
281
+ paths << expr.path
282
+ when Kumi::Syntax::InputReference
283
+ paths << [expr.name]
284
+ when Kumi::Syntax::CallExpression
285
+ expr.args.each { |arg| paths.concat(collect_input_paths(arg)) }
286
+ when Kumi::Syntax::ArrayExpression
287
+ expr.elements.each { |elem| paths.concat(collect_input_paths(elem)) }
288
+ end
289
+ paths
290
+ end
291
+
292
+ def find_first_input_path(expr)
293
+ return nil unless expr
294
+
295
+ # Handle InputElementReference directly
296
+ return expr.path if expr.is_a?(Kumi::Syntax::InputElementReference)
297
+
298
+ # Handle InputReference (convert to path array)
299
+ return [expr.name] if expr.is_a?(Kumi::Syntax::InputReference)
300
+
301
+ # Recursively search in CallExpression arguments
302
+ if expr.is_a?(Kumi::Syntax::CallExpression) && expr.args
303
+ expr.args.each do |arg|
304
+ path = find_first_input_path(arg)
305
+ return path if path
306
+ end
307
+ end
308
+
309
+ # Search in CascadeExpression cases
310
+ if expr.is_a?(Kumi::Syntax::CascadeExpression) && expr.cases
311
+ expr.cases.each do |case_item|
312
+ path = find_first_input_path(case_item.condition)
313
+ return path if path
314
+
315
+ path = find_first_input_path(case_item.result)
316
+ return path if path
317
+ end
318
+ end
319
+
320
+ # Search in expression field if present
321
+ return find_first_input_path(expr.expression) if expr.respond_to?(:expression)
322
+
323
+ nil
324
+ end
325
+
326
+ # Map an input path like [:regions, :offices, :salary] to container dims [:regions, :offices]
327
+ def dims_from_path(path, input_metadata)
328
+ dims = []
329
+ meta = input_metadata
330
+
331
+ path.each do |seg|
332
+ field = meta[seg] || meta[seg.to_sym] || meta[seg.to_s]
333
+ break unless field
334
+
335
+ dims << seg.to_sym if field[:type] == :array
336
+
337
+ meta = field[:children] || {}
338
+ end
339
+
340
+ dims
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
@@ -103,13 +103,14 @@ module Kumi
103
103
 
104
104
  # Validate that access_mode is consistent with children structure
105
105
  if input_decl.access_mode == :element
106
+
106
107
  # Element mode arrays can only have exactly one direct child
107
108
  if input_decl.children.size > 1
108
109
  error_msg = "array with access_mode :element can only have one direct child element, " \
109
110
  "but found #{input_decl.children.size} children"
110
111
  report_error(errors, error_msg, location: input_decl.loc, type: :semantic)
111
112
  end
112
- elsif input_decl.access_mode == :object
113
+ elsif input_decl.access_mode == :field
113
114
  # Object mode allows multiple children
114
115
  end
115
116
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pry"
3
4
  module Kumi
4
5
  module Core
5
6
  module Analyzer
@@ -34,7 +35,6 @@ module Kumi
34
35
  return if safe_conditional_cycle?(cycle_path, graph, cascades)
35
36
 
36
37
  # Allow this cycle - it's safe due to cascade mutual exclusion
37
-
38
38
  report_unexpected_cycle(temp_marks, node, errors)
39
39
 
40
40
  return
@@ -42,7 +42,13 @@ module Kumi
42
42
 
43
43
  temp_marks << node
44
44
  current_path = path + [node]
45
- Array(graph[node]).each { |edge| visit_node.call(edge.to, current_path) }
45
+ # Only follow edges to other declarations, not to input fields
46
+ # This prevents false cycles when a declaration has the same name as an input
47
+ Array(graph[node]).each do |edge|
48
+ next if edge.type == :key # Skip input field dependencies
49
+
50
+ visit_node.call(edge.to, current_path)
51
+ end
46
52
  temp_marks.delete(node)
47
53
  perm_marks << node
48
54
 
@@ -100,7 +106,7 @@ module Kumi
100
106
  def find_declaration_by_name(name)
101
107
  return nil unless schema
102
108
 
103
- schema.attributes.find { |attr| attr.name == name } ||
109
+ schema.values.find { |attr| attr.name == name } ||
104
110
  schema.traits.find { |trait| trait.name == name }
105
111
  end
106
112
  end
@@ -5,7 +5,7 @@ module Kumi
5
5
  module Analyzer
6
6
  module Passes
7
7
  # RESPONSIBILITY: Validate function call arity and argument types against FunctionRegistry
8
- # DEPENDENCIES: :inferred_types from TypeInferencer
8
+ # DEPENDENCIES: :inferred_types from TypeInferencerPass
9
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
@@ -113,7 +113,7 @@ module Kumi
113
113
 
114
114
  def get_declared_field_type(field_name)
115
115
  # Get explicitly declared type from input metadata
116
- input_meta = get_state(:inputs, required: false) || {}
116
+ input_meta = get_state(:input_metadata, required: false) || {}
117
117
  field_meta = input_meta[field_name]
118
118
  field_meta&.dig(:type) || Kumi::Core::Types::ANY
119
119
  end
@@ -130,7 +130,7 @@ module Kumi
130
130
  "`#{expr.value}` of type #{type} (literal value)"
131
131
 
132
132
  when Kumi::Syntax::InputReference
133
- input_meta = get_state(:inputs, required: false) || {}
133
+ input_meta = get_state(:input_metadata, required: false) || {}
134
134
  field_meta = input_meta[expr.name]
135
135
 
136
136
  if field_meta&.dig(:type)
@@ -5,12 +5,12 @@ module Kumi
5
5
  module Analyzer
6
6
  module Passes
7
7
  # RESPONSIBILITY: Validate consistency between declared and inferred types
8
- # DEPENDENCIES: :inputs from InputCollector, :inferred_types from TypeInferencer
8
+ # DEPENDENCIES: :input_metadata from InputCollector, :inferred_types from TypeInferencerPass
9
9
  # PRODUCES: None (validation only)
10
10
  # INTERFACE: new(schema, state).run(errors)
11
11
  class TypeConsistencyChecker < PassBase
12
12
  def run(errors)
13
- input_meta = get_state(:inputs, required: false) || {}
13
+ input_meta = get_state(:input_metadata, required: false) || {}
14
14
 
15
15
  # First, validate that all declared types are valid
16
16
  validate_declared_types(input_meta, errors)
@@ -8,7 +8,7 @@ module Kumi
8
8
  # DEPENDENCIES: Toposorter (needs evaluation_order), DeclarationValidator (needs declarations)
9
9
  # PRODUCES: inferred_types hash mapping declaration names to inferred types
10
10
  # INTERFACE: new(schema, state).run(errors)
11
- class TypeInferencer < PassBase
11
+ class TypeInferencerPass < PassBase
12
12
  def run(errors)
13
13
  types = {}
14
14
  topo_order = get_state(:evaluation_order)
@@ -49,7 +49,7 @@ module Kumi
49
49
  Types.infer_from_value(expr.value)
50
50
  when InputReference
51
51
  # Look up type from field metadata
52
- input_meta = get_state(:inputs, required: false) || {}
52
+ input_meta = get_state(:input_metadata, required: false) || {}
53
53
  meta = input_meta[expr.name]
54
54
  meta&.dig(:type) || :any
55
55
  when DeclarationReference
@@ -156,7 +156,7 @@ module Kumi
156
156
  case expr
157
157
  when InputElementReference
158
158
  # Get the field type from metadata
159
- input_meta = get_state(:inputs, required: false) || {}
159
+ input_meta = get_state(:input_metadata, required: false) || {}
160
160
  array_name = expr.path.first
161
161
  field_name = expr.path[1]
162
162
 
@@ -198,7 +198,7 @@ module Kumi
198
198
 
199
199
  def infer_element_reference_type(expr)
200
200
  # Get array field metadata
201
- input_meta = get_state(:inputs, required: false) || {}
201
+ input_meta = get_state(:input_metadata, required: false) || {}
202
202
 
203
203
  return :any unless expr.path.size >= 2
204
204
 
@@ -5,7 +5,7 @@ module Kumi
5
5
  module Analyzer
6
6
  module Passes
7
7
  # RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
8
- # DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
8
+ # DEPENDENCIES: :declarations from NameIndexer, :input_metadata from InputCollector
9
9
  # PRODUCES: :cascades - Hash of cascade mutual exclusion analysis results
10
10
  # INTERFACE: new(schema, state).run(errors)
11
11
  class UnsatDetector < VisitorPass
@@ -16,7 +16,7 @@ module Kumi
16
16
 
17
17
  def run(errors)
18
18
  definitions = get_state(:declarations)
19
- @input_meta = get_state(:inputs) || {}
19
+ @input_meta = get_state(:input_metadata) || {}
20
20
  @definitions = definitions
21
21
  @evaluator = ConstantEvaluator.new(definitions)
22
22
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ # Typed plan structures for HIR (High-level Intermediate Representation)
7
+ # These plans are produced by analyzer passes and consumed by LowerToIRPass
8
+ # to generate LIR (Low-level IR) operations.
9
+ module Plans
10
+ # Scope plan: defines the dimensional execution context for a declaration
11
+ Scope = Struct.new(:scope, :lifts, :join_hint, :arg_shapes, keyword_init: true) do
12
+ def initialize(scope: [], lifts: [], join_hint: nil, arg_shapes: {})
13
+ super
14
+ freeze
15
+ end
16
+
17
+ def depth
18
+ scope.size
19
+ end
20
+
21
+ def scalar?
22
+ scope.empty?
23
+ end
24
+ end
25
+
26
+ # Join plan: defines how to align multiple arguments at a target scope
27
+ Join = Struct.new(:policy, :target_scope, keyword_init: true) do
28
+ def initialize(policy: :zip, target_scope: [])
29
+ super
30
+ freeze
31
+ end
32
+ end
33
+
34
+ # Reduce plan: defines how to reduce dimensions in array operations
35
+ Reduce = Struct.new(:function, :axis, :source_scope, :result_scope, :flatten_args, keyword_init: true) do
36
+ def initialize(function:, axis: [], source_scope: [], result_scope: [], flatten_args: [])
37
+ super
38
+ freeze
39
+ end
40
+
41
+ def total_reduction?
42
+ axis == :all || result_scope.empty?
43
+ end
44
+
45
+ def partial_reduction?
46
+ !total_reduction?
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ # One plan for a specific path and mode (path:mode)
7
+ AccessPlan = Struct.new(:path, :containers, :leaf, :scope, :depth, :mode,
8
+ :on_missing, :key_policy, :operations, keyword_init: true) do
9
+ def initialize(path:, containers:, leaf:, scope:, depth:, mode:, on_missing:, key_policy:, operations:)
10
+ super
11
+ freeze
12
+ end
13
+
14
+ def accessor_key = "#{path}:#{mode}"
15
+ def ndims = depth
16
+ def scalar? = depth.zero?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Structs
7
+ # Represents metadata for a single input field produced by InputCollector
8
+ InputMeta = Struct.new(
9
+ :type,
10
+ :domain,
11
+ :container,
12
+ :access_mode,
13
+ :enter_via,
14
+ :consume_alias,
15
+ :children,
16
+ keyword_init: true
17
+ ) do
18
+ def deep_freeze!
19
+ if children
20
+ children.each_value(&:deep_freeze!)
21
+ children.freeze
22
+ end
23
+ freeze
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ module Kumi
2
+ module Core
3
+ module Compiler
4
+ class AccessBuilder
5
+ def self.build(plans)
6
+ accessors = {}
7
+ plans.each_value do |variants|
8
+ variants.each do |plan|
9
+ key = plan.respond_to?(:accessor_key) ? plan.accessor_key : "#{plan.path}:#{mode}"
10
+ accessors[key] = build_proc_for(
11
+ mode: plan.mode,
12
+ path_key: plan.path,
13
+ missing: (plan.on_missing || :error).to_sym,
14
+ key_policy: (plan.key_policy || :indifferent).to_sym,
15
+ operations: plan.operations
16
+ )
17
+ end
18
+ end
19
+ accessors.freeze
20
+ end
21
+
22
+ def self.build_proc_for(mode:, path_key:, missing:, key_policy:, operations:)
23
+ case mode
24
+ when :read then Accessors::ReadAccessor.build(operations, path_key, missing, key_policy)
25
+ when :materialize then Accessors::MaterializeAccessor.build(operations, path_key, missing, key_policy)
26
+ when :ravel then Accessors::RavelAccessor.build(operations, path_key, missing, key_policy)
27
+ when :each_indexed then Accessors::EachIndexedAccessor.build(operations, path_key, missing, key_policy, true)
28
+ when :each then Accessors::EachAccessor.build(operations, path_key, missing, key_policy)
29
+ else
30
+ raise "Unknown accessor mode: #{mode.inspect}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end