kumi 0.0.9 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +18 -258
- data/README.md +188 -121
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/features/s-expression-printer.md +2 -2
- data/docs/schema_metadata.md +7 -7
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +34 -14
- data/lib/kumi/compiler.rb +4 -283
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
- 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 +118 -99
- 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 +28 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
- 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 +92 -48
- 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 +137 -0
- 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 +100 -6
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -53
- data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +138 -98
- 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 +37 -16
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- 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/errors.rb +2 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +17 -22
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +15 -4
- 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 +17 -16
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +33 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +36 -9
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/schema_instance.rb +0 -111
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
- data/migrate_to_core_iterative.rb +0 -938
@@ -5,7 +5,7 @@ module Kumi
|
|
5
5
|
module Analyzer
|
6
6
|
module Passes
|
7
7
|
# RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
|
8
|
-
# DEPENDENCIES: :declarations from NameIndexer, :
|
8
|
+
# DEPENDENCIES: :declarations from NameIndexer, :input_metadata from InputCollector
|
9
9
|
# PRODUCES: :cascades - Hash of cascade mutual exclusion analysis results
|
10
10
|
# INTERFACE: new(schema, state).run(errors)
|
11
11
|
class UnsatDetector < VisitorPass
|
@@ -16,7 +16,7 @@ module Kumi
|
|
16
16
|
|
17
17
|
def run(errors)
|
18
18
|
definitions = get_state(:declarations)
|
19
|
-
@input_meta = get_state(:
|
19
|
+
@input_meta = get_state(:input_metadata) || {}
|
20
20
|
@definitions = definitions
|
21
21
|
@evaluator = ConstantEvaluator.new(definitions)
|
22
22
|
|
@@ -40,12 +40,31 @@ module Kumi
|
|
40
40
|
atoms = gather_atoms(decl.expression, definitions, Set.new)
|
41
41
|
next if atoms.empty?
|
42
42
|
|
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
|
+
|
43
51
|
# Use enhanced solver that can detect cross-variable mathematical constraints
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
63
|
+
impossible = result
|
64
|
+
|
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
|
49
68
|
|
50
69
|
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
51
70
|
end
|
@@ -63,15 +82,24 @@ module Kumi
|
|
63
82
|
decl.expression.cases[0...-1].each do |when_case|
|
64
83
|
next unless when_case.condition
|
65
84
|
|
66
|
-
next unless when_case.condition.fn_name == :
|
85
|
+
next unless when_case.condition.fn_name == :cascade_and
|
67
86
|
|
68
87
|
when_case.condition.args.each do |arg|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
75
103
|
trait = definitions[trait_name]
|
76
104
|
if trait
|
77
105
|
conditions << trait.expression
|
@@ -183,8 +211,8 @@ module Kumi
|
|
183
211
|
# We should NOT add OR children to the stack as they would be treated as AND
|
184
212
|
# OR expressions need separate analysis in the main run() method
|
185
213
|
next
|
186
|
-
elsif current.is_a?(CallExpression) && current.fn_name == :
|
187
|
-
#
|
214
|
+
elsif current.is_a?(CallExpression) && current.fn_name == :cascade_and
|
215
|
+
# cascade_and takes individual arguments (not wrapped in array)
|
188
216
|
current.args.each { |arg| stack << arg }
|
189
217
|
elsif current.is_a?(ArrayExpression)
|
190
218
|
# For ArrayExpression, add all elements to the stack
|
@@ -212,7 +240,18 @@ module Kumi
|
|
212
240
|
# This is the correct behavior: each 'on' condition should be checked separately
|
213
241
|
# since only ONE will be evaluated at runtime (they're mutually exclusive by design)
|
214
242
|
|
215
|
-
|
243
|
+
# DEBUG: Add detailed logging for hierarchical broadcasting debugging
|
244
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
245
|
+
puts "DEBUG UNSAT CASCADE: Checking cascade '#{decl.name}' at #{decl.loc}"
|
246
|
+
puts " Total cases: #{decl.expression.cases.length}"
|
247
|
+
end
|
248
|
+
|
249
|
+
decl.expression.cases.each_with_index do |when_case, index|
|
250
|
+
# DEBUG: Log each case
|
251
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
252
|
+
puts " Case #{index}: condition=#{when_case.condition.inspect}"
|
253
|
+
end
|
254
|
+
|
216
255
|
# Skip the base case (it's typically a literal true condition)
|
217
256
|
next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
|
218
257
|
|
@@ -220,52 +259,51 @@ module Kumi
|
|
220
259
|
next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
|
221
260
|
|
222
261
|
# Skip single-trait 'on' branches: trait-level unsat detection covers these
|
223
|
-
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :
|
224
|
-
#
|
225
|
-
|
226
|
-
list = when_case.condition.args.first
|
227
|
-
next if list.elements.size == 1
|
228
|
-
elsif when_case.condition.args.size == 1
|
229
|
-
# Multiple args format
|
230
|
-
next
|
231
|
-
end
|
262
|
+
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
|
+
next
|
232
265
|
end
|
266
|
+
|
233
267
|
# Gather atoms from this individual condition only
|
234
268
|
condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
|
235
|
-
# DEBUG
|
236
|
-
# if when_case.condition.is_a?(CallExpression) && [:all?, :any?, :none?].include?(when_case.condition.fn_name)
|
237
|
-
# puts " Args: #{when_case.condition.args.inspect}"
|
238
|
-
# puts " Atoms found: #{condition_atoms.inspect}"
|
239
|
-
# end
|
240
269
|
|
241
|
-
#
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
270
|
+
# DEBUG: Add detailed logging for hierarchical broadcasting debugging
|
271
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
272
|
+
puts " Condition atoms: #{condition_atoms.map(&:inspect)}"
|
273
|
+
end
|
274
|
+
|
246
275
|
# Use enhanced solver for cascade conditions too
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
276
|
+
if definitions && !definitions.empty?
|
277
|
+
result = Kumi::Core::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
|
278
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
279
|
+
puts " Enhanced solver result: #{result}"
|
280
|
+
end
|
281
|
+
else
|
282
|
+
result = Kumi::Core::AtomUnsatSolver.unsat?(condition_atoms)
|
283
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
284
|
+
puts " Basic solver result: #{result}"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
impossible = result
|
252
288
|
next unless !condition_atoms.empty? && impossible
|
253
289
|
|
254
290
|
# For multi-trait on-clauses, report the trait names rather than the value name
|
255
|
-
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :
|
256
|
-
#
|
257
|
-
trait_bindings =
|
258
|
-
when_case.condition.args.first.elements
|
259
|
-
else
|
260
|
-
when_case.condition.args
|
261
|
-
end
|
291
|
+
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and
|
292
|
+
# cascade_and uses individual arguments
|
293
|
+
trait_bindings = when_case.condition.args
|
262
294
|
|
263
295
|
if trait_bindings.all?(DeclarationReference)
|
264
296
|
traits = trait_bindings.map(&:name).join(" AND ")
|
297
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
298
|
+
puts " -> FLAGGING AS IMPOSSIBLE CASCADE CONDITION: #{traits}"
|
299
|
+
end
|
265
300
|
report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
|
266
301
|
next
|
267
302
|
end
|
268
303
|
end
|
304
|
+
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
305
|
+
puts " -> FLAGGING AS IMPOSSIBLE CASCADE: #{decl.name}"
|
306
|
+
end
|
269
307
|
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc)
|
270
308
|
end
|
271
309
|
end
|
@@ -275,6 +313,12 @@ module Kumi
|
|
275
313
|
when InputReference, DeclarationReference
|
276
314
|
val = @evaluator.evaluate(node)
|
277
315
|
val == :unknown ? node.name : val
|
316
|
+
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
|
+
path_identifier = node.path.join(".").to_s
|
321
|
+
path_identifier.to_sym
|
278
322
|
when Literal
|
279
323
|
node.value
|
280
324
|
else
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
# Typed plan structures for HIR (High-level Intermediate Representation)
|
7
|
+
# These plans are produced by analyzer passes and consumed by LowerToIRPass
|
8
|
+
# to generate LIR (Low-level IR) operations.
|
9
|
+
module Plans
|
10
|
+
# Scope plan: defines the dimensional execution context for a declaration
|
11
|
+
Scope = Struct.new(:scope, :lifts, :join_hint, :arg_shapes, keyword_init: true) do
|
12
|
+
def initialize(scope: [], lifts: [], join_hint: nil, arg_shapes: {})
|
13
|
+
super
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def depth
|
18
|
+
scope.size
|
19
|
+
end
|
20
|
+
|
21
|
+
def scalar?
|
22
|
+
scope.empty?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Join plan: defines how to align multiple arguments at a target scope
|
27
|
+
Join = Struct.new(:policy, :target_scope, keyword_init: true) do
|
28
|
+
def initialize(policy: :zip, target_scope: [])
|
29
|
+
super
|
30
|
+
freeze
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Reduce plan: defines how to reduce dimensions in array operations
|
35
|
+
Reduce = Struct.new(:function, :axis, :source_scope, :result_scope, :flatten_args, keyword_init: true) do
|
36
|
+
def initialize(function:, axis: [], source_scope: [], result_scope: [], flatten_args: [])
|
37
|
+
super
|
38
|
+
freeze
|
39
|
+
end
|
40
|
+
|
41
|
+
def total_reduction?
|
42
|
+
axis == :all || result_scope.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def partial_reduction?
|
46
|
+
!total_reduction?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
# One plan for a specific path and mode (path:mode)
|
7
|
+
AccessPlan = Struct.new(:path, :containers, :leaf, :scope, :depth, :mode,
|
8
|
+
:on_missing, :key_policy, :operations, keyword_init: true) do
|
9
|
+
def initialize(path:, containers:, leaf:, scope:, depth:, mode:, on_missing:, key_policy:, operations:)
|
10
|
+
super
|
11
|
+
freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
def accessor_key = "#{path}:#{mode}"
|
15
|
+
def ndims = depth
|
16
|
+
def scalar? = depth.zero?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Structs
|
7
|
+
# Represents metadata for a single input field produced by InputCollector
|
8
|
+
InputMeta = Struct.new(
|
9
|
+
:type,
|
10
|
+
:domain,
|
11
|
+
:container,
|
12
|
+
:access_mode,
|
13
|
+
:enter_via,
|
14
|
+
:consume_alias,
|
15
|
+
:children,
|
16
|
+
keyword_init: true
|
17
|
+
) do
|
18
|
+
def deep_freeze!
|
19
|
+
if children
|
20
|
+
children.each_value(&:deep_freeze!)
|
21
|
+
children.freeze
|
22
|
+
end
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Kumi
|
2
|
+
module Core
|
3
|
+
module Compiler
|
4
|
+
class AccessBuilder
|
5
|
+
def self.build(plans)
|
6
|
+
accessors = {}
|
7
|
+
plans.each_value do |variants|
|
8
|
+
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
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
accessors.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.build_proc_for(mode:, path_key:, missing:, key_policy:, operations:)
|
23
|
+
case mode
|
24
|
+
when :read then Accessors::ReadAccessor.build(operations, path_key, missing, key_policy)
|
25
|
+
when :materialize then Accessors::MaterializeAccessor.build(operations, path_key, missing, key_policy)
|
26
|
+
when :ravel then Accessors::RavelAccessor.build(operations, path_key, missing, key_policy)
|
27
|
+
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
|
+
else
|
30
|
+
raise "Unknown accessor mode: #{mode.inspect}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -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
|