kumi 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +7 -231
- data/README.md +5 -5
- data/docs/SYNTAX.md +66 -0
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/features/hierarchical-broadcasting.md +67 -1
- data/docs/features/input-declaration-system.md +16 -0
- data/docs/features/s-expression-printer.md +2 -2
- data/lib/kumi/analyzer.rb +34 -12
- data/lib/kumi/compiler.rb +2 -12
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +123 -101
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +2 -2
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -55
- data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
- data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
- data/lib/kumi/core/function_registry.rb +126 -108
- data/lib/kumi/core/input/validator.rb +1 -1
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
- data/lib/kumi/core/ruby_parser/input_builder.rb +30 -9
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/core/types/validator.rb +1 -1
- data/lib/kumi/registry.rb +14 -79
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +14 -3
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +1 -1
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +0 -1
- data/lib/kumi/syntax/root.rb +2 -2
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- metadata +37 -19
- data/lib/kumi/core/cascade_executor_builder.rb +0 -132
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
- data/lib/kumi/core/compiler/function_invoker.rb +0 -55
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
- data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/nested_structure_utils.rb +0 -78
- data/lib/kumi/core/schema_instance.rb +0 -115
- data/lib/kumi/core/vectorized_function_builder.rb +0 -88
- data/lib/kumi/js/compiler.rb +0 -878
- data/lib/kumi/js/function_registry.rb +0 -333
- 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 || container == :hash
|
147
|
+
# Root, object, or hash 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, :hash, 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.
|
28
|
-
@schema.traits.each
|
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
|
data/lib/kumi/core/errors.rb
CHANGED
@@ -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
|