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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +18 -258
- data/README.md +188 -121
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/features/s-expression-printer.md +2 -2
- data/docs/schema_metadata.md +7 -7
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +34 -14
- data/lib/kumi/compiler.rb +4 -283
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +137 -0
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -53
- data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +138 -98
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/errors.rb +2 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +17 -22
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +15 -4
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +17 -16
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +33 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +36 -9
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/schema_instance.rb +0 -111
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
- 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 (
|
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.
|
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
|
-
|
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.
|
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
|
9
|
-
# PRODUCES:
|
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
|
-
|
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(:
|
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(:
|
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: :
|
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(:
|
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
|
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(:
|
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(:
|
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(:
|
201
|
+
input_meta = get_state(:input_metadata, required: false) || {}
|
202
202
|
|
203
203
|
return :any unless expr.path.size >= 2
|
204
204
|
|