kumi 0.0.15 → 0.0.17

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/golden/cascade_logic/schema.kumi +3 -1
  4. data/lib/kumi/analyzer.rb +11 -9
  5. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +0 -81
  6. data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +18 -20
  7. data/lib/kumi/core/analyzer/passes/ir_execution_schedule_pass.rb +67 -0
  8. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +0 -36
  9. data/lib/kumi/core/analyzer/passes/toposorter.rb +1 -39
  10. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +8 -191
  11. data/lib/kumi/core/compiler/access_builder.rb +20 -10
  12. data/lib/kumi/core/compiler/access_codegen.rb +61 -0
  13. data/lib/kumi/core/compiler/access_emit/base.rb +173 -0
  14. data/lib/kumi/core/compiler/access_emit/each_indexed.rb +56 -0
  15. data/lib/kumi/core/compiler/access_emit/materialize.rb +45 -0
  16. data/lib/kumi/core/compiler/access_emit/ravel.rb +50 -0
  17. data/lib/kumi/core/compiler/access_emit/read.rb +32 -0
  18. data/lib/kumi/core/ir/execution_engine/interpreter.rb +36 -181
  19. data/lib/kumi/core/ir/execution_engine/values.rb +8 -8
  20. data/lib/kumi/core/ir/execution_engine.rb +3 -19
  21. data/lib/kumi/dev/parse.rb +12 -12
  22. data/lib/kumi/runtime/executable.rb +22 -175
  23. data/lib/kumi/runtime/run.rb +105 -0
  24. data/lib/kumi/schema.rb +8 -13
  25. data/lib/kumi/version.rb +1 -1
  26. data/lib/kumi.rb +3 -2
  27. metadata +10 -25
  28. data/BACKLOG.md +0 -34
  29. data/config/functions.yaml +0 -352
  30. data/docs/functions/analyzer_integration.md +0 -199
  31. data/docs/functions/signatures.md +0 -171
  32. data/examples/hash_objects_demo.rb +0 -138
  33. data/lib/kumi/core/analyzer/passes/function_signature_pass.rb +0 -199
  34. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +0 -48
  35. data/lib/kumi/core/functions/dimension.rb +0 -98
  36. data/lib/kumi/core/functions/dtypes.rb +0 -20
  37. data/lib/kumi/core/functions/errors.rb +0 -11
  38. data/lib/kumi/core/functions/kernel_adapter.rb +0 -45
  39. data/lib/kumi/core/functions/loader.rb +0 -119
  40. data/lib/kumi/core/functions/registry_v2.rb +0 -68
  41. data/lib/kumi/core/functions/shape.rb +0 -70
  42. data/lib/kumi/core/functions/signature.rb +0 -122
  43. data/lib/kumi/core/functions/signature_parser.rb +0 -86
  44. data/lib/kumi/core/functions/signature_resolver.rb +0 -272
  45. data/lib/kumi/kernels/ruby/aggregate_core.rb +0 -105
  46. data/lib/kumi/kernels/ruby/datetime_scalar.rb +0 -21
  47. data/lib/kumi/kernels/ruby/mask_scalar.rb +0 -15
  48. data/lib/kumi/kernels/ruby/scalar_core.rb +0 -63
  49. data/lib/kumi/kernels/ruby/string_scalar.rb +0 -19
  50. data/lib/kumi/kernels/ruby/vector_struct.rb +0 -39
@@ -6,7 +6,6 @@ module Kumi
6
6
  module Passes
7
7
  # RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
8
8
  # DEPENDENCIES: :declarations from NameIndexer, :input_metadata from InputCollector
9
- # PRODUCES: :cascades - Hash of cascade mutual exclusion analysis results
10
9
  # INTERFACE: new(schema, state).run(errors)
11
10
  class UnsatDetector < VisitorPass
12
11
  include Syntax
@@ -20,142 +19,31 @@ module Kumi
20
19
  @definitions = definitions
21
20
  @evaluator = ConstantEvaluator.new(definitions)
22
21
 
23
- # First pass: analyze cascade conditions for mutual exclusion
24
- cascades = {}
25
22
  each_decl do |decl|
26
- cascades[decl.name] = analyze_cascade_mutual_exclusion(decl, definitions) if decl.expression.is_a?(CascadeExpression)
27
-
28
- # Store cascade metadata for later passes
29
-
30
- # Second pass: check for unsatisfiable constraints
31
23
  if decl.expression.is_a?(CascadeExpression)
