kumi 0.0.4 → 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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Static Analysis Error Examples
4
+ # This file demonstrates various errors that Kumi catches during schema definition
5
+
6
+ require_relative "../lib/kumi"
7
+
8
+ puts "=== Kumi Static Analysis Examples ===\n"
9
+ puts "All errors caught during schema definition, before any data processing!\n\n"
10
+
11
+ # Example 1: Circular Dependency Detection
12
+ puts "1. Circular Dependency Detection:"
13
+ puts " Code with circular references between values..."
14
+ begin
15
+ module CircularDependency
16
+ extend Kumi::Schema
17
+
18
+ schema do
19
+ input { float :base }
20
+
21
+ value :monthly_rate, yearly_rate / 12
22
+ value :yearly_rate, monthly_rate * 12
23
+ end
24
+ end
25
+ rescue Kumi::Errors::SemanticError => e
26
+ puts " → #{e.message}"
27
+ end
28
+
29
+ puts "\n" + "="*60 + "\n"
30
+
31
+ # Example 2: Impossible Logic Detection (UnsatDetector)
32
+ puts "2. Impossible Logic Detection:"
33
+ puts " Code with contradictory conditions..."
34
+ begin
35
+ module ImpossibleLogic
36
+ extend Kumi::Schema
37
+
38
+ schema do
39
+ input { integer :age }
40
+
41
+ trait :child, input.age < 13
42
+ trait :adult, input.age >= 18
43
+
44
+ # This combination can never be true
45
+ value :status do
46
+ on child & adult, "Impossible!"
47
+ base "Normal"
48
+ end
49
+ end
50
+ end
51
+ rescue Kumi::Errors::SemanticError => e
52
+ puts " → #{e.message}"
53
+ end
54
+
55
+ puts "\n" + "="*60 + "\n"
56
+
57
+ # Example 3: Type System Validation
58
+ puts "3. Type Mismatch Detection:"
59
+ puts " Code trying to add incompatible types..."
60
+ begin
61
+ module TypeMismatch
62
+ extend Kumi::Schema
63
+
64
+ schema do
65
+ input do
66
+ string :name
67
+ integer :age
68
+ end
69
+
70
+ # String + Integer type mismatch
71
+ value :invalid_sum, input.name + input.age
72
+ end
73
+ end
74
+ rescue Kumi::Errors::TypeError => e
75
+ puts " → #{e.message}"
76
+ end
77
+
78
+ puts "\n" + "="*60 + "\n"
79
+
80
+ # Example 4: Domain Constraint Analysis
81
+ puts "4. Domain Constraint Violations:"
82
+ puts " Code using values outside declared domains..."
83
+ begin
84
+ module DomainViolation
85
+ extend Kumi::Schema
86
+
87
+ schema do
88
+ input do
89
+ integer :score, domain: 0..100
90
+ string :grade, domain: %w[A B C D F]
91
+ end
92
+
93
+ # 150 is outside the domain 0..100
94
+ trait :impossible_score, input.score == 150
95
+ end
96
+ end
97
+ rescue Kumi::Errors::SemanticError => e
98
+ puts " → #{e.message}"
99
+ end
100
+
101
+ puts "\n" + "="*60 + "\n"
102
+
103
+ # Example 5: Undefined Reference Detection
104
+ puts "5. Undefined Reference Detection:"
105
+ puts " Code referencing non-existent declarations..."
106
+ begin
107
+ module UndefinedReference
108
+ extend Kumi::Schema
109
+
110
+ schema do
111
+ input { integer :amount }
112
+
113
+ # References a trait that doesn't exist
114
+ value :result, ref(:nonexistent_trait) ? 100 : 0
115
+ end
116
+ end
117
+ rescue Kumi::Errors::SemanticError => e
118
+ puts " → #{e.message}"
119
+ end
120
+
121
+ puts "\n" + "="*60 + "\n"
122
+
123
+ # Example 6: Invalid Function Usage
124
+ puts "6. Invalid Function Detection:"
125
+ puts " Code using non-existent functions..."
126
+ begin
127
+ module InvalidFunction
128
+ extend Kumi::Schema
129
+
130
+ schema do
131
+ input { string :text }
132
+
133
+ # Function doesn't exist in registry
134
+ value :result, fn(:nonexistent_function, input.text)
135
+ end
136
+ end
137
+ rescue Kumi::Errors::TypeError => e
138
+ puts " → #{e.message}"
139
+ end
140
+
141
+ puts "\n" + "="*60 + "\n"
142
+
143
+ # Example 7: Complex Schema with Multiple Issues
144
+ puts "7. Multiple Issues Detected:"
145
+ puts " Complex schema with several problems..."
146
+ begin
147
+ module MultipleIssues
148
+ extend Kumi::Schema
149
+
150
+ schema do
151
+ input { integer :value, domain: 1..10 }
152
+
153
+ # Issue 1: Circular dependency
154
+ value :a, b + 1
155
+ value :b, c + 1
156
+ value :c, a + 1
157
+
158
+ # Issue 2: Impossible domain condition
159
+ trait :impossible, (input.value > 10) & (input.value < 5)
160
+
161
+ # Issue 3: Undefined reference
162
+ value :result, ref(:undefined_declaration)
163
+ end
164
+ end
165
+ rescue Kumi::Errors::SemanticError => e
166
+ puts " → " + e.message.split("\n").join("\n → ")
167
+ end
168
+
169
+ puts "\n" + "="*60 + "\n"
170
+ puts "Summary:"
171
+ puts "• Circular dependencies caught before infinite loops"
172
+ puts "• Impossible logic detected through constraint analysis"
173
+ puts "• Type mismatches found during type inference"
174
+ puts "• Domain violations identified through static analysis"
175
+ puts "• Undefined references caught during name resolution"
176
+ puts "• Invalid functions detected during compilation"
177
+ puts "• Multiple issues reported together with precise locations"
178
+ puts "\nAll validation happens during schema definition - no runtime surprises!"
@@ -41,7 +41,7 @@ def build_wide_schema(width)
41
41
  ref(:sum_all), :>, (width * (width + 1) / 2)
