kumi 0.0.5 → 0.0.6

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +51 -6
  3. data/README.md +173 -51
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/SYNTAX.md +93 -1
  6. data/docs/features/README.md +45 -0
  7. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  8. data/docs/features/analysis-type-inference.md +42 -0
  9. data/docs/features/analysis-unsat-detection.md +71 -0
  10. data/docs/features/array-broadcasting.md +170 -0
  11. data/docs/features/input-declaration-system.md +42 -0
  12. data/docs/features/performance.md +16 -0
  13. data/examples/federal_tax_calculator_2024.rb +11 -6
  14. data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
  15. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  16. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
  17. data/lib/kumi/analyzer/passes/dependency_resolver.rb +72 -32
  18. data/lib/kumi/analyzer/passes/input_collector.rb +90 -29
  19. data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
  20. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +9 -9
  21. data/lib/kumi/analyzer/passes/toposorter.rb +42 -6
  22. data/lib/kumi/analyzer/passes/type_checker.rb +32 -10
  23. data/lib/kumi/analyzer/passes/type_inferencer.rb +126 -17
  24. data/lib/kumi/analyzer/passes/unsat_detector.rb +133 -53
  25. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
  26. data/lib/kumi/analyzer.rb +11 -12
  27. data/lib/kumi/compiler.rb +194 -16
  28. data/lib/kumi/constraint_relationship_solver.rb +6 -6
  29. data/lib/kumi/domain/validator.rb +0 -4
  30. data/lib/kumi/explain.rb +20 -20
  31. data/lib/kumi/export/node_registry.rb +26 -12
  32. data/lib/kumi/export/node_serializers.rb +1 -1
  33. data/lib/kumi/function_registry/collection_functions.rb +14 -9
  34. data/lib/kumi/function_registry/function_builder.rb +4 -3
  35. data/lib/kumi/function_registry.rb +8 -2
  36. data/lib/kumi/input/type_matcher.rb +3 -0
  37. data/lib/kumi/input/validator.rb +0 -3
  38. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  39. data/lib/kumi/parser/dsl_cascade_builder.rb +3 -3
  40. data/lib/kumi/parser/expression_converter.rb +6 -6
  41. data/lib/kumi/parser/input_builder.rb +40 -9
  42. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  43. data/lib/kumi/parser/input_proxy.rb +3 -3
  44. data/lib/kumi/parser/nested_input.rb +15 -0
  45. data/lib/kumi/parser/schema_builder.rb +10 -9
  46. data/lib/kumi/parser/sugar.rb +61 -9
  47. data/lib/kumi/syntax/array_expression.rb +15 -0
  48. data/lib/kumi/syntax/call_expression.rb +11 -0
  49. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  50. data/lib/kumi/syntax/case_expression.rb +11 -0
  51. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  52. data/lib/kumi/syntax/hash_expression.rb +11 -0
  53. data/lib/kumi/syntax/input_declaration.rb +12 -0
  54. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  55. data/lib/kumi/syntax/input_reference.rb +12 -0
  56. data/lib/kumi/syntax/literal.rb +11 -0
  57. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  58. data/lib/kumi/syntax/value_declaration.rb +11 -0
  59. data/lib/kumi/vectorization_metadata.rb +108 -0
  60. data/lib/kumi/version.rb +1 -1
  61. metadata +31 -14
  62. data/lib/kumi/domain.rb +0 -8
  63. data/lib/kumi/input.rb +0 -8
  64. data/lib/kumi/syntax/declarations.rb +0 -26
  65. data/lib/kumi/syntax/expressions.rb +0 -34
  66. data/lib/kumi/syntax/terminal_expressions.rb +0 -30
  67. data/lib/kumi/syntax.rb +0 -9
  68. /data/{documents → docs}/DSL.md +0 -0
  69. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # Detects which operations should be broadcast over arrays
