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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5405d7d0612a81e5154bd1d452fdfc150691b022137fc0ee132c47ede1a58e2e
4
- data.tar.gz: '093cf7a6d305c02f92de600b06f62be39f8af90d798a0f93ed3ef59f539ada9b'
3
+ metadata.gz: 9f51be8774c472629e599ce3edd4ab02551edfd52fea8676e2ee93eed5198800
4
+ data.tar.gz: 26532cec84e5c59553031dc86d82abff310d2a79c857776e1d793af2b35e00ea
5
5
  SHA512:
6
- metadata.gz: b3ea711bf465e0c11cc95fabb3809dd632ebbfcc8c36297b161fb1f179fffdda5df1e5c033968e837dd8ed3f983639416de08bd371f30be9f2cefd5543efe1ff
7
- data.tar.gz: 0a63fe824fb604639b4efb9cfc2ce24a93429110adc75cfd3edc905c58b3501273ad07a3cf66415eabce4e099c5fd4d202bd2c4064875d7a73e0c2a26f0689cb
6
+ metadata.gz: 3ba28da3acbf5cd430902c29e34e5786562e7121f9c3851a2e9db3a04a13fc8b1a5e9729649f3daadb523ea55f2c97a1e4560139b9809aa636eff21e9716abbf
7
+ data.tar.gz: 6683358d25786a5e15cba975e785b3178ec0ea13f40a3eeb940e72f25886f5004c499f3e80ddded31fdd23f3981cbe5244db1ab0aec1405b3c52acb68c4b1e60
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.0.15] – 2025-08-21
4
11
  ### Added
5
12
  - (DX) Schema-aware VM profiling with multi-schema performance analysis
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.
@@ -22,7 +23,9 @@ module Kumi
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
25
  Core::Analyzer::Passes::LoadInputCSE, # 18. Eliminates redundant load_input operations
25
- Core::Analyzer::Passes::IRDependencyPass # 19. Extracts IR-level dependencies for VM execution optimization
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
+
26
29
  ].freeze
27
30
 
28
31
  def self.analyze!(schema, passes: DEFAULT_PASSES, **opts)
@@ -44,6 +47,8 @@ module Kumi
44
47
  skipping = !!resume_at
45
48
 
46
49
  passes.each_with_index do |pass_class, idx|
50
+ raise handle_analysis_errors(errors) if (ERROR_THRESHOLD_PASS == pass_class) && !errors.empty?
51
+
47
52
  pass_name = pass_class.name.split("::").last
48
53
 
49
54
  if skipping
@@ -7,19 +7,19 @@ module Kumi
7
7
  # RESPONSIBILITY: Extract IR-level dependencies for VM execution optimization
8
8
  # DEPENDENCIES: :ir_module from LowerToIRPass
9
9
  # PRODUCES: :ir_dependencies - Hash mapping declaration names to referenced bindings
10
- # :name_index - Hash mapping stored binding names to producing declarations
10
+ # :ir_name_index - Hash mapping stored binding names to producing declarations
11
11
  # INTERFACE: new(schema, state).run(errors)
12
- #
12
+ #
13
13
  # NOTE: This pass extracts actual IR-level dependencies by analyzing :ref operations
14
14
  # in the generated IR, providing the dependency information needed for optimized VM scheduling.
15
15
  class IRDependencyPass < PassBase
16
16
  def run(errors)
17
17
  ir_module = get_state(:ir_module, required: true)
18
-
18
+
19
19
  ir_dependencies = build_ir_dependency_map(ir_module)
20
- name_index = build_name_index(ir_module)
21
-
22
- state.with(:ir_dependencies, ir_dependencies).with(:name_index, name_index)
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
23
  end
24
24
 
25
25
  private
@@ -27,41 +27,39 @@ module Kumi
27
27
  # Build a map of declaration -> [stored_bindings_it_references] from the IR
28
28
  def build_ir_dependency_map(ir_module)
29
29
  deps_map = {}
30
-
30
+
31
31
  ir_module.decls.each do |decl|
32
32
  refs = []
33
33
  decl.ops.each do |op|
34
- if op.tag == :ref
35
- refs << op.attrs[:name]
36
- end
34
+ refs << op.attrs[:name] if op.tag == :ref
37
35
  end
38
36
  deps_map[decl.name] = refs
39
37
  end
40
-
38
+
41
39
  deps_map.freeze
42
40
  end
43
41
 
44
42
  # Build name index to map stored binding names to their producing declarations
45
- def build_name_index(ir_module)
46
- name_index = {}
47
-
43
+ def build_ir_name_index(ir_module)
44
+ ir_name_index = {}
45
+
48
46
  ir_module.decls.each do |decl|
49
47
  # Map the primary declaration name
50
- name_index[decl.name] = decl
51
-
48
+ ir_name_index[decl.name] = decl
49
+
52
50
  # Also map any vectorized twin names produced by this declaration
53
51
  decl.ops.each do |op|
54
52
  if op.tag == :store
55
53
  stored_name = op.attrs[:name]
56
- name_index[stored_name] = decl
54
+ ir_name_index[stored_name] = decl
57
55
  end