42
42
 
43
43
  value :final_total do
44
- on :large_total, fn(:add, ref(:sum_all), ref(:avg_all))
44
+ on large_total, fn(:add, ref(:sum_all), ref(:avg_all))
45
45
  base ref(:sum_all)
46
46
  end
47
47
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ # Simple immutable state wrapper to prevent accidental mutations between passes
6
+ class AnalysisState
7
+ def initialize(data = {})
8
+ @data = data.dup.freeze
9
+ end
10
+
11
+ # Get a value (same as hash access)
12
+ def [](key)
13
+ @data[key]
14
+ end
15
+
16
+ # Check if key exists (same as hash)
17
+ def key?(key)
18
+ @data.key?(key)
19
+ end
20
+
21
+ # Get all keys (same as hash)
22
+ def keys
23
+ @data.keys
24
+ end
25
+
26
+ # Create new state with additional data (simple and clean)
27
+ def with(key, value)
28
+ AnalysisState.new(@data.merge(key => value))
29
+ end
30
+
31
+ # Convert back to hash for final result
32
+ def to_h
33
+ @data.dup
34
+ end
35
+ end
36
+ end
37
+ end
@@ -22,29 +22,35 @@ module Kumi
22
22
  return @memo[node] if @memo.key?(node)
23
23
  return node.value if node.is_a?(Literal)
24
24
 
25
- if node.is_a?(Binding)
26
- return :unknown if visited.include?(node.name)
25
+ result = case node
26
+ when DeclarationReference then evaluate_binding(node, visited)
27
+ when CallExpression then evaluate_call_expression(node, visited)
28
+ else :unknown
29
+ end
30
+
31
+ @memo[node] = result unless result == :unknown
32
+ result
33
+ end
27
34
 
28
- visited << node.name
35
+ private
29
36
 
30
- definition = @definitions[node.name]
31
- return :unknown unless definition
37
+ def evaluate_binding(node, visited)
38
+ return :unknown if visited.include?(node.name)
32
39
 
33
- @memo[node] = evaluate(definition.expression, visited)
34
- return @memo[node]
35
- end
40
+ visited << node.name
41
+ definition = @definitions[node.name]
42
+ return :unknown unless definition
36
43
 
37
- if node.is_a?(CallExpression)
38
- return :unknown unless OPERATORS.key?(node.fn_name)
44
+ evaluate(definition.expression, visited)
45
+ end
39
46
 
40
- args = node.args.map { |arg| evaluate(arg, visited) }
41
- return :unknown if args.any?(:unknown)
47
+ def evaluate_call_expression(node, visited)
48
+ return :unknown unless OPERATORS.key?(node.fn_name)
42
49
 
43
- @memo[node] = args.reduce(OPERATORS[node.fn_name])
44
- return @memo[node]
45
- end
50
+ args = node.args.map { |arg| evaluate(arg, visited) }
51
+ return :unknown if args.any?(:unknown)
46
52
 
47
- :unknown
53
+ args.reduce(OPERATORS[node.fn_name])
48
54
  end
49
55
  end
50
56
  end
@@ -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
@@ -4,23 +4,24 @@ module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
6
  # RESPONSIBILITY: Perform local structural validation on each declaration
7
- # DEPENDENCIES: None (can run independently)
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) }
14
14
  end
15
+ state
15
16
  end
16
17
 
17
18
  private
18
19
 
19
20
  def validate_node(node, errors)
20
21
  case node
21
- when Declarations::Attribute
22
+ when Kumi::Syntax::ValueDeclaration
22
23
  validate_attribute(node, errors)
23
- when Declarations::Trait
24
+ when Kumi::Syntax::TraitDeclaration
24
25
  validate_trait(node, errors)
25
26
  end
26
27
  end
@@ -28,13 +29,13 @@ module Kumi
28
29
  def validate_attribute(node, errors)
29
30
  return unless node.expression.nil?
30
31
 
31
- add_error(errors, node.loc, "attribute `#{node.name}` requires an expression")
32
+ report_error(errors, "attribute `#{node.name}` requires an expression", location: node.loc)
32
33
  end
33
34
 
34
35
  def validate_trait(node, errors)
35
- return if node.expression.is_a?(Expressions::CallExpression)
36
+ return if node.expression.is_a?(Kumi::Syntax::CallExpression)
36
37
 
37
- add_error(errors, node.loc, "trait `#{node.name}` must wrap a CallExpression")
38
+ report_error(errors, "trait `#{node.name}` must wrap a CallExpression", location: node.loc)
38
39
  end
39
40
  end
40
41
  end