kumi 0.0.7 → 0.0.8
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 +8 -5
- 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/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 +75 -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
@@ -1,404 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kumi
|
4
|
-
module Analyzer
|
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)
|
10
|
-
class UnsatDetector < VisitorPass
|
11
|
-
include Syntax
|
12
|
-
|
13
|
-
COMPARATORS = %i[> < >= <= == !=].freeze
|
14
|
-
Atom = Kumi::AtomUnsatSolver::Atom
|
15
|
-
|
16
|
-
def run(errors)
|
17
|
-
definitions = get_state(:declarations)
|
18
|
-
@input_meta = get_state(:inputs) || {}
|
19
|
-
@definitions = definitions
|
20
|
-
@evaluator = ConstantEvaluator.new(definitions)
|
21
|
-
|
22
|
-
# First pass: analyze cascade conditions for mutual exclusion
|
23
|
-
cascades = {}
|
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
|
30
|
-
if decl.expression.is_a?(CascadeExpression)
|
31
|
-
# Special handling for cascade expressions
|
32
|
-
check_cascade_expression(decl, definitions, errors)
|
33
|
-
elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
|
34
|
-
# Check for OR expressions which need special disjunctive handling
|
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
|
56
|
-
|
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
|
79
|
-
end
|
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
|
92
|
-
end
|
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
|
-
}
|
103
|
-
end
|
104
|
-
|
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
|
115
|
-
|
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)
|
132
|
-
# For OR expressions: A | B is impossible only if BOTH A AND B are impossible
|
133
|
-
# If either side is satisfiable, the OR is satisfiable
|
134
|
-
left_side, right_side = or_expr.args
|
135
|
-
|
136
|
-
# Check if left side is impossible
|
137
|
-
left_atoms = gather_atoms(left_side, definitions, Set.new)
|
138
|
-
left_impossible = if left_atoms.empty?
|
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)
|
144
|
-
end
|
145
|
-
|
146
|
-
# Check if right side is impossible
|
147
|
-
right_atoms = gather_atoms(right_side, definitions, Set.new)
|
148
|
-
right_impossible = if right_atoms.empty?
|
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)
|
154
|
-
end
|
155
|
-
|
156
|
-
# OR is impossible only if BOTH sides are impossible
|
157
|
-
left_impossible && right_impossible
|
158
|
-
end
|
159
|
-
|
160
|
-
def gather_atoms(node, defs, visited, list = [])
|
161
|
-
return list unless node
|
162
|
-
|
163
|
-
# Use iterative approach with stack to avoid SystemStackError on deep graphs
|
164
|
-
stack = [node]
|
165
|
-
|
166
|
-
until stack.empty?
|
167
|
-
current = stack.pop
|
168
|
-
next unless current
|
169
|
-
|
170
|
-
if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
|
171
|
-
lhs, rhs = current.args
|
172
|
-
|
173
|
-
# Check for domain constraint violations before creating atom
|
174
|
-
list << if impossible_constraint?(lhs, rhs, current.fn_name)
|
175
|
-
# Create a special impossible atom that will always trigger unsat
|
176
|
-
Atom.new(:==, :__impossible__, true)
|
177
|
-
else
|
178
|
-
Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
|
179
|
-
end
|
180
|
-
elsif current.is_a?(CallExpression) && current.fn_name == :or
|
181
|
-
# Special handling for OR expressions - they are disjunctive, not conjunctive
|
182
|
-
# We should NOT add OR children to the stack as they would be treated as AND
|
183
|
-
# OR expressions need separate analysis in the main run() method
|
184
|
-
next
|
185
|
-
elsif current.is_a?(CallExpression) && current.fn_name == :all?
|
186
|
-
# For all? function, add all trait arguments to the stack
|
187
|
-
current.args.each { |arg| stack << arg }
|
188
|
-
elsif current.is_a?(ArrayExpression)
|
189
|
-
# For ArrayExpression, add all elements to the stack
|
190
|
-
current.elements.each { |elem| stack << elem }
|
191
|
-
elsif current.is_a?(DeclarationReference)
|
192
|
-
name = current.name
|
193
|
-
unless visited.include?(name)
|
194
|
-
visited << name
|
195
|
-
stack << defs[name].expression if defs.key?(name)
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
# Add children to stack for processing
|
200
|
-
# IMPORTANT: Skip CascadeExpression children to avoid false positives
|
201
|
-
# Cascades are handled separately by check_cascade_expression() and are disjunctive,
|
202
|
-
# but gather_atoms() treats all collected atoms as conjunctive
|
203
|
-
current.children.each { |child| stack << child } if current.respond_to?(:children) && !current.is_a?(CascadeExpression)
|
204
|
-
end
|
205
|
-
|
206
|
-
list
|
207
|
-
end
|
208
|
-
|
209
|
-
def check_cascade_expression(decl, definitions, errors)
|
210
|
-
# Analyze each cascade branch condition independently
|
211
|
-
# This is the correct behavior: each 'on' condition should be checked separately
|
212
|
-
# since only ONE will be evaluated at runtime (they're mutually exclusive by design)
|
213
|
-
|
214
|
-
decl.expression.cases.each_with_index do |when_case, _index|
|
215
|
-
# Skip the base case (it's typically a literal true condition)
|
216
|
-
next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
|
217
|
-
|
218
|
-
# Skip non-conjunctive conditions (any?, none?) as they are disjunctive
|
219
|
-
next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
|
220
|
-
|
221
|
-
# Skip single-trait 'on' branches: trait-level unsat detection covers these
|
222
|
-
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
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)
|
225
|
-
list = when_case.condition.args.first
|
226
|
-
next if list.elements.size == 1
|
227
|
-
elsif when_case.condition.args.size == 1
|
228
|
-
# Multiple args format
|
229
|
-
next
|
230
|
-
end
|
231
|
-
end
|
232
|
-
# Gather atoms from this individual condition only
|
233
|
-
condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
|
234
|
-
# DEBUG
|
235
|
-
# if when_case.condition.is_a?(CallExpression) && [:all?, :any?, :none?].include?(when_case.condition.fn_name)
|
236
|
-
# puts " Args: #{when_case.condition.args.inspect}"
|
237
|
-
# puts " Atoms found: #{condition_atoms.inspect}"
|
238
|
-
# end
|
239
|
-
|
240
|
-
# Only flag if this individual condition is impossible
|
241
|
-
# if !condition_atoms.empty?
|
242
|
-
# is_unsat = Kumi::AtomUnsatSolver.unsat?(condition_atoms)
|
243
|
-
# puts " Is unsat? #{is_unsat}"
|
244
|
-
# end
|
245
|
-
# Use enhanced solver for cascade conditions too
|
246
|
-
impossible = if definitions && !definitions.empty?
|
247
|
-
Kumi::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
|
248
|
-
else
|
249
|
-
Kumi::AtomUnsatSolver.unsat?(condition_atoms)
|
250
|
-
end
|
251
|
-
next unless !condition_atoms.empty? && impossible
|
252
|
-
|
253
|
-
# For multi-trait on-clauses, report the trait names rather than the value name
|
254
|
-
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
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)
|
257
|
-
when_case.condition.args.first.elements
|
258
|
-
else
|
259
|
-
when_case.condition.args
|
260
|
-
end
|
261
|
-
|
262
|
-
if trait_bindings.all?(DeclarationReference)
|
263
|
-
traits = trait_bindings.map(&:name).join(" AND ")
|
264
|
-
report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
|
265
|
-
next
|
266
|
-
end
|
267
|
-
end
|
268
|
-
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc)
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def term(node, _defs)
|
273
|
-
case node
|
274
|
-
when InputReference, DeclarationReference
|
275
|
-
val = @evaluator.evaluate(node)
|
276
|
-
val == :unknown ? node.name : val
|
277
|
-
when Literal
|
278
|
-
node.value
|
279
|
-
else
|
280
|
-
:unknown
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
def check_domain_constraints(node, definitions, errors)
|
285
|
-
case node
|
286
|
-
when InputReference
|
287
|
-
# Check if InputReference points to a field with domain constraints
|
288
|
-
field_meta = @input_meta[node.name]
|
289
|
-
nil unless field_meta&.dig(:domain)
|
290
|
-
|
291
|
-
# For InputReference, the constraint comes from trait conditions
|
292
|
-
# We don't flag here since the InputReference itself is valid
|
293
|
-
when DeclarationReference
|
294
|
-
# Check if this binding evaluates to a value that violates domain constraints
|
295
|
-
definition = definitions[node.name]
|
296
|
-
return unless definition
|
297
|
-
|
298
|
-
if definition.expression.is_a?(Literal)
|
299
|
-
literal_value = definition.expression.value
|
300
|
-
check_value_against_domains(node.name, literal_value, errors, definition.loc)
|
301
|
-
end
|
302
|
-
end
|
303
|
-
end
|
304
|
-
|
305
|
-
def check_value_against_domains(_var_name, value, _errors, _location)
|
306
|
-
# Check if this value violates any input domain constraints
|
307
|
-
@input_meta.each_value do |field_meta|
|
308
|
-
domain = field_meta[:domain]
|
309
|
-
next unless domain
|
310
|
-
|
311
|
-
if violates_domain?(value, domain)
|
312
|
-
# This indicates a constraint that can never be satisfied
|
313
|
-
# Rather than flagging the cascade, flag the impossible condition
|
314
|
-
return true
|
315
|
-
end
|
316
|
-
end
|
317
|
-
false
|
318
|
-
end
|
319
|
-
|
320
|
-
def violates_domain?(value, domain)
|
321
|
-
case domain
|
322
|
-
when Range
|
323
|
-
!domain.include?(value)
|
324
|
-
when Array
|
325
|
-
!domain.include?(value)
|
326
|
-
when Proc
|
327
|
-
# For Proc domains, we can't statically analyze
|
328
|
-
false
|
329
|
-
else
|
330
|
-
false
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def impossible_constraint?(lhs, rhs, operator)
|
335
|
-
# Case 1: InputReference compared against value outside its domain
|
336
|
-
if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
|
337
|
-
return field_literal_impossible?(lhs, rhs, operator)
|
338
|
-
elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
|
339
|
-
# Reverse case: literal compared to field
|
340
|
-
return field_literal_impossible?(rhs, lhs, flip_operator(operator))
|
341
|
-
end
|
342
|
-
|
343
|
-
# Case 2: DeclarationReference that evaluates to literal compared against impossible value
|
344
|
-
if lhs.is_a?(DeclarationReference) && rhs.is_a?(Literal)
|
345
|
-
return binding_literal_impossible?(lhs, rhs, operator)
|
346
|
-
elsif rhs.is_a?(DeclarationReference) && lhs.is_a?(Literal)
|
347
|
-
return binding_literal_impossible?(rhs, lhs, flip_operator(operator))
|
348
|
-
end
|
349
|
-
|
350
|
-
false
|
351
|
-
end
|
352
|
-
|
353
|
-
def field_literal_impossible?(field_ref, literal, operator)
|
354
|
-
field_meta = @input_meta[field_ref.name]
|
355
|
-
return false unless field_meta&.dig(:domain)
|
356
|
-
|
357
|
-
domain = field_meta[:domain]
|
358
|
-
literal_value = literal.value
|
359
|
-
|
360
|
-
case operator
|
361
|
-
when :==
|
362
|
-
# field == value where value is not in domain
|
363
|
-
violates_domain?(literal_value, domain)
|
364
|
-
when :!=
|
365
|
-
# field != value where value is not in domain is always true (not impossible)
|
366
|
-
false
|
367
|
-
else
|
368
|
-
# For other operators, we'd need more sophisticated analysis
|
369
|
-
false
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
def binding_literal_impossible?(binding, literal, operator)
|
374
|
-
# Check if binding evaluates to a literal that conflicts with the comparison
|
375
|
-
evaluated_value = @evaluator.evaluate(binding)
|
376
|
-
return false if evaluated_value == :unknown
|
377
|
-
|
378
|
-
literal_value = literal.value
|
379
|
-
|
380
|
-
case operator
|
381
|
-
when :==
|
382
|
-
# binding == value where binding evaluates to different value
|
383
|
-
evaluated_value != literal_value
|
384
|
-
else
|
385
|
-
# For other operators, we could add more sophisticated checking
|
386
|
-
false
|
387
|
-
end
|
388
|
-
end
|
389
|
-
|
390
|
-
def flip_operator(operator)
|
391
|
-
case operator
|
392
|
-
when :> then :<
|
393
|
-
when :>= then :<=
|
394
|
-
when :< then :>
|
395
|
-
when :<= then :>=
|
396
|
-
when :== then :==
|
397
|
-
when :!= then :!=
|
398
|
-
else operator
|
399
|
-
end
|
400
|
-
end
|
401
|
-
end
|
402
|
-
end
|
403
|
-
end
|
404
|
-
end
|
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kumi
|
4
|
-
module Analyzer
|
5
|
-
module Passes
|
6
|
-
# Base class for analyzer passes that need to traverse the AST using the visitor pattern.
|
7
|
-
# Inherits the new immutable state interface from PassBase.
|
8
|
-
class VisitorPass < PassBase
|
9
|
-
# Visit a node and all its children using depth-first traversal
|
10
|
-
# @param node [Syntax::Node] The node to visit
|
11
|
-
# @yield [Syntax::Node] Each node in the traversal
|
12
|
-
def visit(node, &block)
|
13
|
-
return unless node
|
14
|
-
|
15
|
-
yield(node)
|
16
|
-
node.children.each { |child| visit(child, &block) }
|
17
|
-
end
|
18
|
-
|
19
|
-
protected
|
20
|
-
|
21
|
-
# Helper to visit each declaration's expression tree
|
22
|
-
# @param errors [Array] Error accumulator
|
23
|
-
# @yield [Syntax::Node, Syntax::Base] Each node and its containing declaration
|
24
|
-
def visit_all_expressions(errors)
|
25
|
-
each_decl do |decl|
|
26
|
-
visit(decl.expression) { |node| yield(node, decl, errors) }
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# Helper to visit only specific node types
|
31
|
-
# @param node_types [Array<Class>] Node types to match
|
32
|
-
# @param errors [Array] Error accumulator
|
33
|
-
# @yield [Syntax::Node, Syntax::Base] Matching nodes and their declarations
|
34
|
-
def visit_nodes_of_type(*node_types, errors:)
|
35
|
-
visit_all_expressions(errors) do |node, decl, errs|
|
36
|
-
yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|