kumi 0.0.14 → 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.
@@ -37,40 +37,45 @@ module Kumi
37
37
  # - DEBUG_VM_ARGS=1 to trace VM execution
38
38
  # - Accessors can be debugged independently with DEBUG_ACCESSOR_OPS=1
39
39
  class Executable
40
- def self.from_analysis(state, registry: nil)
40
+ def self.from_analysis(state, registry: nil, schema_name: nil)
41
41
  ir = state.fetch(:ir_module)
42
42
  access_plans = state.fetch(:access_plans)
43
43
  input_metadata = state[:input_metadata] || {}
44
44
  dependents = state[:dependents] || {}
45
- accessors = Kumi::Core::Compiler::AccessBuilder.build(access_plans)
45
+ schedules = state[:ir_execution_schedules] || {}
46
+
47
+ accessors = Dev::Profiler.phase("compiler.access_builder") do
48
+ Kumi::Core::Compiler::AccessBuilder.build(access_plans)
49
+ end
46
50
 
47
51
  access_meta = {}
48
- field_to_plan_ids = Hash.new { |h, k| h[k] = [] }
49
52
 
50
- access_plans.each_value do |plans|
51
- plans.each do |p|
52
- access_meta[p.accessor_key] = { mode: p.mode, scope: p.scope }
53
+ # access_plans.each_value do |plans|
54
+ # plans.each do |p|
55
+ # access_meta[p.accessor_key] = { mode: p.mode, scope: p.scope }
53
56
 
54
- # Build precise field -> plan_ids mapping for invalidation
55
- root_field = p.accessor_key.to_s.split(":").first.split(".").first.to_sym
56
- field_to_plan_ids[root_field] << p.accessor_key
57
- end
58
- 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
59
62
 
60
63
  # Use the internal functions hash that VM expects
61
64
  registry ||= Kumi::Registry.functions
62
65
  new(ir: ir, accessors: accessors, access_meta: access_meta, registry: registry,
63
- input_metadata: input_metadata, field_to_plan_ids: field_to_plan_ids, dependents: dependents)
66
+ input_metadata: input_metadata, dependents: dependents,
67
+ schema_name: schema_name, schedules: schedules)
64
68
  end
65
69
 
66
- def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:, field_to_plan_ids: {}, dependents: {})
70
+ def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:, dependents: {}, schedules: {}, schema_name: nil)
67
71
  @ir = ir.freeze
68
72
  @acc = accessors.freeze
69
73
  @meta = access_meta.freeze
70
74
  @reg = registry
71
75
  @input_metadata = input_metadata.freeze
72
- @field_to_plan_ids = field_to_plan_ids.freeze
73
76
  @dependents = dependents.freeze
77
+ @schema_name = schema_name
78
+ @schedules = schedules
74
79
  @decl = @ir.decls.map { |d| [d.name, d] }.to_h
75
80
  @accessor_cache = {} # Persistent accessor cache across evaluations
76
81
  end
@@ -78,7 +83,7 @@ module Kumi
78
83
  def decl?(name) = @decl.key?(name)
79
84
 
80
85
  def read(input, mode: :ruby)
81
- 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)
82
87
  end
83
88
 
84
89
  # API compatibility for backward compatibility
@@ -93,26 +98,24 @@ module Kumi
93
98
  end
94
99
  end
95
100
 
96
- def eval_decl(name, input, mode: :ruby, declaration_cache: nil)
101
+ def eval_decl(name, input, mode: :ruby, declaration_cache: {})
97
102
  raise Kumi::Core::Errors::RuntimeError, "unknown decl #{name}" unless decl?(name)
98
103
 