7
+ # DEPENDENCIES: :input_meta, :definitions
8
+ # PRODUCES: :broadcast_metadata
9
+ class BroadcastDetector < PassBase
10
+ def run(errors)
11
+ input_meta = get_state(:input_meta) || {}
12
+ definitions = get_state(:definitions) || {}
13
+
14
+ # Find array fields with their element types
15
+ array_fields = find_array_fields(input_meta)
16
+
17
+ # Build compiler metadata
18
+ compiler_metadata = {
19
+ array_fields: array_fields,
20
+ vectorized_operations: {},
21
+ reduction_operations: {}
22
+ }
23
+
24
+ # Track which values are vectorized for type inference
25
+ vectorized_values = {}
26
+
27
+ # Analyze traits first, then values (to handle dependencies)
28
+ traits = definitions.select { |name, decl| decl.is_a?(Kumi::Syntax::TraitDeclaration) }
29
+ values = definitions.select { |name, decl| decl.is_a?(Kumi::Syntax::ValueDeclaration) }
30
+
31
+ (traits.to_a + values.to_a).each do |name, decl|
32
+ result = analyze_value_vectorization(name, decl.expression, array_fields, vectorized_values, errors)
33
+
34
+
35
+ case result[:type]
36
+ when :vectorized
37
+ compiler_metadata[:vectorized_operations][name] = result[:info]
38
+ # Store array source information for dimension checking
39
+ array_source = extract_array_source(result[:info], array_fields)
40
+ vectorized_values[name] = { vectorized: true, array_source: array_source }
41
+ when :reduction
42
+ compiler_metadata[:reduction_operations][name] = result[:info]
43
+ # Reduction produces scalar, not vectorized
44
+ vectorized_values[name] = { vectorized: false }
45
+ end
46
+ end
47
+
48
+ state.with(:broadcast_metadata, compiler_metadata.freeze)
49
+ end
50
+
51
+ private
52
+
53
+ def find_array_fields(input_meta)
54
+ result = {}
55
+ input_meta.each do |name, meta|
56
+ if meta[:type] == :array && meta[:children]
57
+ result[name] = {
58
+ element_fields: meta[:children].keys,
59
+ element_types: meta[:children].transform_values { |v| v[:type] || :any }
60
+ }
61
+ end
62
+ end
63
+ result
64
+ end
65
+
66
+ def analyze_value_vectorization(name, expr, array_fields, vectorized_values, errors)
67
+ case expr
68
+ when Kumi::Syntax::InputElementReference
69
+ if array_fields.key?(expr.path.first)
70
+ { type: :vectorized, info: { source: :array_field_access, path: expr.path } }
71
+ else
72
+ { type: :scalar }
73
+ end
74
+
75
+ when Kumi::Syntax::DeclarationReference
76
+ # Check if this references a vectorized value
77
+ vector_info = vectorized_values[expr.name]
78
+ if vector_info && vector_info[:vectorized]
79
+ { type: :vectorized, info: { source: :vectorized_declaration, name: expr.name } }
80
+ else
81
+ { type: :scalar }
82
+ end
83
+
84
+ when Kumi::Syntax::CallExpression
85
+ analyze_call_vectorization(name, expr, array_fields, vectorized_values, errors)
86
+
87
+ when Kumi::Syntax::CascadeExpression
88
+ analyze_cascade_vectorization(name, expr, array_fields, vectorized_values, errors)
89
+
90
+ else
91
+ { type: :scalar }
92
+ end
93
+ end
94
+
95
+ def analyze_call_vectorization(name, expr, array_fields, vectorized_values, errors)
96
+ # Check if this is a reduction function using function registry metadata
97
+ if FunctionRegistry.reducer?(expr.fn_name)
98
+ # Only treat as reduction if the argument is actually vectorized
99
+ arg_info = analyze_argument_vectorization(expr.args.first, array_fields, vectorized_values)
100
+ if arg_info[:vectorized]
101
+ { type: :reduction, info: { function: expr.fn_name, source: arg_info[:source] } }
102
+ else
103
+ # Not a vectorized reduction - just a regular function call
104
+ { type: :scalar }
105
+ end
106
+
107
+ else
108
+ # Special case: all?, any?, none? functions with vectorized trait arguments should be treated as vectorized
109
+ # for cascade condition purposes (they get transformed during compilation)
110
+ if [:all?, :any?, :none?].include?(expr.fn_name) && expr.args.length == 1
111
+ arg = expr.args.first
112
+ if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
113
+ trait_ref = arg.elements.first
114
+ if trait_ref.is_a?(Kumi::Syntax::DeclarationReference) && vectorized_values[trait_ref.name]&.[](:vectorized)
115
+ return { type: :vectorized, info: { source: :cascade_condition_with_vectorized_trait, trait: trait_ref.name } }
116
+ end
117
+ end
118
+ end
119
+
120
+ # ANY function with vectorized arguments becomes vectorized (with broadcasting)
121
+ arg_infos = expr.args.map { |arg| analyze_argument_vectorization(arg, array_fields, vectorized_values) }
122
+
123
+ if arg_infos.any? { |info| info[:vectorized] }
124
+ # Check for dimension mismatches when multiple arguments are vectorized
125
+ vectorized_sources = arg_infos.select { |info| info[:vectorized] }.map { |info| info[:array_source] }.compact.uniq
126
+
127
+ if vectorized_sources.length > 1
128
+ # Multiple different array sources - this is a dimension mismatch
129
+ # Generate enhanced error message with type information
130
+ enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, vectorized_sources)
131
+
132
+ report_error(errors, enhanced_message, location: expr.loc, type: :semantic)
133
+ return { type: :scalar } # Treat as scalar to prevent further errors
134
+ end
135
+
136
+ # This is a vectorized operation - ANY function supports broadcasting
137
+ { type: :vectorized, info: {
138
+ operation: expr.fn_name,
139
+ vectorized_args: arg_infos.map.with_index { |info, i| [i, info[:vectorized]] }.to_h
140
+ }}
141
+ else
142
+ { type: :scalar }
143
+ end
144
+ end
145
+ end
146
+
147
+ def analyze_argument_vectorization(arg, array_fields, vectorized_values)
148
+ case arg
149
+ when Kumi::Syntax::InputElementReference
150
+ if array_fields.key?(arg.path.first)
151
+ { vectorized: true, source: :array_field, array_source: arg.path.first }
152
+ else
153
+ { vectorized: false }
154
+ end
155
+
156
+ when Kumi::Syntax::DeclarationReference
157
+ # Check if this references a vectorized value
158
+ vector_info = vectorized_values[arg.name]
159
+ if vector_info && vector_info[:vectorized]
160
+ array_source = vector_info[:array_source]
161
+ { vectorized: true, source: :vectorized_value, array_source: array_source }
162
+ else
163
+ { vectorized: false }
164
+ end
165
+
166
+ when Kumi::Syntax::CallExpression
167
+ # Recursively check
168
+ result = analyze_value_vectorization(nil, arg, array_fields, vectorized_values, [])
169
+ { vectorized: result[:type] == :vectorized, source: :expression }
170
+
171
+ else
172
+ { vectorized: false }
173
+ end
174
+ end
175
+
176
+ def extract_array_source(info, array_fields)
177
+ case info[:source]
178
+ when :array_field_access
179
+ info[:path]&.first
180
+ when :cascade_condition_with_vectorized_trait
181
+ # For cascades, we'd need to trace back to the original source
182
+ nil # TODO: Could be enhanced to trace through trait dependencies
183
+ else
184
+ nil
185
+ end
186
+ end
187
+
188
+ def analyze_cascade_vectorization(name, expr, array_fields, vectorized_values, errors)
189
+ # A cascade is vectorized if:
190
+ # 1. Any of its result expressions are vectorized, OR
191
+ # 2. Any of its conditions reference vectorized values (traits or arrays)
192
+ vectorized_results = []
193
+ vectorized_conditions = []
194
+
195
+ expr.cases.each do |case_expr|
196
+ # Check if result is vectorized
197
+ result_info = analyze_value_vectorization(nil, case_expr.result, array_fields, vectorized_values, errors)
198
+ vectorized_results << (result_info[:type] == :vectorized)
199
+
200
+ # Check if condition is vectorized
201
+ condition_info = analyze_value_vectorization(nil, case_expr.condition, array_fields, vectorized_values, errors)
202
+ vectorized_conditions << (condition_info[:type] == :vectorized)
203
+
204
+ end
205
+
206
+ if vectorized_results.any? || vectorized_conditions.any?
207
+ { type: :vectorized, info: { source: :cascade_with_vectorized_conditions_or_results } }
208
+ else
209
+ { type: :scalar }
210
+ end
211
+ end
212
+
213
+ def build_dimension_mismatch_error(_expr, arg_infos, array_fields, vectorized_sources)
214
+ # Build detailed error message with type information
215
+ summary = "Cannot broadcast operation across arrays from different sources: #{vectorized_sources.join(', ')}. "
216
+
217
+ problem_desc = "Problem: Multiple operands are arrays from different sources:\n"
218
+
219
+ vectorized_args = arg_infos.select { |info| info[:vectorized] }
220
+ vectorized_args.each_with_index do |arg_info, index|
221
+ array_source = arg_info[:array_source]
222
+ next unless array_source && array_fields[array_source]
223
+
224
+ # Determine the type based on array field metadata
225
+ type_desc = determine_array_type(array_source, array_fields)
226
+ problem_desc += " - Operand #{index + 1} resolves to #{type_desc} from array '#{array_source}'\n"
227
+ end
228
+
229
+ explanation = "Direct operations on arrays from different sources is ambiguous and not supported. " \
230
+ "Vectorized operations can only work on fields from the same array input."
231
+
232
+ "#{summary}#{problem_desc}#{explanation}"
233
+ end
234
+
235
+ def determine_array_type(array_source, array_fields)
236
+ field_info = array_fields[array_source]
237
+ return "array(any)" unless field_info[:element_types]
238
+
239
+ # For nested arrays (like items.name where items is an array), this represents array(element_type)
240
+ element_types = field_info[:element_types].values.uniq
241
+ if element_types.length == 1
242
+ "array(#{element_types.first})"
243
+ else
244
+ "array(mixed)"
245
+ end
246
+ end
247
+
248
+ end
249
+ end
250
+ end
251
+ end
@@ -7,7 +7,7 @@ module Kumi
7
7
  # DEPENDENCIES: :definitions
