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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +7 -231
- data/README.md +1 -1
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/features/hierarchical-broadcasting.md +1 -1
- data/docs/features/s-expression-printer.md +2 -2
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/lib/kumi/analyzer.rb +34 -12
- data/lib/kumi/compiler.rb +2 -12
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
- 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 -101
- 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 +2 -1
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
- 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 +2 -2
- 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 +2 -2
- 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 +21 -10
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -55
- data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
- data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
- data/lib/kumi/core/function_registry.rb +126 -108
- 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 +36 -15
- data/lib/kumi/core/ruby_parser/input_builder.rb +5 -5
- 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/registry.rb +14 -79
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +14 -3
- 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 +1 -1
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +0 -1
- data/lib/kumi/syntax/root.rb +2 -2
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- metadata +26 -15
- data/lib/kumi/core/cascade_executor_builder.rb +0 -132
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
- data/lib/kumi/core/compiler/function_invoker.rb +0 -55
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
- data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/nested_structure_utils.rb +0 -78
- data/lib/kumi/core/schema_instance.rb +0 -115
- data/lib/kumi/core/vectorized_function_builder.rb +0 -88
- data/lib/kumi/js/compiler.rb +0 -878
- data/lib/kumi/js/function_registry.rb +0 -333
- 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
|
@@ -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 == :
|
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
|
-
|
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,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
|
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(:
|
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(:
|
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: :
|
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
|
|
@@ -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, :
|
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(:
|
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
|