kumi 0.0.5 → 0.0.7
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 +76 -174
- data/README.md +205 -52
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/SYNTAX.md +95 -8
- 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/docs/schema_metadata/broadcasts.md +53 -0
- data/docs/schema_metadata/cascades.md +45 -0
- data/docs/schema_metadata/declarations.md +54 -0
- data/docs/schema_metadata/dependencies.md +57 -0
- data/docs/schema_metadata/evaluation_order.md +29 -0
- data/docs/schema_metadata/examples.md +95 -0
- data/docs/schema_metadata/inferred_types.md +46 -0
- data/docs/schema_metadata/inputs.md +86 -0
- data/docs/schema_metadata.md +108 -0
- data/examples/federal_tax_calculator_2024.rb +11 -6
- data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +246 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +78 -38
- data/lib/kumi/analyzer/passes/input_collector.rb +91 -30
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +24 -25
- data/lib/kumi/analyzer/passes/toposorter.rb +44 -8
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -14
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/analyzer/passes/type_inferencer.rb +130 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +134 -56
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
- data/lib/kumi/analyzer.rb +16 -17
- data/lib/kumi/compiler.rb +188 -16
- data/lib/kumi/constraint_relationship_solver.rb +6 -6
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporting.rb +1 -1
- data/lib/kumi/explain.rb +32 -20
- 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 +14 -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/json_schema/generator.rb +63 -0
- data/lib/kumi/json_schema/validator.rb +25 -0
- data/lib/kumi/json_schema.rb +14 -0
- data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
- data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +5 -5
- data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +20 -20
- data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_builder.rb +41 -10
- data/lib/kumi/ruby_parser/input_field_proxy.rb +46 -0
- data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +4 -4
- data/lib/kumi/ruby_parser/nested_input.rb +15 -0
- data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/sugar.rb +62 -10
- data/lib/kumi/ruby_parser.rb +10 -0
- data/lib/kumi/schema.rb +10 -4
- data/lib/kumi/schema_instance.rb +6 -6
- data/lib/kumi/schema_metadata.rb +524 -0
- 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/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +14 -0
- metadata +55 -25
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -26
- data/lib/kumi/syntax/expressions.rb +0 -34
- data/lib/kumi/syntax/terminal_expressions.rb +0 -30
- data/lib/kumi/syntax.rb +0 -9
- /data/{documents → docs}/DSL.md +0 -0
- /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -4,14 +4,17 @@ module Kumi
|
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
6
|
# RESPONSIBILITY: Infer types for all declarations based on expression analysis
|
7
|
-
# DEPENDENCIES: Toposorter (needs
|
8
|
-
# PRODUCES:
|
7
|
+
# DEPENDENCIES: Toposorter (needs evaluation_order), DeclarationValidator (needs declarations)
|
8
|
+
# PRODUCES: inferred_types hash mapping declaration names to inferred types
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class TypeInferencer < PassBase
|
11
11
|
def run(errors)
|
12
12
|
types = {}
|
13
|
-
topo_order = get_state(:
|
14
|
-
definitions = get_state(:
|
13
|
+
topo_order = get_state(:evaluation_order)
|
14
|
+
definitions = get_state(:declarations)
|
15
|
+
|
16
|
+
# Get broadcast metadata from broadcast detector
|
17
|
+
broadcast_meta = get_state(:broadcasts, required: false) || {}
|
15
18
|
|
16
19
|
# Process declarations in topological order to ensure dependencies are resolved
|
17
20
|
topo_order.each do |name|
|
@@ -19,44 +22,67 @@ module Kumi
|
|
19
22
|
next unless decl
|
20
23
|
|
21
24
|
begin
|
22
|
-
|
23
|
-
|
25
|
+
# Check if this declaration is marked as vectorized
|
26
|
+
if broadcast_meta[:vectorized_operations]&.key?(name)
|
27
|
+
# Infer the element type and wrap in array
|
28
|
+
element_type = infer_vectorized_element_type(decl.expression, types, broadcast_meta)
|
29
|
+
types[name] = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? { array: :boolean } : { array: element_type }
|
30
|
+
else
|
31
|
+
# Normal type inference
|
32
|
+
inferred_type = infer_expression_type(decl.expression, types, broadcast_meta, name)
|
33
|
+
types[name] = inferred_type
|
34
|
+
end
|
24
35
|
rescue StandardError => e
|
25
36
|
report_type_error(errors, "Type inference failed: #{e.message}", location: decl&.loc)
|
26
37
|
end
|
27
38
|
end
|
28
39
|
|
29
|
-
state.with(:
|
40
|
+
state.with(:inferred_types, types)
|
30
41
|
end
|
31
42
|
|
32
43
|
private
|
33
44
|
|
34
|
-
def infer_expression_type(expr, type_context = {})
|
45
|
+
def infer_expression_type(expr, type_context = {}, broadcast_metadata = {}, current_decl_name = nil)
|
35
46
|
case expr
|
36
47
|
when Literal
|
37
48
|
Types.infer_from_value(expr.value)
|
38
|
-
when
|
49
|
+
when InputReference
|
39
50
|
# Look up type from field metadata
|
40
|
-
input_meta = get_state(:
|
51
|
+
input_meta = get_state(:inputs, required: false) || {}
|
41
52
|
meta = input_meta[expr.name]
|
42
53
|
meta&.dig(:type) || :any
|
43
|
-
when
|
54
|
+
when DeclarationReference
|
44
55
|
type_context[expr.name] || :any
|
45
56
|
when CallExpression
|
46
|
-
infer_call_type(expr, type_context)
|
47
|
-
when
|
48
|
-
infer_list_type(expr, type_context)
|
57
|
+
infer_call_type(expr, type_context, broadcast_metadata, current_decl_name)
|
58
|
+
when ArrayExpression
|
59
|
+
infer_list_type(expr, type_context, broadcast_metadata, current_decl_name)
|
49
60
|
when CascadeExpression
|
50
|
-
infer_cascade_type(expr, type_context)
|
61
|
+
infer_cascade_type(expr, type_context, broadcast_metadata, current_decl_name)
|
62
|
+
when InputElementReference
|
63
|
+
# Element reference returns the field type
|
64
|
+
infer_element_reference_type(expr)
|
51
65
|
else
|
52
66
|
:any
|
53
67
|
end
|
54
68
|
end
|
55
69
|
|
56
|
-
def infer_call_type(call_expr, type_context)
|
70
|
+
def infer_call_type(call_expr, type_context, broadcast_metadata = {}, current_decl_name = nil)
|
57
71
|
fn_name = call_expr.fn_name
|
58
72
|
args = call_expr.args
|
59
73
|
|
74
|
+
# Check broadcast metadata first
|
75
|
+
if current_decl_name && broadcast_metadata[:vectorized_values]&.key?(current_decl_name)
|
76
|
+
# This declaration is marked as vectorized, so it produces an array
|
77
|
+
element_type = infer_vectorized_element_type(call_expr, type_context, broadcast_metadata)
|
78
|
+
return { array: element_type }
|
79
|
+
end
|
80
|
+
|
81
|
+
if current_decl_name && broadcast_metadata[:reducer_values]&.key?(current_decl_name)
|
82
|
+
# This declaration is marked as a reducer, get the result from the function
|
83
|
+
return infer_function_return_type(fn_name, args, type_context, broadcast_metadata)
|
84
|
+
end
|
85
|
+
|
60
86
|
# Check if function exists in registry
|
61
87
|
unless FunctionRegistry.supported?(fn_name)
|
62
88
|
# Don't push error here - let existing TypeChecker handle it
|
@@ -72,7 +98,7 @@ module Kumi
|
|
72
98
|
end
|
73
99
|
|
74
100
|
# Infer argument types
|
75
|
-
arg_types = args.map { |arg| infer_expression_type(arg, type_context) }
|
101
|
+
arg_types = args.map { |arg| infer_expression_type(arg, type_context, broadcast_metadata, current_decl_name) }
|
76
102
|
|
77
103
|
# Validate parameter types (warn but don't fail)
|
78
104
|
param_types = signature[:param_types] || []
|
@@ -90,10 +116,29 @@ module Kumi
|
|
90
116
|
signature[:return_type] || :any
|
91
117
|
end
|
92
118
|
|
93
|
-
def
|
119
|
+
def infer_vectorized_element_type(call_expr, _type_context, _broadcast_metadata)
|
120
|
+
# For vectorized arithmetic operations, infer the element type
|
121
|
+
# For now, assume arithmetic operations on floats produce floats
|
122
|
+
case call_expr.fn_name
|
123
|
+
when :multiply, :add, :subtract, :divide
|
124
|
+
:float
|
125
|
+
else
|
126
|
+
:any
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def infer_function_return_type(fn_name, _args, _type_context, _broadcast_metadata)
|
131
|
+
# Get the function signature
|
132
|
+
return :any unless FunctionRegistry.supported?(fn_name)
|
133
|
+
|
134
|
+
signature = FunctionRegistry.signature(fn_name)
|
135
|
+
signature[:return_type] || :any
|
136
|
+
end
|
137
|
+
|
138
|
+
def infer_list_type(list_expr, type_context, broadcast_metadata = {}, current_decl_name = nil)
|
94
139
|
return Types.array(:any) if list_expr.elements.empty?
|
95
140
|
|
96
|
-
element_types = list_expr.elements.map { |elem| infer_expression_type(elem, type_context) }
|
141
|
+
element_types = list_expr.elements.map { |elem| infer_expression_type(elem, type_context, broadcast_metadata, current_decl_name) }
|
97
142
|
|
98
143
|
# Try to unify all element types
|
99
144
|
unified_type = element_types.reduce { |acc, type| Types.unify(acc, type) }
|
@@ -103,11 +148,75 @@ module Kumi
|
|
103
148
|
Types.array(:any)
|
104
149
|
end
|
105
150
|
|
106
|
-
def
|
151
|
+
def infer_vectorized_element_type(expr, type_context, vectorization_meta)
|
152
|
+
# For vectorized operations, we need to infer the element type
|
153
|
+
case expr
|
154
|
+
when InputElementReference
|
155
|
+
# Get the field type from metadata
|
156
|
+
input_meta = get_state(:inputs, required: false) || {}
|
157
|
+
array_name = expr.path.first
|
158
|
+
field_name = expr.path[1]
|
159
|
+
|
160
|
+
array_meta = input_meta[array_name]
|
161
|
+
return :any unless array_meta&.dig(:type) == :array
|
162
|
+
|
163
|
+
array_meta.dig(:children, field_name, :type) || :any
|
164
|
+
|
165
|
+
when CallExpression
|
166
|
+
# For arithmetic operations, infer from operands
|
167
|
+
if %i[add subtract multiply divide].include?(expr.fn_name)
|
168
|
+
# Get types of operands
|
169
|
+
arg_types = expr.args.map do |arg|
|
170
|
+
if arg.is_a?(InputElementReference)
|
171
|
+
infer_vectorized_element_type(arg, type_context, vectorization_meta)
|
172
|
+
elsif arg.is_a?(DeclarationReference)
|
173
|
+
# Get the element type if it's vectorized
|
174
|
+
ref_type = type_context[arg.name]
|
175
|
+
if ref_type.is_a?(Hash) && ref_type.key?(:array)
|
176
|
+
ref_type[:array]
|
177
|
+
else
|
178
|
+
ref_type || :any
|
179
|
+
end
|
180
|
+
else
|
181
|
+
infer_expression_type(arg, type_context, vectorization_meta)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Unify types for arithmetic
|
186
|
+
Types.unify(*arg_types) || :float
|
187
|
+
else
|
188
|
+
:any
|
189
|
+
end
|
190
|
+
|
191
|
+
else
|
192
|
+
:any
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def infer_element_reference_type(expr)
|
197
|
+
# Get array field metadata
|
198
|
+
input_meta = get_state(:inputs, required: false) || {}
|
199
|
+
|
200
|
+
return :any unless expr.path.size >= 2
|
201
|
+
|
202
|
+
array_name = expr.path.first
|
203
|
+
field_name = expr.path[1]
|
204
|
+
|
205
|
+
array_meta = input_meta[array_name]
|
206
|
+
return :any unless array_meta&.dig(:type) == :array
|
207
|
+
|
208
|
+
# Get the field type from children metadata
|
209
|
+
field_type = array_meta.dig(:children, field_name, :type) || :any
|
210
|
+
|
211
|
+
# Return array of field type (vectorized)
|
212
|
+
{ array: field_type }
|
213
|
+
end
|
214
|
+
|
215
|
+
def infer_cascade_type(cascade_expr, type_context, broadcast_metadata = {}, current_decl_name = nil)
|
107
216
|
return :any if cascade_expr.cases.empty?
|
108
217
|
|
109
218
|
result_types = cascade_expr.cases.map do |case_stmt|
|
110
|
-
infer_expression_type(case_stmt.result, type_context)
|
219
|
+
infer_expression_type(case_stmt.result, type_context, broadcast_metadata, current_decl_name)
|
111
220
|
end
|
112
221
|
|
113
222
|
# Reduce all possible types into a single unified type
|
@@ -3,6 +3,10 @@
|
|
3
3
|
module Kumi
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
|
+
# RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
|
7
|
+
# DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
|
8
|
+
# PRODUCES: :cascades - Hash of cascade mutual exclusion analysis results
|
9
|
+
# INTERFACE: new(schema, state).run(errors)
|
6
10
|
class UnsatDetector < VisitorPass
|
7
11
|
include Syntax
|
8
12
|
|
@@ -10,68 +14,143 @@ module Kumi
|
|
10
14
|
Atom = Kumi::AtomUnsatSolver::Atom
|
11
15
|
|
12
16
|
def run(errors)
|
13
|
-
definitions = get_state(:
|
14
|
-
@input_meta = get_state(:
|
17
|
+
definitions = get_state(:declarations)
|
18
|
+
@input_meta = get_state(:inputs) || {}
|
15
19
|
@definitions = definitions
|
16
20
|
@evaluator = ConstantEvaluator.new(definitions)
|
17
21
|
|
22
|
+
# First pass: analyze cascade conditions for mutual exclusion
|
23
|
+
cascades = {}
|
18
24
|
each_decl do |decl|
|
25
|
+
cascades[decl.name] = analyze_cascade_mutual_exclusion(decl, definitions) if decl.expression.is_a?(CascadeExpression)
|
26
|
+
|
27
|
+
# Store cascade metadata for later passes
|
28
|
+
|
29
|
+
# Second pass: check for unsatisfiable constraints
|
19
30
|
if decl.expression.is_a?(CascadeExpression)
|
20
31
|
# Special handling for cascade expressions
|
21
32
|
check_cascade_expression(decl, definitions, errors)
|
22
|
-
|
33
|
+
elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
|
23
34
|
# Check for OR expressions which need special disjunctive handling
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
impossible = check_or_expression(decl.expression, definitions, errors)
|
36
|
+
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
37
|
+
else
|
38
|
+
# Normal handling for non-cascade expressions
|
39
|
+
atoms = gather_atoms(decl.expression, definitions, Set.new)
|
40
|
+
next if atoms.empty?
|
41
|
+
|
42
|
+
# Use enhanced solver that can detect cross-variable mathematical constraints
|
43
|
+
impossible = if definitions && !definitions.empty?
|
44
|
+
Kumi::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
|
45
|
+
else
|
46
|
+
Kumi::AtomUnsatSolver.unsat?(atoms)
|
47
|
+
end
|
48
|
+
|
49
|
+
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
50
|
+
end
|
51
|
+
end
|
52
|
+
state.with(:cascades, cascades)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
38
56
|
|
39
|
-
|
57
|
+
def analyze_cascade_mutual_exclusion(decl, definitions)
|
58
|
+
conditions = []
|
59
|
+
condition_traits = []
|
60
|
+
|
61
|
+
# Extract all cascade conditions (except base case)
|
62
|
+
decl.expression.cases[0...-1].each do |when_case|
|
63
|
+
next unless when_case.condition
|
64
|
+
|
65
|
+
next unless when_case.condition.fn_name == :all?
|
66
|
+
|
67
|
+
when_case.condition.args.each do |arg|
|
68
|
+
next unless arg.is_a?(ArrayExpression)
|
69
|
+
|
70
|
+
arg.elements.each do |element|
|
71
|
+
next unless element.is_a?(DeclarationReference)
|
72
|
+
|
73
|
+
trait_name = element.name
|
74
|
+
trait = definitions[trait_name]
|
75
|
+
if trait
|
76
|
+
conditions << trait.expression
|
77
|
+
condition_traits << trait_name
|
78
|
+
end
|
40
79
|
end
|
41
80
|
end
|
81
|
+
# end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check mutual exclusion for all pairs
|
85
|
+
total_pairs = conditions.size * (conditions.size - 1) / 2
|
86
|
+
exclusive_pairs = 0
|
87
|
+
|
88
|
+
if conditions.size >= 2
|
89
|
+
conditions.combination(2).each do |cond1, cond2|
|
90
|
+
exclusive_pairs += 1 if conditions_mutually_exclusive?(cond1, cond2)
|
91
|
+
end
|
42
92
|
end
|
43
|
-
|
93
|
+
|
94
|
+
all_mutually_exclusive = total_pairs.positive? && (exclusive_pairs == total_pairs)
|
95
|
+
|
96
|
+
{
|
97
|
+
condition_traits: condition_traits,
|
98
|
+
condition_count: conditions.size,
|
99
|
+
all_mutually_exclusive: all_mutually_exclusive,
|
100
|
+
exclusive_pairs: exclusive_pairs,
|
101
|
+
total_pairs: total_pairs
|
102
|
+
}
|
44
103
|
end
|
45
104
|
|
46
|
-
|
105
|
+
def conditions_mutually_exclusive?(cond1, cond2)
|
106
|
+
if cond1.is_a?(CallExpression) && cond1.fn_name == :== &&
|
107
|
+
cond2.is_a?(CallExpression) && cond2.fn_name == :==
|
108
|
+
|
109
|
+
c1_field, c1_value = cond1.args
|
110
|
+
c2_field, c2_value = cond2.args
|
111
|
+
|
112
|
+
# Same field, different values = mutually exclusive
|
113
|
+
return true if same_field?(c1_field, c2_field) && different_values?(c1_value, c2_value)
|
114
|
+
end
|
47
115
|
|
48
|
-
|
116
|
+
false
|
117
|
+
end
|
118
|
+
|
119
|
+
def same_field?(field1, field2)
|
120
|
+
return false unless field1.is_a?(InputReference) && field2.is_a?(InputReference)
|
121
|
+
|
122
|
+
field1.name == field2.name
|
123
|
+
end
|
124
|
+
|
125
|
+
def different_values?(val1, val2)
|
126
|
+
return false unless val1.is_a?(Literal) && val2.is_a?(Literal)
|
127
|
+
|
128
|
+
val1.value != val2.value
|
129
|
+
end
|
130
|
+
|
131
|
+
def check_or_expression(or_expr, definitions, _errors)
|
49
132
|
# For OR expressions: A | B is impossible only if BOTH A AND B are impossible
|
50
133
|
# If either side is satisfiable, the OR is satisfiable
|
51
134
|
left_side, right_side = or_expr.args
|
52
135
|
|
53
136
|
# Check if left side is impossible
|
54
137
|
left_atoms = gather_atoms(left_side, definitions, Set.new)
|
55
|
-
left_impossible = if
|
56
|
-
if definitions && !definitions.empty?
|
57
|
-
Kumi::ConstraintRelationshipSolver.unsat?(left_atoms, definitions, input_meta: @input_meta)
|
58
|
-
else
|
59
|
-
Kumi::AtomUnsatSolver.unsat?(left_atoms)
|
60
|
-
end
|
61
|
-
else
|
138
|
+
left_impossible = if left_atoms.empty?
|
62
139
|
false
|
140
|
+
elsif definitions && !definitions.empty?
|
141
|
+
Kumi::ConstraintRelationshipSolver.unsat?(left_atoms, definitions, input_meta: @input_meta)
|
142
|
+
else
|
143
|
+
Kumi::AtomUnsatSolver.unsat?(left_atoms)
|
63
144
|
end
|
64
145
|
|
65
146
|
# Check if right side is impossible
|
66
147
|
right_atoms = gather_atoms(right_side, definitions, Set.new)
|
67
|
-
right_impossible = if
|
68
|
-
if definitions && !definitions.empty?
|
69
|
-
Kumi::ConstraintRelationshipSolver.unsat?(right_atoms, definitions, input_meta: @input_meta)
|
70
|
-
else
|
71
|
-
Kumi::AtomUnsatSolver.unsat?(right_atoms)
|
72
|
-
end
|
73
|
-
else
|
148
|
+
right_impossible = if right_atoms.empty?
|
74
149
|
false
|
150
|
+
elsif definitions && !definitions.empty?
|
151
|
+
Kumi::ConstraintRelationshipSolver.unsat?(right_atoms, definitions, input_meta: @input_meta)
|
152
|
+
else
|
153
|
+
Kumi::AtomUnsatSolver.unsat?(right_atoms)
|
75
154
|
end
|
76
155
|
|
77
156
|
# OR is impossible only if BOTH sides are impossible
|
@@ -106,10 +185,10 @@ module Kumi
|
|
106
185
|
elsif current.is_a?(CallExpression) && current.fn_name == :all?
|
107
186
|
# For all? function, add all trait arguments to the stack
|
108
187
|
current.args.each { |arg| stack << arg }
|
109
|
-
elsif current.is_a?(
|
110
|
-
# For
|
188
|
+
elsif current.is_a?(ArrayExpression)
|
189
|
+
# For ArrayExpression, add all elements to the stack
|
111
190
|
current.elements.each { |elem| stack << elem }
|
112
|
-
elsif current.is_a?(
|
191
|
+
elsif current.is_a?(DeclarationReference)
|
113
192
|
name = current.name
|
114
193
|
unless visited.include?(name)
|
115
194
|
visited << name
|
@@ -141,8 +220,8 @@ module Kumi
|
|
141
220
|
|
142
221
|
# Skip single-trait 'on' branches: trait-level unsat detection covers these
|
143
222
|
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
144
|
-
# Handle both
|
145
|
-
if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(
|
223
|
+
# Handle both ArrayExpression (old format) and multiple args (new format)
|
224
|
+
if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
|
146
225
|
list = when_case.condition.args.first
|
147
226
|
next if list.elements.size == 1
|
148
227
|
elsif when_case.condition.args.size == 1
|
@@ -154,7 +233,6 @@ module Kumi
|
|
154
233
|
condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
|
155
234
|
# DEBUG
|
156
235
|
# if when_case.condition.is_a?(CallExpression) && [:all?, :any?, :none?].include?(when_case.condition.fn_name)
|
157
|
-
# puts "DEBUG: Processing #{when_case.condition.fn_name} condition"
|
158
236
|
# puts " Args: #{when_case.condition.args.inspect}"
|
159
237
|
# puts " Atoms found: #{condition_atoms.inspect}"
|
160
238
|
# end
|
@@ -174,14 +252,14 @@ module Kumi
|
|
174
252
|
|
175
253
|
# For multi-trait on-clauses, report the trait names rather than the value name
|
176
254
|
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
177
|
-
# Handle both
|
178
|
-
trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(
|
255
|
+
# Handle both ArrayExpression (old format) and multiple args (new format)
|
256
|
+
trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
|
179
257
|
when_case.condition.args.first.elements
|
180
258
|
else
|
181
259
|
when_case.condition.args
|
182
260
|
end
|
183
261
|
|
184
|
-
if trait_bindings.all?(
|
262
|
+
if trait_bindings.all?(DeclarationReference)
|
185
263
|
traits = trait_bindings.map(&:name).join(" AND ")
|
186
264
|
report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
|
187
265
|
next
|
@@ -193,7 +271,7 @@ module Kumi
|
|
193
271
|
|
194
272
|
def term(node, _defs)
|
195
273
|
case node
|
196
|
-
when
|
274
|
+
when InputReference, DeclarationReference
|
197
275
|
val = @evaluator.evaluate(node)
|
198
276
|
val == :unknown ? node.name : val
|
199
277
|
when Literal
|
@@ -205,14 +283,14 @@ module Kumi
|
|
205
283
|
|
206
284
|
def check_domain_constraints(node, definitions, errors)
|
207
285
|
case node
|
208
|
-
when
|
209
|
-
# Check if
|
286
|
+
when InputReference
|
287
|
+
# Check if InputReference points to a field with domain constraints
|
210
288
|
field_meta = @input_meta[node.name]
|
211
289
|
nil unless field_meta&.dig(:domain)
|
212
290
|
|
213
|
-
# For
|
214
|
-
# We don't flag here since the
|
215
|
-
when
|
291
|
+
# For InputReference, the constraint comes from trait conditions
|
292
|
+
# We don't flag here since the InputReference itself is valid
|
293
|
+
when DeclarationReference
|
216
294
|
# Check if this binding evaluates to a value that violates domain constraints
|
217
295
|
definition = definitions[node.name]
|
218
296
|
return unless definition
|
@@ -254,18 +332,18 @@ module Kumi
|
|
254
332
|
end
|
255
333
|
|
256
334
|
def impossible_constraint?(lhs, rhs, operator)
|
257
|
-
# Case 1:
|
258
|
-
if lhs.is_a?(
|
335
|
+
# Case 1: InputReference compared against value outside its domain
|
336
|
+
if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
|
259
337
|
return field_literal_impossible?(lhs, rhs, operator)
|
260
|
-
elsif rhs.is_a?(
|
338
|
+
elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
|
261
339
|
# Reverse case: literal compared to field
|
262
340
|
return field_literal_impossible?(rhs, lhs, flip_operator(operator))
|
263
341
|
end
|
264
342
|
|
265
|
-
# Case 2:
|
266
|
-
if lhs.is_a?(
|
343
|
+
# Case 2: DeclarationReference that evaluates to literal compared against impossible value
|
344
|
+
if lhs.is_a?(DeclarationReference) && rhs.is_a?(Literal)
|
267
345
|
return binding_literal_impossible?(lhs, rhs, operator)
|
268
|
-
elsif rhs.is_a?(
|
346
|
+
elsif rhs.is_a?(DeclarationReference) && lhs.is_a?(Literal)
|
269
347
|
return binding_literal_impossible?(rhs, lhs, flip_operator(operator))
|
270
348
|
end
|
271
349
|
|
@@ -20,7 +20,7 @@ module Kumi
|
|
20
20
|
|
21
21
|
# Helper to visit each declaration's expression tree
|
22
22
|
# @param errors [Array] Error accumulator
|
23
|
-
# @yield [Syntax::Node, Syntax::
|
23
|
+
# @yield [Syntax::Node, Syntax::Base] Each node and its containing declaration
|
24
24
|
def visit_all_expressions(errors)
|
25
25
|
each_decl do |decl|
|
26
26
|
visit(decl.expression) { |node| yield(node, decl, errors) }
|
@@ -30,7 +30,7 @@ module Kumi
|
|
30
30
|
# Helper to visit only specific node types
|
31
31
|
# @param node_types [Array<Class>] Node types to match
|
32
32
|
# @param errors [Array] Error accumulator
|
33
|
-
# @yield [Syntax::Node, Syntax::
|
33
|
+
# @yield [Syntax::Node, Syntax::Base] Matching nodes and their declarations
|
34
34
|
def visit_nodes_of_type(*node_types, errors:)
|
35
35
|
visit_all_expressions(errors) do |node, decl, errs|
|
36
36
|
yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
|
data/lib/kumi/analyzer.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "analyzer/analysis_state"
|
4
|
-
|
5
3
|
module Kumi
|
6
4
|
module Analyzer
|
7
5
|
Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
|
@@ -9,16 +7,17 @@ module Kumi
|
|
9
7
|
module_function
|
10
8
|
|
11
9
|
DEFAULT_PASSES = [
|
12
|
-
Passes::NameIndexer,
|
13
|
-
Passes::InputCollector,
|
14
|
-
Passes::
|
15
|
-
Passes::SemanticConstraintValidator,
|
16
|
-
Passes::DependencyResolver,
|
17
|
-
Passes::UnsatDetector,
|
18
|
-
Passes::Toposorter,
|
19
|
-
Passes::
|
20
|
-
Passes::
|
21
|
-
Passes::
|
10
|
+
Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
|
11
|
+
Passes::InputCollector, # 2. Collects field metadata from input declarations.
|
12
|
+
Passes::DeclarationValidator, # 3. Checks the basic structure of each rule.
|
13
|
+
Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
|
14
|
+
Passes::DependencyResolver, # 5. Builds the dependency graph with conditional dependencies.
|
15
|
+
Passes::UnsatDetector, # 6. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
|
16
|
+
Passes::Toposorter, # 7. Creates the final evaluation order, allowing safe cycles.
|
17
|
+
Passes::BroadcastDetector, # 8. Detects which operations should be broadcast over arrays (must run before type inference).
|
18
|
+
Passes::TypeInferencer, # 9. Infers types for all declarations (uses vectorization metadata).
|
19
|
+
Passes::TypeConsistencyChecker, # 10. Validates declared vs inferred type consistency.
|
20
|
+
Passes::TypeChecker # 11. Validates types using inferred information.
|
22
21
|
].freeze
|
23
22
|
|
24
23
|
def analyze!(schema, passes: DEFAULT_PASSES, **opts)
|
@@ -53,11 +52,11 @@ module Kumi
|
|
53
52
|
|
54
53
|
def self.create_analysis_result(state)
|
55
54
|
Result.new(
|
56
|
-
definitions: state[:
|
57
|
-
dependency_graph: state[:
|
58
|
-
leaf_map: state[:
|
59
|
-
topo_order: state[:
|
60
|
-
decl_types: state[:
|
55
|
+
definitions: state[:declarations],
|
56
|
+
dependency_graph: state[:dependencies],
|
57
|
+
leaf_map: state[:leaves],
|
58
|
+
topo_order: state[:evaluation_order],
|
59
|
+
decl_types: state[:inferred_types],
|
61
60
|
state: state.to_h
|
62
61
|
)
|
63
62
|
end
|