8
8
  # PRODUCES: None (validation only)
9
9
  # INTERFACE: new(schema, state).run(errors)
10
- class DefinitionValidator < VisitorPass
10
+ class DeclarationValidator < VisitorPass
11
11
  def run(errors)
12
12
  each_decl do |decl|
13
13
  visit(decl) { |node| validate_node(node, errors) }
@@ -19,9 +19,9 @@ module Kumi
19
19
 
20
20
  def validate_node(node, errors)
21
21
  case node
22
- when Declarations::Attribute
22
+ when Kumi::Syntax::ValueDeclaration
23
23
  validate_attribute(node, errors)
24
- when Declarations::Trait
24
+ when Kumi::Syntax::TraitDeclaration
25
25
  validate_trait(node, errors)
26
26
  end
27
27
  end
@@ -33,7 +33,7 @@ module Kumi
33
33
  end
34
34
 
35
35
  def validate_trait(node, errors)
36
- return if node.expression.is_a?(Expressions::CallExpression)
36
+ return if node.expression.is_a?(Kumi::Syntax::CallExpression)
37
37
 
38
38
  report_error(errors, "trait `#{node.name}` must wrap a CallExpression", location: node.loc)
39
39
  end
@@ -3,13 +3,24 @@
3
3
  module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
- # RESPONSIBILITY: Build dependency graph and leaf map, validate references
7
- # DEPENDENCIES: :definitions, :input_meta
8
- # PRODUCES: :dependency_graph - Hash of name → [DependencyEdge], :leaf_map - Hash of name → Set[nodes]
6
+ # RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
7
+ # DEPENDENCIES: :definitions from NameIndexer, :input_meta from InputCollector
8
+ # PRODUCES: :dependency_graph, :transitive_dependents, :leaf_map - Dependency analysis results
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class DependencyResolver < PassBase
11
- # A Struct to hold rich dependency information
12
- DependencyEdge = Struct.new(:to, :type, :via, keyword_init: true)
11
+ # Enhanced edge with conditional flag and cascade metadata
12
+ class DependencyEdge
13
+ attr_reader :to, :type, :via, :conditional, :cascade_owner
14
+
15
+ def initialize(to:, type:, via:, conditional: false, cascade_owner: nil)
16
+ @to = to
17
+ @type = type
18
+ @via = via
19
+ @conditional = conditional
20
+ @cascade_owner = cascade_owner
21
+ end
22
+ end
23
+
13
24
  include Syntax
