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
@@ -42,44 +42,40 @@ module Kumi
|
|
42
42
|
access_plans = state.fetch(:access_plans)
|
43
43
|
input_metadata = state[:input_metadata] || {}
|
44
44
|
dependents = state[:dependents] || {}
|
45
|
-
|
46
|
-
|
45
|
+
schedules = state[:ir_execution_schedules] || {}
|
46
|
+
|
47
47
|
accessors = Dev::Profiler.phase("compiler.access_builder") do
|
48
48
|
Kumi::Core::Compiler::AccessBuilder.build(access_plans)
|
49
49
|
end
|
50
50
|
|
51
51
|
access_meta = {}
|
52
|
-
field_to_plan_ids = Hash.new { |h, k| h[k] = [] }
|
53
52
|
|
54
|
-
access_plans.each_value do |plans|
|
55
|
-
|
56
|
-
|
53
|
+
# access_plans.each_value do |plans|
|
54
|
+
# plans.each do |p|
|
55
|
+
# access_meta[p.accessor_key] = { mode: p.mode, scope: p.scope }
|
57
56
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
57
|
+
# # Build precise field -> plan_ids mapping for invalidation
|
58
|
+
# root_field = p.accessor_key.to_s.split(":").first.split(".").first.to_sym
|
59
|
+
# field_to_plan_ids[root_field] << p.accessor_key
|
60
|
+
# end
|
61
|
+
# end
|
63
62
|
|
64
63
|
# Use the internal functions hash that VM expects
|
65
64
|
registry ||= Kumi::Registry.functions
|
66
65
|
new(ir: ir, accessors: accessors, access_meta: access_meta, registry: registry,
|
67
|
-
input_metadata: input_metadata,
|
68
|
-
|
66
|
+
input_metadata: input_metadata, dependents: dependents,
|
67
|
+
schema_name: schema_name, schedules: schedules)
|
69
68
|
end
|
70
69
|
|
71
|
-
def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:,
|
72
|
-
name_index: {}, schema_name: nil)
|
70
|
+
def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:, dependents: {}, schedules: {}, schema_name: nil)
|
73
71
|
@ir = ir.freeze
|
74
72
|
@acc = accessors.freeze
|
75
73
|
@meta = access_meta.freeze
|
76
74
|
@reg = registry
|
77
75
|
@input_metadata = input_metadata.freeze
|
78
|
-
@field_to_plan_ids = field_to_plan_ids.freeze
|
79
76
|
@dependents = dependents.freeze
|
80
|
-
@ir_dependencies = ir_dependencies.freeze # decl -> [stored_bindings_it_references]
|
81
|
-
@name_index = name_index.freeze # store_name -> producing decl
|
82
77
|
@schema_name = schema_name
|
78
|
+
@schedules = schedules
|
83
79
|
@decl = @ir.decls.map { |d| [d.name, d] }.to_h
|
84
80
|
@accessor_cache = {} # Persistent accessor cache across evaluations
|
85
81
|
end
|
@@ -87,7 +83,7 @@ module Kumi
|
|
87
83
|
def decl?(name) = @decl.key?(name)
|
88
84
|
|
89
85
|
def read(input, mode: :ruby)
|
90
|
-
Run.new(self, input, mode: mode, input_metadata: @input_metadata, dependents: @dependents)
|
86
|
+
Run.new(self, input, mode: mode, input_metadata: @input_metadata, dependents: @dependents, declarations: @decl.keys)
|
91
87
|
end
|
92
88
|
|
93
89
|
# API compatibility for backward compatibility
|
@@ -102,73 +98,30 @@ module Kumi
|
|
102
98
|
end
|
103
99
|
end
|
104
100
|
|
105
|
-
def eval_decl(name, input, mode: :ruby, declaration_cache:
|
101
|
+
def eval_decl(name, input, mode: :ruby, declaration_cache: {})
|
106
102
|
raise Kumi::Core::Errors::RuntimeError, "unknown decl #{name}" unless decl?(name)
|
107
103
|
|
104
|
+
schedule = @schedules[name]
|
108
105
|
# If the caller asked for a specific binding, schedule deps once
|
109
|
-
decls_to_run = topo_closure_for_target(name)
|
110
106
|
|
111
|
-
|
112
|
-
input: input,
|
107
|
+
runtime = {
|
113
108
|
accessor_cache: @accessor_cache,
|
114
|
-
declaration_cache: declaration_cache
|
115
|
-
|
116
|
-
|
117
|
-
name_index: @name_index, # for error messages, twins, etc.
|
118
|
-
schema_name: @schema_name
|
109
|
+
declaration_cache: declaration_cache, # run-local cache
|
110
|
+
schema_name: @schema_name,
|
111
|
+
target: name
|
119
112
|
}
|
120
113
|
|
121
114
|
out = Dev::Profiler.phase("vm.run", target: name) do
|
122
|
-
Kumi::Core::IR::ExecutionEngine.run(
|
115
|
+
Kumi::Core::IR::ExecutionEngine.run(schedule, input: input, runtime: runtime, accessors: @acc, registry: @reg).fetch(name)
|
123
116
|
end
|
124
117
|
|
125
118
|
mode == :ruby ? unwrap(@decl[name], out) : out
|
126
119
|
end
|
127
120
|
|
128
|
-
def clear_field_accessor_cache(field_name)
|
129
|
-
# Use precise field -> plan_ids mapping for exact invalidation
|
130
|
-
plan_ids = @field_to_plan_ids[field_name] || []
|
131
|
-
# Cache keys are [plan_id, input_object_id] arrays
|
132
|
-
@accessor_cache.delete_if { |(pid, _), _| plan_ids.include?(pid) }
|
133
|
-
end
|
134
|
-
|
135
121
|
def unwrap(_decl, v)
|
136
122
|
v[:k] == :scalar ? v[:v] : v # no grouping needed
|
137
123
|
end
|
138
124
|
|
139
|
-
def topo_closure_for_target(store_name)
|
140
|
-
target_decl = @name_index[store_name]
|
141
|
-
raise "Unknown target store #{store_name}" unless target_decl
|
142
|
-
|
143
|
-
# DFS collect closure of decl names using pre-computed IR-level dependencies
|
144
|
-
seen = {}
|
145
|
-
order = []
|
146
|
-
visiting = {}
|
147
|
-
|
148
|
-
visit = lambda do |dname|
|
149
|
-
return if seen[dname]
|
150
|
-
raise "Cycle detected in DAG scheduler: #{dname}. Mutual recursion should be caught earlier by UnsatDetector." if visiting[dname]
|
151
|
-
|
152
|
-
visiting[dname] = true
|
153
|
-
|
154
|
-
# Visit declarations that produce the bindings this decl references
|
155
|
-
Array(@ir_dependencies[dname]).each do |ref_binding|
|
156
|
-
# Find which declaration produces this binding
|
157
|
-
producer = @name_index[ref_binding]
|
158
|
-
visit.call(producer.name) if producer
|
159
|
-
end
|
160
|
-
|
161
|
-
visiting.delete(dname)
|
162
|
-
seen[dname] = true
|
163
|
-
order << dname
|
164
|
-
end
|
165
|
-
|
166
|
-
visit.call(target_decl.name)
|
167
|
-
|
168
|
-
# 'order' is postorder; it already yields producers before consumers
|
169
|
-
order.map { |dname| @decl[dname] }
|
170
|
-
end
|
171
|
-
|
172
125
|
private
|
173
126
|
|
174
127
|
def validate_keys(keys)
|
@@ -178,111 +131,5 @@ module Kumi
|
|
178
131
|
raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
|
179
132
|
end
|
180
133
|
end
|
181
|
-
|
182
|
-
class Run
|
183
|
-
def initialize(program, input, mode:, input_metadata:, dependents:)
|
184
|
-
@program = program
|
185
|
-
@input = input
|
186
|
-
@mode = mode
|
187
|
-
@input_metadata = input_metadata
|
188
|
-
@dependents = dependents
|
189
|
-
@cache = {}
|
190
|
-
end
|
191
|
-
|
192
|
-
def get(name)
|
193
|
-
unless @cache.key?(name)
|
194
|
-
# Get the result in VM internal format
|
195
|
-
vm_result = @program.eval_decl(name, @input, mode: :wrapped, declaration_cache: @cache)
|
196
|
-
# Store VM format for cross-VM caching
|
197
|
-
@cache[name] = vm_result
|
198
|
-
end
|
199
|
-
|
200
|
-
# Convert to requested format when returning
|
201
|
-
vm_result = @cache[name]
|
202
|
-
@mode == :wrapped ? vm_result : @program.unwrap(nil, vm_result)
|
203
|
-
end
|
204
|
-
|
205
|
-
def [](name)
|
206
|
-
get(name)
|
207
|
-
end
|
208
|
-
|
209
|
-
def slice(*keys)
|
210
|
-
return {} if keys.empty?
|
211
|
-
|
212
|
-
keys.each_with_object({}) { |key, result| result[key] = get(key) }
|
213
|
-
end
|
214
|
-
|
215
|
-
def compiled_schema
|
216
|
-
@program
|
217
|
-
end
|
218
|
-
|
219
|
-
def method_missing(sym, *args, **kwargs, &)
|
220
|
-
return super unless args.empty? && kwargs.empty? && @program.decl?(sym)
|
221
|
-
|
222
|
-
get(sym)
|
223
|
-
end
|
224
|
-
|
225
|
-
def respond_to_missing?(sym, priv = false)
|
226
|
-
@program.decl?(sym) || super
|
227
|
-
end
|
228
|
-
|
229
|
-
def update(**changes)
|
230
|
-
affected_declarations = Set.new
|
231
|
-
|
232
|
-
changes.each do |field, value|
|
233
|
-
# Validate field exists
|
234
|
-
raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
|
235
|
-
|
236
|
-
# Validate domain constraints
|
237
|
-
validate_domain_constraint(field, value)
|
238
|
-
|
239
|
-
# Update the input data IN-PLACE to preserve object_id for cache keys
|
240
|
-
@input[field] = value
|
241
|
-
|
242
|
-
# Clear accessor cache for this specific field
|
243
|
-
@program.clear_field_accessor_cache(field)
|
244
|
-
|
245
|
-
# Collect all declarations that depend on this input field
|
246
|
-
field_dependents = @dependents[field] || []
|
247
|
-
affected_declarations.merge(field_dependents)
|
248
|
-
end
|
249
|
-
|
250
|
-
# Only clear cache for affected declarations, not all declarations
|
251
|
-
affected_declarations.each { |decl| @cache.delete(decl) }
|
252
|
-
|
253
|
-
self
|
254
|
-
end
|
255
|
-
|
256
|
-
private
|
257
|
-
|
258
|
-
def input_field_exists?(field)
|
259
|
-
# Check if field is declared in input block
|
260
|
-
@input_metadata.key?(field) || @input.key?(field)
|
261
|
-
end
|
262
|
-
|
263
|
-
def validate_domain_constraint(field, value)
|
264
|
-
field_meta = @input_metadata[field]
|
265
|
-
return unless field_meta&.dig(:domain)
|
266
|
-
|
267
|
-
domain = field_meta[:domain]
|
268
|
-
return unless violates_domain?(value, domain)
|
269
|
-
|
270
|
-
raise ArgumentError, "value #{value} is not in domain #{domain}"
|
271
|
-
end
|
272
|
-
|
273
|
-
def violates_domain?(value, domain)
|
274
|
-
case domain
|
275
|
-
when Range
|
276
|
-
!domain.include?(value)
|
277
|
-
when Array
|
278
|
-
!domain.include?(value)
|
279
|
-
when Proc
|
280
|
-
# For Proc domains, we can't statically analyze
|
281
|
-
false
|
282
|
-
else
|
283
|
-
false
|
284
|
-
end
|
285
|
-
end
|
286
|
-
end
|
287
134
|
end
|
288
135
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Runtime
|
5
|
+
class Run
|
6
|
+
def initialize(program, input, mode:, input_metadata:, dependents:, declarations:)
|
7
|
+
@program = program
|
8
|
+
@input = input
|
9
|
+
@mode = mode
|
10
|
+
@input_metadata = input_metadata
|
11
|
+
@declarations = declarations
|
12
|
+
@dependents = dependents
|
13
|
+
@cache = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def key?(name)
|
17
|
+
@declarations.include? name
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(name)
|
21
|
+
unless @cache.key?(name)
|
22
|
+
# Get the result in VM internal format
|
23
|
+
vm_result = @program.eval_decl(name, @input, mode: :wrapped, declaration_cache: @cache)
|
24
|
+
# Store VM format for cross-VM caching
|
25
|
+
@cache[name] = vm_result
|
26
|
+
end
|
27
|
+
|
28
|
+
# Convert to requested format when returning
|
29
|
+
vm_result = @cache[name]
|
30
|
+
@mode == :wrapped ? vm_result : @program.unwrap(nil, vm_result)
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h
|
34
|
+
slice(*@declarations)
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](name)
|
38
|
+
get(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def slice(*keys)
|
42
|
+
return {} if keys.empty?
|
43
|
+
|
44
|
+
keys.each_with_object({}) { |key, result| result[key] = get(key) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def compiled_schema
|
48
|
+
@program
|
49
|
+
end
|
50
|
+
|
51
|
+
def method_missing(sym, *args, **kwargs, &)
|
52
|
+
return super unless args.empty? && kwargs.empty? && key?(sym)
|
53
|
+
|
54
|
+
get(sym)
|
55
|
+
end
|
56
|
+
|
57
|
+
def respond_to_missing?(sym, priv = false)
|
58
|
+
key?(sym) || super
|
59
|
+
end
|
60
|
+
|
61
|
+
def update(**changes)
|
62
|
+
affected_declarations = Set.new
|
63
|
+
|
64
|
+
changes.each do |field, value|
|
65
|
+
raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
|
66
|
+
|
67
|
+
validate_domain_constraint(field, value)
|
68
|
+
|
69
|
+
@input[field] = value
|
70
|
+
if (deps = @dependents[field])
|
71
|
+
deps.each { |d| @cache.delete(d) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def input_field_exists?(field)
|
81
|
+
# Check if field is declared in input block
|
82
|
+
@input_metadata.key?(field)
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_domain_constraint(field, value)
|
86
|
+
field_meta = @input_metadata[field]
|
87
|
+
return unless field_meta&.dig(:domain)
|
88
|
+
|
89
|
+
domain = field_meta[:domain]
|
90
|
+
return unless violates_domain?(value, domain)
|
91
|
+
|
92
|
+
raise ArgumentError, "value #{value} is not in domain #{domain}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def violates_domain?(value, domain)
|
96
|
+
case domain
|
97
|
+
when Range, Array
|
98
|
+
!domain.include?(value)
|
99
|
+
else
|
100
|
+
false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/kumi/schema.rb
CHANGED
@@ -4,18 +4,12 @@ require "ostruct"
|
|
4
4
|
|
5
5
|
module Kumi
|
6
6
|
module Schema
|
7
|
-
attr_reader :__syntax_tree__, :__analyzer_result__, :
|
8
|
-
|
9
|
-
Inspector = Struct.new(:syntax_tree, :analyzer_result, :compiled_schema) do
|
10
|
-
def inspect
|
11
|
-
"#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, compiled_schema: #{compiled_schema.inspect}>"
|
12
|
-
end
|
13
|
-
end
|
7
|
+
attr_reader :__syntax_tree__, :__analyzer_result__, :__executable__
|
14
8
|
|
15
9
|
def from(context)
|
16
10
|
# VERY IMPORTANT: This method is overriden on specs in order to use dual mode.
|
17
11
|
|
18
|
-
raise("No schema defined") unless @
|
12
|
+
raise("No schema defined") unless @__executable__
|
19
13
|
|
20
14
|
# Validate input types and domain constraints
|
21
15
|
input_meta = @__analyzer_result__.state[:input_metadata] || {}
|
@@ -23,11 +17,12 @@ module Kumi
|
|
23
17
|
|
24
18
|
raise Errors::InputValidationError, violations unless violations.empty?
|
25
19
|
|
26
|
-
|
20
|
+
# TODO: Lazily start a Runner
|
21
|
+
@__executable__.read(context, mode: :ruby)
|
27
22
|
end
|
28
23
|
|
29
24
|
def explain(context, *keys)
|
30
|
-
raise("No schema defined") unless @
|
25
|
+
raise("No schema defined") unless @__executable__
|
31
26
|
|
32
27
|
# Validate input types and domain constraints
|
33
28
|
input_meta = @__analyzer_result__.state[:input_metadata] || {}
|
@@ -58,11 +53,11 @@ module Kumi
|
|
58
53
|
@__analyzer_result__ = Dev::Profiler.phase("analyzer") do
|
59
54
|
Analyzer.analyze!(@__syntax_tree__).freeze
|
60
55
|
end
|
61
|
-
@
|
62
|
-
Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__, schema_name:
|
56
|
+
@__executable__ = Dev::Profiler.phase("compiler") do
|
57
|
+
Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__, schema_name: name).freeze
|
63
58
|
end
|
64
59
|
|
65
|
-
|
60
|
+
nil
|
66
61
|
end
|
67
62
|
|
68
63
|
def schema_metadata
|
data/lib/kumi/version.rb
CHANGED
data/lib/kumi.rb
CHANGED
@@ -11,8 +11,9 @@ loader.inflector.inflect(
|
|
11
11
|
"ir_dependency_pass" => "IRDependencyPass",
|
12
12
|
"vm" => "VM",
|
13
13
|
"ir" => "IR",
|
14
|
-
|
15
|
-
|
14
|
+
"ir_dump" => "IRDump",
|
15
|
+
"ir_render" => "IRRender",
|
16
|
+
"ir_execution_schedule_pass" => "IRExecutionSchedulePass"
|
16
17
|
)
|
17
18
|
loader.setup
|
18
19
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- André Muta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -94,6 +94,7 @@ files:
|
|
94
94
|
- lib/kumi/core/analyzer/passes/input_access_planner_pass.rb
|
95
95
|
- lib/kumi/core/analyzer/passes/input_collector.rb
|
96
96
|
- lib/kumi/core/analyzer/passes/ir_dependency_pass.rb
|
97
|
+
- lib/kumi/core/analyzer/passes/ir_execution_schedule_pass.rb
|
97
98
|
- lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb
|
98
99
|
- lib/kumi/core/analyzer/passes/load_input_cse.rb
|
99
100
|
- lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb
|
@@ -113,6 +114,12 @@ files:
|
|
113
114
|
- lib/kumi/core/analyzer/structs/input_meta.rb
|
114
115
|
- lib/kumi/core/atom_unsat_solver.rb
|
115
116
|
- lib/kumi/core/compiler/access_builder.rb
|
117
|
+
- lib/kumi/core/compiler/access_codegen.rb
|
118
|
+
- lib/kumi/core/compiler/access_emit/base.rb
|
119
|
+
- lib/kumi/core/compiler/access_emit/each_indexed.rb
|
120
|
+
- lib/kumi/core/compiler/access_emit/materialize.rb
|
121
|
+
- lib/kumi/core/compiler/access_emit/ravel.rb
|
122
|
+
- lib/kumi/core/compiler/access_emit/read.rb
|
116
123
|
- lib/kumi/core/compiler/access_planner.rb
|
117
124
|
- lib/kumi/core/compiler/accessors/base.rb
|
118
125
|
- lib/kumi/core/compiler/accessors/each_indexed_accessor.rb
|
@@ -207,6 +214,7 @@ files:
|
|
207
214
|
- lib/kumi/kernels/ruby/vector_struct.rb
|
208
215
|
- lib/kumi/registry.rb
|
209
216
|
- lib/kumi/runtime/executable.rb
|
217
|
+
- lib/kumi/runtime/run.rb
|
210
218
|
- lib/kumi/schema.rb
|
211
219
|
- lib/kumi/schema_metadata.rb
|
212
220
|
- lib/kumi/support/diff.rb
|