kumi 0.0.7 → 0.0.9
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 +1 -1
- data/README.md +21 -5
- data/docs/AST.md +7 -0
- data/docs/features/README.md +7 -0
- data/docs/features/s-expression-printer.md +77 -0
- data/examples/game_of_life.rb +1 -1
- data/examples/static_analysis_errors.rb +7 -7
- data/lib/kumi/analyzer.rb +15 -15
- data/lib/kumi/compiler.rb +6 -6
- data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
- data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
- data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
- data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
- data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
- data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
- data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
- data/lib/kumi/core/atom_unsat_solver.rb +396 -0
- data/lib/kumi/core/compiled_schema.rb +43 -0
- data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
- data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
- data/lib/kumi/core/domain/range_analyzer.rb +85 -0
- data/lib/kumi/core/domain/validator.rb +82 -0
- data/lib/kumi/core/domain/violation_formatter.rb +42 -0
- data/lib/kumi/core/error_reporter.rb +166 -0
- data/lib/kumi/core/error_reporting.rb +97 -0
- data/lib/kumi/core/errors.rb +120 -0
- data/lib/kumi/core/evaluation_wrapper.rb +40 -0
- data/lib/kumi/core/explain.rb +295 -0
- data/lib/kumi/core/export/deserializer.rb +41 -0
- data/lib/kumi/core/export/errors.rb +14 -0
- data/lib/kumi/core/export/node_builders.rb +142 -0
- data/lib/kumi/core/export/node_registry.rb +54 -0
- data/lib/kumi/core/export/node_serializers.rb +158 -0
- data/lib/kumi/core/export/serializer.rb +25 -0
- data/lib/kumi/core/export.rb +35 -0
- data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
- data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
- data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
- data/lib/kumi/core/function_registry/function_builder.rb +95 -0
- data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
- data/lib/kumi/core/function_registry/math_functions.rb +74 -0
- data/lib/kumi/core/function_registry/string_functions.rb +57 -0
- data/lib/kumi/core/function_registry/type_functions.rb +53 -0
- data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
- data/lib/kumi/core/input/type_matcher.rb +97 -0
- data/lib/kumi/core/input/validator.rb +51 -0
- data/lib/kumi/core/input/violation_creator.rb +52 -0
- data/lib/kumi/core/json_schema/generator.rb +65 -0
- data/lib/kumi/core/json_schema/validator.rb +27 -0
- data/lib/kumi/core/json_schema.rb +16 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
- data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
- data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
- data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
- data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
- data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
- data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
- data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
- data/lib/kumi/core/ruby_parser/parser.rb +71 -0
- data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
- data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
- data/lib/kumi/core/ruby_parser.rb +12 -0
- data/lib/kumi/core/schema_instance.rb +111 -0
- data/lib/kumi/core/types/builder.rb +23 -0
- data/lib/kumi/core/types/compatibility.rb +96 -0
- data/lib/kumi/core/types/formatter.rb +26 -0
- data/lib/kumi/core/types/inference.rb +42 -0
- data/lib/kumi/core/types/normalizer.rb +72 -0
- data/lib/kumi/core/types/validator.rb +37 -0
- data/lib/kumi/core/types.rb +66 -0
- data/lib/kumi/core/vectorization_metadata.rb +110 -0
- data/lib/kumi/errors.rb +1 -112
- data/lib/kumi/registry.rb +37 -0
- data/lib/kumi/schema.rb +5 -5
- data/lib/kumi/schema_metadata.rb +3 -3
- data/lib/kumi/support/s_expression_printer.rb +161 -0
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +5 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/node.rb +34 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/migrate_to_core_iterative.rb +938 -0
- data/scripts/generate_function_docs.rb +9 -9
- metadata +77 -72
- data/lib/kumi/analyzer/analysis_state.rb +0 -37
- data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
- data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
- data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
- data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
- data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
- data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
- data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
- data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
- data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
- data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
- data/lib/kumi/atom_unsat_solver.rb +0 -394
- data/lib/kumi/compiled_schema.rb +0 -41
- data/lib/kumi/constraint_relationship_solver.rb +0 -638
- data/lib/kumi/domain/enum_analyzer.rb +0 -53
- data/lib/kumi/domain/range_analyzer.rb +0 -83
- data/lib/kumi/domain/validator.rb +0 -80
- data/lib/kumi/domain/violation_formatter.rb +0 -40
- data/lib/kumi/error_reporter.rb +0 -164
- data/lib/kumi/error_reporting.rb +0 -95
- data/lib/kumi/evaluation_wrapper.rb +0 -38
- data/lib/kumi/explain.rb +0 -293
- data/lib/kumi/export/deserializer.rb +0 -39
- data/lib/kumi/export/errors.rb +0 -12
- data/lib/kumi/export/node_builders.rb +0 -140
- data/lib/kumi/export/node_registry.rb +0 -52
- data/lib/kumi/export/node_serializers.rb +0 -156
- data/lib/kumi/export/serializer.rb +0 -23
- data/lib/kumi/export.rb +0 -33
- data/lib/kumi/function_registry/collection_functions.rb +0 -200
- data/lib/kumi/function_registry/comparison_functions.rb +0 -31
- data/lib/kumi/function_registry/conditional_functions.rb +0 -36
- data/lib/kumi/function_registry/function_builder.rb +0 -93
- data/lib/kumi/function_registry/logical_functions.rb +0 -42
- data/lib/kumi/function_registry/math_functions.rb +0 -72
- data/lib/kumi/function_registry/string_functions.rb +0 -54
- data/lib/kumi/function_registry/type_functions.rb +0 -51
- data/lib/kumi/input/type_matcher.rb +0 -95
- data/lib/kumi/input/validator.rb +0 -49
- data/lib/kumi/input/violation_creator.rb +0 -50
- data/lib/kumi/json_schema/generator.rb +0 -63
- data/lib/kumi/json_schema/validator.rb +0 -25
- data/lib/kumi/json_schema.rb +0 -14
- data/lib/kumi/ruby_parser/build_context.rb +0 -25
- data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
- data/lib/kumi/ruby_parser/dsl.rb +0 -12
- data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
- data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
- data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
- data/lib/kumi/ruby_parser/input_builder.rb +0 -125
- data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
- data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
- data/lib/kumi/ruby_parser/nested_input.rb +0 -15
- data/lib/kumi/ruby_parser/parser.rb +0 -69
- data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
- data/lib/kumi/ruby_parser/sugar.rb +0 -261
- data/lib/kumi/ruby_parser.rb +0 -10
- data/lib/kumi/schema_instance.rb +0 -109
- data/lib/kumi/types/builder.rb +0 -21
- data/lib/kumi/types/compatibility.rb +0 -94
- data/lib/kumi/types/formatter.rb +0 -24
- data/lib/kumi/types/inference.rb +0 -40
- data/lib/kumi/types/normalizer.rb +0 -70
- data/lib/kumi/types/validator.rb +0 -35
- data/lib/kumi/types.rb +0 -64
- data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# Detects which operations should be broadcast over arrays
|
8
|
+
# DEPENDENCIES: :inputs, :declarations
|
9
|
+
# PRODUCES: :broadcasts
|
10
|
+
class BroadcastDetector < PassBase
|
11
|
+
def run(errors)
|
12
|
+
input_meta = get_state(:inputs) || {}
|
13
|
+
definitions = get_state(:declarations) || {}
|
14
|
+
|
15
|
+
# Find array fields with their element types
|
16
|
+
array_fields = find_array_fields(input_meta)
|
17
|
+
|
18
|
+
# Build compiler metadata
|
19
|
+
compiler_metadata = {
|
20
|
+
array_fields: array_fields,
|
21
|
+
vectorized_operations: {},
|
22
|
+
reduction_operations: {}
|
23
|
+
}
|
24
|
+
|
25
|
+
# Track which values are vectorized for type inference
|
26
|
+
vectorized_values = {}
|
27
|
+
|
28
|
+
# Analyze traits first, then values (to handle dependencies)
|
29
|
+
traits = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::TraitDeclaration) }
|
30
|
+
values = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::ValueDeclaration) }
|
31
|
+
|
32
|
+
(traits.to_a + values.to_a).each do |name, decl|
|
33
|
+
result = analyze_value_vectorization(name, decl.expression, array_fields, vectorized_values, errors)
|
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(:broadcasts, 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
|
+
next unless meta[:type] == :array && meta[:children]
|
57
|
+
|
58
|
+
result[name] = {
|
59
|
+
element_fields: meta[:children].keys,
|
60
|
+
element_types: meta[:children].transform_values { |v| v[:type] || :any }
|
61
|
+
}
|
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 Kumi::Registry.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 %i[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] }.filter_map { |info| info[:array_source] }.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
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def analyze_cascade_vectorization(_name, expr, array_fields, vectorized_values, errors)
|
187
|
+
# A cascade is vectorized if:
|
188
|
+
# 1. Any of its result expressions are vectorized, OR
|
189
|
+
# 2. Any of its conditions reference vectorized values (traits or arrays)
|
190
|
+
vectorized_results = []
|
191
|
+
vectorized_conditions = []
|
192
|
+
|
193
|
+
expr.cases.each do |case_expr|
|
194
|
+
# Check if result is vectorized
|
195
|
+
result_info = analyze_value_vectorization(nil, case_expr.result, array_fields, vectorized_values, errors)
|
196
|
+
vectorized_results << (result_info[:type] == :vectorized)
|
197
|
+
|
198
|
+
# Check if condition is vectorized
|
199
|
+
condition_info = analyze_value_vectorization(nil, case_expr.condition, array_fields, vectorized_values, errors)
|
200
|
+
vectorized_conditions << (condition_info[:type] == :vectorized)
|
201
|
+
end
|
202
|
+
|
203
|
+
if vectorized_results.any? || vectorized_conditions.any?
|
204
|
+
{ type: :vectorized, info: { source: :cascade_with_vectorized_conditions_or_results } }
|
205
|
+
else
|
206
|
+
{ type: :scalar }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def build_dimension_mismatch_error(_expr, arg_infos, array_fields, vectorized_sources)
|
211
|
+
# Build detailed error message with type information
|
212
|
+
summary = "Cannot broadcast operation across arrays from different sources: #{vectorized_sources.join(', ')}. "
|
213
|
+
|
214
|
+
problem_desc = "Problem: Multiple operands are arrays from different sources:\n"
|
215
|
+
|
216
|
+
vectorized_args = arg_infos.select { |info| info[:vectorized] }
|
217
|
+
vectorized_args.each_with_index do |arg_info, index|
|
218
|
+
array_source = arg_info[:array_source]
|
219
|
+
next unless array_source && array_fields[array_source]
|
220
|
+
|
221
|
+
# Determine the type based on array field metadata
|
222
|
+
type_desc = determine_array_type(array_source, array_fields)
|
223
|
+
problem_desc += " - Operand #{index + 1} resolves to #{type_desc} from array '#{array_source}'\n"
|
224
|
+
end
|
225
|
+
|
226
|
+
explanation = "Direct operations on arrays from different sources is ambiguous and not supported. " \
|
227
|
+
"Vectorized operations can only work on fields from the same array input."
|
228
|
+
|
229
|
+
"#{summary}#{problem_desc}#{explanation}"
|
230
|
+
end
|
231
|
+
|
232
|
+
def determine_array_type(array_source, array_fields)
|
233
|
+
field_info = array_fields[array_source]
|
234
|
+
return "array(any)" unless field_info[:element_types]
|
235
|
+
|
236
|
+
# For nested arrays (like items.name where items is an array), this represents array(element_type)
|
237
|
+
element_types = field_info[:element_types].values.uniq
|
238
|
+
if element_types.length == 1
|
239
|
+
"array(#{element_types.first})"
|
240
|
+
else
|
241
|
+
"array(mixed)"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Perform local structural validation on each declaration
|
8
|
+
# DEPENDENCIES: :definitions
|
9
|
+
# PRODUCES: None (validation only)
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class DeclarationValidator < VisitorPass
|
12
|
+
def run(errors)
|
13
|
+
each_decl do |decl|
|
14
|
+
visit(decl) { |node| validate_node(node, errors) }
|
15
|
+
end
|
16
|
+
state
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate_node(node, errors)
|
22
|
+
case node
|
23
|
+
when Kumi::Syntax::ValueDeclaration
|
24
|
+
validate_attribute(node, errors)
|
25
|
+
when Kumi::Syntax::TraitDeclaration
|
26
|
+
validate_trait(node, errors)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_attribute(node, errors)
|
31
|
+
return unless node.expression.nil?
|
32
|
+
|
33
|
+
report_error(errors, "attribute `#{node.name}` requires an expression", location: node.loc)
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_trait(node, errors)
|
37
|
+
return if node.expression.is_a?(Kumi::Syntax::CallExpression)
|
38
|
+
|
39
|
+
report_error(errors, "trait `#{node.name}` must wrap a CallExpression", location: node.loc)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
|
8
|
+
# DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
|
9
|
+
# PRODUCES: :dependencies, :dependents, :leaves - Dependency analysis results
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class DependencyResolver < PassBase
|
12
|
+
# Enhanced edge with conditional flag and cascade metadata
|
13
|
+
class DependencyEdge
|
14
|
+
attr_reader :to, :type, :via, :conditional, :cascade_owner
|
15
|
+
|
16
|
+
def initialize(to:, type:, via:, conditional: false, cascade_owner: nil)
|
17
|
+
@to = to
|
18
|
+
@type = type
|
19
|
+
@via = via
|
20
|
+
@conditional = conditional
|
21
|
+
@cascade_owner = cascade_owner
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
include Syntax
|
26
|
+
|
27
|
+
def run(errors)
|
28
|
+
definitions = get_state(:declarations)
|
29
|
+
input_meta = get_state(:inputs)
|
30
|
+
|
31
|
+
dependency_graph = Hash.new { |h, k| h[k] = [] }
|
32
|
+
reverse_dependencies = Hash.new { |h, k| h[k] = [] }
|
33
|
+
leaf_map = Hash.new { |h, k| h[k] = Set.new }
|
34
|
+
|
35
|
+
each_decl do |decl|
|
36
|
+
# Traverse the expression for each declaration, passing context down
|
37
|
+
visit_with_context(decl.expression, { decl_name: decl.name }) do |node, context|
|
38
|
+
process_node(node, decl, dependency_graph, reverse_dependencies, leaf_map, definitions, input_meta, errors, context)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Compute transitive closure of reverse dependencies
|
43
|
+
transitive_dependents = compute_transitive_closure(reverse_dependencies)
|
44
|
+
|
45
|
+
state.with(:dependencies, dependency_graph.transform_values(&:freeze).freeze)
|
46
|
+
.with(:dependents, transitive_dependents.freeze)
|
47
|
+
.with(:leaves, leaf_map.transform_values(&:freeze).freeze)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def process_node(node, decl, graph, reverse_deps, leaves, definitions, _input_meta, errors, context)
|
53
|
+
case node
|
54
|
+
when DeclarationReference
|
55
|
+
report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
|
56
|
+
|
57
|
+
# Determine if this is a conditional dependency
|
58
|
+
conditional = context[:in_cascade_branch] || context[:in_cascade_base] || false
|
59
|
+
cascade_owner = conditional ? (context[:cascade_owner] || context[:decl_name]) : nil
|
60
|
+
|
61
|
+
add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via],
|
62
|
+
conditional: conditional,
|
63
|
+
cascade_owner: cascade_owner)
|
64
|
+
when InputReference
|
65
|
+
add_dependency_edge(graph, reverse_deps, decl.name, node.name, :key, context[:via])
|
66
|
+
leaves[decl.name] << node
|
67
|
+
when InputElementReference
|
68
|
+
# adds the root input declaration as a dependency
|
69
|
+
root_input_declr_name = node.path.first
|
70
|
+
add_dependency_edge(graph, reverse_deps, decl.name, root_input_declr_name, :key, context[:via])
|
71
|
+
when Literal
|
72
|
+
leaves[decl.name] << node
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_dependency_edge(graph, reverse_deps, from, to, type, via, conditional: false, cascade_owner: nil)
|
77
|
+
edge = DependencyEdge.new(
|
78
|
+
to: to,
|
79
|
+
type: type,
|
80
|
+
via: via,
|
81
|
+
conditional: conditional,
|
82
|
+
cascade_owner: cascade_owner
|
83
|
+
)
|
84
|
+
graph[from] << edge
|
85
|
+
reverse_deps[to] << from
|
86
|
+
end
|
87
|
+
|
88
|
+
# Custom visitor that understands cascade structure
|
89
|
+
def visit_with_context(node, context = {}, &block)
|
90
|
+
return unless node
|
91
|
+
|
92
|
+
yield(node, context)
|
93
|
+
|
94
|
+
case node
|
95
|
+
when CascadeExpression
|
96
|
+
# Visit condition nodes and result expressions (non-base cases)
|
97
|
+
node.cases[0...-1].each do |when_case|
|
98
|
+
if when_case.condition
|
99
|
+
# Visit condition normally
|
100
|
+
visit_with_context(when_case.condition, context, &block)
|
101
|
+
end
|
102
|
+
# Visit result expressions as conditional dependencies
|
103
|
+
conditional_context = context.merge(in_cascade_branch: true, cascade_owner: context[:decl_name])
|
104
|
+
visit_with_context(when_case.result, conditional_context, &block)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Visit base case with conditional flag
|
108
|
+
if node.cases.last
|
109
|
+
base_context = context.merge(in_cascade_base: true)
|
110
|
+
visit_with_context(node.cases.last.result, base_context, &block)
|
111
|
+
end
|
112
|
+
when CallExpression
|
113
|
+
new_context = context.merge(via: node.fn_name)
|
114
|
+
node.children.each { |child| visit_with_context(child, new_context, &block) }
|
115
|
+
else
|
116
|
+
node.children.each { |child| visit_with_context(child, context, &block) } if node.respond_to?(:children)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def compute_transitive_closure(reverse_dependencies)
|
121
|
+
transitive = {}
|
122
|
+
all_keys = reverse_dependencies.keys
|
123
|
+
|
124
|
+
all_keys.each do |key|
|
125
|
+
visited = Set.new
|
126
|
+
to_visit = [key]
|
127
|
+
dependents = Set.new
|
128
|
+
|
129
|
+
while to_visit.any?
|
130
|
+
current = to_visit.shift
|
131
|
+
next if visited.include?(current)
|
132
|
+
|
133
|
+
visited.add(current)
|
134
|
+
|
135
|
+
direct_dependents = reverse_dependencies[current] || []
|
136
|
+
direct_dependents.each do |dependent|
|
137
|
+
next if visited.include?(dependent)
|
138
|
+
|
139
|
+
dependents << dependent
|
140
|
+
to_visit << dependent
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
transitive[key] = dependents.to_a
|
145
|
+
end
|
146
|
+
|
147
|
+
transitive
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Collect field metadata from input declarations and validate consistency
|
8
|
+
# DEPENDENCIES: :definitions
|
9
|
+
# PRODUCES: :inputs - Hash mapping field names to {type:, domain:} metadata
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class InputCollector < PassBase
|
12
|
+
def run(errors)
|
13
|
+
input_meta = {}
|
14
|
+
|
15
|
+
schema.inputs.each do |field_decl|
|
16
|
+
unless field_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
17
|
+
report_error(errors, "Expected InputDeclaration node, got #{field_decl.class}", location: field_decl.loc)
|
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
|
32
|
+
end
|
33
|
+
|
34
|
+
state.with(:inputs, freeze_nested_hash(input_meta))
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def collect_field_metadata(field_decl, errors)
|
40
|
+
validate_domain_type(field_decl, errors) if field_decl.domain
|
41
|
+
|
42
|
+
metadata = {
|
43
|
+
type: field_decl.type,
|
44
|
+
domain: field_decl.domain
|
45
|
+
}
|
46
|
+
|
47
|
+
# Process children if present
|
48
|
+
if field_decl.children && !field_decl.children.empty?
|
49
|
+
children_meta = {}
|
50
|
+
field_decl.children.each do |child_decl|
|
51
|
+
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
52
|
+
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
53
|
+
next
|
54
|
+
end
|
55
|
+
children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
|
56
|
+
end
|
57
|
+
metadata[:children] = children_meta
|
58
|
+
end
|
59
|
+
|
60
|
+
metadata
|
61
|
+
end
|
62
|
+
|
63
|
+
def merge_field_metadata(existing, field_decl, errors)
|
64
|
+
name = field_decl.name
|
65
|
+
|
66
|
+
# Check for type compatibility
|
67
|
+
if existing[:type] != field_decl.type && field_decl.type && existing[:type]
|
68
|
+
report_error(errors,
|
69
|
+
"Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
|
70
|
+
location: field_decl.loc)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check for domain compatibility
|
74
|
+
if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
|
75
|
+
report_error(errors,
|
76
|
+
"Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
|
77
|
+
location: field_decl.loc)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validate domain type if provided
|
81
|
+
validate_domain_type(field_decl, errors) if field_decl.domain
|
82
|
+
|
83
|
+
# Merge metadata (later declarations override nil values)
|
84
|
+
merged = {
|
85
|
+
type: field_decl.type || existing[:type],
|
86
|
+
domain: field_decl.domain || existing[:domain]
|
87
|
+
}
|
88
|
+
|
89
|
+
# Merge children if present
|
90
|
+
if field_decl.children && !field_decl.children.empty?
|
91
|
+
existing_children = existing[:children] || {}
|
92
|
+
new_children = {}
|
93
|
+
|
94
|
+
field_decl.children.each do |child_decl|
|
95
|
+
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
96
|
+
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
97
|
+
next
|
98
|
+
end
|
99
|
+
|
100
|
+
child_name = child_decl.name
|
101
|
+
new_children[child_name] = if existing_children[child_name]
|
102
|
+
merge_field_metadata(existing_children[child_name], child_decl, errors)
|
103
|
+
else
|
104
|
+
collect_field_metadata(child_decl, errors)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
merged[:children] = new_children
|
109
|
+
elsif existing[:children]
|
110
|
+
merged[:children] = existing[:children]
|
111
|
+
end
|
112
|
+
|
113
|
+
merged
|
114
|
+
end
|
115
|
+
|
116
|
+
def freeze_nested_hash(hash)
|
117
|
+
hash.each_value do |value|
|
118
|
+
freeze_nested_hash(value) if value.is_a?(Hash)
|
119
|
+
end
|
120
|
+
hash.freeze
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_domain_type(field_decl, errors)
|
124
|
+
domain = field_decl.domain
|
125
|
+
return if valid_domain_type?(domain)
|
126
|
+
|
127
|
+
report_error(errors,
|
128
|
+
"Field :#{field_decl.name} has invalid domain constraint: #{domain.inspect}. Domain must be a Range, Array, or Proc",
|
129
|
+
location: field_decl.loc)
|
130
|
+
end
|
131
|
+
|
132
|
+
def valid_domain_type?(domain)
|
133
|
+
domain.is_a?(Range) || domain.is_a?(Array) || domain.is_a?(Proc)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Build definitions index and detect duplicate names
|
8
|
+
# DEPENDENCIES: None (first pass in pipeline)
|
9
|
+
# PRODUCES: :declarations - Hash mapping names to declaration nodes
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class NameIndexer < PassBase
|
12
|
+
def run(errors)
|
13
|
+
definitions = {}
|
14
|
+
|
15
|
+
each_decl do |decl|
|
16
|
+
report_error(errors, "duplicated definition `#{decl.name}`", location: decl.loc) if definitions.key?(decl.name)
|
17
|
+
definitions[decl.name] = decl
|
18
|
+
end
|
19
|
+
|
20
|
+
state.with(:declarations, definitions.freeze)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# Base class for analyzer passes with simple immutable state
|
8
|
+
class PassBase
|
9
|
+
include Kumi::Syntax
|
10
|
+
include Kumi::Core::ErrorReporting
|
11
|
+
|
12
|
+
# @param schema [Syntax::Root] The schema to analyze
|
13
|
+
# @param state [AnalysisState] Current analysis state
|
14
|
+
def initialize(schema, state)
|
15
|
+
@schema = schema
|
16
|
+
@state = state
|
17
|
+
end
|
18
|
+
|
19
|
+
# Main pass execution - subclasses implement this
|
20
|
+
# @param errors [Array] Error accumulator array
|
21
|
+
# @return [AnalysisState] New state after pass execution
|
22
|
+
def run(errors)
|
23
|
+
raise NotImplementedError, "#{self.class.name} must implement #run"
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
attr_reader :schema, :state
|
29
|
+
|
30
|
+
# Iterate over all declarations (attributes and traits) in the schema
|
31
|
+
# @yield [Syntax::Attribute|Syntax::Trait] Each declaration
|
32
|
+
def each_decl(&block)
|
33
|
+
schema.attributes.each(&block)
|
34
|
+
schema.traits.each(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get state value - compatible with old interface
|
38
|
+
def get_state(key, required: true)
|
39
|
+
raise StandardError, "Required state key '#{key}' not found" if required && !state.key?(key)
|
40
|
+
|
41
|
+
state[key]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add error to the error list
|
45
|
+
def add_error(errors, location, message)
|
46
|
+
errors << ErrorReporter.create_error(message, location: location, type: :semantic)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|