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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec64db5a7b14df1a8656e2cedc569153a0f092d69a99544c5aa103241f61d931
4
- data.tar.gz: 84bd4c7360eb74364e47103901fe8ccbcf6282a9cb40111d42b05357a2172c9e
3
+ metadata.gz: 9f51be8774c472629e599ce3edd4ab02551edfd52fea8676e2ee93eed5198800
4
+ data.tar.gz: 26532cec84e5c59553031dc86d82abff310d2a79c857776e1d793af2b35e00ea
5
5
  SHA512:
6
- metadata.gz: da7a9f3135e63779676ea5a802f48288ff2675935ab8dd8dcc751aaf309392275589f08fb92fede4dce351707369b5113e28540e5f4853629a02f4a1945ca75a
7
- data.tar.gz: 5a45952056f81d9b4551bc7b48d3fd35c25065eb058e3cd6737879377a5c6e5f61d1cbba5b51955f54ad6ca14a797254e3381c69cd6b9eb145f3929e6e962dae
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.
@@ -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 # 18. Eliminates redundant load_input operations
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 = pass_instance.run(errors)
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, allowing safe conditional cycles
9
- # DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer, :cascades from UnsatDetector
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('::').last,
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
- node.children.each { |child| index_node_recursive(child, index) }
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
- if node.respond_to?(:expression)
55
- index_node_recursive(node.expression, index)
56
- end
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
- # Check if this is a safe conditional cycle
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
- key = plan.respond_to?(:accessor_key) ? plan.accessor_key : "#{plan.path}:#{mode}"
10
- accessors[key] = build_proc_for(
11
- mode: plan.mode,
12
- path_key: plan.path,
13
- missing: (plan.on_missing || :error).to_sym,
14
- key_policy: (plan.key_policy || :indifferent).to_sym,
15
- operations: plan.operations
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