14
25
 
15
26
  def run(errors)
@@ -22,7 +33,7 @@ module Kumi
22
33
 
23
34
  each_decl do |decl|
24
35
  # Traverse the expression for each declaration, passing context down
25
- visit_with_context(decl.expression) do |node, context|
36
+ visit_with_context(decl.expression, { decl_name: decl.name }) do |node, context|
26
37
  process_node(node, decl, dependency_graph, reverse_dependencies, leaf_map, definitions, input_meta, errors, context)
27
38
  end
28
39
  end
@@ -39,29 +50,74 @@ module Kumi
39
50
 
40
51
  def process_node(node, decl, graph, reverse_deps, leaves, definitions, input_meta, errors, context)
41
52
  case node
42
- when Binding
53
+ when DeclarationReference
43
54
  report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
44
- add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via])
45
- when FieldRef
46
- report_error(errors, "undeclared input `#{node.name}`", location: node.loc) unless input_meta.key?(node.name)
55
+
56
+ # Determine if this is a conditional dependency
57
+ conditional = context[:in_cascade_branch] || context[:in_cascade_base] || false
58
+ cascade_owner = conditional ? (context[:cascade_owner] || context[:decl_name]) : nil
59
+
60
+ add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via],
61
+ conditional: conditional,
62
+ cascade_owner: cascade_owner)
63
+ when InputReference
47
64
  add_dependency_edge(graph, reverse_deps, decl.name, node.name, :key, context[:via])