99
- vm_context = {
100
- input: input,
101
- target: name,
104
+ schedule = @schedules[name]
105
+ # If the caller asked for a specific binding, schedule deps once
106
+
107
+ runtime = {
102
108
  accessor_cache: @accessor_cache,
103
- declaration_cache: declaration_cache
109
+ declaration_cache: declaration_cache, # run-local cache
110
+ schema_name: @schema_name,
111
+ target: name
104
112
  }
105
-
106
- out = Kumi::Core::IR::ExecutionEngine.run(@ir, vm_context, accessors: @acc, registry: @reg).fetch(name)
107
113
 
108
- mode == :ruby ? unwrap(@decl[name], out) : out
109
- end
114
+ out = Dev::Profiler.phase("vm.run", target: name) do
115
+ Kumi::Core::IR::ExecutionEngine.run(schedule, input: input, runtime: runtime, accessors: @acc, registry: @reg).fetch(name)
116
+ end
110
117
 
111
- def clear_field_accessor_cache(field_name)
112
- # Use precise field -> plan_ids mapping for exact invalidation
113
- plan_ids = @field_to_plan_ids[field_name] || []
114
- # Cache keys are [plan_id, input_object_id] arrays
115
- @accessor_cache.delete_if { |(pid, _), _| plan_ids.include?(pid) }
118
+ mode == :ruby ? unwrap(@decl[name], out) : out
116
119
  end
117
120
 
118
121
  def unwrap(_decl, v)
@@ -128,129 +131,5 @@ module Kumi
128
131
  raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
129
132
  end
130
133
  end
131
-
132
- class Run
133
- def initialize(program, input, mode:, input_metadata:, dependents:)
134
- @program = program
135
- @input = input
136
- @mode = mode
137
- @input_metadata = input_metadata
138
- @dependents = dependents
139
- @cache = {}
140
- end
141
-
142
- def get(name)
143
- unless @cache.key?(name)
144
- # Get the result in VM internal format
145
- vm_result = @program.eval_decl(name, @input, mode: :wrapped, declaration_cache: @cache)
146
- # Store VM format for cross-VM caching
147
- @cache[name] = vm_result
148
- end
149
-
150
- # Convert to requested format when returning
151
- vm_result = @cache[name]
152
- @mode == :wrapped ? vm_result : @program.unwrap(nil, vm_result)
153
- end
154
-
155
- def [](name)
156
- get(name)
157
- end
158
-
159
- def slice(*keys)
160
- return {} if keys.empty?
161
-
162
- keys.each_with_object({}) { |key, result| result[key] = get(key) }
163
- end
164
-
165
- def compiled_schema
166
- @program
167
- end
168
-
169
- def method_missing(sym, *args, **kwargs, &)
170
- return super unless args.empty? && kwargs.empty? && @program.decl?(sym)
171
-
172
- get(sym)
173
- end
174
-
175
- def respond_to_missing?(sym, priv = false)
176
- @program.decl?(sym) || super
177
- end
178
-
179
- def update(**changes)
180
- affected_declarations = Set.new
181
-
182
- changes.each do |field, value|
183
- # Validate field exists
184
- raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
185
-
186
- # Validate domain constraints
187
- validate_domain_constraint(field, value)
188
-
189
- # Update the input data IN-PLACE to preserve object_id for cache keys
190
- @input[field] = value
191
-
192
- # Clear accessor cache for this specific field
193
- @program.clear_field_accessor_cache(field)
194
-
195
- # Collect all declarations that depend on this input field
196
- field_dependents = @dependents[field] || []
197
- affected_declarations.merge(field_dependents)
198
- end
199
-
200
- # Only clear cache for affected declarations, not all declarations
201
- affected_declarations.each { |decl| @cache.delete(decl) }
202
-
203
- self
204
- end
205
-
206
- def wrapped!
207
- @mode = :wrapped
208
- @cache.clear
209
- self
210
- end
211
-
212
- def ruby!
213
- @mode = :ruby
214
- @cache.clear
215
- self
216
- end
217
-
218
- private
219
-
220
- def input_field_exists?(field)
221
- # Check if field is declared in input block
222
- @input_metadata.key?(field) || @input.key?(field)
223
- end
224
-
225
- def validate_domain_constraint(field, value)
226
- field_meta = @input_metadata[field]
227
- return unless field_meta&.dig(:domain)
228
-
229
- domain = field_meta[:domain]
230
- return unless violates_domain?(value, domain)
231
-
232
- raise ArgumentError, "value #{value} is not in domain #{domain}"
233
- end
234
-
235
- def violates_domain?(value, domain)
236
- case domain
237
- when Range
238
- !domain.include?(value)
239
- when Array
240
- !domain.include?(value)
241
- when Proc
242
- # For Proc domains, we can't statically analyze
243
- false
244
- else
245
- false
246
- end
247
- end
248
-
249
- def deep_merge(a, b)
250
- return b unless a.is_a?(Hash) && b.is_a?(Hash)
251
-
252
- a.merge(b) { |_k, v1, v2| deep_merge(v1, v2) }
253
- end
254
- end
255
134
  end
256
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__, :__compiled_schema__
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 @__compiled_schema__
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
- @__compiled_schema__.read(context, mode: :ruby)
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 @__compiled_schema__
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] || {}
@@ -49,14 +44,20 @@ module Kumi
49
44
  def schema(&)
