kumi 0.0.10 → 0.0.11

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +7 -231
  4. data/README.md +1 -1
  5. data/docs/VECTOR_SEMANTICS.md +286 -0
  6. data/docs/features/hierarchical-broadcasting.md +1 -1
  7. data/docs/features/s-expression-printer.md +2 -2
  8. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  9. data/lib/kumi/analyzer.rb +34 -12
  10. data/lib/kumi/compiler.rb +2 -12
  11. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  13. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  14. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -101
  15. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  16. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  17. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  18. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  19. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
  20. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  21. data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
  22. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  23. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  24. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
  25. data/lib/kumi/core/analyzer/plans.rb +52 -0
  26. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  27. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  28. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  29. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  30. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  31. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  32. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  33. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  34. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  35. data/lib/kumi/core/compiler_base.rb +2 -2
  36. data/lib/kumi/core/error_reporter.rb +6 -5
  37. data/lib/kumi/core/errors.rb +4 -0
  38. data/lib/kumi/core/explain.rb +157 -205
  39. data/lib/kumi/core/export/node_builders.rb +2 -2
  40. data/lib/kumi/core/export/node_serializers.rb +1 -1
  41. data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
  42. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  43. data/lib/kumi/core/function_registry/function_builder.rb +142 -55
  44. data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
  45. data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
  46. data/lib/kumi/core/function_registry.rb +126 -108
  47. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  48. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  49. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  50. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  51. data/lib/kumi/core/ir.rb +58 -0
  52. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  53. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  54. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
  55. data/lib/kumi/core/ruby_parser/input_builder.rb +5 -5
  56. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  57. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  58. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  59. data/lib/kumi/registry.rb +14 -79
  60. data/lib/kumi/runtime/executable.rb +213 -0
  61. data/lib/kumi/schema.rb +14 -3
  62. data/lib/kumi/schema_metadata.rb +2 -2
  63. data/lib/kumi/support/ir_dump.rb +491 -0
  64. data/lib/kumi/support/s_expression_printer.rb +1 -1
  65. data/lib/kumi/syntax/location.rb +5 -0
  66. data/lib/kumi/syntax/node.rb +0 -1
  67. data/lib/kumi/syntax/root.rb +2 -2
  68. data/lib/kumi/version.rb +1 -1
  69. data/lib/kumi.rb +6 -15
  70. metadata +26 -15
  71. data/lib/kumi/core/cascade_executor_builder.rb +0 -132
  72. data/lib/kumi/core/compiled_schema.rb +0 -43
  73. data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
  74. data/lib/kumi/core/compiler/function_invoker.rb +0 -55
  75. data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
  76. data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
  77. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  78. data/lib/kumi/core/nested_structure_utils.rb +0 -78
  79. data/lib/kumi/core/schema_instance.rb +0 -115
  80. data/lib/kumi/core/vectorized_function_builder.rb +0 -88
  81. data/lib/kumi/js/compiler.rb +0 -878
  82. data/lib/kumi/js/function_registry.rb +0 -333
  83. data/migrate_to_core_iterative.rb +0 -938
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../analyzer/structs/input_meta"
4
+ require_relative "../analyzer/structs/access_plan"
5
+
6
+ module Kumi
7
+ module Core
8
+ module Compiler
9
+ # Generates deterministic access plans from normalized input metadata.
10
+ #
11
+ # Metadata expectations (produced by InputCollector):
12
+ # - Each node has:
13
+ # :container => :scalar | :read | :array
14
+ # :children => { name => meta } (optional)
15
+ # - Each non-root node (i.e., any child) carries edge hints from its parent:
16
+ # :enter_via => :field | :array # how the parent reaches THIS node
17
+ # :consume_alias => true|false # inline array edge; planner does not need this to emit ops
18
+ #
19
+ # Planning rules (single source of truth):
20
+ # - Root is an implicit object.
21
+ # - If parent is :array, always emit :enter_array before stepping to the child.
22
+ # - If child.enter_via == :field → also emit :enter_hash(child_name).
23
+ # - If child.enter_via == :array → inline edge, do NOT emit :enter_hash for the alias.
24
+ # - If parent is :read (or root), emit :enter_hash(child_name).
25
+ #
26
+ # Modes (one plan per mode):
27
+ # - Scalar paths (no array in lineage) → [:read]
28
+ # - Vector paths (≥1 array in lineage) → [:each_indexed, :materialize, :ravel]
29
+ # - If @defaults[:mode] is set, emit only that mode (alias :read → :read).
30
+ class AccessPlanner
31
+ def self.plan(meta, options = {}) = new(meta, options).plan
32
+ def self.plan_for(meta, path, options = {}) = new(meta, options).plan_for(path)
33
+
34
+ def initialize(meta, options = {})
35
+ @meta = meta
36
+ @defaults = { on_missing: :error, key_policy: :indifferent, mode: nil }.merge(options)
37
+ @plans = {}
38
+ end
39
+
40
+ def plan
41
+ @meta.each_key { |root| walk_and_emit([root.to_s]) }
42
+ @plans
43
+ end
44
+
45
+ def plan_for(path)
46
+ segs = path.split(".")
47
+ ensure_path!(segs)
48
+ emit_for_segments(segs, explicit_mode: @defaults[:mode])
49
+ @plans
50
+ end
51
+
52
+ private
53
+
54
+ def walk_and_emit(path)
55
+ emit_for_segments(path)
56
+ node = meta_node_for(path)
57
+ return if node[:children].nil?
58
+
59
+ node[:children].each_key do |c|
60
+ walk_and_emit(path + [c.to_s])
61
+ end
62
+ end
63
+
64
+ def emit_for_segments(path, explicit_mode: nil)
65
+ lineage = container_lineage(path)
66
+ base = build_base_plan(path, lineage)
67
+ node = meta_node_for(path)
68
+
69
+ modes = explicit_mode || infer_modes(lineage, node)
70
+ modes = [modes] unless modes.is_a?(Array)
71
+
72
+ list = (@plans[base[:path]] ||= [])
73
+ modes.each do |mode|
74
+ operations = build_operations(path, mode)
75
+
76
+ list << Kumi::Core::Analyzer::AccessPlan.new(
77
+ path: base[:path],
78
+ containers: base[:containers],
79
+ leaf: base[:leaf],
80
+ scope: base[:scope],
81
+ depth: base[:depth],
82
+ mode: mode, # :read | :each_indexed | :materialize | :ravel
83
+ on_missing: base[:on_missing],
84
+ key_policy: base[:key_policy],
85
+ operations: operations
86
+ )
87
+ end
88
+ end
89
+
90
+ def build_base_plan(path, lineage)
91
+ {
92
+ path: path.join("."),
93
+ containers: lineage, # symbols of array segments in the path
94
+ leaf: path.last.to_sym,
95
+ scope: lineage.dup, # alias kept for analyzer symmetry
96
+ depth: lineage.length, # rank
97
+ on_missing: @defaults[:on_missing],
98
+ key_policy: @defaults[:key_policy]
99
+
100
+ }.freeze
101
+ end
102
+
103
+ def infer_modes(lineage, _node)
104
+ lineage.empty? ? [:read] : %i[each_indexed materialize ravel]
105
+ end
106
+
107
+ # Core op builder: apply the parent→child edge rule per segment.
108
+ def build_operations(path, mode)
109
+ ops = []
110
+ parent_meta = nil
111
+ cur = @meta
112
+
113
+ puts "\n🔨 Building operations for path: #{path.join('.')}:#{mode}" if ENV["DEBUG_ACCESSOR_OPS"]
114
+
115
+ path.each_with_index do |seg, idx|
116
+ node = ig(cur, seg) or raise ArgumentError, "Unknown segment '#{seg}' in '#{path.join('.')}'"
117
+
118
+ puts " Segment #{idx}: '#{seg}'" if ENV["DEBUG_ACCESSOR_OPS"]
119
+
120
+ # Validate required fields before using them
121
+ container = parent_meta&.[](:container)
122
+ enter_via = if is_root_segment?(idx)
123
+ nil
124
+ else
125
+ node[:enter_via] do
126
+ raise ArgumentError,
127
+ "Missing :enter_via for non-root segment '#{seg}' at '#{path.join('.')}'. Contract violation."
128
+ end
129
+ end
130
+
131
+ if container == :array
132
+ # Array parent: always step into elements first
133
+ ops << enter_array
134
+ puts " Added: enter_array" if ENV["DEBUG_ACCESSOR_OPS"]
135
+
136
+ # Then either inline (no hash) or field hop to named member
137
+ if enter_via == :hash
138
+ ops << enter_hash(seg)
139
+ puts " Added: enter_hash('#{seg}')" if ENV["DEBUG_ACCESSOR_OPS"]
140
+ elsif enter_via == :array
141
+ # Inline alias, no hash operation needed
142
+ puts " Skipped enter_hash (inline alias)" if ENV["DEBUG_ACCESSOR_OPS"]
143
+ else
144
+ raise ArgumentError, "Invalid :enter_via '#{enter_via}' for array child '#{seg}'. Must be :hash or :array"
145
+ end
146
+ elsif container.nil? || container == :object
147
+ # Root or object parent - always emit enter_hash
148
+ ops << enter_hash(seg)
149
+ puts " Added: enter_hash('#{seg}')" if ENV["DEBUG_ACCESSOR_OPS"]
150
+ else
151
+ raise ArgumentError, "Invalid parent :container '#{container}' for segment '#{seg}'. Expected :array, :object, or nil (root)"
152
+ end
153
+
154
+ parent_meta = node
155
+ cur = node[:children] || {}
156
+ end
157
+
158
+ terminal = parent_meta
159
+
160
+ if terminal && terminal[:container] == :array && %i[each_indexed ravel].include?(mode)
161
+ ops << enter_array
162
+ # :materialize and :read do not step into elements
163
+ end
164
+
165
+ # # If we land on an array and this mode iterates elements, step into it.
166
+ puts " Final operations: #{ops.inspect}" if ENV["DEBUG_ACCESSOR_OPS"]
167
+
168
+ ops
169
+ end
170
+
171
+ def container_lineage(path)
172
+ lineage = []
173
+ cur = @meta
174
+ path.each do |seg|
175
+ m = ig(cur, seg)
176
+ container = m[:container] do
177
+ raise ArgumentError, "Missing :container for '#{seg}' in path '#{path.join('.')}'. Contract violation."
178
+ end
179
+ lineage << seg.to_sym if container == :array
180
+ cur = m[:children] || {}
181
+ end
182
+ lineage
183
+ end
184
+
185
+ def meta_node_for(path)
186
+ cur = @meta
187
+ last = nil
188
+ path.each do |seg|
189
+ m = ig(cur, seg)
190
+ last = m
191
+ cur = m[:children] || {}
192
+ end
193
+ last
194
+ end
195
+
196
+ def ensure_path!(path)
197
+ raise ArgumentError, "Unknown path: #{path.join('.')}" unless meta_node_for(path)
198
+ end
199
+
200
+ def ig(h, k)
201
+ h[k.to_sym] or raise ArgumentError, "Missing required field '#{k}' in metadata. Available keys: #{h.keys.inspect}"
202
+ end
203
+
204
+ def enter_hash(key)
205
+ { type: :enter_hash, key: key.to_s,
206
+ on_missing: @defaults[:on_missing], key_policy: @defaults[:key_policy] }
207
+ end
208
+
209
+ def enter_array
210
+ { type: :enter_array, on_missing: @defaults[:on_missing] }
211
+ end
212
+
213
+ def is_root_segment?(idx)
214
+ idx == 0
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Compiler
6
+ module Accessors
7
+ module Base
8
+ MISSING = :__missing__
9
+
10
+ # -------- assertions --------
11
+ def assert_hash!(node, path_key, mode)
12
+ raise TypeError, "Expected Hash at '#{path_key}' (#{mode})" unless node.is_a?(Hash)
13
+ end
14
+
15
+ def assert_array!(node, path_key, mode)
16
+ return if node.is_a?(Array)
17
+
18
+ warn_mismatch(node, path_key) if ENV["DEBUG_ACCESS_BUILDER"]
19
+ raise TypeError, "Expected Array at '#{path_key}' (#{mode}); got #{node.class}"
20
+ end
21
+
22
+ def warn_mismatch(node, path_key)
23
+ puts "DEBUG AccessBuilder error at #{path_key}: got #{node.class}, value=#{node.inspect}"
24
+ end
25
+
26
+ # -------- key fetch with policy --------
27
+ def fetch_key(hash, key, policy)
28
+ case policy
29
+ when :indifferent
30
+ return hash[key] if hash.key?(key)
31
+ return hash[key.to_sym] if hash.key?(key.to_sym)
32
+ return hash[key.to_s] if hash.key?(key.to_s)
33
+
34
+ MISSING
35
+ when :string
36
+ hash.key?(key.to_s) ? hash[key.to_s] : MISSING
37
+ when :symbol
38
+ hash.key?(key.to_sym) ? hash[key.to_sym] : MISSING
39
+ else
40
+ hash.key?(key) ? hash[key] : MISSING
41
+ end
42
+ end
43
+
44
+ # -------- op helpers --------
45
+ def next_enters_array?(operations, pc)
46
+ nxt = operations[pc + 1]
47
+ nxt && nxt[:type] == :enter_array
48
+ end
49
+
50
+ def missing_key_action(policy)
51
+ if policy == :nil
52
+ :yield_nil
53
+ else
54
+ (policy == :skip ? :skip : :raise)
55
+ end
56
+ end
57
+
58
+ def missing_array_action(policy)
59
+ if policy == :nil
60
+ :yield_nil
61
+ else
62
+ (policy == :skip ? :skip : :raise)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Compiler
6
+ module Accessors
7
+ class EachIndexedAccessor
8
+ extend Base
9
+
10
+ def self.build(operations, path_key, policy, key_policy, with_indices = true)
11
+ walker = build_each_walker(operations, path_key, policy, key_policy)
12
+ if with_indices
13
+ lambda do |data, &blk|
14
+ if blk
15
+ walker.call(data, 0, [], ->(val, idx) { blk.call(val, idx) })
16
+ nil
17
+ else
18
+ out = []
19
+ walker.call(data, 0, [], ->(val, idx) { out << [val, idx] })
20
+ out
21
+ end
22
+ end
23
+ else
24
+ lambda do |data, &blk|
25
+ if blk
26
+ walker.call(data, 0, [], ->(val, _idx) { blk.call(val) })
27
+ nil
28
+ else
29
+ out = []
30
+ walker.call(data, 0, [], ->(val, _idx) { out << val })
31
+ out
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ # Depth-first traversal yielding (value, nd_index)
38
+ def self.build_each_walker(operations, path_key, policy, key_policy)
39
+ mode = :each_indexed
40
+ walk = nil
41
+ walk = lambda do |node, pc, ndx, y|
42
+ if pc >= operations.length
43
+ y.call(node, ndx)
44
+ return
45
+ end
46
+
47
+ op = operations[pc]
48
+ case op[:type]
49
+ when :enter_hash
50
+ # If the *next* op is an array hop, relax to indifferent for that fetch
51
+ policy_for = next_enters_array?(operations, pc) ? :indifferent : key_policy
52
+ next_node = fetch_key(node, op[:key], policy_for)
53
+ if next_node == Base::MISSING
54
+ case missing_key_action(policy)
55
+ when :yield_nil then y.call(nil, ndx)
56
+ when :skip then return
57
+ when :raise then raise KeyError, "Missing key '#{op[:key]}' at '#{path_key}' (#{mode})"
58
+ end
59
+ return
60
+ end
61
+ walk.call(next_node, pc + 1, ndx, y)
62
+
63
+ when :enter_array
64
+ if node.nil?
65
+ case missing_array_action(policy)
66
+ when :yield_nil then y.call(nil, ndx)
67
+ when :skip then return
68
+ when :raise then raise KeyError, "Missing array at '#{path_key}' (#{mode})"
69
+ end
70
+ return
71
+ end
72
+ assert_array!(node, path_key, mode)
73
+ node.each_with_index { |child, i| walk.call(child, pc + 1, ndx + [i], y) }
74
+
75
+ else
76
+ raise "Unknown operation: #{op.inspect}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Compiler
6
+ module Accessors
7
+ class MaterializeAccessor
8
+ extend Base
9
+
10
+ def self.build(operations, path_key, policy, key_policy)
11
+ mode = :materialize
12
+ lambda do |data|
13
+ walk = nil
14
+ walk = lambda do |node, pc|
15
+ return node if pc >= operations.length
16
+
17
+ op = operations[pc]
18
+ case op[:type]
19
+ when :enter_hash
20
+ assert_hash!(node, path_key, mode)
21
+ preview_array = next_enters_array?(operations, pc)
22
+ policy_for = preview_array ? :indifferent : key_policy
23
+ next_node = fetch_key(node, op[:key], policy_for)
24
+ if next_node == Base::MISSING
25
+ case missing_key_action(policy)
26
+ when :yield_nil then return nil
27
+ when :skip then return preview_array ? [] : nil
28
+ when :raise then raise KeyError, "Missing key '#{op[:key]}' at '#{path_key}' (#{mode})"
29
+ end
30
+ end
31
+ walk.call(next_node, pc + 1)
32
+
33
+ when :enter_array
34
+ if node.nil?
35
+ case missing_array_action(policy)
36
+ when :yield_nil then return nil
37
+ when :skip then return []
38
+ when :raise then raise KeyError, "Missing array at '#{path_key}' (#{mode})"
39
+ end
40
+ end
41
+ assert_array!(node, path_key, mode)
42
+ node.map { |child| walk.call(child, pc + 1) }
43
+
44
+ else
45
+ raise "Unknown operation: #{op.inspect}"
46
+ end
47
+ end
48
+ walk.call(data, 0)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Compiler
6
+ module Accessors
7
+ # Ravel: collect leaf elements reached by the op sequence.
8
+ # Invariants guaranteed by the planner for :ravel:
9
+ # - Every array edge along the path has an :enter_array op.
10
+ # - If the leaf container is an array, a terminal :enter_array is appended,
11
+ # so the leaf we see here is the element, not the array.
12
+ class RavelAccessor
13
+ extend Base
14
+
15
+ def self.build(operations, path_key, policy, key_policy)
16
+ mode = :ravel
17
+
18
+ lambda do |data|
19
+ out = []
20
+
21
+ walk = nil
22
+ walk = lambda do |node, pc|
23
+ # Leaf: ops exhausted ⇒ emit this element (scalar/object/array element).
24
+ if pc >= operations.length
25
+ out << node
26
+ return
27
+ end
28
+
29
+ op = operations[pc]
30
+ case op[:type]
31
+ when :enter_hash
32
+ # If the next step is an array, we don’t care about key symbol/string
33
+ # (we’ll just iterate) → use indifferent lookup.
34
+ preview_array = next_enters_array?(operations, pc)
35
+ policy_for = preview_array ? :indifferent : key_policy
36
+
37
+ next_node = fetch_key(node, op[:key], policy_for)
38
+ if next_node == Base::MISSING
39
+ case missing_key_action(policy)
40
+ when :yield_nil then out << nil
41
+ when :skip then return
42
+ when :raise then raise KeyError, "Missing key '#{op[:key]}' at '#{path_key}' (#{mode})"
43
+ end
44
+ return
45
+ end
46
+ walk.call(next_node, pc + 1)
47
+
48
+ when :enter_array
49
+ if node.nil?
50
+ case missing_array_action(policy)
51
+ when :yield_nil then out << nil
52
+ when :skip then return
53
+ when :raise then raise KeyError, "Missing array at '#{path_key}' (#{mode})"
54
+ end
55
+ return
56
+ end
57
+ assert_array!(node, path_key, mode)
58
+ node.each { |child| walk.call(child, pc + 1) }
59
+
60
+ else
61
+ raise "Unknown operation: #{op.inspect}"
62
+ end
63
+ end
64
+
65
+ walk.call(data, 0)
66
+ out
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Compiler
6
+ module Accessors
7
+ class ReadAccessor
8
+ extend Base
9
+
10
+ def self.build(operations, path_key, policy, key_policy)
11
+ mode = :read
12
+ lambda do |data|
13
+ node = data
14
+ operations.each do |op|
15
+ case op[:type]
16
+ when :enter_hash
17
+ assert_hash!(node, path_key, mode)
18
+ next_node = fetch_key(node, op[:key], key_policy)
19
+ if next_node == Base::MISSING
20
+ case missing_key_action(policy)
21
+ when :yield_nil then return nil
22
+ when :skip then return nil
23
+ when :raise then raise KeyError, "Missing key '#{op[:key]}' at '#{path_key}' (#{mode})"
24
+ end
25
+ end
26
+ node = next_node
27
+ when :enter_array
28
+ # Should never be present for rank-0 plans
29
+ raise TypeError, "Array encountered in :read accessor at '#{path_key}'"
30
+ else
31
+ raise "Unknown operation: #{op.inspect}"
32
+ end
33
+ end
34
+ node
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -24,8 +24,8 @@ module Kumi
24
24
 
