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
@@ -5,11 +5,11 @@ module Kumi
|
|
5
5
|
module Analyzer
|
6
6
|
module Passes
|
7
7
|
# Detects which operations should be broadcast over arrays
|
8
|
-
# DEPENDENCIES: :
|
8
|
+
# DEPENDENCIES: :input_metadata, :declarations
|
9
9
|
# PRODUCES: :broadcasts
|
10
10
|
class BroadcastDetector < PassBase
|
11
11
|
def run(errors)
|
12
|
-
input_meta = get_state(:
|
12
|
+
input_meta = get_state(:input_metadata) || {}
|
13
13
|
definitions = get_state(:declarations) || {}
|
14
14
|
|
15
15
|
# Find array fields with their element types
|
@@ -40,6 +40,10 @@ module Kumi
|
|
40
40
|
result = analyze_value_vectorization(name, decl.expression, array_fields, nested_paths, vectorized_values, errors,
|
41
41
|
definitions)
|
42
42
|
|
43
|
+
if ENV["DEBUG_BROADCAST_CLEAN"]
|
44
|
+
puts "#{name}: #{result[:type]} #{format_broadcast_info(result)}"
|
45
|
+
end
|
46
|
+
|
43
47
|
case result[:type]
|
44
48
|
when :vectorized
|
45
49
|
compiler_metadata[:vectorized_operations][name] = result[:info]
|
@@ -70,6 +74,43 @@ module Kumi
|
|
70
74
|
|
71
75
|
private
|
72
76
|
|
77
|
+
def infer_argument_scope(arg, array_fields, nested_paths)
|
78
|
+
case arg
|
79
|
+
when Kumi::Syntax::InputElementReference
|
80
|
+
if nested_paths.key?(arg.path)
|
81
|
+
# Extract scope from path - each array dimension in the path
|
82
|
+
arg.path.select.with_index { |_seg, i| nested_paths[arg.path[0..i]] }
|
83
|
+
else
|
84
|
+
arg.path.select { |seg| array_fields.key?(seg) }
|
85
|
+
end
|
86
|
+
when Kumi::Syntax::CallExpression
|
87
|
+
# For nested calls, find the deepest input reference
|
88
|
+
deepest_scope = []
|
89
|
+
arg.args.each do |nested_arg|
|
90
|
+
scope = infer_argument_scope(nested_arg, array_fields, nested_paths)
|
91
|
+
deepest_scope = scope if scope.length > deepest_scope.length
|
92
|
+
end
|
93
|
+
deepest_scope
|
94
|
+
else
|
95
|
+
[]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def format_broadcast_info(result)
|
100
|
+
case result[:type]
|
101
|
+
when :vectorized
|
102
|
+
info = result[:info]
|
103
|
+
"→ #{info[:source]} (path: #{info[:path]&.join('.')})"
|
104
|
+
when :reduction
|
105
|
+
info = result[:info]
|
106
|
+
"→ fn:#{info[:function]} (arg: #{info[:argument]&.class&.name&.split('::')&.last})"
|
107
|
+
when :scalar
|
108
|
+
"→ scalar"
|
109
|
+
else
|
110
|
+
"→ #{result[:info]}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
73
114
|
def compute_compilation_metadata(name, _decl, compiler_metadata, _vectorized_values, _array_fields)
|
74
115
|
metadata = {
|
75
116
|
operation_mode: :broadcast, # Default mode
|
@@ -169,7 +210,7 @@ module Kumi
|
|
169
210
|
current_access_mode = parent_access_mode
|
170
211
|
if current_meta[:type] == :array
|
171
212
|
array_depth += 1
|
172
|
-
current_access_mode = current_meta[:access_mode] || :
|
213
|
+
current_access_mode = current_meta[:access_mode] || :field # Default to :field if not specified
|
173
214
|
end
|
174
215
|
|
175
216
|
# If this field has children, recurse into them
|
@@ -208,7 +249,6 @@ module Kumi
|
|
208
249
|
# Check if this path exists in nested_paths metadata (supports nested arrays)
|
209
250
|
if nested_paths.key?(expr.path)
|
210
251
|
{ type: :vectorized, info: { source: :nested_array_access, path: expr.path, nested_metadata: nested_paths[expr.path] } }
|
211
|
-
# Fallback to old array_fields detection for backward compatibility
|
212
252
|
elsif array_fields.key?(expr.path.first)
|
213
253
|
{ type: :vectorized, info: { source: :array_field_access, path: expr.path } }
|
214
254
|
else
|
@@ -236,76 +276,106 @@ module Kumi
|
|
236
276
|
end
|
237
277
|
|
238
278
|
def analyze_call_vectorization(_name, expr, array_fields, nested_paths, vectorized_values, errors, definitions = nil)
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
arg_info = analyze_argument_vectorization(expr.args.first, array_fields, nested_paths, vectorized_values, definitions)
|
243
|
-
if arg_info[:vectorized]
|
244
|
-
# Pre-compute which argument indices need flattening
|
245
|
-
flatten_indices = []
|
246
|
-
expr.args.each_with_index do |arg, index|
|
247
|
-
arg_vectorization = analyze_argument_vectorization(arg, array_fields, nested_paths, vectorized_values, definitions)
|
248
|
-
flatten_indices << index if arg_vectorization[:vectorized]
|
249
|
-
end
|
250
|
-
|
251
|
-
{ type: :reduction, info: {
|
252
|
-
function: expr.fn_name,
|
253
|
-
source: arg_info[:source],
|
254
|
-
argument: expr.args.first,
|
255
|
-
flatten_argument_indices: flatten_indices
|
256
|
-
} }
|
257
|
-
else
|
258
|
-
# Not a vectorized reduction - just a regular function call
|
259
|
-
{ type: :scalar }
|
260
|
-
end
|
279
|
+
entry = Kumi::Registry.entry(expr.fn_name)
|
280
|
+
is_reducer = entry&.reducer
|
281
|
+
is_structure = entry&.structure_function
|
261
282
|
|
262
|
-
|
283
|
+
# 1) Analyze all args once
|
284
|
+
arg_infos = expr.args.map do |arg|
|
285
|
+
analyze_argument_vectorization(arg, array_fields, nested_paths, vectorized_values, definitions)
|
286
|
+
end
|
287
|
+
vec_idx = arg_infos.each_index.select { |i| arg_infos[i][:vectorized] }
|
288
|
+
vec_any = !vec_idx.empty?
|
263
289
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
arg.is_a?(Kumi::Syntax::DeclarationReference) && vectorized_values[arg.name]&.[](:vectorized)
|
269
|
-
end
|
270
|
-
if vectorized_trait
|
271
|
-
return { type: :vectorized, info: { source: :cascade_condition_with_vectorized_trait, trait: vectorized_trait.name } }
|
272
|
-
end
|
290
|
+
# 2) Special form: cascade_and (vectorized if any trait arg is vectorized)
|
291
|
+
if expr.fn_name == :cascade_and
|
292
|
+
vectorized_trait = expr.args.find do |arg|
|
293
|
+
arg.is_a?(Kumi::Syntax::DeclarationReference) && vectorized_values[arg.name]&.[](:vectorized)
|
273
294
|
end
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
analyze_argument_vectorization(arg, array_fields, nested_paths, vectorized_values, definitions)
|
295
|
+
if vectorized_trait
|
296
|
+
return { type: :vectorized,
|
297
|
+
info: { source: :cascade_condition_with_vectorized_trait, trait: vectorized_trait&.name } }
|
278
298
|
end
|
279
299
|
|
280
|
-
|
281
|
-
|
282
|
-
vectorized_sources = arg_infos.select { |info| info[:vectorized] }.filter_map { |info| info[:array_source] }.uniq
|
300
|
+
return { type: :scalar }
|
301
|
+
end
|
283
302
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, vectorized_sources)
|
303
|
+
# 3) Reducers: only reduce when the input is actually vectorized
|
304
|
+
if is_reducer
|
305
|
+
return { type: :scalar } unless vec_any
|
288
306
|
|
289
|
-
|
290
|
-
|
291
|
-
|
307
|
+
# which args were vectorized?
|
308
|
+
flatten_indices = vec_idx.dup
|
309
|
+
vectorized_arg_index = vec_idx.first
|
310
|
+
argument_ast = expr.args[vectorized_arg_index]
|
311
|
+
|
312
|
+
src_info = arg_infos[vectorized_arg_index]
|
313
|
+
|
314
|
+
return {
|
315
|
+
type: :reduction,
|
316
|
+
info: {
|
317
|
+
function: expr.fn_name,
|
318
|
+
source: src_info[:source],
|
319
|
+
argument: argument_ast, # << keep AST of the vectorized argument
|
320
|
+
flatten_argument_indices: flatten_indices
|
321
|
+
}
|
322
|
+
}
|
323
|
+
end
|
292
324
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
325
|
+
# 4) Structure (non-reducer) functions like `size`
|
326
|
+
if is_structure
|
327
|
+
# If any arg is itself a PURE reducer call (e.g., size(sum(x))), the inner collapses first ⇒ outer is scalar
|
328
|
+
# But dual-nature functions (both reducer AND structure) should be treated as structure functions when nested
|
329
|
+
return { type: :scalar } if expr.args.any? do |a|
|
330
|
+
if a.is_a?(Kumi::Syntax::CallExpression)
|
331
|
+
arg_entry = Kumi::Registry.entry(a.fn_name)
|
332
|
+
arg_entry&.reducer && !arg_entry&.structure_function # Pure reducer only
|
297
333
|
else
|
298
|
-
|
299
|
-
{ type: :vectorized, info: {
|
300
|
-
operation: expr.fn_name,
|
301
|
-
vectorized_args: arg_infos.map.with_index { |info, i| [i, info[:vectorized]] }.to_h
|
302
|
-
} }
|
334
|
+
false
|
303
335
|
end
|
304
|
-
else
|
305
|
-
# No vectorized arguments - regular scalar function
|
306
|
-
{ type: :scalar }
|
307
336
|
end
|
337
|
+
|
338
|
+
# Structure fn over a vectorized element path ⇒ per-parent vectorization
|
339
|
+
return { type: :scalar } unless vec_any
|
340
|
+
|
341
|
+
src_info = arg_infos[vec_idx.first]
|
342
|
+
parent_scope = src_info[:parent_scope] || src_info[:source] # fallback if analyzer encodes parent separately
|
343
|
+
return {
|
344
|
+
type: :vectorized,
|
345
|
+
info: {
|
346
|
+
operation: expr.fn_name,
|
347
|
+
source: src_info[:source],
|
348
|
+
parent_scope: parent_scope,
|
349
|
+
vectorized_args: vec_idx.to_h { |i| [i, true] }
|
350
|
+
}
|
351
|
+
}
|
352
|
+
|
353
|
+
# Structure fn over a scalar/materialized container ⇒ scalar
|
354
|
+
|
308
355
|
end
|
356
|
+
|
357
|
+
# 5) Generic vectorized map (non-structure, non-reducer)
|
358
|
+
if vec_any
|
359
|
+
# Dimension / source compatibility check
|
360
|
+
sources = vec_idx.map { |i| arg_infos[i][:array_source] }.compact.uniq
|
361
|
+
if sources.size > 1
|
362
|
+
enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, sources)
|
363
|
+
report_error(errors, enhanced_message, location: expr.loc, type: :semantic)
|
364
|
+
return { type: :scalar } # fail safe to prevent cascading errors
|
365
|
+
end
|
366
|
+
|
367
|
+
return {
|
368
|
+
type: :vectorized,
|
369
|
+
info: {
|
370
|
+
operation: expr.fn_name,
|
371
|
+
source: arg_infos[vec_idx.first][:source],
|
372
|
+
vectorized_args: vec_idx.to_h { |i| [i, true] }
|
373
|
+
}
|
374
|
+
}
|
375
|
+
end
|
376
|
+
|
377
|
+
# 6) Pure scalar
|
378
|
+
{ type: :scalar }
|
309
379
|
end
|
310
380
|
|
311
381
|
def structure_function?(fn_name)
|
@@ -337,9 +407,32 @@ module Kumi
|
|
337
407
|
end
|
338
408
|
|
339
409
|
when Kumi::Syntax::CallExpression
|
340
|
-
# Recursively check
|
410
|
+
# Recursively check nested call
|
341
411
|
result = analyze_value_vectorization(nil, arg, array_fields, nested_paths, vectorized_values, [], definitions)
|
342
|
-
|
412
|
+
# Handle different result types appropriately
|
413
|
+
case result[:type]
|
414
|
+
when :reduction
|
415
|
+
# Reductions can produce vectors if they preserve some dimensions
|
416
|
+
# This aligns with lower_to_ir logic for grouped reductions
|
417
|
+
info = result[:info]
|
418
|
+
if info && info[:argument]
|
419
|
+
# Check if the reduction argument has array scope that would be preserved
|
420
|
+
arg_scope = infer_argument_scope(info[:argument], array_fields, nested_paths)
|
421
|
+
if arg_scope.length > 1
|
422
|
+
# Multi-dimensional reduction - likely preserves outer dimension (per-player)
|
423
|
+
{ vectorized: true, source: :grouped_reduction, array_source: arg_scope.first }
|
424
|
+
else
|
425
|
+
# Single dimension or scalar reduction
|
426
|
+
{ vectorized: false, source: :scalar_from_reduction }
|
427
|
+
end
|
428
|
+
else
|
429
|
+
{ vectorized: false, source: :scalar_from_reduction }
|
430
|
+
end
|
431
|
+
when :vectorized
|
432
|
+
{ vectorized: true, source: :expression }
|
433
|
+
else
|
434
|
+
{ vectorized: false, source: :scalar }
|
435
|
+
end
|
343
436
|
|
344
437
|
else
|
345
438
|
{ vectorized: false }
|
@@ -26,7 +26,7 @@ module Kumi
|
|
26
26
|
|
27
27
|
def run(errors)
|
28
28
|
definitions = get_state(:declarations)
|
29
|
-
input_meta = get_state(:
|
29
|
+
input_meta = get_state(:input_metadata)
|
30
30
|
|
31
31
|
dependency_graph = Hash.new { |h, k| h[k] = [] }
|
32
32
|
reverse_dependencies = Hash.new { |h, k| h[k] = [] }
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
class InputAccessPlannerPass < PassBase
|
8
|
+
def run(errors)
|
9
|
+
input_metadata = get_state(:input_metadata)
|
10
|
+
|
11
|
+
options = {
|
12
|
+
on_missing: :error,
|
13
|
+
key_policy: :indifferent
|
14
|
+
}
|
15
|
+
|
16
|
+
# TODO : Allow by input definition on policies or at least general policy definition
|
17
|
+
plans = Kumi::Core::Compiler::AccessPlanner.plan(input_metadata, options)
|
18
|
+
|
19
|
+
# Quick validation
|
20
|
+
validate_plans!(plans, errors)
|
21
|
+
|
22
|
+
# Create new state with access plans
|
23
|
+
state.with(:access_plans, plans.freeze)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validate_plans!(plans, errors)
|
29
|
+
plans.each do |path, plan_list|
|
30
|
+
add_error(errors, nil, "No access plans generated for path: #{path}") if plan_list.nil? || plan_list.empty?
|
31
|
+
|
32
|
+
plan_list&.each do |plan|
|
33
|
+
unless plan[:operations].is_a?(Array)
|
34
|
+
add_error(errors, nil, "Invalid operations for path #{path}: expected Array, got #{plan[:operations].class}")
|
35
|
+
end
|
36
|
+
|
37
|
+
unless plan[:mode].is_a?(Symbol)
|
38
|
+
add_error(errors, nil, "Invalid mode for path #{path}: expected Symbol, got #{plan[:mode].class}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -4,135 +4,157 @@ module Kumi
|
|
4
4
|
module Core
|
5
5
|
module Analyzer
|
6
6
|
module Passes
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
7
|
+
# Emits per-node metadata:
|
8
|
+
# :type, :domain
|
9
|
+
# :container => :scalar | :field | :array
|
10
|
+
# :access_mode => :field | :element # how THIS node is read once reached
|
11
|
+
# :enter_via => :hash | :array # how we HOP from parent to THIS node
|
12
|
+
# :consume_alias => true|false # inline array hop (alias is not a hash key)
|
13
|
+
# :children => { name => node_meta } # optional
|
14
|
+
#
|
15
|
+
# Invariants:
|
16
|
+
# - Any nested array (child depth ≥ 1) must declare its element (i.e., have children).
|
17
|
+
# - Depth-0 inputs always: enter_via :hash, consume_alias false, access_mode :field.
|
11
18
|
class InputCollector < PassBase
|
12
19
|
def run(errors)
|
13
20
|
input_meta = {}
|
14
21
|
|
15
|
-
schema.inputs.each do |
|
16
|
-
|
17
|
-
|
18
|
-
next
|
19
|
-
end
|
20
|
-
|
21
|
-
name = field_decl.name
|
22
|
-
existing = input_meta[name]
|
23
|
-
|
24
|
-
if existing
|
25
|
-
# Check for compatibility and merge
|
26
|
-
merged_meta = merge_field_metadata(existing, field_decl, errors)
|
27
|
-
input_meta[name] = merged_meta if merged_meta
|
28
|
-
else
|
29
|
-
# New field - collect its metadata
|
30
|
-
input_meta[name] = collect_field_metadata(field_decl, errors)
|
31
|
-
end
|
22
|
+
schema.inputs.each do |decl|
|
23
|
+
name = decl.name
|
24
|
+
input_meta[name] = collect_field_metadata(decl, errors, depth: 0, name: name)
|
32
25
|
end
|
33
26
|
|
34
|
-
|
27
|
+
input_meta.each_value(&:deep_freeze!)
|
28
|
+
state.with(:input_metadata, input_meta.freeze)
|
35
29
|
end
|
36
30
|
|
37
31
|
private
|
38
32
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# Process children if present
|
49
|
-
if field_decl.children && !field_decl.children.empty?
|
50
|
-
children_meta = {}
|
51
|
-
field_decl.children.each do |child_decl|
|
52
|
-
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
53
|
-
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
54
|
-
next
|
55
|
-
end
|
56
|
-
children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
|
33
|
+
# ---------- builders ----------
|
34
|
+
|
35
|
+
def collect_field_metadata(decl, errors, depth:, name:)
|
36
|
+
children = nil
|
37
|
+
if decl.children&.any?
|
38
|
+
children = {}
|
39
|
+
decl.children.each do |child|
|
40
|
+
children[child.name] = collect_field_metadata(child, errors, depth: depth + 1, name: child.name)
|
57
41
|
end
|
58
|
-
metadata[:children] = children_meta
|
59
42
|
end
|
60
43
|
|
61
|
-
|
44
|
+
access_mode = decl.access_mode || :field
|
45
|
+
|
46
|
+
meta = Structs::InputMeta.new(
|
47
|
+
type: decl.type,
|
48
|
+
domain: decl.domain,
|
49
|
+
container: kind_from_type(decl.type),
|
50
|
+
access_mode: access_mode,
|
51
|
+
enter_via: :hash,
|
52
|
+
consume_alias: false,
|
53
|
+
children: children
|
54
|
+
)
|
55
|
+
stamp_edges_from!(meta, errors, parent_depth: depth)
|
56
|
+
validate_access_modes!(meta, errors, parent_depth: depth)
|
57
|
+
meta
|
62
58
|
end
|
63
59
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
60
|
+
# ---------- edge stamping + validation ----------
|
61
|
+
#
|
62
|
+
# Sets child[:enter_via], child[:consume_alias], child[:access_mode] defaults,
|
63
|
+
# and validates nested arrays declare their element.
|
64
|
+
#
|
65
|
+
# Rules:
|
66
|
+
# - Common: any ARRAY child at child-depth ≥ 1 must have children (no bare nested array).
|
67
|
+
# - Parent :object → any child:
|
68
|
+
# child.enter_via = :hash; child.consume_alias = false; child.access_mode ||= :field
|
69
|
+
# - Parent :array:
|
70
|
+
# * If exactly one child:
|
71
|
+
# - child.container ∈ {:scalar, :array} → via :array, consume_alias true, access_mode :element
|
72
|
+
# - child.container == :field → via :hash, consume_alias false, access_mode :field
|
73
|
+
# * Else (element object): every child → via :hash, consume_alias false, access_mode :field
|
74
|
+
def stamp_edges_from!(parent_meta, errors, parent_depth:)
|
75
|
+
kids = parent_meta.children || {}
|
76
|
+
return if kids.empty?
|
77
|
+
|
78
|
+
# Validate nested arrays anywhere below root
|
79
|
+
kids.each do |kname, child|
|
80
|
+
next unless child.container == :array
|
81
|
+
|
82
|
+
if !child.children || child.children.empty?
|
83
|
+
report_error(errors, "Nested array at :#{kname} must declare its element", location: nil)
|
84
|
+
end
|
72
85
|
end
|
73
86
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
87
|
+
case parent_meta.container
|
88
|
+
when :object, :hash
|
89
|
+
kids.each_value do |child|
|
90
|
+
child.enter_via = :hash
|
91
|
+
child.consume_alias = false
|
92
|
+
child.access_mode ||= :field # Only set if not explicitly specified
|
93
|
+
end
|
80
94
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
existing_children = existing[:children] || {}
|
94
|
-
new_children = {}
|
95
|
-
|
96
|
-
field_decl.children.each do |child_decl|
|
97
|
-
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
98
|
-
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
99
|
-
next
|
95
|
+
when :array
|
96
|
+
# Array parents MUST explicitly declare their access mode
|
97
|
+
access_mode = parent_meta.access_mode
|
98
|
+
raise "Array must explicitly declare access_mode (:field or :element)" unless access_mode
|
99
|
+
|
100
|
+
case access_mode
|
101
|
+
when :field
|
102
|
+
# Array of objects: all children are fields accessed via hash
|
103
|
+
kids.each_value do |child|
|
104
|
+
child.enter_via = :hash
|
105
|
+
child.consume_alias = false
|
106
|
+
child.access_mode = :field
|
100
107
|
end
|
101
108
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
end
|
109
|
+
when :element
|
110
|
+
_name, only = kids.first
|
111
|
+
only.enter_via = :array
|
112
|
+
only.consume_alias = true
|
113
|
+
only.access_mode = :element
|
109
114
|
|
110
|
-
|
111
|
-
|
112
|
-
|
115
|
+
else
|
116
|
+
raise "Invalid access_mode :#{access_mode} for array (must be :field or :element)"
|
117
|
+
end
|
113
118
|
end
|
114
|
-
|
115
|
-
merged
|
116
119
|
end
|
117
120
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
121
|
+
# Enforce access_mode semantics are only used in valid contexts.
|
122
|
+
def validate_access_modes!(parent_meta, errors, parent_depth:)
|
123
|
+
kids = parent_meta.children || {}
|
124
|
+
return if kids.empty?
|
125
|
+
|
126
|
+
kids.each do |kname, child|
|
127
|
+
mode = child.access_mode
|
128
|
+
next unless mode
|
124
129
|
|
125
|
-
|
126
|
-
|
127
|
-
|
130
|
+
unless %i[field element].include?(mode)
|
131
|
+
report_error(errors, "Invalid access_mode for :#{kname}: #{mode.inspect}", location: nil)
|
132
|
+
next
|
133
|
+
end
|
128
134
|
|
129
|
-
|
130
|
-
|
131
|
-
|
135
|
+
if mode == :element
|
136
|
+
if parent_meta.container == :array
|
137
|
+
single = (kids.size == 1)
|
138
|
+
unless single && %i[scalar array].include?(child.container)
|
139
|
+
report_error(errors, "access_mode :element only valid for single scalar/array element (at :#{kname})", location: nil)
|
140
|
+
end
|
141
|
+
else
|
142
|
+
# Only scalar children under non-array parents are invalid with :element mode
|
143
|
+
# Arrays under hash/object parents can have :element mode (for arrays of scalars)
|
144
|
+
if child.container == :scalar
|
145
|
+
report_error(errors, "access_mode :element only valid under array parent (at :#{kname})", location: nil)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
132
150
|
end
|
133
151
|
|
134
|
-
def
|
135
|
-
|
152
|
+
def kind_from_type(t)
|
153
|
+
return :array if t == :array
|
154
|
+
return :hash if t == :hash
|
155
|
+
return :object if t == :field
|
156
|
+
|
157
|
+
:scalar
|
136
158
|
end
|
137
159
|
end
|
138
160
|
end
|