48
- leaves[decl.name] << node # put it back
65
+ leaves[decl.name] << node
66
+ when InputElementReference
67
+ # adds the root input declaration as a dependency
68
+ root_input_declr_name = node.path.first
69
+ add_dependency_edge(graph, reverse_deps, decl.name, root_input_declr_name, :key, context[:via])
49
70
  when Literal
50
71
  leaves[decl.name] << node
51
72
  end
52
73
  end
53
74
 
54
- def add_dependency_edge(graph, reverse_deps, from, to, type, via)
55
- edge = DependencyEdge.new(to: to, type: type, via: via)
75
+ def add_dependency_edge(graph, reverse_deps, from, to, type, via, conditional: false, cascade_owner: nil)
76
+ edge = DependencyEdge.new(
77
+ to: to,
78
+ type: type,
79
+ via: via,
80
+ conditional: conditional,
81
+ cascade_owner: cascade_owner
82
+ )
56
83
  graph[from] << edge
57
84
  reverse_deps[to] << from
58
85
  end
59
86
 
60
- # Compute transitive closure: for each key, find ALL declarations that depend on it
87
+ # Custom visitor that understands cascade structure
88
+ def visit_with_context(node, context = {}, &block)
89
+ return unless node
90
+
91
+ yield(node, context)
92
+
93
+ case node
94
+ when CascadeExpression
95
+ # Visit condition nodes and result expressions (non-base cases)
96
+ node.cases[0...-1].each do |when_case|
97
+ if when_case.condition
98
+ # Visit condition normally
99
+ visit_with_context(when_case.condition, context, &block)
100
+ end
101
+ # Visit result expressions as conditional dependencies
102
+ conditional_context = context.merge(in_cascade_branch: true, cascade_owner: context[:decl_name])
103
+ visit_with_context(when_case.result, conditional_context, &block)
104
+ end
105
+
106
+ # Visit base case with conditional flag
107
+ if node.cases.last
108
+ base_context = context.merge(in_cascade_base: true)
109
+ visit_with_context(node.cases.last.result, base_context, &block)
110
+ end
111
+ when CallExpression
112
+ new_context = context.merge(via: node.fn_name)
113
+ node.children.each { |child| visit_with_context(child, new_context, &block) }
114
+ else
115
+ node.children.each { |child| visit_with_context(child, context, &block) } if node.respond_to?(:children)
116
+ end
117
+ end
118
+
61
119
  def compute_transitive_closure(reverse_dependencies)
62
120
  transitive = {}
63
-
64
- # Collect all keys first to avoid iteration issues
65
121
  all_keys = reverse_dependencies.keys
66
122
 
67
123
  all_keys.each do |key|
@@ -75,7 +131,6 @@ module Kumi
75
131
 
76
132
  visited.add(current)
77
133
 
78
- # Get direct dependents
79
134
  direct_dependents = reverse_dependencies[current] || []
80
135
  direct_dependents.each do |dependent|
81
136
  next if visited.include?(dependent)