50
45
  # from_location = caller_locations(1, 1).first
51
46
  # raise "Called from #{from_location.path}:#{from_location.lineno}"
52
- @__syntax_tree__ = Core::RubyParser::Dsl.build_syntax_tree(&).freeze
47
+ @__syntax_tree__ = Dev::Profiler.phase("frontend.parse") do
48
+ Core::RubyParser::Dsl.build_syntax_tree(&).freeze
49
+ end
53
50
 
54
51
  puts Support::SExpressionPrinter.print(@__syntax_tree__, indent: 2) if ENV["KUMI_DEBUG"] || ENV["KUMI_PRINT_SYNTAX_TREE"]
55
52
 
56
- @__analyzer_result__ = Analyzer.analyze!(@__syntax_tree__).freeze
57
- @__compiled_schema__ = Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__).freeze
53
+ @__analyzer_result__ = Dev::Profiler.phase("analyzer") do
54
+ Analyzer.analyze!(@__syntax_tree__).freeze
55
+ end
56
+ @__executable__ = Dev::Profiler.phase("compiler") do
57
+ Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__, schema_name: name).freeze
58
+ end
58
59
 
59
- Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
60
+ nil
60
61
  end
61
62
 
62
63
  def schema_metadata
data/lib/kumi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kumi
4
- VERSION = "0.0.14"
4
+ VERSION = "0.0.16"
5
5
  end
data/lib/kumi.rb CHANGED
@@ -8,10 +8,12 @@ loader.ignore("#{__dir__}/kumi-cli")
8
8
  loader.inflector.inflect(
9
9
  "lower_to_ir_pass" => "LowerToIRPass",
10
10
  "load_input_cse" => "LoadInputCSE",
11
+ "ir_dependency_pass" => "IRDependencyPass",
11
12
  "vm" => "VM",
12
13
  "ir" => "IR",
13
- 'ir_dump' => 'IRDump',
14
- 'ir_render' => 'IRRender',
14
+ "ir_dump" => "IRDump",
15
+ "ir_render" => "IRRender",
16
+ "ir_execution_schedule_pass" => "IRExecutionSchedulePass"
15
17
  )
16
18
  loader.setup
17
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.14
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-21 00:00:00.000000000 Z
11
+ date: 2025-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -48,10 +48,10 @@ files:
48
48
  - docs/compiler_design_principles.md
49
49
  - docs/dev/analyzer-debug.md
50
50
  - docs/dev/parse-command.md
51
+ - docs/dev/vm-profiling.md
51
52
  - docs/development/README.md
52
53
  - docs/development/error-reporting.md
53
54
  - docs/features/README.md
54
- - docs/features/analysis-cascade-mutual-exclusion.md
55
55
  - docs/features/analysis-type-inference.md
56
56
  - docs/features/analysis-unsat-detection.md
57
57
  - docs/features/hierarchical-broadcasting.md
@@ -93,6 +93,8 @@ files:
93
93
  - lib/kumi/core/analyzer/passes/function_signature_pass.rb
94
94
  - lib/kumi/core/analyzer/passes/input_access_planner_pass.rb
95
95
  - lib/kumi/core/analyzer/passes/input_collector.rb
96
+ - lib/kumi/core/analyzer/passes/ir_dependency_pass.rb
97
+ - lib/kumi/core/analyzer/passes/ir_execution_schedule_pass.rb
96
98
  - lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb
97
99
  - lib/kumi/core/analyzer/passes/load_input_cse.rb
98
100
  - lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb
@@ -112,6 +114,12 @@ files:
112
114
  - lib/kumi/core/analyzer/structs/input_meta.rb
113
115
  - lib/kumi/core/atom_unsat_solver.rb
114
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
115
123
  - lib/kumi/core/compiler/access_planner.rb
116
124
  - lib/kumi/core/compiler/accessors/base.rb
117
125
  - lib/kumi/core/compiler/accessors/each_indexed_accessor.rb
@@ -188,8 +196,11 @@ files:
188
196
  - lib/kumi/core/types/inference.rb
189
197
  - lib/kumi/core/types/normalizer.rb
190
198
  - lib/kumi/core/types/validator.rb