32
- # Special handling for cascade expressions
33
24
  check_cascade_expression(decl, definitions, errors)
34
25
  elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
35
- # Check for OR expressions which need special disjunctive handling
36
26
  impossible = check_or_expression(decl.expression, definitions, errors)
37
27
  report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
38
28
  else
39
- # Normal handling for non-cascade expressions
40
29
  atoms = gather_atoms(decl.expression, definitions, Set.new)
41
30
  next if atoms.empty?
42
31
 
43
- # DEBUG: Add detailed logging for hierarchical broadcasting debugging
44
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
45
- puts "DEBUG UNSAT: Checking declaration '#{decl.name}' at #{decl.loc}"
46
- puts " Expression: #{decl.expression.inspect}"
47
- puts " Gathered atoms: #{atoms.map(&:inspect)}"
48
- puts " Input meta: #{@input_meta.keys.inspect}" if @input_meta
49
- end
50
-
51
- # Use enhanced solver that can detect cross-variable mathematical constraints
52
- if definitions && !definitions.empty?
53
- result = Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
54
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
55
- puts " Enhanced solver result: #{result}"
56
- end
57
- else
58
- result = Kumi::Core::AtomUnsatSolver.unsat?(atoms)
59
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
60
- puts " Basic solver result: #{result}"
61
- end
62
- end
32
+ result = if definitions && !definitions.empty?
33
+ Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
34
+ else
35
+ Kumi::Core::AtomUnsatSolver.unsat?(atoms)
36
+ end
63
37
  impossible = result
64
38
 