@@ -90,21 +145,6 @@ module Kumi
90
145
 
91
146
  transitive
92
147
  end
93
-
94
- # Custom visitor that passes context (like function name) down the tree
95
- def visit_with_context(node, context = {}, &block)
96
- return unless node
97
-
98
- yield(node, context)
99
-
100
- new_context = if node.is_a?(Expressions::CallExpression)
101
- { via: node.fn_name }
102
- else
103
- context
104
- end
105
-
106
- node.children.each { |child| visit_with_context(child, new_context, &block) }
107
- end
108
148
  end
109
149
  end
110
150
  end
@@ -8,14 +8,12 @@ module Kumi
8
8
  # PRODUCES: :input_meta - Hash mapping field names to {type:, domain:} metadata
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class InputCollector < PassBase
11
- include Syntax::TerminalExpressions
12
-
13
11
  def run(errors)
14
12
  input_meta = {}
15
13
 
16
14
  schema.inputs.each do |field_decl|
17
- unless field_decl.is_a?(FieldDecl)
18
- report_error(errors, "Expected FieldDecl node, got #{field_decl.class}", location: field_decl.loc)
15
+ unless field_decl.is_a?(Kumi::Syntax::InputDeclaration)
16
+ report_error(errors, "Expected InputDeclaration node, got #{field_decl.class}", location: field_decl.loc)
19
17
  next
20
18
  end
21
19
 
@@ -23,40 +21,103 @@ module Kumi
23
21
  existing = input_meta[name]
24
22
 
25
23
  if existing
26
- # Check for compatibility
27
- if existing[:type] != field_decl.type && field_decl.type && existing[:type]
28
- report_error(errors,
29
- "Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
30
- location: field_decl.loc)
31
- end
24
+ # Check for compatibility and merge
25
+ merged_meta = merge_field_metadata(existing, field_decl, errors)
26
+ input_meta[name] = merged_meta if merged_meta
27
+ else
28
+ # New field - collect its metadata
29
+ input_meta[name] = collect_field_metadata(field_decl, errors)
30
+ end
31
+ end
32
+
33
+ state.with(:input_meta, freeze_nested_hash(input_meta))
34
+ end
32
35
 
33
- if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
34
- report_error(errors,
35
- "Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
36
- location: field_decl.loc)
36
+ private
37
+
38
+ def collect_field_metadata(field_decl, errors)
39
+ validate_domain_type(field_decl, errors) if field_decl.domain
40
+
41
+ metadata = {
42
+ type: field_decl.type,
43
+ domain: field_decl.domain
44
+ }
45
+
46
+ # Process children if present
47
+ if field_decl.children && !field_decl.children.empty?
48
+ children_meta = {}
49
+ field_decl.children.each do |child_decl|
50
+ unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
51
+ report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
52
+ next
37
53
  end
54
+ children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
55
+ end
56
+ metadata[:children] = children_meta
57
+ end
38
58
 
39
- # Validate domain type if provided
40
- validate_domain_type(field_decl, errors) if field_decl.domain
59
+ metadata
60
+ end
41
61
 
42
- # Merge metadata (later declarations override nil values)
43
- input_meta[name] = {
44
- type: field_decl.type || existing[:type],
45
- domain: field_decl.domain || existing[:domain]
46
- }
47
- else
48
- validate_domain_type(field_decl, errors) if field_decl.domain
49
- input_meta[name] = {
50
- type: field_decl.type,
51
- domain: field_decl.domain
52
- }
62
+ def merge_field_metadata(existing, field_decl, errors)
63
+ name = field_decl.name
64
+
65
+ # Check for type compatibility
66
+ if existing[:type] != field_decl.type && field_decl.type && existing[:type]
67
+ report_error(errors,
68
+ "Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
69
+ location: field_decl.loc)
70
+ end
71
+
72
+ # Check for domain compatibility
73
+ if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
74
+ report_error(errors,
75
+ "Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
76
+ location: field_decl.loc)
77
+ end
78
+
79
+ # Validate domain type if provided
80
+ validate_domain_type(field_decl, errors) if field_decl.domain
81
+
82
+ # Merge metadata (later declarations override nil values)
83
+ merged = {
84
+ type: field_decl.type || existing[:type],
85
+ domain: field_decl.domain || existing[:domain]
86
+ }
87
+
88
+ # Merge children if present
89
+ if field_decl.children && !field_decl.children.empty?
90
+ existing_children = existing[:children] || {}
91
+ new_children = {}
92
+
93
+ field_decl.children.each do |child_decl|
94
+ unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
95
+ report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
96
+ next
97
+ end
98
+
99
+ child_name = child_decl.name
100
+ new_children[child_name] = if existing_children[child_name]
101
+ merge_field_metadata(existing_children[child_name], child_decl, errors)
102
+ else
103
+ collect_field_metadata(child_decl, errors)
104
+ end
53
105
  end
