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.
- checksums.yaml +4 -4
- data/CLAUDE.md +160 -8
- data/README.md +278 -200
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/DSL.md +3 -3
- data/{documents → docs}/SYNTAX.md +107 -24
- data/docs/features/README.md +45 -0
- data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
- data/docs/features/analysis-type-inference.md +42 -0
- data/docs/features/analysis-unsat-detection.md +71 -0
- data/docs/features/array-broadcasting.md +170 -0
- data/docs/features/input-declaration-system.md +42 -0
- data/docs/features/performance.md +16 -0
- data/examples/federal_tax_calculator_2024.rb +43 -40
- data/examples/game_of_life.rb +97 -0
- data/examples/simple_rpg_game.rb +1000 -0
- data/examples/static_analysis_errors.rb +178 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
- data/lib/kumi/analyzer/analysis_state.rb +37 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
- data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
- data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
- data/lib/kumi/analyzer.rb +41 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/compiler.rb +194 -16
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +20 -4
- data/lib/kumi/explain.rb +28 -28
- data/lib/kumi/export/node_registry.rb +26 -12
- data/lib/kumi/export/node_serializers.rb +1 -1
- data/lib/kumi/function_registry/collection_functions.rb +117 -9
- data/lib/kumi/function_registry/function_builder.rb +4 -3
- data/lib/kumi/function_registry.rb +8 -2
- data/lib/kumi/input/type_matcher.rb +3 -0
- data/lib/kumi/input/validator.rb +0 -3
- data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/input_builder.rb +40 -9
- data/lib/kumi/parser/input_field_proxy.rb +46 -0
- data/lib/kumi/parser/input_proxy.rb +3 -3
- data/lib/kumi/parser/nested_input.rb +15 -0
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +10 -9
- data/lib/kumi/parser/sugar.rb +171 -18
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/array_expression.rb +15 -0
- data/lib/kumi/syntax/call_expression.rb +11 -0
- data/lib/kumi/syntax/cascade_expression.rb +11 -0
- data/lib/kumi/syntax/case_expression.rb +11 -0
- data/lib/kumi/syntax/declaration_reference.rb +11 -0
- data/lib/kumi/syntax/hash_expression.rb +11 -0
- data/lib/kumi/syntax/input_declaration.rb +12 -0
- data/lib/kumi/syntax/input_element_reference.rb +12 -0
- data/lib/kumi/syntax/input_reference.rb +12 -0
- data/lib/kumi/syntax/literal.rb +11 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +38 -17
- data/CHANGELOG.md +0 -25
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -23
- data/lib/kumi/syntax/expressions.rb +0 -30
- data/lib/kumi/syntax/terminal_expressions.rb +0 -27
- data/lib/kumi/syntax.rb +0 -9
- data/test_impossible_cascade.rb +0 -51
- /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!"
|
@@ -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
|
-
|
26
|
-
|
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
|
-
|
35
|
+
private
|
29
36
|
|
30
|
-
|
31
|
-
|
37
|
+
def evaluate_binding(node, visited)
|
38
|
+
return :unknown if visited.include?(node.name)
|
32
39
|
|
33
|
-
|
34
|
-
|
35
|
-
|
40
|
+
visited << node.name
|
41
|
+
definition = @definitions[node.name]
|
42
|
+
return :unknown unless definition
|
36
43
|
|
37
|
-
|
38
|
-
|
44
|
+
evaluate(definition.expression, visited)
|
45
|
+
end
|
39
46
|
|
40
|
-
|
41
|
-
|
47
|
+
def evaluate_call_expression(node, visited)
|
48
|
+
return :unknown unless OPERATORS.key?(node.fn_name)
|
42
49
|
|
43
|
-
|
44
|
-
|
45
|
-
end
|
50
|
+
args = node.args.map { |arg| evaluate(arg, visited) }
|
51
|
+
return :unknown if args.any?(:unknown)
|
46
52
|
|
47
|
-
|
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:
|
7
|
+
# DEPENDENCIES: :definitions
|
8
8
|
# PRODUCES: None (validation only)
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
|
-
class
|
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
|
22
|
+
when Kumi::Syntax::ValueDeclaration
|
22
23
|
validate_attribute(node, errors)
|
23
|
-
when
|
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
|
-
|
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?(
|
36
|
+
return if node.expression.is_a?(Kumi::Syntax::CallExpression)
|
36
37
|
|
37
|
-
|
38
|
+
report_error(errors, "trait `#{node.name}` must wrap a CallExpression", location: node.loc)
|
38
39
|
end
|
39
40
|
end
|
40
41
|
end
|