25
25
  def build_index
26
26
  @index = {}
27
- @schema.attributes.each { |a| @index[a.name] = a }
28
- @schema.traits.each { |t| @index[t.name] = t }
27
+ @schema.values.each { |a| @index[a.name] = a }
28
+ @schema.traits.each { |t| @index[t.name] = t }
29
29
  end
30
30
 
31
31
  def determine_operation_mode_for_path(_path)
@@ -12,7 +12,7 @@ module Kumi
12
12
  # 4. Support both immediate raising and error accumulation patterns
13
13
  module ErrorReporter
14
14
  # Standard error structure for internal use
15
- ErrorEntry = Struct.new(:location, :message, :type, :context, keyword_init: true) do
15
+ ErrorEntry = Struct.new(:location, :message, :type, :context, :backtrace, keyword_init: true) do
16
16
  def to_s
17
17
  location_str = format_location(location)
18
18
  "#{location_str}: #{message}"
@@ -47,12 +47,13 @@ module Kumi
47
47
  # @param type [Symbol] Optional error category (:syntax, :semantic, :type, etc.)
48
48
  # @param context [Hash] Optional additional context
49
49
  # @return [ErrorEntry] Structured error entry
50
- def create_error(message, location: nil, type: :semantic, context: {})
50
+ def create_error(message, location: nil, type: :semantic, context: {}, backtrace: nil)
51
51
  ErrorEntry.new(
52
52
  location: location,
53
53
  message: message,
54
54
  type: type,
55
- context: context
55
+ context: context,
56
+ backtrace: backtrace
56
57
  )