106
+
107
+ merged[:children] = new_children
108
+ elsif existing[:children]
109
+ merged[:children] = existing[:children]
54
110
  end
55
111
 
56
- state.with(:input_meta, input_meta.freeze)
112
+ merged
57
113
  end
58
114
 
59
- private
115
+ def freeze_nested_hash(hash)
116
+ hash.each_value do |value|
117
+ freeze_nested_hash(value) if value.is_a?(Hash)
118
+ end
119
+ hash.freeze
120
+ end
60
121
 
61
122
  def validate_domain_type(field_decl, errors)
62
123
  domain = field_decl.domain
@@ -27,7 +27,7 @@ module Kumi
27
27
  attr_reader :schema, :state
28
28
 
29
29
  # Iterate over all declarations (attributes and traits) in the schema
30
- # @yield [Syntax::Declarations::Attribute|Syntax::Declarations::Trait] Each declaration
30
+ # @yield [Syntax::Attribute|Syntax::Trait] Each declaration
31
31
  def each_decl(&block)
32
32
  schema.attributes.each(&block)
33
33
  schema.traits.each(&block)
@@ -10,7 +10,7 @@ module Kumi
10
10
  #
11
11
  # This pass enforces semantic constraints that must hold regardless of which parser
12
12
  # was used to construct the AST. It validates:
13
- # 1. Cascade conditions are only trait references (Binding nodes)
13
+ # 1. Cascade conditions are only trait references (DeclarationReference nodes)
14
14
  # 2. Trait expressions evaluate to boolean values (CallExpression nodes)
15
15
  # 3. Function names exist in the function registry
16
16
  # 4. Expression types are valid for their context
@@ -26,17 +26,17 @@ module Kumi
26
26
 
27
27
  def validate_semantic_constraints(node, decl, errors)
28
28
  case node
29
- when Declarations::Trait
29
+ when Kumi::Syntax::TraitDeclaration
30
30
  validate_trait_expression(node, errors)
31
- when Expressions::WhenCaseExpression
31
+ when Kumi::Syntax::CaseExpression
32
32
  validate_cascade_condition(node, errors)
33
- when Expressions::CallExpression
33
+ when Kumi::Syntax::CallExpression
34
34
  validate_function_call(node, errors)
35
35
  end
36
36
  end
37
37
 
38
38
  def validate_trait_expression(trait, errors)
39
- return if trait.expression.is_a?(Expressions::CallExpression)
39
+ return if trait.expression.is_a?(Kumi::Syntax::CallExpression)
40
40
 
41
41
  report_error(
42
42
  errors,
@@ -50,20 +50,20 @@ module Kumi
50
50
  condition = when_case.condition
51
51
 
52
52
  case condition
53
- when TerminalExpressions::Binding
53
+ when Kumi::Syntax::DeclarationReference
54
54
  # Valid: trait reference
55
55
  return
56
- when Expressions::CallExpression
56
+ when Kumi::Syntax::CallExpression
57
57
  # Valid if it's a boolean composition of traits (all?, any?, none?)
58
58
  return if boolean_trait_composition?(condition)
59
59
 
60
60
  # For now, allow other CallExpressions - they'll be validated by other passes
61
61
  return
62
- when TerminalExpressions::Literal
62
+ when Kumi::Syntax::Literal
63
63
  # Allow literal conditions (like true/false) - they might be valid
64
64
  return
65
65
  else
66
- # Only reject truly invalid conditions like FieldRef or complex expressions
66
+ # Only reject truly invalid conditions like InputReference or complex expressions
67
67
  report_error(
68
68
  errors,
69
69
  "cascade condition must be trait reference",