kumi 0.0.9 → 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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +18 -258
  4. data/README.md +188 -121
  5. data/docs/AST.md +1 -1
  6. data/docs/FUNCTIONS.md +52 -8
  7. data/docs/VECTOR_SEMANTICS.md +286 -0
  8. data/docs/compiler_design_principles.md +86 -0
  9. data/docs/features/README.md +15 -2
  10. data/docs/features/hierarchical-broadcasting.md +349 -0
  11. data/docs/features/javascript-transpiler.md +148 -0
  12. data/docs/features/performance.md +1 -3
  13. data/docs/features/s-expression-printer.md +2 -2
  14. data/docs/schema_metadata.md +7 -7
  15. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  16. data/examples/game_of_life.rb +2 -4
  17. data/lib/kumi/analyzer.rb +34 -14
  18. data/lib/kumi/compiler.rb +4 -283
  19. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
  20. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  21. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  22. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
  23. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  24. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  25. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  26. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  27. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  29. data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
  30. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  31. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  32. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
  33. data/lib/kumi/core/analyzer/plans.rb +52 -0
  34. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  35. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  36. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  37. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  38. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  39. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  40. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  41. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  42. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  43. data/lib/kumi/core/compiler_base.rb +137 -0
  44. data/lib/kumi/core/error_reporter.rb +6 -5
  45. data/lib/kumi/core/errors.rb +4 -0
  46. data/lib/kumi/core/explain.rb +157 -205
  47. data/lib/kumi/core/export/node_builders.rb +2 -2
  48. data/lib/kumi/core/export/node_serializers.rb +1 -1
  49. data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
  50. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  51. data/lib/kumi/core/function_registry/function_builder.rb +142 -53
  52. data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
  53. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  54. data/lib/kumi/core/function_registry.rb +138 -98
  55. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  56. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  57. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  58. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  59. data/lib/kumi/core/ir.rb +58 -0
  60. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  61. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  62. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  64. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  65. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  66. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  67. data/lib/kumi/errors.rb +2 -0
  68. data/lib/kumi/js.rb +23 -0
  69. data/lib/kumi/registry.rb +17 -22
  70. data/lib/kumi/runtime/executable.rb +213 -0
  71. data/lib/kumi/schema.rb +15 -4
  72. data/lib/kumi/schema_metadata.rb +2 -2
  73. data/lib/kumi/support/ir_dump.rb +491 -0
  74. data/lib/kumi/support/s_expression_printer.rb +17 -16
  75. data/lib/kumi/syntax/array_expression.rb +6 -6
  76. data/lib/kumi/syntax/call_expression.rb +4 -4
  77. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  78. data/lib/kumi/syntax/case_expression.rb +4 -4
  79. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  80. data/lib/kumi/syntax/hash_expression.rb +4 -4
  81. data/lib/kumi/syntax/input_declaration.rb +6 -5
  82. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  83. data/lib/kumi/syntax/input_reference.rb +5 -5
  84. data/lib/kumi/syntax/literal.rb +4 -4
  85. data/lib/kumi/syntax/location.rb +5 -0
  86. data/lib/kumi/syntax/node.rb +33 -34
  87. data/lib/kumi/syntax/root.rb +6 -6
  88. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  89. data/lib/kumi/syntax/value_declaration.rb +4 -4
  90. data/lib/kumi/version.rb +1 -1
  91. data/lib/kumi.rb +6 -15
  92. data/scripts/analyze_broadcast_methods.rb +68 -0
  93. data/scripts/analyze_cascade_methods.rb +74 -0
  94. data/scripts/check_broadcasting_coverage.rb +51 -0
  95. data/scripts/find_dead_code.rb +114 -0
  96. metadata +36 -9
  97. data/docs/features/array-broadcasting.md +0 -170
  98. data/lib/kumi/cli.rb +0 -449
  99. data/lib/kumi/core/compiled_schema.rb +0 -43
  100. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  101. data/lib/kumi/core/schema_instance.rb +0 -111
  102. data/lib/kumi/core/vectorization_metadata.rb +0 -110
  103. 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
@@ -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,26 @@ 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
+
107
+ # Element mode arrays can only have exactly one direct child
108
+ if input_decl.children.size > 1
109
+ error_msg = "array with access_mode :element can only have one direct child element, " \
110
+ "but found #{input_decl.children.size} children"
111
+ report_error(errors, error_msg, location: input_decl.loc, type: :semantic)
112
+ end
113
+ elsif input_decl.access_mode == :field
114
+ # Object mode allows multiple children
115
+ end
116
+
117
+ # Recursively validate children
118
+ input_decl.children.each { |child| validate_input_declaration(child, errors) }
119
+ end
120
+
93
121
  def boolean_trait_composition?(call_expr)
94
122
  # Allow boolean composition functions that operate on trait collections
95
123
  %i[all? any? none?].include?(call_expr.fn_name)
@@ -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,15 +5,19 @@ 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
9
- # PRODUCES: None (validation only)
8
+ # DEPENDENCIES: :inferred_types from TypeInferencerPass
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
@@ -109,7 +113,7 @@ module Kumi
109
113
 
110
114
  def get_declared_field_type(field_name)
111
115
  # Get explicitly declared type from input metadata
112
- input_meta = get_state(:inputs, required: false) || {}
116
+ input_meta = get_state(:input_metadata, required: false) || {}
113
117
  field_meta = input_meta[field_name]
114
118
  field_meta&.dig(:type) || Kumi::Core::Types::ANY
115
119
  end
@@ -126,7 +130,7 @@ module Kumi
126
130
  "`#{expr.value}` of type #{type} (literal value)"
127
131
 
128
132
  when Kumi::Syntax::InputReference
129
- input_meta = get_state(:inputs, required: false) || {}
133
+ input_meta = get_state(:input_metadata, required: false) || {}
130
134
  field_meta = input_meta[expr.name]
131
135
 
132
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