57
58
  end
58
59
 
@@ -76,8 +77,8 @@ module Kumi
76
77
  # @param error_class [Class] Exception class to raise
77
78
  # @param type [Symbol] Error category
78
79
  # @param context [Hash] Additional context
79
- def raise_error(message, location: nil, error_class: Errors::SemanticError, type: :semantic, context: {})
80
- entry = create_error(message, location: location, type: type, context: context)
80
+ def raise_error(message, location: nil, error_class: Errors::SemanticError, type: :semantic, backtrace: nil, context: {})
81
+ entry = create_error(message, location: location, type: type, context: context, backtrace: backtrace || caller)
81
82
  # Pass both the formatted message and the original location to the error constructor
82
83
  raise error_class.new(entry.to_s, location)
83
84
  end
@@ -24,6 +24,8 @@ module Kumi
24
24
 
25
25
  class UnknownFunction < Error; end
26
26
 
27
+ class AnalysisError < Error; end
28
+
27
29
  class SemanticError < LocatedError; end
28
30
 
29
31
  class TypeError < SemanticError; end
@@ -32,6 +34,8 @@ module Kumi
32
34
 
33
35
  class SyntaxError < LocatedError; end
34
36
 
37
+ class CompilationError < Error; end
38
+
35
39
  class RuntimeError < Error; end
36
40
 
37
41
  class DomainViolationError < Error