58
56
  end
59
57
  end
60
-
61
- name_index.freeze
58
+
59
+ ir_name_index.freeze
62
60
  end
63
61
  end
64
62
  end
65
63
  end
66
64
  end
67
- 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
@@ -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,34 +26,32 @@ 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)
@@ -96,7 +94,6 @@ module Kumi
96
94
  order.freeze
97
95
  end
98
96
 
99
-
100
97
  def report_unexpected_cycle(temp_marks, current_node, errors)
101
98
  cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
102
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
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+ module Kumi
3
+ module Core
4
+ module Compiler
5
+ module AccessEmit
6
+ module Base
7
+ module_function
8
+
9
+ # ---------- IR segmentation ----------
10
+ def segment_ops(ops)
11
+ segs, cur = [], []
12
+ i = 0
13
+ while i < ops.length
14
+ case ops[i][:type]
15
+ when :enter_hash
16
+ preview = (i + 1 < ops.length) && ops[i + 1][:type] == :enter_array
17
+ cur << [:enter_hash, ops[i][:key].to_s, preview]
18
+ when :enter_array
19
+ segs << cur unless cur.empty?
20
+ segs << :array
21
+ cur = []
22
+ else
23
+ raise "Unknown operation: #{ops[i].inspect}"
24
+ end
25
+ i += 1
26
+ end
27
+ segs << cur unless cur.empty?
28
+ segs
29
+ end
30
+
31
+ # ---------- codegen helpers ----------
32
+ def fetch_hash_code(node_var:, key:, key_policy:, preview_array:, mode:, policy:, path_key:, map_depth:)
33
+ effective_policy = preview_array ? :indifferent : (key_policy || :indifferent)
34
+ str = key.to_s.inspect
35
+ sym = key.to_sym.inspect
36
+
37
+ fetch =
38
+ case effective_policy
39
+ when :string
40
+ %(next_node = #{node_var}.key?(#{str}) ? #{node_var}[#{str}] : :__missing__)
41
+ when :symbol
42
+ %(next_node = #{node_var}.key?(#{sym}) ? #{node_var}[#{sym}] : :__missing__)
43
+ else # :indifferent
44
+ <<~RB.chomp
45
+ next_node =
46
+ if #{node_var}.key?(#{str}); #{node_var}[#{str}]
47
+ elsif #{node_var}.key?(#{sym}); #{node_var}[#{sym}]
48
+ elsif #{node_var}.key?(#{str}); #{node_var}[#{str}] # (string twice ok / predictable)
49
+ else :__missing__
50
+ end
51
+ RB
52
+ end
53
+
54
+ miss_action = build_miss_action(policy, mode, map_depth, preview_array, key: key, path_key: path_key)
55
+
56
+ <<~RB.chomp
57
+ raise TypeError, "Expected Hash at '#{path_key}' (#{mode})" unless #{node_var}.is_a?(Hash)
58
+ #{fetch}
59
+ if next_node == :__missing__
60
+ #{miss_action}
61
+ end
62
+ #{node_var} = next_node
63
+ RB
64
+ end
65
+
66
+ def array_guard_code(node_var:, mode:, policy:, path_key:, map_depth:)
67
+ miss_action = build_array_miss_action(policy, mode, map_depth, path_key)
68
+ <<~RB.chomp
69
+ if #{node_var}.nil?
70
+ #{miss_action}
71
+ end
72
+ unless #{node_var}.is_a?(Array)
73
+ raise TypeError, "Expected Array at '#{path_key}' (#{mode}); got \#{#{node_var}.class}"
74
+ end
75
+ RB
76
+ end
77
+
78
+ # ---------- missing behaviors ----------
79
+ def build_miss_action(policy, mode, map_depth, preview_array, key:, path_key:)
80
+ case policy
81
+ when :nil
82
+ if mode == :ravel
83
+ base = "out << nil"
84
+ cont = map_depth.positive? ? "next" : "return out"
85
+ "#{base}\n#{cont}"
86
+ elsif mode == :each_indexed
87
+ if map_depth.positive?
88
+ <<~RB.chomp
89
+ if block
90
+ block.call(nil, idx_vec.dup)
91
+ next
92
+ else
93
+ out << [nil, idx_vec.dup]
94
+ next
95
+ end
96
+ RB
97
+ else
98
+ <<~RB.chomp
99
+ if block
100
+ block.call(nil, idx_vec.dup)
101
+ return nil
102
+ else
103
+ out << [nil, idx_vec.dup]
104
+ return out
105
+ end
106
+ RB
107
+ end
108
+ else # :materialize, :read
109
+ # Important: for :materialize this is ALWAYS nil (never [])
110
+ return_val = 'nil'
111
+ map_depth.positive? ? "next #{return_val}" : "return #{return_val}"
112
+ end
113
+ when :skip
114
+ if mode == :materialize
115
+ return_val = preview_array ? '[]' : 'nil'
116
+ map_depth.positive? ? "next #{return_val}" : "return #{return_val}"
117
+ else
118
+ map_depth.positive? ? "next" : (mode == :each_indexed ? "if block; return nil; else; return out; end" : "return out")
119
+ end
120
+ else # :error
121
+ %(raise KeyError, "Missing key '#{key}' at '#{path_key}' (#{mode})")
122
+ end
123
+ end
124
+
125
+ def build_array_miss_action(policy, mode, map_depth, path_key)
126
+ case policy
127
+ when :nil
128
+ if mode == :materialize
129
+ map_depth.positive? ? "next nil" : "return nil"
130
+ elsif mode == :each_indexed
131
+ if map_depth.positive?
132
+ <<~RB.chomp
133
+ if block
134
+ block.call(nil, idx_vec.dup)
135
+ next
136
+ else
137
+ out << [nil, idx_vec.dup]
138
+ next
139
+ end
140
+ RB
141
+ else
142
+ <<~RB.chomp
143
+ if block
144
+ block.call(nil, idx_vec.dup)
145
+ return nil
146
+ else
147
+ out << [nil, idx_vec.dup]
148
+ return out
149
+ end
150
+ RB
151
+ end
152
+ else # :ravel / others
153
+ base = "out << nil"
154
+ cont = map_depth.positive? ? "next" : "return out"
155
+ "#{base}\n#{cont}"
156
+ end
157
+ when :skip
158
+ if mode == :materialize
159
+ map_depth.positive? ? "next []" : "return []"
160
+ elsif mode == :each_indexed
161
+ map_depth.positive? ? "next" : "if block; return nil; else; return out; end"
162
+ else # :ravel
163
+ map_depth.positive? ? "next" : "return out"
164
+ end
165
+ else
166
+ %(raise TypeError, "Missing array at '#{path_key}' (#{mode})")
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ module Kumi::Core::Compiler::AccessEmit
3
+ module EachIndexed
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, &block|\n"
13
+ code << " out = []\n"
14
+ code << " node0 = data\n"
15
+ code << " idx_vec = []\n"
16
+ nodev, depth, loop_depth = "node0", 0, 0
17
+
18
+ segs.each do |seg|
19
+ if seg == :array
20
+ code << " #{array_guard_code(node_var: nodev, mode: :each_indexed, policy: policy, path_key: path_key, map_depth: loop_depth)}\n"
21
+ code << " ary#{loop_depth} = #{nodev}\n"
22
+ code << " len#{loop_depth} = ary#{loop_depth}.length\n"
23
+ code << " i#{loop_depth} = -1\n"
24
+ code << " while (i#{loop_depth} += 1) < len#{loop_depth}\n"
25
+ code << " idx_vec[#{loop_depth}] = i#{loop_depth}\n"
26
+ child = "node#{depth + 1}"
27
+ code << " #{child} = ary#{loop_depth}[i#{loop_depth}]\n"
28
+ nodev = child; depth += 1; loop_depth += 1
29
+ else
30
+ seg.each do |(_, key, preview)|
31
+ code << fetch_hash_code(node_var: nodev, key: key, key_policy: key_policy,
32
+ preview_array: preview, mode: :each_indexed, policy: policy,
33
+ path_key: path_key, map_depth: loop_depth)
34
+ code << "\n"
35
+ end
36
+ end
37
+ end
38
+
39
+ code << " if block\n"
40
+ code << " block.call(#{nodev}, idx_vec.dup)\n"
41
+ code << " else\n"
42
+ code << " out << [#{nodev}, idx_vec.dup]\n"
43
+ code << " end\n"
44
+
45
+ while loop_depth.positive?
46
+ code << " end\n"
47
+ loop_depth -= 1
48
+ nodev = "node#{depth - 1}"
49
+ depth -= 1
50
+ end
51
+
52
+ code << " block ? nil : out\nend\n"
53
+ code
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ module Kumi::Core::Compiler::AccessEmit
3
+ module Materialize
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
+ nodev, depth, map_depth = "node0", 0, 0
14
+ code << " #{nodev} = data\n"
15
+
16
+ segs.each do |seg|
17
+ if seg == :array
18
+ code << " #{array_guard_code(node_var: nodev, mode: :materialize, policy: policy, path_key: path_key, map_depth: map_depth)}\n"
19
+ child = "node#{depth + 1}"
20
+ code << " #{nodev} = #{nodev}.map do |__e#{depth}|\n"
21
+ code << " #{child} = __e#{depth}\n"
22
+ nodev = child; depth += 1; map_depth += 1
23
+ else
24
+ seg.each do |(_, key, preview)|
25
+ code << " "
26
+ code << fetch_hash_code(node_var: nodev, key: key, key_policy: key_policy,
27
+ preview_array: preview, mode: :materialize, policy: policy,
28
+ path_key: path_key, map_depth: map_depth)
29
+ code << "\n"
30
+ end
31
+ end
32
+ end
33
+
34
+ while map_depth.positive?
35
+ code << " " * map_depth + "#{nodev}\n"
36
+ code << " " * (map_depth - 1) + "end\n"
37
+ nodev = "node#{depth - 1}"
38
+ depth -= 1
39
+ map_depth -= 1
40
+ end
41
+ code << " #{nodev}\nend\n"
42
+ code
43
+ end
44
+ end
45
+ end