65
- if impossible && (ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257"))
66
- puts " -> FLAGGING AS IMPOSSIBLE: #{decl.name}"
67
- end
68
-
69
39
  report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
70
40
  end
71
41
  end
72
- state.with(:cascades, cascades)
73
- end
74
-
75
- private
76
-
77
- def analyze_cascade_mutual_exclusion(decl, definitions)
78
- conditions = []
79
- condition_traits = []
80
-
81
- # Extract all cascade conditions (except base case)
82
- decl.expression.cases[0...-1].each do |when_case|
83
- next unless when_case.condition
84
-
85
- next unless when_case.condition.fn_name == :cascade_and
86
-
87
- when_case.condition.args.each do |arg|
88
- if arg.is_a?(ArrayExpression)
89
- # Handle array elements (for array broadcasting)
90
- arg.elements.each do |element|
91
- next unless element.is_a?(DeclarationReference)
92
-
93
- trait_name = element.name
94
- trait = definitions[trait_name]
95
- if trait
96
- conditions << trait.expression
97
- condition_traits << trait_name
98
- end
99
- end
100
- elsif arg.is_a?(DeclarationReference)
101
- # Handle direct trait references (simple case)
102
- trait_name = arg.name
103
- trait = definitions[trait_name]
104
- if trait
105
- conditions << trait.expression
106
- condition_traits << trait_name
107
- end
108
- end
109
- end
110
- # end
111
- end
112
-
113
- # Check mutual exclusion for all pairs
114
- total_pairs = conditions.size * (conditions.size - 1) / 2
115
- exclusive_pairs = 0
116
-
117
- if conditions.size >= 2
118
- conditions.combination(2).each do |cond1, cond2|
119
- exclusive_pairs += 1 if conditions_mutually_exclusive?(cond1, cond2)
120
- end
121
- end
122
-
123
- all_mutually_exclusive = total_pairs.positive? && (exclusive_pairs == total_pairs)
124
-
125
- {
126
- condition_traits: condition_traits,
127
- condition_count: conditions.size,
128
- all_mutually_exclusive: all_mutually_exclusive,
129
- exclusive_pairs: exclusive_pairs,
130
- total_pairs: total_pairs
131
- }
132
- end
133
-
134
- def conditions_mutually_exclusive?(cond1, cond2)
135
- if cond1.is_a?(CallExpression) && cond1.fn_name == :== &&
136
- cond2.is_a?(CallExpression) && cond2.fn_name == :==
137
-
138
- c1_field, c1_value = cond1.args
139
- c2_field, c2_value = cond2.args
140
-
141
- # Same field, different values = mutually exclusive
142
- return true if same_field?(c1_field, c2_field) && different_values?(c1_value, c2_value)
143
- end
144
42
 
145
- false
43
+ state
146
44
  end
147
45
 
148
- def same_field?(field1, field2)
149
- return false unless field1.is_a?(InputReference) && field2.is_a?(InputReference)
150
-
151
- field1.name == field2.name
152
- end
153
-
154
- def different_values?(val1, val2)
155
- return false unless val1.is_a?(Literal) && val2.is_a?(Literal)
156
-
157
- val1.value != val2.value
158
- end
46
+ private
159
47
 
160
48
  def check_or_expression(or_expr, definitions, _errors)
161
49
  # For OR expressions: A | B is impossible only if BOTH A AND B are impossible
@@ -199,23 +87,16 @@ module Kumi
199
87
  if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
200
88
  lhs, rhs = current.args
201
89
 
202
- # Check for domain constraint violations before creating atom
203
90
  list << if impossible_constraint?(lhs, rhs, current.fn_name)
204
- # Create a special impossible atom that will always trigger unsat
205
91
  Atom.new(:==, :__impossible__, true)
206
92
  else
207
93
  Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
208
94
  end
209
95
  elsif current.is_a?(CallExpression) && current.fn_name == :or
210
- # Special handling for OR expressions - they are disjunctive, not conjunctive
211
- # We should NOT add OR children to the stack as they would be treated as AND
212
- # OR expressions need separate analysis in the main run() method
213
96
  next
214
97
  elsif current.is_a?(CallExpression) && current.fn_name == :cascade_and
215
- # cascade_and takes individual arguments (not wrapped in array)
216
98
  current.args.each { |arg| stack << arg }
217
99
  elsif current.is_a?(ArrayExpression)
218
- # For ArrayExpression, add all elements to the stack
219
100
  current.elements.each { |elem| stack << elem }
220
101
  elsif current.is_a?(DeclarationReference)
221
102
  name = current.name
@@ -225,10 +106,6 @@ module Kumi
225
106
  end
226
107
  end
227
108
 
228
- # Add children to stack for processing
229
- # IMPORTANT: Skip CascadeExpression children to avoid false positives
230
- # Cascades are handled separately by check_cascade_expression() and are disjunctive,
231
- # but gather_atoms() treats all collected atoms as conjunctive
232
109
  current.children.each { |child| stack << child } if current.respond_to?(:children) && !current.is_a?(CascadeExpression)
233
110
  end
234
111
 
@@ -247,32 +124,24 @@ module Kumi
247
124
  end
248
125
 
249
126
  decl.expression.cases.each_with_index do |when_case, index|
250
- # DEBUG: Log each case
251
127
  if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
252
128
  puts " Case #{index}: condition=#{when_case.condition.inspect}"
253
129
  end
254
130
 
255
- # Skip the base case (it's typically a literal true condition)
256
131
  next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
257
132
 
258
- # Skip non-conjunctive conditions (any?, none?) as they are disjunctive
259
133
  next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
260
134
 
261
- # Skip single-trait 'on' branches: trait-level unsat detection covers these
262
135
  if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and && (when_case.condition.args.size == 1)
263
- # cascade_and uses individual arguments - skip if only one trait
264
136
  next
265
137
  end
266
138
 
267
- # Gather atoms from this individual condition only
268
139
  condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
269
140
 
270
- # DEBUG: Add detailed logging for hierarchical broadcasting debugging
271
141
  if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
272
142
  puts " Condition atoms: #{condition_atoms.map(&:inspect)}"
273
143
  end
274
144
 
275
- # Use enhanced solver for cascade conditions too
276
145
  if definitions && !definitions.empty?
277
146
  result = Kumi::Core::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
278
147
  if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
@@ -287,9 +156,7 @@ module Kumi
287
156
  impossible = result
288
157
  next unless !condition_atoms.empty? && impossible
289
158
 
290
- # For multi-trait on-clauses, report the trait names rather than the value name
291
159
  if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and
292
- # cascade_and uses individual arguments
293
160
  trait_bindings = when_case.condition.args
294
161
 
295
162
  if trait_bindings.all?(DeclarationReference)
@@ -314,9 +181,6 @@ module Kumi
314
181
  val = @evaluator.evaluate(node)
315
182
  val == :unknown ? node.name : val
316
183
  when InputElementReference
317
- # For hierarchical paths like input.companies.regions.offices.teams.department,
318
- # create a unique identifier that represents the specific path
319
- # This prevents false positives where different paths are treated as the same :unknown
320
184
  path_identifier = node.path.join(".").to_s
321
185
  path_identifier.to_sym
322
186
  when Literal
@@ -326,51 +190,10 @@ module Kumi
326
190
  end
327
191
  end
328
192
 
329
- def check_domain_constraints(node, definitions, errors)
330
- case node
331
- when InputReference
332
- # Check if InputReference points to a field with domain constraints
333
- field_meta = @input_meta[node.name]
334
- nil unless field_meta&.dig(:domain)
335
-
336
- # For InputReference, the constraint comes from trait conditions
337
- # We don't flag here since the InputReference itself is valid
338
- when DeclarationReference
339
- # Check if this binding evaluates to a value that violates domain constraints
340
- definition = definitions[node.name]
341
- return unless definition
342
-
343
- if definition.expression.is_a?(Literal)
344
- literal_value = definition.expression.value
345
- check_value_against_domains(node.name, literal_value, errors, definition.loc)
346
- end
347
- end
348
- end
349
-
350
- def check_value_against_domains(_var_name, value, _errors, _location)
351
- # Check if this value violates any input domain constraints
352
- @input_meta.each_value do |field_meta|
353
- domain = field_meta[:domain]
354
- next unless domain
355
-
356
- if violates_domain?(value, domain)
357
- # This indicates a constraint that can never be satisfied
358
- # Rather than flagging the cascade, flag the impossible condition
359
- return true
360
- end
361
- end
362
- false
363
- end
364
-
365
193
  def violates_domain?(value, domain)
366
194
  case domain
367
- when Range
368
- !domain.include?(value)
369
- when Array
195
+ when Range, Array
370
196
  !domain.include?(value)
371
- when Proc
372
- # For Proc domains, we can't statically analyze
373
- false
374
197
  else
375
198
  false
376
199
  end
@@ -381,7 +204,6 @@ module Kumi
381
204
  if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
382
205
  return field_literal_impossible?(lhs, rhs, operator)
383
206
  elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
384
- # Reverse case: literal compared to field
385
207
  return field_literal_impossible?(rhs, lhs, flip_operator(operator))
386
208
  end
387
209
 
@@ -404,13 +226,10 @@ module Kumi
404
226
 
405
227
  case operator
406
228
  when :==
407
- # field == value where value is not in domain
408
229
  violates_domain?(literal_value, domain)
409
230
  when :!=
410
- # field != value where value is not in domain is always true (not impossible)
411
231
  false
412
232
  else
413
- # For other operators, we'd need more sophisticated analysis
414
233
  false
415
234
  end
416
235
  end
@@ -424,10 +243,8 @@ module Kumi
424
243
 
425
244
  case operator
426
245
  when :==
427
- # binding == value where binding evaluates to different value
428
246
  evaluated_value != literal_value
429
247
  else
430
- # For other operators, we could add more sophisticated checking
431
248
  false
432
249
  end
433
250
  end
@@ -2,18 +2,29 @@ module Kumi
2
2
  module Core
3
3
  module Compiler
4
4
  class AccessBuilder
5
- def self.build(plans)
5
+ class << self
6
+ attr_accessor :yjit
7
+ end
8
+ self.yjit = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
9
+
10
+ def self.build(plans, strategy: nil)
11
+ strategy ||= yjit ? :interp : :codegen
6
12
  accessors = {}
13
+
7
14
  plans.each_value do |variants|
8
15
  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
- )
16
+ accessors[plan.accessor_key] =
17
+ case strategy
18
+ when :codegen then AccessCodegen.fetch_or_compile(plan)
19
+ else
20
+ build_proc_for(
21
+ mode: plan.mode,
22
+ path_key: plan.path,
23
+ missing: (plan.on_missing || :error).to_sym,
24
+ key_policy: (plan.key_policy || :indifferent).to_sym,
25
+ operations: plan.operations
26
+ )
27
+ end
17
28
  end
18
29
  end
19
30
  accessors.freeze
@@ -25,7 +36,6 @@ module Kumi
25
36
  when :materialize then Accessors::MaterializeAccessor.build(operations, path_key, missing, key_policy)
26
37
  when :ravel then Accessors::RavelAccessor.build(operations, path_key, missing, key_policy)
27
38
  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
39
  else
30
40
  raise "Unknown accessor mode: #{mode.inspect}"
31
41
  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