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
@@ -0,0 +1,993 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../../support/ir_dump"
|
4
|
+
|
5
|
+
module Kumi
|
6
|
+
module Core
|
7
|
+
module Analyzer
|
8
|
+
module Passes
|
9
|
+
# Lowers analyzed AST into Low-level IR for the VM
|
10
|
+
#
|
11
|
+
# INPUTS (from Analyzer state):
|
12
|
+
# - :evaluation_order → topologically sorted declaration names
|
13
|
+
# - :declarations → parsed & validated AST nodes
|
14
|
+
# - :access_plans → AccessPlanner output (plan id, scope, depth, mode, operations)
|
15
|
+
# - :join_reduce_plans → (optional) precomputed join/reduce strategy
|
16
|
+
# - :input_metadata → normalized InputMeta tree
|
17
|
+
# - :scope_plans → (optional) per-declaration scope hints
|
18
|
+
#
|
19
|
+
# OUTPUT:
|
20
|
+
# - :ir_module (Kumi::Core::IR::Module) with IR decls
|
21
|
+
#
|
22
|
+
# RESPONSIBILITIES:
|
23
|
+
# 1) Plan selection & LoadInput emission
|
24
|
+
# - Choose the correct plan id for each input path:
|
25
|
+
# * :read → scalar fetch (no element traversal)
|
26
|
+
# * :ravel → leaf values, flattened over path’s array lineage
|
27
|
+
# * :each_indexed → vector fetch with hierarchical indices (for lineage)
|
28
|
+
# * :materialize → preserves nested structure (used when required)
|
29
|
+
# - Set LoadInput attrs: scope:, is_scalar:, has_idx:
|
30
|
+
# * is_scalar = (mode == :read || mode == :materialize)
|
31
|
+
# * has_idx = (mode == :each_indexed)
|
32
|
+
#
|
33
|
+
# 2) Shape tracking (SlotShape)
|
34
|
+
# - Track the kind of every slot: Scalar vs Vec(scope, has_idx)
|
35
|
+
# - Used to:
|
36
|
+
# * decide when to emit AlignTo (automatic vector alignment)
|
37
|
+
# * decide if the declaration needs a twin (:name__vec) + Lift
|
38
|
+
#
|
39
|
+
# 3) Automatic alignment for elementwise maps
|
40
|
+
# - For Map(fn, args...):
|
41
|
+
# * Find the carrier vector (max scope length among :elem params)
|
42
|
+
# * If another arg is a vector with a compatible prefix scope, insert AlignTo(to_scope: carrier_scope)
|
43
|
+
# * If incompatible, raise "cross-scope map without join"
|
44
|
+
#
|
45
|
+
# 4) Reducers & structure functions
|
46
|
+
# - Reducers (e.g., sum, min, max, avg, count_if) lower to Reduce on the first vector arg
|
47
|
+
# - Structure functions (e.g., size, flatten) are executed via Map unless explicitly marked reducer
|
48
|
+
#
|
49
|
+
# 5) Cascades & traits
|
50
|
+
# - CascadeExpression lowered to nested `if` (switch) form
|
51
|
+
# - Trait algebra remains purely boolean; no VM special-casing
|
52
|
+
#
|
53
|
+
# 6) Declaration twins & Lift
|
54
|
+
# - If the final expression shape is a Vec with indices:
|
55
|
+
# * Store to :name__vec (internal twin)
|
56
|
+
# * Emit Lift(to_scope: vec.scope) to regroup rows into nested arrays
|
57
|
+
# * Store lifted scalar to :name
|
58
|
+
# - If scalar result: store only :name
|
59
|
+
# - `DeclarationReference` resolves to :name__vec when a vector is required downstream
|
60
|
+
#
|
61
|
+
# INVARIANTS:
|
62
|
+
# - All structural intent (vectorization, grouping, alignment) is decided during lowering.
|
63
|
+
# - VM is mechanical; it does not sniff types or infer structure.
|
64
|
+
# - AlignTo requires prefix-compatible scopes; otherwise we error early.
|
65
|
+
# - Lift consumes a Vec with indices and returns a Scalar(nested_array) by grouping rows with `group_rows`.
|
66
|
+
#
|
67
|
+
# DEBUGGING:
|
68
|
+
# - Set DEBUG_LOWER=1 to print per-declaration IR ops and alignment decisions.
|
69
|
+
SlotShape = Struct.new(:kind, :scope, :has_idx) do
|
70
|
+
def self.scalar
|
71
|
+
new(:scalar, [], false)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.vec(scope, has_idx: true)
|
75
|
+
new(:vec, Array(scope), has_idx)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class LowerToIRPass < PassBase
|
80
|
+
def run(errors)
|
81
|
+
@vec_names = Set.new
|
82
|
+
@vec_meta = {}
|
83
|
+
|
84
|
+
evaluation_order = get_state(:evaluation_order, required: true)
|
85
|
+
declarations = get_state(:declarations, required: true)
|
86
|
+
access_plans = get_state(:access_plans, required: true)
|
87
|
+
join_reduce_plans = get_state(:join_reduce_plans, required: false) || {}
|
88
|
+
input_metadata = get_state(:input_metadata, required: true)
|
89
|
+
scope_plans = get_state(:scope_plans, required: false) || {}
|
90
|
+
|
91
|
+
ir_decls = []
|
92
|
+
|
93
|
+
@join_reduce_plans = join_reduce_plans
|
94
|
+
@declarations = declarations
|
95
|
+
|
96
|
+
evaluation_order.each do |name|
|
97
|
+
decl = declarations[name]
|
98
|
+
next unless decl
|
99
|
+
|
100
|
+
begin
|
101
|
+
scope_plan = scope_plans[name]
|
102
|
+
@current_decl = name
|
103
|
+
@lower_cache = {} # reset per declaration
|
104
|
+
|
105
|
+
ir_decl = lower_declaration(name, decl, access_plans, join_reduce_plans, scope_plan)
|
106
|
+
ir_decls << ir_decl
|
107
|
+
rescue StandardError => e
|
108
|
+
location = decl.respond_to?(:loc) ? decl.loc : nil
|
109
|
+
backtrace = e.backtrace.first(5).join("\n")
|
110
|
+
message = "Failed to lower declaration #{name}: #{e.message}\n#{backtrace}"
|
111
|
+
add_error(errors, location, "Failed to lower declaration #{name}: #{message}")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
if ENV["DEBUG_LOWER"]
|
116
|
+
puts "DEBUG eval order: #{evaluation_order.inspect}"
|
117
|
+
puts "DEBUG ir decl order: #{ir_decls.map(&:name).inspect}"
|
118
|
+
end
|
119
|
+
order_index = evaluation_order.each_with_index.to_h
|
120
|
+
ir_decls.sort_by! { |d| order_index.fetch(d.name, Float::INFINITY) }
|
121
|
+
|
122
|
+
ir_module = Kumi::Core::IR::Module.new(
|
123
|
+
inputs: input_metadata,
|
124
|
+
decls: ir_decls
|
125
|
+
)
|
126
|
+
|
127
|
+
if ENV["DEBUG_LOWER"]
|
128
|
+
puts "DEBUG Lowered IR Module:"
|
129
|
+
ir_module.decls.each do |decl|
|
130
|
+
puts " Declaration: #{decl.name} (#{decl.kind})"
|
131
|
+
decl.ops.each_with_index do |op, i|
|
132
|
+
puts " Op#{i}: #{op.tag} #{op.attrs.inspect} args=#{op.args.inspect}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if ENV["DUMP_IR"]
|
138
|
+
# Collect analysis state that this pass actually uses
|
139
|
+
analysis_state = {
|
140
|
+
evaluation_order: evaluation_order,
|
141
|
+
declarations: declarations,
|
142
|
+
# access_plans: access_plans,
|
143
|
+
join_reduce_plans: join_reduce_plans,
|
144
|
+
# scope_plans: scope_plans,
|
145
|
+
input_metadata: input_metadata,
|
146
|
+
vec_names: @vec_names,
|
147
|
+
vec_meta: @vec_meta,
|
148
|
+
inferred_types: get_state(:inferred_types, required: false) || {}
|
149
|
+
}
|
150
|
+
|
151
|
+
pretty_ir = Kumi::Support::IRDump.pretty_print(ir_module, analysis_state: analysis_state)
|
152
|
+
File.write(ENV["DUMP_IR"], pretty_ir)
|
153
|
+
puts "DEBUG IR dumped to #{ENV['DUMP_IR']}"
|
154
|
+
end
|
155
|
+
|
156
|
+
state.with(:ir_module, ir_module)
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def determine_slot_shape(slot, ops, access_plans)
|
162
|
+
return SlotShape.scalar if slot.nil?
|
163
|
+
|
164
|
+
op = ops[slot]
|
165
|
+
|
166
|
+
case op.tag
|
167
|
+
when :const
|
168
|
+
SlotShape.scalar
|
169
|
+
|
170
|
+
when :load_input
|
171
|
+
if op.attrs[:is_scalar]
|
172
|
+
SlotShape.scalar
|
173
|
+
else
|
174
|
+
plan_id = op.attrs[:plan_id]
|
175
|
+
plan = access_plans.values.flatten.find { |p| p.accessor_key == plan_id }
|
176
|
+
SlotShape.vec(op.attrs[:scope] || [], has_idx: plan&.mode == :each_indexed)
|
177
|
+
end
|
178
|
+
|
179
|
+
when :array
|
180
|
+
arg_shapes = op.args.map { |i| determine_slot_shape(i, ops, access_plans) }
|
181
|
+
return SlotShape.scalar if arg_shapes.all? { |s| s.kind == :scalar }
|
182
|
+
|
183
|
+
carrier = arg_shapes.select { |s| s.kind == :vec }.max_by { |s| s.scope.length }
|
184
|
+
SlotShape.vec(carrier.scope, has_idx: carrier.has_idx)
|
185
|
+
|
186
|
+
when :map
|
187
|
+
arg_shapes = op.args.map { |i| determine_slot_shape(i, ops, access_plans) }
|
188
|
+
return SlotShape.scalar if arg_shapes.all? { |s| s.kind == :scalar }
|
189
|
+
|
190
|
+
carrier = arg_shapes.select { |s| s.kind == :vec }.max_by { |s| s.scope.length }
|
191
|
+
SlotShape.vec(carrier.scope, has_idx: carrier.has_idx)
|
192
|
+
|
193
|
+
when :align_to
|
194
|
+
SlotShape.vec(op.attrs[:to_scope], has_idx: true)
|
195
|
+
|
196
|
+
when :reduce
|
197
|
+
rs = Array(op.attrs[:result_scope] || [])
|
198
|
+
rs.empty? ? SlotShape.scalar : SlotShape.vec(rs, has_idx: true)
|
199
|
+
|
200
|
+
when :lift
|
201
|
+
SlotShape.scalar # lift groups to nested Ruby arrays
|
202
|
+
when :switch
|
203
|
+
branch_shapes =
|
204
|
+
op.attrs[:cases].map { |(_, v)| determine_slot_shape(v, ops, access_plans) } +
|
205
|
+
[determine_slot_shape(op.attrs[:default], ops, access_plans)]
|
206
|
+
if (vec = branch_shapes.find { |s| s.kind == :vec })
|
207
|
+
SlotShape.vec(vec.scope, has_idx: vec.has_idx)
|
208
|
+
else
|
209
|
+
SlotShape.scalar
|
210
|
+
end
|
211
|
+
|
212
|
+
when :ref
|
213
|
+
if (m = @vec_meta && @vec_meta[op.attrs[:name]])
|
214
|
+
SlotShape.vec(m[:scope], has_idx: m[:has_idx])
|
215
|
+
else
|
216
|
+
SlotShape.scalar
|
217
|
+
end
|
218
|
+
|
219
|
+
else
|
220
|
+
SlotShape.scalar
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def insert_align_to_if_needed(arg_slots, ops, access_plans, on_missing: :error)
|
225
|
+
shapes = arg_slots.map { |s| determine_slot_shape(s, ops, access_plans) }
|
226
|
+
|
227
|
+
vec_is = arg_slots.each_index.select { |i| shapes[i].kind == :vec }
|
228
|
+
return arg_slots if vec_is.size < 2
|
229
|
+
|
230
|
+
carrier_i = vec_is.max_by { |i| shapes[i].scope.length }
|
231
|
+
carrier_scope = shapes[carrier_i].scope
|
232
|
+
carrier_slot = arg_slots[carrier_i]
|
233
|
+
|
234
|
+
aligned = arg_slots.dup
|
235
|
+
vec_is.each do |i|
|
236
|
+
next if shapes[i].scope == carrier_scope
|
237
|
+
|
238
|
+
short, long = [shapes[i].scope, carrier_scope].sort_by(&:length)
|
239
|
+
unless long.first(short.length) == short
|
240
|
+
raise "cross-scope map without join: #{shapes[i].scope.inspect} vs #{carrier_scope.inspect}"
|
241
|
+
end
|
242
|
+
|
243
|
+
src_slot = aligned[i] # <- chain on the current slot, not the original
|
244
|
+
op = Kumi::Core::IR::Ops.AlignTo(
|
245
|
+
carrier_slot, src_slot,
|
246
|
+
to_scope: carrier_scope, require_unique: true, on_missing: on_missing
|
247
|
+
)
|
248
|
+
ops << op
|
249
|
+
aligned[i] = ops.size - 1
|
250
|
+
end
|
251
|
+
|
252
|
+
aligned
|
253
|
+
end
|
254
|
+
|
255
|
+
def apply_scalar_to_vector_broadcast(scalar_slot, target_scope, ops, access_plans)
|
256
|
+
# Create a carrier vector at the target scope by loading the appropriate input
|
257
|
+
# For scope [:items], we need to load the items array to get the vector shape
|
258
|
+
if target_scope.empty?
|
259
|
+
puts "DEBUG: Empty target scope, returning scalar" if ENV["DEBUG_BROADCAST"]
|
260
|
+
return scalar_slot # Can't broadcast to empty scope
|
261
|
+
end
|
262
|
+
|
263
|
+
# Create a load operation for the target scope to get vector shape
|
264
|
+
# For [:items] scope, load the items array
|
265
|
+
# Access plans are keyed by strings, not arrays
|
266
|
+
if target_scope.length == 1
|
267
|
+
input_key = target_scope.first.to_s
|
268
|
+
plans = access_plans[input_key]
|
269
|
+
puts "DEBUG: Looking for plans for #{input_key.inspect}, found: #{plans&.length || 0} plans" if ENV["DEBUG_BROADCAST"]
|
270
|
+
else
|
271
|
+
puts "DEBUG: Complex target scope #{target_scope.inspect}, not supported yet" if ENV["DEBUG_BROADCAST"]
|
272
|
+
return scalar_slot
|
273
|
+
end
|
274
|
+
|
275
|
+
if plans&.any?
|
276
|
+
# Find an indexed plan that gives us the vector shape
|
277
|
+
indexed_plan = plans.find { |p| p.mode == :each_indexed }
|
278
|
+
puts "DEBUG: Indexed plan found: #{indexed_plan.inspect}" if ENV["DEBUG_BROADCAST"] && indexed_plan
|
279
|
+
if indexed_plan
|
280
|
+
# Load the input to create a carrier vector
|
281
|
+
ops << Kumi::Core::IR::Ops.LoadInput(indexed_plan.accessor_key, scope: indexed_plan.scope, has_idx: true)
|
282
|
+
carrier_slot = ops.size - 1
|
283
|
+
puts "DEBUG: Created carrier at slot #{carrier_slot}" if ENV["DEBUG_BROADCAST"]
|
284
|
+
|
285
|
+
# Now broadcast scalar against the carrier - use first arg from carrier, rest from scalar
|
286
|
+
ops << Kumi::Core::IR::Ops.Map(:if, 3, carrier_slot, scalar_slot, scalar_slot)
|
287
|
+
result_slot = ops.size - 1
|
288
|
+
puts "DEBUG: Created broadcast MAP at slot #{result_slot}" if ENV["DEBUG_BROADCAST"]
|
289
|
+
result_slot
|
290
|
+
else
|
291
|
+
puts "DEBUG: No indexed plan found, returning scalar" if ENV["DEBUG_BROADCAST"]
|
292
|
+
# No indexed plan available, return scalar as-is
|
293
|
+
scalar_slot
|
294
|
+
end
|
295
|
+
else
|
296
|
+
puts "DEBUG: No access plans found, returning scalar" if ENV["DEBUG_BROADCAST"]
|
297
|
+
# No access plans for target scope, return scalar as-is
|
298
|
+
scalar_slot
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def lower_declaration(name, decl, access_plans, join_reduce_plans, scope_plan)
|
303
|
+
ops = []
|
304
|
+
|
305
|
+
plan = @join_reduce_plans[name]
|
306
|
+
req_scope =
|
307
|
+
if plan && plan.respond_to?(:result_scope)
|
308
|
+
Array(plan.result_scope) # [] for full reduction, [:players] for per-player, etc.
|
309
|
+
elsif plan && plan.respond_to?(:policy) && plan.policy == :broadcast
|
310
|
+
Array(plan.target_scope) # Broadcast to target scope
|
311
|
+
elsif top_level_reducer?(decl)
|
312
|
+
[] # collapse all axes by default
|
313
|
+
else
|
314
|
+
scope_plan&.scope # fallback (vector values, arrays, etc.)
|
315
|
+
end
|
316
|
+
|
317
|
+
last_slot = lower_expression(decl.expression, ops, access_plans, scope_plan,
|
318
|
+
need_indices = true, req_scope)
|
319
|
+
|
320
|
+
# Apply broadcasting for scalar-to-vector join plans
|
321
|
+
if plan && plan.respond_to?(:policy) && plan.policy == :broadcast
|
322
|
+
puts "DEBUG: Applying scalar broadcast for #{name} to scope #{plan.target_scope.inspect}" if ENV["DEBUG_BROADCAST"]
|
323
|
+
last_slot = apply_scalar_to_vector_broadcast(last_slot, plan.target_scope, ops, access_plans)
|
324
|
+
puts "DEBUG: Broadcast result slot: #{last_slot}" if ENV["DEBUG_BROADCAST"]
|
325
|
+
end
|
326
|
+
|
327
|
+
shape = determine_slot_shape(last_slot, ops, access_plans)
|
328
|
+
puts "DEBUG: Shape after broadcast for #{name}: #{shape.inspect}" if ENV["DEBUG_BROADCAST"]
|
329
|
+
|
330
|
+
if shape.kind == :vec
|
331
|
+
vec_name = :"#{name}__vec"
|
332
|
+
@vec_meta[vec_name] = { scope: shape.scope, has_idx: shape.has_idx }
|
333
|
+
|
334
|
+
# internal twin for downstream refs
|
335
|
+
ops << Kumi::Core::IR::Ops.Store(vec_name, last_slot)
|
336
|
+
|
337
|
+
# public presentation
|
338
|
+
ops << if shape.has_idx
|
339
|
+
Kumi::Core::IR::Ops.Lift(shape.scope, last_slot)
|
340
|
+
else
|
341
|
+
Kumi::Core::IR::Ops.Reduce(:to_array, [], [], [], last_slot)
|
342
|
+
end
|
343
|
+
last_slot = ops.size - 1
|
344
|
+
end
|
345
|
+
|
346
|
+
# ➌ store public name (scalar or transformed vec)
|
347
|
+
ops << Kumi::Core::IR::Ops.Store(name, last_slot)
|
348
|
+
|
349
|
+
Kumi::Core::IR::Decl.new(
|
350
|
+
name: name,
|
351
|
+
kind: decl.is_a?(Syntax::ValueDeclaration) ? :value : :trait,
|
352
|
+
shape: (shape.kind == :vec ? :vec : :scalar),
|
353
|
+
ops: ops
|
354
|
+
)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Lowers an analyzed AST node into IR ops and returns the slot index.
|
358
|
+
# - ops: mutable IR ops array (per-declaration)
|
359
|
+
# - need_indices: whether to prefer :each_indexed plan for inputs
|
360
|
+
# - required_scope: consumer-required scope (guides grouped reductions)
|
361
|
+
# - cacheable: whether this lowering may be cached (branch bodies under guards: false)
|
362
|
+
def lower_expression(expr, ops, access_plans, scope_plan, need_indices, required_scope = nil, cacheable: true)
|
363
|
+
@lower_cache ||= {}
|
364
|
+
key = [@current_decl, expr.object_id, Array(required_scope), !!need_indices]
|
365
|
+
if cacheable && (hit = @lower_cache[key])
|
366
|
+
return hit
|
367
|
+
end
|
368
|
+
|
369
|
+
if ENV["DEBUG_LOWER"] && expr.is_a?(Syntax::CallExpression)
|
370
|
+
puts " LOWER_EXPR[#{@current_decl}] #{expr.fn_name}(#{expr.args.size} args) req_scope=#{required_scope.inspect}"
|
371
|
+
end
|
372
|
+
|
373
|
+
slot =
|
374
|
+
case expr
|
375
|
+
when Syntax::Literal
|
376
|
+
ops << Kumi::Core::IR::Ops.Const(expr.value)
|
377
|
+
ops.size - 1
|
378
|
+
|
379
|
+
when Syntax::InputReference
|
380
|
+
plan_id = pick_plan_id_for_input([expr.name], access_plans,
|
381
|
+
scope_plan: scope_plan, need_indices: need_indices)
|
382
|
+
plans = access_plans.fetch(expr.name.to_s, [])
|
383
|
+
selected = plans.find { |p| p.accessor_key == plan_id }
|
384
|
+
scope = selected ? selected.scope : []
|
385
|
+
is_scalar = selected && %i[read materialize].include?(selected.mode)
|
386
|
+
has_idx = selected && selected.mode == :each_indexed
|
387
|
+
ops << Kumi::Core::IR::Ops.LoadInput(plan_id, scope: scope, is_scalar: is_scalar, has_idx: has_idx)
|
388
|
+
ops.size - 1
|
389
|
+
|
390
|
+
when Syntax::InputElementReference
|
391
|
+
plan_id = pick_plan_id_for_input(expr.path, access_plans,
|
392
|
+
scope_plan: scope_plan, need_indices: need_indices)
|
393
|
+
path_str = expr.path.join(".")
|
394
|
+
plans = access_plans.fetch(path_str, [])
|
395
|
+
selected = plans.find { |p| p.accessor_key == plan_id }
|
396
|
+
scope = selected ? selected.scope : []
|
397
|
+
is_scalar = selected && %i[read materialize].include?(selected.mode)
|
398
|
+
has_idx = selected && selected.mode == :each_indexed
|
399
|
+
ops << Kumi::Core::IR::Ops.LoadInput(plan_id, scope: scope, is_scalar: is_scalar, has_idx: has_idx)
|
400
|
+
ops.size - 1
|
401
|
+
|
402
|
+
when Syntax::DeclarationReference
|
403
|
+
# Check if this declaration has a vectorized twin at the required scope
|
404
|
+
twin = :"#{expr.name}__vec"
|
405
|
+
twin_meta = @vec_meta && @vec_meta[twin]
|
406
|
+
|
407
|
+
if required_scope && !Array(required_scope).empty?
|
408
|
+
# Consumer needs a grouped view of this declaration.
|
409
|
+
if twin_meta && twin_meta[:scope] == Array(required_scope)
|
410
|
+
# We have a vectorized twin at exactly the required scope - use it!
|
411
|
+
ops << Kumi::Core::IR::Ops.Ref(twin)
|
412
|
+
return ops.size - 1
|
413
|
+
else
|
414
|
+
# Need to inline re-lower the referenced declaration's *expression*,
|
415
|
+
# forcing indices, and grouping to the requested scope.
|
416
|
+
decl = @declarations.fetch(expr.name) { raise "unknown decl #{expr.name}" }
|
417
|
+
slot = lower_expression(decl.expression, ops, access_plans, scope_plan,
|
418
|
+
true, # need_indices (grouping requires indexed source)
|
419
|
+
required_scope, # group-to scope
|
420
|
+
cacheable: true) # per-decl slot cache will dedupe
|
421
|
+
return slot
|
422
|
+
end
|
423
|
+
else
|
424
|
+
# Plain (scalar) use, or already-materialized vec twin
|
425
|
+
ref = twin_meta ? twin : expr.name
|
426
|
+
ops << Kumi::Core::IR::Ops.Ref(ref)
|
427
|
+
return ops.size - 1
|
428
|
+
end
|
429
|
+
|
430
|
+
when Syntax::CallExpression
|
431
|
+
entry = Kumi::Registry.entry(expr.fn_name)
|
432
|
+
|
433
|
+
if ENV["DEBUG_LOWER"] && has_nested_reducer?(expr)
|
434
|
+
puts " NESTED_REDUCER_DETECTED in #{expr.fn_name} with req_scope=#{required_scope.inspect}"
|
435
|
+
end
|
436
|
+
|
437
|
+
# Special handling for comparison operations containing nested reductions
|
438
|
+
if !entry&.reducer && has_nested_reducer?(expr)
|
439
|
+
puts " SPECIAL_NESTED_REDUCTION_HANDLING for #{expr.fn_name}" if ENV["DEBUG_LOWER"]
|
440
|
+
|
441
|
+
# For comparison ops with nested reducers, we need to ensure
|
442
|
+
# the nested reducer gets the right required_scope (per-player)
|
443
|
+
# instead of the full dimensional scope from infer_expr_scope
|
444
|
+
|
445
|
+
# Get the desired result scope from our scope plan (per-player scope)
|
446
|
+
# This should be [:players] for per-player operations
|
447
|
+
plan = @join_reduce_plans[@current_decl]
|
448
|
+
target_scope = if plan.is_a?(Kumi::Core::Analyzer::Plans::Reduce) && plan.result_scope && !plan.result_scope.empty?
|
449
|
+
plan.result_scope
|
450
|
+
elsif required_scope && !required_scope.empty?
|
451
|
+
required_scope
|
452
|
+
else
|
453
|
+
# Try to infer per-player scope from the nested reducer argument
|
454
|
+
nested_reducer_arg = find_nested_reducer_arg(expr)
|
455
|
+
if nested_reducer_arg
|
456
|
+
infer_per_player_scope(nested_reducer_arg)
|
457
|
+
else
|
458
|
+
[]
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
puts " NESTED_REDUCTION target_scope=#{target_scope.inspect}" if ENV["DEBUG_LOWER"]
|
463
|
+
|
464
|
+
# Lower arguments with the correct scope for nested reducers
|
465
|
+
arg_slots = expr.args.map do |a|
|
466
|
+
lower_expression(a, ops, access_plans, scope_plan,
|
467
|
+
need_indices, target_scope, cacheable: cacheable)
|
468
|
+
end
|
469
|
+
|
470
|
+
aligned = target_scope.empty? ? arg_slots : insert_align_to_if_needed(arg_slots, ops, access_plans, on_missing: :error)
|
471
|
+
ops << Kumi::Core::IR::Ops.Map(expr.fn_name, expr.args.size, *aligned)
|
472
|
+
return ops.size - 1
|
473
|
+
|
474
|
+
elsif entry&.reducer
|
475
|
+
# Need indices iff grouping is requested
|
476
|
+
child_need_idx = !Array(required_scope).empty?
|
477
|
+
|
478
|
+
arg_slots = expr.args.map do |a|
|
479
|
+
lower_expression(a, ops, access_plans, scope_plan,
|
480
|
+
child_need_idx, # <<< important
|
481
|
+
nil, # children of reducer don't inherit grouping
|
482
|
+
cacheable: true)
|
483
|
+
end
|
484
|
+
vec_i = arg_slots.index { |s| determine_slot_shape(s, ops, access_plans).kind == :vec }
|
485
|
+
if vec_i
|
486
|
+
src_slot = arg_slots[vec_i]
|
487
|
+
src_shape = determine_slot_shape(src_slot, ops, access_plans)
|
488
|
+
|
489
|
+
# If grouping requested but source lacks indices (e.g. cached ravel), reload it with indices
|
490
|
+
if !Array(required_scope).empty? && !src_shape.has_idx
|
491
|
+
src_slot = lower_expression(expr.args[vec_i], ops, access_plans, scope_plan,
|
492
|
+
true, # force indices
|
493
|
+
nil,
|
494
|
+
cacheable: true)
|
495
|
+
src_shape = determine_slot_shape(src_slot, ops, access_plans)
|
496
|
+
end
|
497
|
+
|
498
|
+
if ENV["DEBUG_LOWER"]
|
499
|
+
puts " emit_reduce(#{expr.fn_name}, #{src_slot}, #{src_shape.scope.inspect}, #{Array(required_scope).inspect})"
|
500
|
+
end
|
501
|
+
return emit_reduce(ops, expr.fn_name, src_slot, src_shape.scope, required_scope)
|
502
|
+
else
|
503
|
+
ops << Kumi::Core::IR::Ops.Map(expr.fn_name, arg_slots.size, *arg_slots)
|
504
|
+
return ops.size - 1
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# non-reducer path unchanged…
|
509
|
+
|
510
|
+
# Non-reducer: pointwise. Choose carrier = deepest vec among args.
|
511
|
+
target = infer_expr_scope(expr, access_plans) # static, no ops emitted
|
512
|
+
arg_slots = expr.args.map do |a|
|
513
|
+
lower_expression(a, ops, access_plans, scope_plan,
|
514
|
+
need_indices, target, cacheable: cacheable)
|
515
|
+
end
|
516
|
+
aligned = insert_align_to_if_needed(arg_slots, ops, access_plans, on_missing: :error)
|
517
|
+
ops << Kumi::Core::IR::Ops.Map(expr.fn_name, expr.args.size, *aligned)
|
518
|
+
ops.size - 1
|
519
|
+
|
520
|
+
when Syntax::ArrayExpression
|
521
|
+
target = infer_expr_scope(expr, access_plans) # LUB across children
|
522
|
+
puts "DEBUG array target scope=#{target.inspect}" if ENV["DEBUG_LOWER"]
|
523
|
+
elem_slots = expr.elements.map do |e|
|
524
|
+
lower_expression(e, ops, access_plans, scope_plan,
|
525
|
+
need_indices, # pass-through
|
526
|
+
target, # <<< required_scope = target
|
527
|
+
cacheable: true)
|
528
|
+
end
|
529
|
+
elem_slots = insert_align_to_if_needed(elem_slots, ops, access_plans, on_missing: :error) unless target.empty?
|
530
|
+
ops << Kumi::Core::IR::Ops.Array(elem_slots.size, *elem_slots)
|
531
|
+
return ops.size - 1
|
532
|
+
|
533
|
+
when Syntax::CascadeExpression
|
534
|
+
# Find a base (true) case, if present
|
535
|
+
base_case = expr.cases.find { |c| c.condition.is_a?(Syntax::Literal) && c.condition.value == true }
|
536
|
+
default_expr = base_case ? base_case.result : Kumi::Syntax::Literal.new(nil)
|
537
|
+
branches = expr.cases.reject { |c| c.equal?(base_case) }
|
538
|
+
|
539
|
+
# Lower each condition once to probe shapes (cacheable)
|
540
|
+
precond_slots = branches.map do |c|
|
541
|
+
lower_expression(c.condition, ops, access_plans, scope_plan,
|
542
|
+
need_indices, nil, cacheable: cacheable)
|
543
|
+
end
|
544
|
+
precond_shapes = precond_slots.map { |s| determine_slot_shape(s, ops, access_plans) }
|
545
|
+
vec_cond_is = precond_shapes.each_index.select { |i| precond_shapes[i].kind == :vec }
|
546
|
+
|
547
|
+
# Tiny helpers for boolean maps
|
548
|
+
map1 = lambda { |fn, a|
|
549
|
+
ops << Kumi::Core::IR::Ops.Map(fn, 1, a)
|
550
|
+
ops.size - 1
|
551
|
+
}
|
552
|
+
map2 = lambda { |fn, a, b|
|
553
|
+
ops << Kumi::Core::IR::Ops.Map(fn, 2, a, b)
|
554
|
+
ops.size - 1
|
555
|
+
}
|
556
|
+
|
557
|
+
if vec_cond_is.empty?
|
558
|
+
# ------------------------------------------------------------------
|
559
|
+
# SCALAR CASCADE (lazy): evaluate branch bodies under guards
|
560
|
+
# ------------------------------------------------------------------
|
561
|
+
any_prev_true = lower_expression(Kumi::Syntax::Literal.new(false), ops, access_plans,
|
562
|
+
scope_plan, need_indices, nil, cacheable: false)
|
563
|
+
|
564
|
+
cases_attr = [] # [[cond_slot, value_slot], ...]
|
565
|
+
|
566
|
+
branches.each_with_index do |c, i|
|
567
|
+
not_any = map1.call(:not, any_prev_true)
|
568
|
+
guard_i = map2.call(:and, not_any, precond_slots[i])
|
569
|
+
|
570
|
+
ops << Kumi::Core::IR::Ops.GuardPush(guard_i)
|
571
|
+
val_slot = lower_expression(c.result, ops, access_plans, scope_plan,
|
572
|
+
need_indices, nil, cacheable: false)
|
573
|
+
ops << Kumi::Core::IR::Ops.GuardPop
|
574
|
+
|
575
|
+
cases_attr << [precond_slots[i], val_slot]
|
576
|
+
any_prev_true = map2.call(:or, any_prev_true, precond_slots[i])
|
577
|
+
end
|
578
|
+
|
579
|
+
not_any = map1.call(:not, any_prev_true)
|
580
|
+
ops << Kumi::Core::IR::Ops.GuardPush(not_any)
|
581
|
+
default_slot = lower_expression(default_expr, ops, access_plans, scope_plan,
|
582
|
+
need_indices, nil, cacheable: false)
|
583
|
+
ops << Kumi::Core::IR::Ops.GuardPop
|
584
|
+
|
585
|
+
ops << Kumi::Core::IR::Ops.Switch(cases_attr, default_slot)
|
586
|
+
ops.size - 1
|
587
|
+
else
|
588
|
+
# -------------------------
|
589
|
+
# VECTOR CASCADE (per-row lazy)
|
590
|
+
# -------------------------
|
591
|
+
|
592
|
+
# First lower raw conditions to peek at shapes.
|
593
|
+
raw_cond_slots = branches.map do |c|
|
594
|
+
lower_expression(c.condition, ops, access_plans, scope_plan,
|
595
|
+
need_indices, nil, cacheable: true)
|
596
|
+
end
|
597
|
+
raw_shapes = raw_cond_slots.map { |s| determine_slot_shape(s, ops, access_plans) }
|
598
|
+
vec_is = raw_shapes.each_index.select { |i| raw_shapes[i].kind == :vec }
|
599
|
+
|
600
|
+
# Choose cascade_scope: prefer scope_plan (from scope resolution),
|
601
|
+
# fallback to LUB of vector condition scopes.
|
602
|
+
if scope_plan && !scope_plan.scope.nil? && !scope_plan.scope.empty?
|
603
|
+
cascade_scope = Array(scope_plan.scope)
|
604
|
+
else
|
605
|
+
candidate_scopes = vec_is.map { |i| raw_shapes[i].scope }
|
606
|
+
cascade_scope = lub_scopes(candidate_scopes)
|
607
|
+
cascade_scope = [] if cascade_scope.nil?
|
608
|
+
end
|
609
|
+
|
610
|
+
# Re-lower each condition *properly* at cascade_scope (reproject deeper ones).
|
611
|
+
conds_at_scope = branches.map do |c|
|
612
|
+
lower_cascade_pred(c.condition, cascade_scope, ops, access_plans, scope_plan)
|
613
|
+
end
|
614
|
+
|
615
|
+
# Booleans utilities
|
616
|
+
map1 = lambda { |fn, a|
|
617
|
+
ops << Kumi::Core::IR::Ops.Map(fn, 1, a)
|
618
|
+
ops.size - 1
|
619
|
+
}
|
620
|
+
map2 = lambda { |fn, a, b|
|
621
|
+
ops << Kumi::Core::IR::Ops.Map(fn, 2, a, b)
|
622
|
+
ops.size - 1
|
623
|
+
}
|
624
|
+
|
625
|
+
# Build lazy guards per branch at cascade_scope
|
626
|
+
any_prev = lower_expression(Kumi::Syntax::Literal.new(false), ops, access_plans,
|
627
|
+
scope_plan, need_indices, nil, cacheable: false)
|
628
|
+
val_slots = []
|
629
|
+
|
630
|
+
branches.each_with_index do |c, i|
|
631
|
+
not_prev = map1.call(:not, any_prev)
|
632
|
+
need_i = map2.call(:and, not_prev, conds_at_scope[i]) # @ cascade_scope
|
633
|
+
|
634
|
+
ops << Kumi::Core::IR::Ops.GuardPush(need_i)
|
635
|
+
vslot = lower_expression(c.result, ops, access_plans, scope_plan,
|
636
|
+
need_indices, cascade_scope, cacheable: false)
|
637
|
+
# ensure vector results live at cascade_scope
|
638
|
+
vslot = align_to_cascade_if_vec(vslot, cascade_scope, ops, access_plans)
|
639
|
+
ops << Kumi::Core::IR::Ops.GuardPop
|
640
|
+
|
641
|
+
val_slots << vslot
|
642
|
+
any_prev = map2.call(:or, any_prev, conds_at_scope[i]) # still @ cascade_scope
|
643
|
+
end
|
644
|
+
|
645
|
+
# Default branch
|
646
|
+
not_prev = map1.call(:not, any_prev)
|
647
|
+
ops << Kumi::Core::IR::Ops.GuardPush(not_prev)
|
648
|
+
default_slot = lower_expression(default_expr, ops, access_plans, scope_plan,
|
649
|
+
need_indices, cascade_scope, cacheable: false)
|
650
|
+
default_slot = align_to_cascade_if_vec(default_slot, cascade_scope, ops, access_plans)
|
651
|
+
ops << Kumi::Core::IR::Ops.GuardPop
|
652
|
+
|
653
|
+
# Assemble via nested element-wise selection
|
654
|
+
nested = default_slot
|
655
|
+
(branches.length - 1).downto(0) do |i|
|
656
|
+
ops << Kumi::Core::IR::Ops.Map(:if, 3, conds_at_scope[i], val_slots[i], nested)
|
657
|
+
nested = ops.size - 1
|
658
|
+
end
|
659
|
+
nested
|
660
|
+
|
661
|
+
end
|
662
|
+
|
663
|
+
else
|
664
|
+
raise "Unsupported expression type: #{expr.class.name}"
|
665
|
+
end
|
666
|
+
|
667
|
+
@lower_cache[key] = slot if cacheable
|
668
|
+
slot
|
669
|
+
end
|
670
|
+
|
671
|
+
def pick_plan_id_for_input(path, access_plans, scope_plan:, need_indices:)
|
672
|
+
path_str = path.join(".")
|
673
|
+
plans = access_plans.fetch(path_str) { raise "No access plan for #{path_str}" }
|
674
|
+
depth = plans.first.depth
|
675
|
+
if depth > 0
|
676
|
+
mode = need_indices ? :each_indexed : :ravel
|
677
|
+
plans.find { |p| p.mode == mode }&.accessor_key or
|
678
|
+
raise("No #{mode.inspect} plan for #{path_str}")
|
679
|
+
else
|
680
|
+
plans.find { |p| p.mode == :read }&.accessor_key or
|
681
|
+
raise("No :read plan for #{path_str}")
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
def align_to_cascade_if_vec(slot, cascade_scope, ops, access_plans)
|
686
|
+
sh = determine_slot_shape(slot, ops, access_plans)
|
687
|
+
return slot if sh.kind == :scalar && cascade_scope.empty? # scalar cascade, keep scalar
|
688
|
+
return slot if sh.scope == cascade_scope
|
689
|
+
|
690
|
+
# Handle scalar-to-vector broadcasting for vectorized cascades
|
691
|
+
if sh.kind == :scalar && !cascade_scope.empty?
|
692
|
+
# Find a carrier vector at cascade scope to broadcast scalar against
|
693
|
+
target_slot = nil
|
694
|
+
ops.each_with_index do |op, i|
|
695
|
+
next unless %i[load_input map].include?(op.tag)
|
696
|
+
|
697
|
+
shape = determine_slot_shape(i, ops, access_plans)
|
698
|
+
if shape.kind == :vec && shape.scope == cascade_scope && shape.has_idx
|
699
|
+
target_slot = i
|
700
|
+
break
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
raise "Cannot broadcast scalar to cascade scope #{cascade_scope.inspect} - no carrier vector found" unless target_slot
|
705
|
+
|
706
|
+
# Use MAP with a special broadcast function - but first I need to create one
|
707
|
+
# For now, let's try using the 'if' function to broadcast: if(true, scalar, carrier) -> broadcasts scalar
|
708
|
+
const_true = ops.size
|
709
|
+
ops << Kumi::Core::IR::Ops.Const(true)
|
710
|
+
|
711
|
+
ops << Kumi::Core::IR::Ops.Map(:if, 3, const_true, slot, target_slot)
|
712
|
+
return ops.size - 1
|
713
|
+
|
714
|
+
# No carrier found, can't broadcast
|
715
|
+
|
716
|
+
end
|
717
|
+
|
718
|
+
short, long = [sh.scope, cascade_scope].sort_by(&:length)
|
719
|
+
unless long.first(short.length) == short
|
720
|
+
raise "cascade branch result scope #{sh.scope.inspect} not compatible with cascade scope #{cascade_scope.inspect}"
|
721
|
+
end
|
722
|
+
|
723
|
+
raise "unsupported cascade scope #{cascade_scope.inspect} for slot #{slot}" if cascade_scope.empty?
|
724
|
+
end
|
725
|
+
|
726
|
+
def prefix?(short, long)
|
727
|
+
long.first(short.length) == short
|
728
|
+
end
|
729
|
+
|
730
|
+
def infer_expr_scope(expr, access_plans)
|
731
|
+
case expr
|
732
|
+
when Syntax::DeclarationReference
|
733
|
+
meta = @vec_meta && @vec_meta[:"#{expr.name}__vec"]
|
734
|
+
meta ? Array(meta[:scope]) : []
|
735
|
+
when Syntax::InputElementReference
|
736
|
+
key = expr.path.join(".")
|
737
|
+
plan = access_plans.fetch(key, []).find { |p| p.mode == :each_indexed }
|
738
|
+
plan ? Array(plan.scope) : []
|
739
|
+
when Syntax::InputReference
|
740
|
+
plans = access_plans.fetch(expr.name.to_s, [])
|
741
|
+
plan = plans.find { |p| p.mode == :each_indexed }
|
742
|
+
plan ? Array(plan.scope) : []
|
743
|
+
when Syntax::CallExpression
|
744
|
+
# reducers: use source vec scope; non-reducers: deepest carrier among args
|
745
|
+
scopes = expr.args.map { |a| infer_expr_scope(a, access_plans) }
|
746
|
+
scopes.max_by(&:length) || []
|
747
|
+
when Syntax::ArrayExpression
|
748
|
+
scopes = expr.elements.map { |e| infer_expr_scope(e, access_plans) }
|
749
|
+
lub_scopes(scopes) # <-- important
|
750
|
+
else
|
751
|
+
[]
|
752
|
+
end
|
753
|
+
end
|
754
|
+
|
755
|
+
def lower_cascade_pred(cond, cascade_scope, ops, access_plans, scope_plan)
|
756
|
+
case cond
|
757
|
+
when Syntax::DeclarationReference
|
758
|
+
# Check if this declaration has a vectorized twin at the required scope
|
759
|
+
twin = :"#{cond.name}__vec"
|
760
|
+
twin_meta = @vec_meta && @vec_meta[twin]
|
761
|
+
|
762
|
+
if cascade_scope && !Array(cascade_scope).empty?
|
763
|
+
# Consumer needs a grouped view of this declaration.
|
764
|
+
if twin_meta && twin_meta[:scope] == Array(cascade_scope)
|
765
|
+
# We have a vectorized twin at exactly the required scope - use it!
|
766
|
+
ops << Kumi::Core::IR::Ops.Ref(twin)
|
767
|
+
ops.size - 1
|
768
|
+
else
|
769
|
+
# Need to inline re-lower the referenced declaration's *expression*
|
770
|
+
decl = @declarations.fetch(cond.name) { raise "unknown decl #{cond.name}" }
|
771
|
+
slot = lower_expression(decl.expression, ops, access_plans, scope_plan,
|
772
|
+
true, Array(cascade_scope), cacheable: true)
|
773
|
+
project_mask_to_scope(slot, cascade_scope, ops, access_plans)
|
774
|
+
end
|
775
|
+
else
|
776
|
+
# Plain (scalar) use, or already-materialized vec twin
|
777
|
+
ref = twin_meta ? twin : cond.name
|
778
|
+
ops << Kumi::Core::IR::Ops.Ref(ref)
|
779
|
+
ops.size - 1
|
780
|
+
end
|
781
|
+
|
782
|
+
when Syntax::CallExpression
|
783
|
+
if cond.fn_name == :cascade_and
|
784
|
+
parts = cond.args.map { |a| lower_cascade_pred(a, cascade_scope, ops, access_plans, scope_plan) }
|
785
|
+
# They’re all @ cascade_scope (or scalar) now; align scalars broadcast, vecs already match.
|
786
|
+
parts.reduce do |acc, s|
|
787
|
+
ops << Kumi::Core::IR::Ops.Map(:and, 2, acc, s)
|
788
|
+
ops.size - 1
|
789
|
+
end
|
790
|
+
else
|
791
|
+
slot = lower_expression(cond, ops, access_plans, scope_plan,
|
792
|
+
true, Array(cascade_scope), cacheable: false)
|
793
|
+
project_mask_to_scope(slot, cascade_scope, ops, access_plans)
|
794
|
+
end
|
795
|
+
|
796
|
+
else
|
797
|
+
slot = lower_expression(cond, ops, access_plans, scope_plan,
|
798
|
+
true, Array(cascade_scope), cacheable: false)
|
799
|
+
project_mask_to_scope(slot, cascade_scope, ops, access_plans)
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
def common_prefix(a, b)
|
804
|
+
a = Array(a)
|
805
|
+
b = Array(b)
|
806
|
+
i = 0
|
807
|
+
i += 1 while i < a.length && i < b.length && a[i] == b[i]
|
808
|
+
a.first(i)
|
809
|
+
end
|
810
|
+
|
811
|
+
def lub_scopes(scopes)
|
812
|
+
scopes = scopes.reject { |s| s.nil? || s.empty? }
|
813
|
+
return [] if scopes.empty?
|
814
|
+
|
815
|
+
scopes.reduce(scopes.first) { |acc, s| common_prefix(acc, s) }
|
816
|
+
end
|
817
|
+
|
818
|
+
def emit_reduce(ops, fn_name, src_slot, src_scope, required_scope)
|
819
|
+
rs = Array(required_scope || [])
|
820
|
+
ss = Array(src_scope)
|
821
|
+
|
822
|
+
# No-op: grouping to full source scope
|
823
|
+
return src_slot if !rs.empty? && rs == ss
|
824
|
+
|
825
|
+
axis = rs.empty? ? ss : (ss - rs)
|
826
|
+
puts " emit_reduce #{fn_name} on #{src_slot} with axis #{axis.inspect} and result scope #{rs.inspect}" if ENV["DEBUG_LOWER"]
|
827
|
+
ops << Kumi::Core::IR::Ops.Reduce(fn_name, axis, rs, [], src_slot)
|
828
|
+
ops.size - 1
|
829
|
+
end
|
830
|
+
|
831
|
+
def vec_twin_name(base, scope)
|
832
|
+
scope_tag = Array(scope).map(&:to_s).join("_") # e.g. "players"
|
833
|
+
:"#{base}__vec__#{scope_tag}"
|
834
|
+
end
|
835
|
+
|
836
|
+
def find_vec_twin(name, scope)
|
837
|
+
t = vec_twin_name(name, scope)
|
838
|
+
@vec_meta[t] ? t : nil
|
839
|
+
end
|
840
|
+
|
841
|
+
def top_level_reducer?(decl)
|
842
|
+
ce = decl.expression
|
843
|
+
return false unless ce.is_a?(Kumi::Syntax::CallExpression)
|
844
|
+
|
845
|
+
entry = Kumi::Registry.entry(ce.fn_name)
|
846
|
+
entry&.reducer && !entry&.structure_function
|
847
|
+
end
|
848
|
+
|
849
|
+
def has_nested_reducer?(expr)
|
850
|
+
return false unless expr.is_a?(Kumi::Syntax::CallExpression)
|
851
|
+
|
852
|
+
expr.args.any? do |arg|
|
853
|
+
case arg
|
854
|
+
when Kumi::Syntax::CallExpression
|
855
|
+
entry = Kumi::Registry.entry(arg.fn_name)
|
856
|
+
return true if entry&.reducer
|
857
|
+
|
858
|
+
has_nested_reducer?(arg) # recursive check
|
859
|
+
else
|
860
|
+
false
|
861
|
+
end
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
def find_nested_reducer_arg(expr)
|
866
|
+
return nil unless expr.is_a?(Kumi::Syntax::CallExpression)
|
867
|
+
|
868
|
+
expr.args.each do |arg|
|
869
|
+
case arg
|
870
|
+
when Kumi::Syntax::CallExpression
|
871
|
+
entry = Kumi::Registry.entry(arg.fn_name)
|
872
|
+
return arg if entry&.reducer
|
873
|
+
|
874
|
+
nested = find_nested_reducer_arg(arg)
|
875
|
+
return nested if nested
|
876
|
+
end
|
877
|
+
end
|
878
|
+
nil
|
879
|
+
end
|
880
|
+
|
881
|
+
def infer_per_player_scope(reducer_expr)
|
882
|
+
return [] unless reducer_expr.is_a?(Kumi::Syntax::CallExpression)
|
883
|
+
|
884
|
+
# Look at the reducer's argument to determine the full scope
|
885
|
+
arg = reducer_expr.args.first
|
886
|
+
return [] unless arg
|
887
|
+
|
888
|
+
case arg
|
889
|
+
when Kumi::Syntax::InputElementReference
|
890
|
+
# For paths like [:players, :score_matrices, :session, :points]
|
891
|
+
# We want to keep [:players] and reduce over the rest
|
892
|
+
arg.path.empty? ? [] : [arg.path.first]
|
893
|
+
when Kumi::Syntax::CallExpression
|
894
|
+
# For nested expressions, get the deepest input path and take first element
|
895
|
+
deepest = find_deepest_input_path(arg)
|
896
|
+
deepest && !deepest.empty? ? [deepest.first] : []
|
897
|
+
else
|
898
|
+
[]
|
899
|
+
end
|
900
|
+
end
|
901
|
+
|
902
|
+
def find_deepest_input_path(expr)
|
903
|
+
case expr
|
904
|
+
when Kumi::Syntax::InputElementReference
|
905
|
+
expr.path
|
906
|
+
when Kumi::Syntax::InputReference
|
907
|
+
[expr.name]
|
908
|
+
when Kumi::Syntax::CallExpression
|
909
|
+
paths = expr.args.map { |a| find_deepest_input_path(a) }.compact
|
910
|
+
paths.max_by(&:length)
|
911
|
+
else
|
912
|
+
nil
|
913
|
+
end
|
914
|
+
end
|
915
|
+
|
916
|
+
# Make sure a boolean mask lives at exactly cascade_scope.
|
917
|
+
def project_mask_to_scope(slot, cascade_scope, ops, access_plans)
|
918
|
+
sh = determine_slot_shape(slot, ops, access_plans)
|
919
|
+
return slot if sh.scope == cascade_scope
|
920
|
+
|
921
|
+
# If we have a scalar condition but need it at cascade scope, broadcast it
|
922
|
+
if sh.kind == :scalar && cascade_scope && !Array(cascade_scope).empty?
|
923
|
+
# Find a target vector that already has the cascade scope
|
924
|
+
target_slot = nil
|
925
|
+
ops.each_with_index do |op, i|
|
926
|
+
next unless %i[load_input map].include?(op.tag)
|
927
|
+
|
928
|
+
shape = determine_slot_shape(i, ops, access_plans)
|
929
|
+
if shape.kind == :vec && shape.scope == Array(cascade_scope) && shape.has_idx
|
930
|
+
target_slot = i
|
931
|
+
break
|
932
|
+
end
|
933
|
+
end
|
934
|
+
|
935
|
+
return slot unless target_slot
|
936
|
+
|
937
|
+
ops << Kumi::Core::IR::Ops.AlignTo(target_slot, slot, to_scope: Array(cascade_scope), on_missing: :error,
|
938
|
+
require_unique: true)
|
939
|
+
return ops.size - 1
|
940
|
+
|
941
|
+
# Can't broadcast, use as-is
|
942
|
+
|
943
|
+
end
|
944
|
+
|
945
|
+
return slot if sh.kind == :scalar
|
946
|
+
|
947
|
+
cascade_scope = Array(cascade_scope)
|
948
|
+
slot_scope = Array(sh.scope)
|
949
|
+
|
950
|
+
# Check prefix compatibility
|
951
|
+
short, long = [cascade_scope, slot_scope].sort_by(&:length)
|
952
|
+
unless long.first(short.length) == short
|
953
|
+
raise "cascade condition scope #{slot_scope.inspect} is not prefix-compatible with #{cascade_scope.inspect}"
|
954
|
+
end
|
955
|
+
|
956
|
+
if slot_scope.length < cascade_scope.length
|
957
|
+
# Need to broadcast UP: slot scope is shorter, needs to be aligned to cascade scope
|
958
|
+
# Find a target vector that already has the cascade scope
|
959
|
+
target_slot = nil
|
960
|
+
ops.each_with_index do |op, i|
|
961
|
+
next unless %i[load_input map].include?(op.tag)
|
962
|
+
|
963
|
+
shape = determine_slot_shape(i, ops, access_plans)
|
964
|
+
if shape.kind == :vec && shape.scope == cascade_scope && shape.has_idx
|
965
|
+
target_slot = i
|
966
|
+
break
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
if target_slot
|
971
|
+
ops << Kumi::Core::IR::Ops.AlignTo(target_slot, slot, to_scope: cascade_scope, on_missing: :error, require_unique: true)
|
972
|
+
ops.size - 1
|
973
|
+
else
|
974
|
+
# Fallback: use the slot itself (might not work but worth trying)
|
975
|
+
ops << Kumi::Core::IR::Ops.AlignTo(slot, slot, to_scope: cascade_scope, on_missing: :error, require_unique: true)
|
976
|
+
ops.size - 1
|
977
|
+
end
|
978
|
+
else
|
979
|
+
# Need to reduce DOWN: slot scope is longer, reduce extra dimensions
|
980
|
+
extra_axes = slot_scope - cascade_scope
|
981
|
+
if extra_axes.empty?
|
982
|
+
slot # should not happen due to early return above
|
983
|
+
else
|
984
|
+
ops << Kumi::Core::IR::Ops.Reduce(:any?, extra_axes, cascade_scope, [], slot)
|
985
|
+
ops.size - 1
|
986
|
+
end
|
987
|
+
end
|
988
|
+
end
|
989
|
+
end
|
990
|
+
end
|
991
|
+
end
|
992
|
+
end
|
993
|
+
end
|