kumi 0.0.15 → 0.0.16
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 +7 -0
- data/lib/kumi/analyzer.rb +6 -1
- data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +18 -20
- data/lib/kumi/core/analyzer/passes/ir_execution_schedule_pass.rb +67 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +12 -15
- data/lib/kumi/core/compiler/access_builder.rb +22 -9
- data/lib/kumi/core/compiler/access_codegen.rb +61 -0
- data/lib/kumi/core/compiler/access_emit/base.rb +173 -0
- data/lib/kumi/core/compiler/access_emit/each_indexed.rb +56 -0
- data/lib/kumi/core/compiler/access_emit/materialize.rb +45 -0
- data/lib/kumi/core/compiler/access_emit/ravel.rb +50 -0
- data/lib/kumi/core/compiler/access_emit/read.rb +32 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +36 -181
- data/lib/kumi/core/ir/execution_engine/values.rb +8 -8
- data/lib/kumi/core/ir/execution_engine.rb +3 -19
- data/lib/kumi/dev/parse.rb +12 -12
- data/lib/kumi/runtime/executable.rb +22 -175
- data/lib/kumi/runtime/run.rb +105 -0
- data/lib/kumi/schema.rb +8 -13
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +3 -2
- metadata +10 -2
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kumi::Core::Compiler::AccessEmit
|
3
|
+
module Ravel
|
4
|
+
extend Base
|
5
|
+
module_function
|
6
|
+
def build(plan)
|
7
|
+
policy = plan.on_missing || :error
|
8
|
+
key_policy = plan.key_policy || :indifferent
|
9
|
+
path_key = plan.path
|
10
|
+
segs = segment_ops(plan.operations)
|
11
|
+
|
12
|
+
code = +"lambda do |data|\n"
|
13
|
+
code << " out = []\n"
|
14
|
+
nodev, depth, loop_depth = "node0", 0, 0
|
15
|
+
code << " #{nodev} = data\n"
|
16
|
+
|
17
|
+
segs.each do |seg|
|
18
|
+
if seg == :array
|
19
|
+
code << " #{array_guard_code(node_var: nodev, mode: :ravel, policy: policy, path_key: path_key, map_depth: loop_depth)}\n"
|
20
|
+
code << " ary#{loop_depth} = #{nodev}\n"
|
21
|
+
code << " len#{loop_depth} = ary#{loop_depth}.length\n"
|
22
|
+
code << " i#{loop_depth} = -1\n"
|
23
|
+
code << " while (i#{loop_depth} += 1) < len#{loop_depth}\n"
|
24
|
+
child = "node#{depth + 1}"
|
25
|
+
code << " #{child} = ary#{loop_depth}[i#{loop_depth}]\n"
|
26
|
+
nodev = child; depth += 1; loop_depth += 1
|
27
|
+
else
|
28
|
+
seg.each do |(_, key, preview)|
|
29
|
+
code << " "
|
30
|
+
code << fetch_hash_code(node_var: nodev, key: key, key_policy: key_policy,
|
31
|
+
preview_array: preview, mode: :ravel, policy: policy,
|
32
|
+
path_key: path_key, map_depth: loop_depth)
|
33
|
+
code << "\n"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
code << " out << #{nodev}\n"
|
39
|
+
while loop_depth.positive?
|
40
|
+
code << " end\n"
|
41
|
+
loop_depth -= 1
|
42
|
+
nodev = "node#{depth - 1}"
|
43
|
+
depth -= 1
|
44
|
+
end
|
45
|
+
|
46
|
+
code << " out\nend\n"
|
47
|
+
code
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Kumi::Core::Compiler::AccessEmit
|
3
|
+
module Read
|
4
|
+
extend Base
|
5
|
+
module_function
|
6
|
+
def build(plan)
|
7
|
+
policy = plan.on_missing || :error
|
8
|
+
key_policy = plan.key_policy || :indifferent
|
9
|
+
path_key = plan.path
|
10
|
+
ops = plan.operations
|
11
|
+
|
12
|
+
body = ops.map do |op|
|
13
|
+
case op[:type]
|
14
|
+
when :enter_hash
|
15
|
+
fetch_hash_code(node_var: "node", key: op[:key], key_policy: key_policy,
|
16
|
+
preview_array: false, mode: :read, policy: policy,
|
17
|
+
path_key: path_key, map_depth: 0)
|
18
|
+
when :enter_array
|
19
|
+
%(raise TypeError, "Array encountered in :read accessor at '#{path_key}'")
|
20
|
+
end
|
21
|
+
end.join("\n ")
|
22
|
+
|
23
|
+
<<~RUBY
|
24
|
+
lambda do |data|
|
25
|
+
node = data
|
26
|
+
#{body}
|
27
|
+
node
|
28
|
+
end
|
29
|
+
RUBY
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -8,70 +8,34 @@ module Kumi
|
|
8
8
|
module Interpreter
|
9
9
|
PRODUCES_SLOT = %i[const load_input ref array map reduce lift align_to switch].freeze
|
10
10
|
NON_PRODUCERS = %i[guard_push guard_pop assign store].freeze
|
11
|
+
EMPTY_ARY = [].freeze
|
11
12
|
|
12
|
-
def self.
|
13
|
-
|
14
|
-
ir_module.decls.each do |decl|
|
15
|
-
decl.ops.each do |op|
|
16
|
-
next unless op.tag == :store
|
17
|
-
|
18
|
-
name = op.attrs[:name]
|
19
|
-
index[name] = decl if name
|
20
|
-
end
|
21
|
-
end
|
22
|
-
index
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.run(ir_module, ctx, accessors:, registry:)
|
26
|
-
# Validate registry is properly initialized
|
27
|
-
raise ArgumentError, "Registry cannot be nil" if registry.nil?
|
28
|
-
raise ArgumentError, "Registry must be a Hash, got #{registry.class}" unless registry.is_a?(Hash)
|
29
|
-
|
13
|
+
def self.run(schedule, input:, runtime:, accessors:, registry:)
|
14
|
+
prof = Profiler.enabled?
|
30
15
|
# --- PROFILER: init per run (but not in persistent mode) ---
|
31
|
-
if
|
32
|
-
schema_name =
|
33
|
-
|
34
|
-
|
35
|
-
Profiler.set_schema_name(schema_name)
|
36
|
-
else
|
37
|
-
# Normal mode: full reset with schema name
|
38
|
-
Profiler.reset!(meta: { decls: ir_module.decls&.size || 0, schema_name: schema_name })
|
39
|
-
end
|
16
|
+
if prof
|
17
|
+
schema_name = runtime[:schema_name] || "UnknownSchema"
|
18
|
+
# In persistent mode, just update schema name without full reset
|
19
|
+
Profiler.set_schema_name(schema_name)
|
40
20
|
end
|
41
21
|
|
42
22
|
outputs = {}
|
43
|
-
target =
|
23
|
+
target = runtime[:target]
|
44
24
|
guard_stack = [true]
|
45
25
|
|
46
|
-
#
|
47
|
-
declaration_cache =
|
48
|
-
|
49
|
-
# Build name index for targeting by stored names
|
50
|
-
name_index = ctx[:name_index] || (target ? build_name_index(ir_module) : nil)
|
26
|
+
# Caches live in runtime (engine frame), not input
|
27
|
+
declaration_cache = runtime[:declaration_cache]
|
51
28
|
|
52
29
|
# Choose declarations to execute - prefer explicit schedule if present
|
53
|
-
decls_to_run =
|
54
|
-
if ctx[:decls_to_run]
|
55
|
-
ctx[:decls_to_run] # array of decl objects
|
56
|
-
elsif target
|
57
|
-
# Prefer a decl that STORES the target (covers __vec twins)
|
58
|
-
d = name_index && name_index[target]
|
59
|
-
# Fallback: allow targeting by decl name (legacy behavior)
|
60
|
-
d ||= ir_module.decls.find { |dd| dd.name == target }
|
61
|
-
raise "Unknown target: #{target}" unless d
|
62
|
-
|
63
|
-
[d]
|
64
|
-
else
|
65
|
-
ir_module.decls
|
66
|
-
end
|
30
|
+
# decls_to_run = runtime[:decls_to_run] || ir_module.decls
|
67
31
|
|
68
|
-
|
32
|
+
schedule.each do |decl|
|
69
33
|
slots = []
|
70
34
|
guard_stack = [true] # reset per decl
|
71
35
|
|
72
36
|
decl.ops.each_with_index do |op, op_index|
|
73
|
-
t0 =
|
74
|
-
cpu_t0 =
|
37
|
+
t0 = prof ? Profiler.t0 : nil
|
38
|
+
cpu_t0 = prof ? Profiler.cpu_t0 : nil
|
75
39
|
rows_touched = nil
|
76
40
|
if ENV["ASSERT_VM_SLOTS"] == "1"
|
77
41
|
expected = op_index
|
@@ -97,7 +61,7 @@ module Kumi
|
|
97
61
|
false
|
98
62
|
end
|
99
63
|
slots << nil # keep slot_id == op_index
|
100
|
-
if
|
64
|
+
if prof
|
101
65
|
Profiler.record!(decl: decl.name, idx: op_index, tag: op.tag, op: op, t0: t0, cpu_t0: cpu_t0, rows: 0,
|
102
66
|
note: "enter")
|
103
67
|
end
|
@@ -122,92 +86,46 @@ module Kumi
|
|
122
86
|
|
123
87
|
case op.tag
|
124
88
|
|
125
|
-
when :assign
|
126
|
-
dst = op.attrs[:dst]
|
127
|
-
src = op.attrs[:src]
|
128
|
-
raise "assign: dst/src OOB" if dst >= slots.length || src >= slots.length
|
129
|
-
|
130
|
-
slots[dst] = slots[src]
|
131
|
-
Profiler.record!(decl: decl.name, idx: op_index, tag: :assign, op: op, t0: t0, cpu_t0: cpu_t0, rows: 1) if t0
|
132
|
-
|
133
89
|
when :const
|
134
90
|
result = Values.scalar(op.attrs[:value])
|
135
|
-
puts "DEBUG Const #{op.attrs[:value].inspect}: result=#{result}" if ENV["DEBUG_VM_ARGS"]
|
136
91
|
slots << result
|
137
92
|
Profiler.record!(decl: decl.name, idx: op_index, tag: :const, op: op, t0: t0, cpu_t0: cpu_t0, rows: 1) if t0
|
138
93
|
|
139
94
|
when :load_input
|
140
95
|
plan_id = op.attrs[:plan_id]
|
141
|
-
scope = op.attrs[:scope] ||
|
96
|
+
scope = op.attrs[:scope] || EMPTY_ARY
|
142
97
|
scalar = op.attrs[:is_scalar]
|
143
98
|
indexed = op.attrs[:has_idx]
|
144
99
|
|
145
|
-
#
|
146
|
-
acc_cache = ctx[:accessor_cache] || {}
|
147
|
-
input_obj = ctx[:input] || ctx["input"]
|
148
|
-
cache_key = [plan_id, input_obj.object_id]
|
100
|
+
raw = accessors[plan_id].call(input) # <- memoized by ExecutionEngine
|
149
101
|
|
150
|
-
if acc_cache.key?(cache_key)
|
151
|
-
raw = acc_cache[cache_key]
|
152
|
-
hit = true
|
153
|
-
else
|
154
|
-
raw = accessors.fetch(plan_id).call(input_obj)
|
155
|
-
acc_cache[cache_key] = raw
|
156
|
-
hit = false
|
157
|
-
end
|
158
|
-
|
159
|
-
puts "DEBUG LoadInput plan_id: #{plan_id} raw_values: #{raw.inspect} cache_hit: #{hit}" if ENV["DEBUG_VM_ARGS"]
|
160
102
|
slots << if scalar
|
161
103
|
Values.scalar(raw)
|
162
104
|
elsif indexed
|
163
|
-
rows_touched = raw.respond_to?(:size) ? raw.size : raw.count
|
105
|
+
rows_touched = prof && raw.respond_to?(:size) ? raw.size : raw.count
|
164
106
|
Values.vec(scope, raw.map { |v, idx| { v: v, idx: Array(idx) } }, true)
|
165
107
|
else
|
166
|
-
rows_touched = raw.respond_to?(:size) ? raw.size : raw.count
|
108
|
+
rows_touched = prof && raw.respond_to?(:size) ? raw.size : raw.count
|
167
109
|
Values.vec(scope, raw.map { |v| { v: v } }, false)
|
168
110
|
end
|
169
111
|
rows_touched ||= 1
|
170
|
-
cache_note = hit ? "hit:#{plan_id}" : "miss:#{plan_id}"
|
171
112
|
if t0
|
172
113
|
Profiler.record!(decl: decl.name, idx: op_index, tag: :load_input, op: op, t0: t0, cpu_t0: cpu_t0,
|
173
|
-
rows: rows_touched, note:
|
114
|
+
rows: rows_touched, note: "ok")
|
174
115
|
end
|
175
116
|
|
176
117
|
when :ref
|
177
118
|
name = op.attrs[:name]
|
178
|
-
|
179
|
-
if outputs.key?(name)
|
180
|
-
referenced = outputs[name]
|
181
|
-
hit = :outputs
|
182
|
-
elsif declaration_cache.key?(name)
|
183
|
-
referenced = declaration_cache[name]
|
184
|
-
hit = :cache
|
185
|
-
else
|
186
|
-
raise "unscheduled ref #{name}: producer not executed or dependency analysis failed"
|
187
|
-
end
|
188
|
-
|
189
|
-
if ENV["DEBUG_VM_ARGS"]
|
190
|
-
puts "DEBUG Ref #{name}: #{referenced[:k] == :scalar ? "scalar(#{referenced[:v].inspect})" : "#{referenced[:k]}(#{referenced[:rows]&.size || 0} rows)"}"
|
191
|
-
end
|
119
|
+
referenced = outputs[name] { raise "unscheduled ref #{name}: producer not executed or dependency analysis failed" }
|
192
120
|
|
193
121
|
slots << referenced
|
194
122
|
rows_touched = referenced[:k] == :vec ? (referenced[:rows]&.size || 0) : 1
|
195
|
-
if
|
123
|
+
if prof
|
196
124
|
Profiler.record!(decl: decl.name, idx: op_index, tag: :ref, op: op, t0: t0, cpu_t0: cpu_t0,
|
197
125
|
rows: rows_touched, note: hit)
|
198
126
|
end
|
199
127
|
|
200
128
|
when :array
|
201
|
-
# Validate slot indices before accessing
|
202
|
-
op.args.each do |slot_idx|
|
203
|
-
if slot_idx >= slots.length
|
204
|
-
raise "Array operation: slot index #{slot_idx} out of bounds (slots.length=#{slots.length})"
|
205
|
-
elsif slots[slot_idx].nil?
|
206
|
-
raise "Array operation: slot #{slot_idx} is nil " \
|
207
|
-
"(available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
129
|
parts = op.args.map { |i| slots[i] }
|
212
130
|
if parts.all? { |p| p[:k] == :scalar }
|
213
131
|
slots << Values.scalar(parts.map { |p| p[:v] })
|
@@ -231,63 +149,43 @@ module Kumi
|
|
231
149
|
fn_name = op.attrs[:fn]
|
232
150
|
fn_entry = registry[fn_name] or raise "Function #{fn_name} not found in registry"
|
233
151
|
fn = fn_entry.fn
|
234
|
-
puts "DEBUG Map #{fn_name}: args=#{op.args.inspect}" if ENV["DEBUG_VM_ARGS"]
|
235
152
|
|
236
153
|
# Validate slot indices before accessing
|
237
|
-
op.args.each do |slot_idx|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
end
|
154
|
+
# op.args.each do |slot_idx|
|
155
|
+
# if slot_idx >= slots.length
|
156
|
+
# raise "Map operation #{fn_name}: slot index #{slot_idx} out of bounds (slots.length=#{slots.length})"
|
157
|
+
# elsif slots[slot_idx].nil?
|
158
|
+
# raise "Map operation #{fn_name}: slot #{slot_idx} is nil " \
|
159
|
+
# "(available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
160
|
+
# end
|
161
|
+
# end
|
245
162
|
|
246
163
|
args = op.args.map { |slot_idx| slots[slot_idx] }
|
247
164
|
|
248
165
|
if args.all? { |a| a[:k] == :scalar }
|
249
|
-
puts "DEBUG Scalar call #{fn_name}: args=#{args.map { |a| a[:v] }.inspect}" if ENV["DEBUG_VM_ARGS"]
|
250
166
|
scalar_args = args.map { |a| a[:v] }
|
251
167
|
result = fn.call(*scalar_args)
|
252
168
|
slots << Values.scalar(result)
|
253
169
|
else
|
254
170
|
base = args.find { |a| a[:k] == :vec } or raise "Map needs a vec carrier"
|
255
|
-
puts "DEBUG Vec call #{fn_name}: base=#{base.inspect}" if ENV["DEBUG_VM_ARGS"]
|
256
171
|
# Preserve original order: broadcast scalars in-place
|
257
172
|
arg_vecs = args.map { |a| a[:k] == :scalar ? Combinators.broadcast_scalar(a, base) : a }
|
258
|
-
puts "DEBUG Vec call #{fn_name}: arg_vecs=#{arg_vecs.inspect}" if ENV["DEBUG_VM_ARGS"]
|
259
173
|
scopes = arg_vecs.map { |v| v[:scope] }.uniq
|
260
|
-
puts "DEBUG Vec call #{fn_name}: scopes=#{scopes.inspect}" if ENV["DEBUG_VM_ARGS"]
|
261
174
|
raise "Cross-scope Map without Join" unless scopes.size <= 1
|
262
175
|
|
263
176
|
zipped = Combinators.zip_same_scope(*arg_vecs)
|
264
177
|
|
265
|
-
# if ENV["DEBUG_VM_ARGS"] && fn_name == :if
|
266
|
-
# puts "DEBUG Vec call #{fn_name}: zipped rows:"
|
267
|
-
# zipped[:rows].each_with_index do |row, i|
|
268
|
-
# puts " [#{i}] args=#{Array(row[:v]).inspect}"
|
269
|
-
# end
|
270
|
-
# end
|
271
|
-
|
272
|
-
puts "DEBUG Vec call #{fn_name}: zipped rows=#{zipped[:rows].inspect}" if ENV["DEBUG_VM_ARGS"]
|
273
178
|
rows = zipped[:rows].map do |row|
|
274
179
|
row_args = Array(row[:v])
|
275
180
|
vr = fn.call(*row_args)
|
276
181
|
row.key?(:idx) ? { v: vr, idx: row[:idx] } : { v: vr }
|
277
182
|
end
|
278
|
-
puts "DEBUG Vec call #{fn_name}: result rows=#{rows.inspect}" if ENV["DEBUG_VM_ARGS"]
|
279
183
|
|
280
184
|
slots << Values.vec(base[:scope], rows, base[:has_idx])
|
281
185
|
end
|
282
186
|
|
283
187
|
when :switch
|
284
188
|
chosen = op.attrs[:cases].find do |(cond_slot, _)|
|
285
|
-
if cond_slot >= slots.length
|
286
|
-
raise "Switch operation: condition slot #{cond_slot} out of bounds (slots.length=#{slots.length})"
|
287
|
-
elsif slots[cond_slot].nil?
|
288
|
-
raise "Switch operation: condition slot #{cond_slot} is nil (available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
289
|
-
end
|
290
|
-
|
291
189
|
c = slots[cond_slot]
|
292
190
|
if c[:k] == :scalar
|
293
191
|
!!c[:v]
|
@@ -297,22 +195,12 @@ module Kumi
|
|
297
195
|
end
|
298
196
|
end
|
299
197
|
result_slot = chosen ? chosen[1] : op.attrs[:default]
|
300
|
-
if result_slot >= slots.length
|
301
|
-
raise "Switch operation: result slot #{result_slot} out of bounds (slots.length=#{slots.length})"
|
302
|
-
elsif slots[result_slot].nil?
|
303
|
-
raise "Switch operation: result slot #{result_slot} is nil (available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
304
|
-
end
|
305
198
|
|
306
199
|
slots << slots[result_slot]
|
307
200
|
|
308
201
|
when :store
|
309
202
|
name = op.attrs[:name]
|
310
203
|
src = op.args[0] or raise "store: missing source slot"
|
311
|
-
if src >= slots.length
|
312
|
-
raise "Store operation '#{name}': source slot #{src} out of bounds (slots.length=#{slots.length})"
|
313
|
-
elsif slots[src].nil?
|
314
|
-
raise "Store operation '#{name}': source slot #{src} is nil (available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
315
|
-
end
|
316
204
|
|
317
205
|
result = slots[src]
|
318
206
|
outputs[name] = result
|
@@ -329,10 +217,8 @@ module Kumi
|
|
329
217
|
fn = fn_entry.fn
|
330
218
|
|
331
219
|
src = slots[op.args[0]]
|
332
|
-
|
333
|
-
|
334
|
-
result_scope = Array(op.attrs[:result_scope] || [])
|
335
|
-
axis = Array(op.attrs[:axis] || [])
|
220
|
+
result_scope = op.attrs[:result_scope]
|
221
|
+
axis = op.attrs[:axis]
|
336
222
|
|
337
223
|
if result_scope.empty?
|
338
224
|
# === GLOBAL REDUCE ===
|
@@ -340,12 +226,6 @@ module Kumi
|
|
340
226
|
vals = src[:rows].map { |r| r[:v] }
|
341
227
|
slots << Values.scalar(fn.call(vals))
|
342
228
|
else
|
343
|
-
# === GROUPED REDUCE ===
|
344
|
-
# Must have indices to group by prefix keys.
|
345
|
-
unless src[:has_idx]
|
346
|
-
raise "Grouped reduce requires indexed input (got ravel) for #{op.attrs[:fn]} at #{result_scope.inspect}"
|
347
|
-
end
|
348
|
-
|
349
229
|
group_len = result_scope.length
|
350
230
|
|
351
231
|
# Preserve stable source order so zips with other @result_scope vecs line up.
|
@@ -368,39 +248,17 @@ module Kumi
|
|
368
248
|
|
369
249
|
when :lift
|
370
250
|
src_slot = op.args[0]
|
371
|
-
if src_slot >= slots.length
|
372
|
-
raise "Lift operation: source slot #{src_slot} out of bounds (slots.length=#{slots.length})"
|
373
|
-
elsif slots[src_slot].nil?
|
374
|
-
raise "Lift operation: source slot #{src_slot} is nil (available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
375
|
-
end
|
376
251
|
|
377
252
|
v = slots[src_slot]
|
378
|
-
to_scope = op.attrs[:to_scope] ||
|
253
|
+
to_scope = op.attrs[:to_scope] || EMPTY_ARY
|
379
254
|
depth = [to_scope.length, v[:rank] || v[:rows].first&.dig(:idx)&.length || 0].min
|
380
255
|
slots << Values.scalar(Combinators.group_rows(v[:rows], depth))
|
381
256
|
|
382
257
|
when :align_to
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
if tgt_slot >= slots.length
|
387
|
-
raise "AlignTo operation: target slot #{tgt_slot} out of bounds (slots.length=#{slots.length})"
|
388
|
-
elsif slots[tgt_slot].nil?
|
389
|
-
raise "AlignTo operation: target slot #{tgt_slot} is nil " \
|
390
|
-
"(available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
391
|
-
end
|
392
|
-
|
393
|
-
if src_slot >= slots.length
|
394
|
-
raise "AlignTo operation: source slot #{src_slot} out of bounds (slots.length=#{slots.length})"
|
395
|
-
elsif slots[src_slot].nil?
|
396
|
-
raise "AlignTo operation: source slot #{src_slot} is nil " \
|
397
|
-
"(available slots: #{slots.length}, non-nil slots: #{slots.compact.length})"
|
398
|
-
end
|
258
|
+
tgt = slots[op.args[0]]
|
259
|
+
src = slots[op.args[1]]
|
399
260
|
|
400
|
-
|
401
|
-
src = slots[src_slot]
|
402
|
-
|
403
|
-
to_scope = op.attrs[:to_scope] || []
|
261
|
+
to_scope = op.attrs[:to_scope] || EMPTY_ARY
|
404
262
|
require_unique = op.attrs[:require_unique] || false
|
405
263
|
on_missing = op.attrs[:on_missing] || :error
|
406
264
|
|
@@ -409,9 +267,6 @@ module Kumi
|
|
409
267
|
on_missing: on_missing)
|
410
268
|
slots << aligned
|
411
269
|
|
412
|
-
when :join
|
413
|
-
raise NotImplementedError, "Join not implemented yet"
|
414
|
-
|
415
270
|
else
|
416
271
|
raise "Unknown operation: #{op.tag}"
|
417
272
|
end
|
@@ -13,14 +13,14 @@ module Kumi
|
|
13
13
|
|
14
14
|
# Create a vector with scope and rows
|
15
15
|
def self.vec(scope, rows, has_idx)
|
16
|
-
if has_idx
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
16
|
+
rank = if has_idx
|
17
|
+
rows.empty? ? 0 : rows.first[:idx].length
|
18
|
+
# TODO: > Make sure this is not costly
|
19
|
+
# raise if rows.any? { |r| r[:idx].length != rank }
|
20
|
+
# rows = rows.sort_by { |r| r[:idx] } # one-time sort
|
21
|
+
else
|
22
|
+
0
|
23
|
+
end
|
24
24
|
|
25
25
|
{ k: :vec, scope: scope, rows: rows, has_idx: has_idx, rank: rank }
|
26
26
|
end
|
@@ -41,29 +41,13 @@ module Kumi
|
|
41
41
|
# - DEBUG_VM_ARGS=1 prints per-op execution and arguments.
|
42
42
|
# - DEBUG_GROUP_ROWS=1 prints grouping decisions during Lift.
|
43
43
|
module ExecutionEngine
|
44
|
-
def self.run(
|
45
|
-
|
46
|
-
memoized_accessors = Dev::Profiler.phase("engine.memoization") do
|
47
|
-
# Include input data in cache key to avoid cross-context pollution
|
48
|
-
input_key = ctx[:input]&.hash || ctx["input"]&.hash || 0
|
49
|
-
add_persistent_memoization(accessors, ctx[:accessor_cache], input_key)
|
50
|
-
end
|
44
|
+
def self.run(schedule, input:, accessors:, registry:, runtime: {})
|
45
|
+
runtime[:accessor_cache] ||= {}
|
51
46
|
|
52
47
|
Dev::Profiler.phase("engine.interpreter") do
|
53
|
-
Interpreter.run(
|
48
|
+
Interpreter.run(schedule, input: input, runtime: runtime, accessors: accessors, registry: registry)
|
54
49
|
end
|
55
50
|
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def self.add_persistent_memoization(accessors, cache, input_key)
|
60
|
-
accessors.map do |plan_id, accessor_fn|
|
61
|
-
[plan_id, lambda do |input_data|
|
62
|
-
cache_key = [plan_id, input_key]
|
63
|
-
cache[cache_key] ||= accessor_fn.call(input_data)
|
64
|
-
end]
|
65
|
-
end.to_h
|
66
|
-
end
|
67
51
|
end
|
68
52
|
end
|
69
53
|
end
|
data/lib/kumi/dev/parse.rb
CHANGED
@@ -35,20 +35,20 @@ module Kumi
|
|
35
35
|
end
|
36
36
|
|
37
37
|
# Report trace file if enabled
|
38
|
-
if opts[:trace] && res.respond_to?(:trace_file)
|
39
|
-
puts "Trace written to: #{res.trace_file}"
|
40
|
-
end
|
38
|
+
puts "Trace written to: #{res.trace_file}" if opts[:trace] && res.respond_to?(:trace_file)
|
41
39
|
|
42
40
|
# Determine file extension and renderer
|
43
41
|
extension = opts[:json] ? "json" : "txt"
|
44
|
-
|
42
|
+
|
43
|
+
file_name = File.basename(schema_path)
|
44
|
+
golden_path = File.join(File.dirname(schema_path), "expected", "#{file_name}_ir.#{extension}")
|
45
45
|
|
46
46
|
# Render IR
|
47
47
|
rendered = if opts[:json]
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
48
|
+
Dev::IR.to_json(res.ir, pretty: true)
|
49
|
+
else
|
50
|
+
Dev::IR.to_text(res.ir)
|
51
|
+
end
|
52
52
|
|
53
53
|
# Handle write mode
|
54
54
|
if opts[:write]
|
@@ -71,7 +71,7 @@ module Kumi
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
# Handle no-diff mode
|
74
|
+
# Handle no-diff mode
|
75
75
|
if opts[:no_diff]
|
76
76
|
puts rendered
|
77
77
|
return true
|
@@ -84,7 +84,7 @@ module Kumi
|
|
84
84
|
Tempfile.create(["actual", File.extname(golden_path)]) do |actual_file|
|
85
85
|
actual_file.write(rendered)
|
86
86
|
actual_file.flush
|
87
|
-
|
87
|
+
|
88
88
|
result = `diff -u --label=expected --label=actual #{golden_path} #{actual_file.path}`
|
89
89
|
if result.empty?
|
90
90
|
puts "No changes (#{golden_path})"
|
@@ -97,9 +97,9 @@ module Kumi
|
|
97
97
|
else
|
98
98
|
# No golden file exists, just print the output
|
99
99
|
puts rendered
|
100
|
-
|
100
|
+
true
|
101
101
|
end
|
102
102
|
end
|
103
103
|
end
|
104
104
|
end
|
105
|
-
end
|
105
|
+
end
|