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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +0 -27
- data/docs/dev/vm-profiling.md +95 -0
- data/docs/features/README.md +0 -7
- data/lib/kumi/analyzer.rb +10 -2
- data/lib/kumi/compiler.rb +6 -5
- data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +65 -0
- data/lib/kumi/core/analyzer/passes/ir_execution_schedule_pass.rb +67 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +15 -50
- 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 +56 -189
- data/lib/kumi/core/ir/execution_engine/profiler.rb +139 -11
- data/lib/kumi/core/ir/execution_engine/values.rb +8 -8
- data/lib/kumi/core/ir/execution_engine.rb +5 -30
- data/lib/kumi/dev/parse.rb +12 -12
- data/lib/kumi/dev/profile_aggregator.rb +301 -0
- data/lib/kumi/dev/profile_runner.rb +199 -0
- data/lib/kumi/dev/runner.rb +3 -1
- data/lib/kumi/dev.rb +14 -0
- data/lib/kumi/runtime/executable.rb +32 -153
- data/lib/kumi/runtime/run.rb +105 -0
- data/lib/kumi/schema.rb +15 -14
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +4 -2
- metadata +15 -3
- data/docs/features/analysis-cascade-mutual-exclusion.md +0 -89
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f51be8774c472629e599ce3edd4ab02551edfd52fea8676e2ee93eed5198800
|
4
|
+
data.tar.gz: 26532cec84e5c59553031dc86d82abff310d2a79c857776e1d793af2b35e00ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ba28da3acbf5cd430902c29e34e5786562e7121f9c3851a2e9db3a04a13fc8b1a5e9729649f3daadb523ea55f2c97a1e4560139b9809aa636eff21e9716abbf
|
7
|
+
data.tar.gz: 6683358d25786a5e15cba975e785b3178ec0ea13f40a3eeb940e72f25886f5004c499f3e80ddded31fdd23f3981cbe5244db1ab0aec1405b3c52acb68c4b1e60
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,45 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.16] – 2025-08-22
|
4
|
+
|
5
|
+
### Performance
|
6
|
+
- Input accessor code generation replaces nested lambda chains with compiled Ruby methods
|
7
|
+
- Fix cache handling in Runtime - it was being recreated on updates
|
8
|
+
- Add early shortcut for Analyzer Passes.
|
9
|
+
|
10
|
+
## [0.0.15] – 2025-08-21
|
11
|
+
### Added
|
12
|
+
- (DX) Schema-aware VM profiling with multi-schema performance analysis
|
13
|
+
- DAG-based execution optimization with pre-computed dependency resolution
|
14
|
+
|
15
|
+
### Performance
|
16
|
+
- Reference operations eliminated as VM bottleneck via O(1) hash lookups
|
17
|
+
|
18
|
+
## [0.0.14] – 2025-08-21
|
19
|
+
### Added
|
20
|
+
- Text schema frontend with `.kumi` file format support
|
21
|
+
- `bin/kumi parse` command for schema analysis and golden file testing
|
22
|
+
- LoadInputCSE optimization pass to eliminate redundant load operations
|
23
|
+
- Runtime accessor caching with precise field-based invalidation
|
24
|
+
- VM profiler with wall time, CPU time, and cache hit rate analysis
|
25
|
+
- Structured analyzer debug system with state inspection
|
26
|
+
- Checkpoint system for capturing and comparing analyzer states
|
27
|
+
- State serialization (StateSerde) for golden testing and regression detection
|
28
|
+
- Debug object printers with configurable truncation
|
29
|
+
- Multi-run averaging for stable performance benchmarking
|
30
|
+
|
31
|
+
### Fixed
|
32
|
+
- VM targeting for `__vec` twin declarations that were failing to resolve
|
33
|
+
- Demand-driven reference resolution with proper name indexing and cycle detection
|
34
|
+
- Accessor cache invalidation now uses precise field dependencies instead of clearing all caches
|
35
|
+
- StateSerde JSON serialization issues with frozen hashes, Sets, and Symbols
|
36
|
+
|
37
|
+
### Performance
|
38
|
+
- 14x improvement on update-heavy workloads (1.88k → 26.88k iterations/second)
|
39
|
+
- 30-40% reduction in IR module size for schemas with repeated field access
|
40
|
+
- Eliminated load_input performance bottleneck that was consuming ~99% of execution time
|
41
|
+
- Optional caching system (enabled via KUMI_VM_CACHE=1) for performance-critical scenarios
|
42
|
+
|
3
43
|
## [0.0.13] – 2025-08-14
|
4
44
|
### Added
|
5
45
|
- Runtime performance optimizations for interpreter execution
|
data/README.md
CHANGED
@@ -207,33 +207,6 @@ end
|
|
207
207
|
# ❌ Function arity error: divide expects 2 arguments, got 1
|
208
208
|
```
|
209
209
|
|
210
|
-
**Mutual Recursion**: Kumi supports mutual recursion when cascade conditions are mutually exclusive:
|
211
|
-
|
212
|
-
```ruby
|
213
|
-
trait :is_forward, input.operation == "forward"
|
214
|
-
trait :is_reverse, input.operation == "reverse"
|
215
|
-
|
216
|
-
# Safe mutual recursion - conditions are mutually exclusive
|
217
|
-
value :forward_processor do
|
218
|
-
on is_forward, input.value * 2 # Direct calculation
|
219
|
-
on is_reverse, reveAnalysisrse_processor + 10 # Delegates to reverse (safe)
|
220
|
-
base "invalid operation"
|
221
|
-
end
|
222
|
-
|
223
|
-
value :reverse_processor do
|
224
|
-
on is_forward, forward_processor - 5 # Delegates to forward (safe)
|
225
|
-
on is_reverse, input.value / 2 # Direct calculation
|
226
|
-
base "invalid operation"
|
227
|
-
end
|
228
|
-
|
229
|
-
# Usage examples:
|
230
|
-
# operation="forward", value=10 => forward: 20, reverse: 15
|
231
|
-
# operation="reverse", value=10 => forward: 15, reverse: 5
|
232
|
-
# operation="unknown", value=10 => both: "invalid operation"
|
233
|
-
```
|
234
|
-
|
235
|
-
This compiles because `operation` can only be "forward" or "reverse", never both. Each recursion executes one step before hitting a direct calculation.
|
236
|
-
|
237
210
|
#### **Runtime Introspection: Debug and Understand**
|
238
211
|
|
239
212
|
**Explainability**: Trace exactly how any value is computed, step-by-step. This is invaluable for debugging complex logic and auditing results.
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# VM Profiling with Schema Differentiation
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Profiles VM operation execution with schema-level differentiation. Tracks operations by schema type for multi-schema performance analysis.
|
6
|
+
|
7
|
+
## Core Components
|
8
|
+
|
9
|
+
**Profiler**: `lib/kumi/core/ir/execution_engine/profiler.rb`
|
10
|
+
- Streams VM operation events with schema identification
|
11
|
+
- Supports persistent mode for cross-run analysis
|
12
|
+
- JSONL event format with operation metadata
|
13
|
+
|
14
|
+
**Profile Aggregator**: `lib/kumi/dev/profile_aggregator.rb`
|
15
|
+
- Analyzes profiling data by schema type
|
16
|
+
- Generates summary and detailed performance reports
|
17
|
+
- Schema breakdown showing operations and timing per schema
|
18
|
+
|
19
|
+
**CLI Integration**: `bin/kumi profile`
|
20
|
+
- Processes JSONL profiling data files
|
21
|
+
- Multiple output formats: summary, detailed, raw
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
### Basic Profiling
|
26
|
+
|
27
|
+
```bash
|
28
|
+
# Single schema with operations
|
29
|
+
KUMI_PROFILE=1 KUMI_PROFILE_OPS=1 KUMI_PROFILE_FILE=profile.jsonl ruby script.rb
|
30
|
+
|
31
|
+
# Persistent mode across multiple runs
|
32
|
+
KUMI_PROFILE=1 KUMI_PROFILE_PERSISTENT=1 KUMI_PROFILE_OPS=1 KUMI_PROFILE_FILE=profile.jsonl ruby script.rb
|
33
|
+
|
34
|
+
# Streaming mode for real-time analysis
|
35
|
+
KUMI_PROFILE=1 KUMI_PROFILE_STREAM=1 KUMI_PROFILE_OPS=1 KUMI_PROFILE_FILE=profile.jsonl ruby script.rb
|
36
|
+
```
|
37
|
+
|
38
|
+
### CLI Analysis
|
39
|
+
|
40
|
+
```bash
|
41
|
+
# Summary report with schema breakdown
|
42
|
+
kumi profile profile.jsonl --summary
|
43
|
+
|
44
|
+
# Detailed per-operation analysis
|
45
|
+
kumi profile profile.jsonl --detailed
|
46
|
+
|
47
|
+
# Raw event stream
|
48
|
+
kumi profile profile.jsonl --raw
|
49
|
+
```
|
50
|
+
|
51
|
+
## Environment Variables
|
52
|
+
|
53
|
+
**Core**:
|
54
|
+
- `KUMI_PROFILE=1` - Enable profiling
|
55
|
+
- `KUMI_PROFILE_FILE=path` - Output file (required)
|
56
|
+
- `KUMI_PROFILE_OPS=1` - Enable VM operation profiling
|
57
|
+
|
58
|
+
**Modes**:
|
59
|
+
- `KUMI_PROFILE_PERSISTENT=1` - Append to existing files across runs
|
60
|
+
- `KUMI_PROFILE_STREAM=1` - Stream individual events vs batch
|
61
|
+
- `KUMI_PROFILE_TRUNCATE=1` - Truncate existing files
|
62
|
+
|
63
|
+
## Event Format
|
64
|
+
|
65
|
+
JSONL with operation metadata:
|
66
|
+
|
67
|
+
```json
|
68
|
+
{"event":"vm_operation","schema":"TestSchema","operation":"LoadInput","duration_ms":0.001,"timestamp":"2025-01-20T10:30:45.123Z"}
|
69
|
+
{"event":"vm_operation","schema":"TestSchema","operation":"Map","duration_ms":0.002,"timestamp":"2025-01-20T10:30:45.125Z"}
|
70
|
+
```
|
71
|
+
|
72
|
+
## Schema Differentiation
|
73
|
+
|
74
|
+
Tracks operations by schema class name for multi-schema analysis:
|
75
|
+
|
76
|
+
**Implementation**:
|
77
|
+
- Schema name propagated through compilation pipeline
|
78
|
+
- Profiler tags each VM operation with schema identifier
|
79
|
+
- Aggregator groups operations by schema type
|
80
|
+
|
81
|
+
**Output Example**:
|
82
|
+
```
|
83
|
+
Total operations: 24 (0.8746ms)
|
84
|
+
Schemas analyzed: SchemaA, SchemaB
|
85
|
+
SchemaA: 12 operations, 0.3242ms
|
86
|
+
SchemaB: 12 operations, 0.0504ms
|
87
|
+
```
|
88
|
+
|
89
|
+
## Performance Analysis
|
90
|
+
|
91
|
+
**Reference Operations**: Typically dominate execution time in complex schemas
|
92
|
+
**Map Operations**: Element-wise computations on arrays
|
93
|
+
**LoadInput Operations**: Data access operations
|
94
|
+
|
95
|
+
Use schema breakdown to identify performance differences between schema types.
|
data/docs/features/README.md
CHANGED
@@ -9,13 +9,6 @@ Analyzes rule combinations to detect logical impossibilities across dependency c
|
|
9
9
|
- Validates domain constraints
|
10
10
|
- Reports multiple errors
|
11
11
|
|
12
|
-
### [Cascade Mutual Exclusion](analysis-cascade-mutual-exclusion.md)
|
13
|
-
Enables safe mutual recursion when cascade conditions are mutually exclusive.
|
14
|
-
|
15
|
-
- Allows mathematically sound recursive patterns
|
16
|
-
- Detects mutually exclusive conditions
|
17
|
-
- Prevents unsafe cycles while enabling safe ones
|
18
|
-
|
19
12
|
### [Type Inference](analysis-type-inference.md)
|
20
13
|
Determines types from expressions and propagates them through dependencies.
|
21
14
|
|
data/lib/kumi/analyzer.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
module Kumi
|
4
4
|
module Analyzer
|
5
5
|
Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
|
6
|
+
ERROR_THRESHOLD_PASS = Core::Analyzer::Passes::LowerToIRPass
|
6
7
|
|
7
8
|
DEFAULT_PASSES = [
|
8
9
|
Core::Analyzer::Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
|
@@ -21,7 +22,10 @@ module Kumi
|
|
21
22
|
Core::Analyzer::Passes::ScopeResolutionPass, # 15. Plans execution scope and lifting needs for declarations.
|
22
23
|
Core::Analyzer::Passes::JoinReducePlanningPass, # 16. Plans join/reduce operations (Generates IR Structs)
|
23
24
|
Core::Analyzer::Passes::LowerToIRPass, # 17. Lowers the schema to IR (Generates IR Structs)
|
24
|
-
Core::Analyzer::Passes::LoadInputCSE
|
25
|
+
Core::Analyzer::Passes::LoadInputCSE, # 18. Eliminates redundant load_input operations
|
26
|
+
Core::Analyzer::Passes::IRDependencyPass, # 19. Extracts IR-level dependencies for VM execution optimization
|
27
|
+
Core::Analyzer::Passes::IRExecutionSchedulePass # 20. Builds a precomputed execution schedule.
|
28
|
+
|
25
29
|
].freeze
|
26
30
|
|
27
31
|
def self.analyze!(schema, passes: DEFAULT_PASSES, **opts)
|
@@ -43,6 +47,8 @@ module Kumi
|
|
43
47
|
skipping = !!resume_at
|
44
48
|
|
45
49
|
passes.each_with_index do |pass_class, idx|
|
50
|
+
raise handle_analysis_errors(errors) if (ERROR_THRESHOLD_PASS == pass_class) && !errors.empty?
|
51
|
+
|
46
52
|
pass_name = pass_class.name.split("::").last
|
47
53
|
|
48
54
|
if skipping
|
@@ -58,7 +64,9 @@ module Kumi
|
|
58
64
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
59
65
|
pass_instance = pass_class.new(schema, state)
|
60
66
|
begin
|
61
|
-
state =
|
67
|
+
state = Dev::Profiler.phase("analyzer.pass", pass: pass_name) do
|
68
|
+
pass_instance.run(errors)
|
69
|
+
end
|
62
70
|
rescue StandardError => e
|
63
71
|
# TODO: - GREATLY improve this, need to capture the context of the error
|
64
72
|
# and the pass that failed and line number if relevant
|
data/lib/kumi/compiler.rb
CHANGED
@@ -3,18 +3,19 @@
|
|
3
3
|
module Kumi
|
4
4
|
# Compiles an analyzed schema into executable lambdas
|
5
5
|
class Compiler < Core::CompilerBase
|
6
|
-
def self.compile(schema, analyzer:)
|
7
|
-
new(schema, analyzer).compile
|
6
|
+
def self.compile(schema, analyzer:, schema_name: nil)
|
7
|
+
new(schema, analyzer, schema_name: schema_name).compile
|
8
8
|
end
|
9
9
|
|
10
|
-
def initialize(schema, analyzer)
|
11
|
-
super
|
10
|
+
def initialize(schema, analyzer, schema_name: nil)
|
11
|
+
super(schema, analyzer)
|
12
12
|
@bindings = {}
|
13
|
+
@schema_name = schema_name
|
13
14
|
end
|
14
15
|
|
15
16
|
def compile
|
16
17
|
# Switch to LIR: Use the analysis state instead of old compilation
|
17
|
-
Runtime::Executable.from_analysis(@analysis.state)
|
18
|
+
Runtime::Executable.from_analysis(@analysis.state, schema_name: @schema_name)
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Extract IR-level dependencies for VM execution optimization
|
8
|
+
# DEPENDENCIES: :ir_module from LowerToIRPass
|
9
|
+
# PRODUCES: :ir_dependencies - Hash mapping declaration names to referenced bindings
|
10
|
+
# :ir_name_index - Hash mapping stored binding names to producing declarations
|
11
|
+
# INTERFACE: new(schema, state).run(errors)
|
12
|
+
#
|
13
|
+
# NOTE: This pass extracts actual IR-level dependencies by analyzing :ref operations
|
14
|
+
# in the generated IR, providing the dependency information needed for optimized VM scheduling.
|
15
|
+
class IRDependencyPass < PassBase
|
16
|
+
def run(errors)
|
17
|
+
ir_module = get_state(:ir_module, required: true)
|
18
|
+
|
19
|
+
ir_dependencies = build_ir_dependency_map(ir_module)
|
20
|
+
ir_name_index = build_ir_name_index(ir_module)
|
21
|
+
|
22
|
+
state.with(:ir_dependencies, ir_dependencies).with(:ir_name_index, ir_name_index)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Build a map of declaration -> [stored_bindings_it_references] from the IR
|
28
|
+
def build_ir_dependency_map(ir_module)
|
29
|
+
deps_map = {}
|
30
|
+
|
31
|
+
ir_module.decls.each do |decl|
|
32
|
+
refs = []
|
33
|
+
decl.ops.each do |op|
|
34
|
+
refs << op.attrs[:name] if op.tag == :ref
|
35
|
+
end
|
36
|
+
deps_map[decl.name] = refs
|
37
|
+
end
|
38
|
+
|
39
|
+
deps_map.freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
# Build name index to map stored binding names to their producing declarations
|
43
|
+
def build_ir_name_index(ir_module)
|
44
|
+
ir_name_index = {}
|
45
|
+
|
46
|
+
ir_module.decls.each do |decl|
|
47
|
+
# Map the primary declaration name
|
48
|
+
ir_name_index[decl.name] = decl
|
49
|
+
|
50
|
+
# Also map any vectorized twin names produced by this declaration
|
51
|
+
decl.ops.each do |op|
|
52
|
+
if op.tag == :store
|
53
|
+
stored_name = op.attrs[:name]
|
54
|
+
ir_name_index[stored_name] = decl
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
ir_name_index.freeze
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# PRODUCES: :execution_schedules => { store_name(Symbol) => [Decl, ...] }
|
8
|
+
class IRExecutionSchedulePass < PassBase
|
9
|
+
def run(errors)
|
10
|
+
ir = get_state(:ir_module, required: true)
|
11
|
+
deps = get_state(:ir_dependencies, required: true) # decl_name => [binding_name, ...]
|
12
|
+
name_index = get_state(:ir_name_index, required: true) # binding_name => Decl (← use IR-specific index)
|
13
|
+
|
14
|
+
by_name = ir.decls.to_h { |d| [d.name, d] }
|
15
|
+
pos = ir.decls.each_with_index.to_h # for deterministic ordering
|
16
|
+
|
17
|
+
closure_cache = {}
|
18
|
+
visiting = {}
|
19
|
+
|
20
|
+
visit = lambda do |dn|
|
21
|
+
return closure_cache[dn] if closure_cache.key?(dn)
|
22
|
+
|
23
|
+
raise Kumi::Core::Errors::TypeError, "cycle detected in IR at #{dn.inspect}" if visiting[dn]
|
24
|
+
|
25
|
+
visiting[dn] = true
|
26
|
+
|
27
|
+
# Resolve binding refs -> producing decl names
|
28
|
+
preds = Array(deps[dn]).filter_map { |b| name_index[b]&.name }.uniq
|
29
|
+
|
30
|
+
# Deterministic order: earlier IR decls first
|
31
|
+
preds.sort_by! { |n| pos[n] || Float::INFINITY }
|
32
|
+
|
33
|
+
order = []
|
34
|
+
preds.each do |p|
|
35
|
+
next if p == dn # guard against self-deps; treat as error if you prefer
|
36
|
+
|
37
|
+
order.concat(visit.call(p))
|
38
|
+
end
|
39
|
+
order << dn unless order.last == dn
|
40
|
+
|
41
|
+
visiting.delete(dn)
|
42
|
+
closure_cache[dn] = order.uniq.freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
schedules = {}
|
46
|
+
|
47
|
+
ir.decls.each do |decl|
|
48
|
+
target_names = [decl.name] + decl.ops.select { _1.tag == :store }.map { _1.attrs[:name] }
|
49
|
+
|
50
|
+
seq = visit.call(decl.name).map { |dn| by_name.fetch(dn) }.freeze
|
51
|
+
|
52
|
+
target_names.each do |t|
|
53
|
+
if schedules.key?(t) && schedules[t] != seq
|
54
|
+
raise Kumi::Core::Errors::TypeError,
|
55
|
+
"duplicate schedule target #{t.inspect} produced by #{schedules[t].last.name} and #{decl.name}"
|
56
|
+
end
|
57
|
+
schedules[t] = seq
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
state.with(:ir_execution_schedules, schedules.freeze)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -5,8 +5,8 @@ module Kumi
|
|
5
5
|
module Core
|
6
6
|
module Analyzer
|
7
7
|
module Passes
|
8
|
-
# RESPONSIBILITY: Compute topological ordering of declarations,
|
9
|
-
# DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer
|
8
|
+
# RESPONSIBILITY: Compute topological ordering of declarations, blocking all cycles
|
9
|
+
# DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer
|
10
10
|
# PRODUCES: :evaluation_order - Array of declaration names in evaluation order
|
11
11
|
# :node_index - Hash mapping object_id to node metadata for later passes
|
12
12
|
# INTERFACE: new(schema, state).run(errors)
|
@@ -18,7 +18,7 @@ module Kumi
|
|
18
18
|
# Create node index for later passes to use
|
19
19
|
node_index = build_node_index(definitions)
|
20
20
|
order = compute_topological_order(dependency_graph, definitions, errors)
|
21
|
-
|
21
|
+
|
22
22
|
state.with(:evaluation_order, order).with(:node_index, node_index)
|
23
23
|
end
|
24
24
|
|
@@ -26,53 +26,45 @@ module Kumi
|
|
26
26
|
|
27
27
|
def build_node_index(definitions)
|
28
28
|
index = {}
|
29
|
-
|
29
|
+
|
30
30
|
# Walk all declarations and their expressions to index every node
|
31
31
|
definitions.each_value do |decl|
|
32
32
|
index_node_recursive(decl, index)
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
index
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def index_node_recursive(node, index)
|
39
39
|
return unless node
|
40
|
-
|
40
|
+
|
41
41
|
# Index this node by its object_id
|
42
42
|
index[node.object_id] = {
|
43
43
|
node: node,
|
44
|
-
type: node.class.name.split(
|
44
|
+
type: node.class.name.split("::").last,
|
45
45
|
metadata: {}
|
46
46
|
}
|
47
|
-
|
47
|
+
|
48
48
|
# Use the same approach as the visitor pattern - recursively index all children
|
49
|
-
if node.respond_to?(:children)
|
50
|
-
|
51
|
-
end
|
52
|
-
|
49
|
+
node.children.each { |child| index_node_recursive(child, index) } if node.respond_to?(:children)
|
50
|
+
|
53
51
|
# Index expression for declaration nodes
|
54
|
-
|
55
|
-
|
56
|
-
|
52
|
+
return unless node.respond_to?(:expression)
|
53
|
+
|
54
|
+
index_node_recursive(node.expression, index)
|
57
55
|
end
|
58
56
|
|
59
57
|
def compute_topological_order(graph, definitions, errors)
|
60
58
|
temp_marks = Set.new
|
61
59
|
perm_marks = Set.new
|
62
60
|
order = []
|
63
|
-
cascades = get_state(:cascades) || {}
|
64
61
|
|
65
62
|
visit_node = lambda do |node, path = []|
|
66
63
|
return if perm_marks.include?(node)
|
67
64
|
|
68
65
|
if temp_marks.include?(node)
|
69
|
-
#
|
70
|
-
cycle_path = path + [node]
|
71
|
-
return if safe_conditional_cycle?(cycle_path, graph, cascades)
|
72
|
-
|
73
|
-
# Allow this cycle - it's safe due to cascade mutual exclusion
|
66
|
+
# Block all cycles - no mutual recursion allowed
|
74
67
|
report_unexpected_cycle(temp_marks, node, errors)
|
75
|
-
|
76
68
|
return
|
77
69
|
end
|
78
70
|
|
@@ -102,33 +94,6 @@ module Kumi
|
|
102
94
|
order.freeze
|
103
95
|
end
|
104
96
|
|
105
|
-
def safe_conditional_cycle?(cycle_path, graph, cascades)
|
106
|
-
return false if cycle_path.nil? || cycle_path.size < 2
|
107
|
-
|
108
|
-
# Find where the cycle starts - look for the first occurrence of the repeated node
|
109
|
-
last_node = cycle_path.last
|
110
|
-
return false if last_node.nil?
|
111
|
-
|
112
|
-
cycle_start = cycle_path.index(last_node)
|
113
|
-
return false unless cycle_start && cycle_start < cycle_path.size - 1
|
114
|
-
|
115
|
-
cycle_nodes = cycle_path[cycle_start..]
|
116
|
-
|
117
|
-
# Check if all edges in the cycle are conditional
|
118
|
-
cycle_nodes.each_cons(2) do |from, to|
|
119
|
-
edges = graph[from] || []
|
120
|
-
edge = edges.find { |e| e.to == to }
|
121
|
-
|
122
|
-
return false unless edge&.conditional
|
123
|
-
|
124
|
-
# Check if the cascade has mutually exclusive conditions
|
125
|
-
cascade_meta = cascades[edge.cascade_owner]
|
126
|
-
return false unless cascade_meta&.dig(:all_mutually_exclusive)
|
127
|
-
end
|
128
|
-
|
129
|
-
true
|
130
|
-
end
|
131
|
-
|
132
97
|
def report_unexpected_cycle(temp_marks, current_node, errors)
|
133
98
|
cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
|
134
99
|
|
@@ -2,18 +2,32 @@ module Kumi
|
|
2
2
|
module Core
|
3
3
|
module Compiler
|
4
4
|
class AccessBuilder
|
5
|
+
class << self
|
6
|
+
attr_accessor :strategy
|
7
|
+
end
|
8
|
+
|
9
|
+
self.strategy = if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
|
10
|
+
:interp
|
11
|
+
else
|
12
|
+
:codegen
|
13
|
+
end
|
14
|
+
|
5
15
|
def self.build(plans)
|
6
16
|
accessors = {}
|
7
17
|
plans.each_value do |variants|
|
8
18
|
variants.each do |plan|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
19
|
+
accessors[plan.accessor_key] =
|
20
|
+
case strategy
|
21
|
+
when :codegen then AccessCodegen.fetch_or_compile(plan)
|
22
|
+
else
|
23
|
+
build_proc_for(
|
24
|
+
mode: plan.mode,
|
25
|
+
path_key: plan.path,
|
26
|
+
missing: (plan.on_missing || :error).to_sym,
|
27
|
+
key_policy: (plan.key_policy || :indifferent).to_sym,
|
28
|
+
operations: plan.operations
|
29
|
+
)
|
30
|
+
end
|
17
31
|
end
|
18
32
|
end
|
19
33
|
accessors.freeze
|
@@ -25,7 +39,6 @@ module Kumi
|
|
25
39
|
when :materialize then Accessors::MaterializeAccessor.build(operations, path_key, missing, key_policy)
|
26
40
|
when :ravel then Accessors::RavelAccessor.build(operations, path_key, missing, key_policy)
|
27
41
|
when :each_indexed then Accessors::EachIndexedAccessor.build(operations, path_key, missing, key_policy, true)
|
28
|
-
when :each then Accessors::EachAccessor.build(operations, path_key, missing, key_policy)
|
29
42
|
else
|
30
43
|
raise "Unknown accessor mode: #{mode.inspect}"
|
31
44
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest/sha1"
|
4
|
+
|
5
|
+
module Kumi
|
6
|
+
module Core
|
7
|
+
module Compiler
|
8
|
+
class AccessCodegen
|
9
|
+
CACHE = {}
|
10
|
+
CACHE_MUTEX = Mutex.new
|
11
|
+
|
12
|
+
def self.fetch_or_compile(plan)
|
13
|
+
key = Digest::SHA1.hexdigest(Marshal.dump([plan.mode, plan.operations, plan.on_missing, plan.key_policy, plan.path]))
|
14
|
+
CACHE_MUTEX.synchronize do
|
15
|
+
CACHE[key] ||= compile(plan).tap(&:freeze)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.compile(plan)
|
20
|
+
case plan.mode
|
21
|
+
when :read then gen_read(plan)
|
22
|
+
when :materialize then gen_materialize(plan)
|
23
|
+
when :ravel then gen_ravel(plan)
|
24
|
+
when :each_indexed then gen_each_indexed(plan)
|
25
|
+
else
|
26
|
+
raise "Unknown accessor mode: #{plan.mode.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private_class_method def self.gen_read(plan)
|
31
|
+
code = AccessEmit::Read.build(plan)
|
32
|
+
debug_code(code, plan, "READ") if ENV["DEBUG_CODEGEN"]
|
33
|
+
eval(code, TOPLEVEL_BINDING)
|
34
|
+
end
|
35
|
+
|
36
|
+
private_class_method def self.gen_materialize(plan)
|
37
|
+
code = AccessEmit::Materialize.build(plan)
|
38
|
+
debug_code(code, plan, "MATERIALIZE") if ENV["DEBUG_CODEGEN"]
|
39
|
+
eval(code, TOPLEVEL_BINDING)
|
40
|
+
end
|
41
|
+
|
42
|
+
private_class_method def self.gen_ravel(plan)
|
43
|
+
code = AccessEmit::Ravel.build(plan)
|
44
|
+
debug_code(code, plan, "RAVEL") if ENV["DEBUG_CODEGEN"]
|
45
|
+
eval(code, TOPLEVEL_BINDING)
|
46
|
+
end
|
47
|
+
|
48
|
+
private_class_method def self.gen_each_indexed(plan)
|
49
|
+
code = AccessEmit::EachIndexed.build(plan)
|
50
|
+
debug_code(code, plan, "EACH_INDEXED") if ENV["DEBUG_CODEGEN"]
|
51
|
+
eval(code, TOPLEVEL_BINDING)
|
52
|
+
end
|
53
|
+
|
54
|
+
private_class_method def self.debug_code(code, plan, mode_name)
|
55
|
+
puts "=== Generated #{mode_name} code for #{plan.path}:#{plan.mode} ==="
|
56
|
+
puts code
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|