199
+ - lib/kumi/dev.rb
191
200
  - lib/kumi/dev/ir.rb
192
201
  - lib/kumi/dev/parse.rb
202
+ - lib/kumi/dev/profile_aggregator.rb
203
+ - lib/kumi/dev/profile_runner.rb
193
204
  - lib/kumi/dev/runner.rb
194
205
  - lib/kumi/errors.rb
195
206
  - lib/kumi/frontends.rb
@@ -203,6 +214,7 @@ files:
203
214
  - lib/kumi/kernels/ruby/vector_struct.rb
204
215
  - lib/kumi/registry.rb
205
216
  - lib/kumi/runtime/executable.rb
217
+ - lib/kumi/runtime/run.rb
206
218
  - lib/kumi/schema.rb
207
219
  - lib/kumi/schema_metadata.rb
208
220
  - lib/kumi/support/diff.rb
@@ -1,89 +0,0 @@
1
- # Cascade Mutual Exclusion Detection
2
-
3
- Analyzes cascade expressions to allow safe recursive patterns when conditions are mutually exclusive.
4
-
5
- ## Overview
6
-
7
- The cascade mutual exclusion detector identifies when all conditions in a cascade expression cannot be true simultaneously, enabling safe mutual recursion patterns that would otherwise be rejected as cycles.
8
-
9
- ## Core Mechanism
10
-
11
- The system performs three-stage analysis:
12
-
13
- 1. **Conditional Dependency Tracking** - DependencyResolver marks base case dependencies as conditional
14
- 2. **Mutual Exclusion Analysis** - UnsatDetector determines if cascade conditions are mutually exclusive
15
- 3. **Safe Cycle Detection** - Toposorter allows cycles where all edges are conditional and conditions are mutually exclusive
16
-
17
- ## Example: Processing Workflow
18
-
19
- ```ruby
20
- schema do
21
- input do
22
- string :operation # "forward", "reverse", "unknown"
23
- integer :value
24
- end
25
-
26
- trait :is_forward, input.operation == "forward"
27
- trait :is_reverse, input.operation == "reverse"
28
-
29
- # Safe mutual recursion - conditions are mutually exclusive
30
- value :forward_processor do
31
- on is_forward, input.value * 2 # Direct calculation
32
- on is_reverse, reverse_processor + 10 # Delegates to reverse (safe)
33
- base "invalid operation" # Fallback for unknown operations
34
- end
35
-
36
- value :reverse_processor do
37
- on is_forward, forward_processor - 5 # Delegates to forward (safe)
38
- on is_reverse, input.value / 2 # Direct calculation
39
- base "invalid operation" # Fallback for unknown operations
40
- end
41
- end
42
- ```
43
-
44
- ## Safety Guarantees
45
-
46
- **Allowed**: Cycles where conditions are mutually exclusive
47
- - `is_forward` and `is_reverse` cannot both be true (operation has single value)
48
- - Each recursion executes exactly one step before hitting direct calculation
49
- - Bounded recursion with guaranteed termination
50
-
51
- **Rejected**: Cycles with overlapping conditions
52
- ```ruby
53
- # This would be rejected - conditions can overlap
54
- value :unsafe_cycle do
55
- on input.n > 0, "positive"
56
- on input.n > 5, "large" # Both can be true!
57
- base fn(:not, unsafe_cycle)
58
- end
59
- ```
60
-
61
- ## Implementation Details
62
-
63
- ### Conditional Dependencies
64
- Base case dependencies are marked as conditional because they only execute when no explicit conditions match.
65
-
66
- ### Mutual Exclusion Analysis
67
- Conditions are analyzed for mutual exclusion:
68
- - Same field equality comparisons: `field == value1` vs `field == value2`
69
- - Domain constraints ensuring impossibility
70
- - All condition pairs must be mutually exclusive
71
-
72
- ### Metadata Generation
73
- Analysis results stored in `cascade_metadata` state:
74
- ```ruby
75
- {
76
- condition_traits: [:is_forward, :is_reverse],
77
- condition_count: 2,
78
- all_mutually_exclusive: true,
79
- exclusive_pairs: 1,
80
- total_pairs: 1
81
- }
82
- ```
83
-
84
- ## Use Cases
85
-
86
- - Processing workflows with bidirectional logic
87
- - State machine fallback patterns
88
- - Recursive decision trees with termination conditions
89
- - Complex business rules with safe delegation patterns