kumi 0.0.3 → 0.0.5
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 +109 -2
- data/README.md +174 -205
- data/documents/DSL.md +3 -3
- data/documents/SYNTAX.md +17 -26
- data/examples/federal_tax_calculator_2024.rb +36 -38
- 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/definition_validator.rb +4 -3
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
- data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
- data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
- data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
- data/lib/kumi/analyzer.rb +42 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +22 -4
- data/lib/kumi/explain.rb +9 -10
- data/lib/kumi/function_registry/collection_functions.rb +103 -0
- data/lib/kumi/function_registry/string_functions.rb +1 -1
- data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/guard_rails.rb +2 -2
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +1 -1
- data/lib/kumi/parser/sugar.rb +117 -16
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/declarations.rb +3 -0
- data/lib/kumi/syntax/expressions.rb +4 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/terminal_expressions.rb +3 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +10 -6
- data/CHANGELOG.md +0 -25
- data/test_impossible_cascade.rb +0 -51
@@ -13,7 +13,7 @@ module Kumi
|
|
13
13
|
definitions = get_state(:definitions, required: false) || {}
|
14
14
|
|
15
15
|
order = compute_topological_order(dependency_graph, definitions, errors)
|
16
|
-
|
16
|
+
state.with(:topo_order, order)
|
17
17
|
end
|
18
18
|
|
19
19
|
private
|
@@ -47,7 +47,7 @@ module Kumi
|
|
47
47
|
# (i.e., declarations with no dependencies)
|
48
48
|
definitions.each_key { |node| visit_node.call(node) }
|
49
49
|
|
50
|
-
order
|
50
|
+
order.freeze
|
51
51
|
end
|
52
52
|
|
53
53
|
def report_unexpected_cycle(temp_marks, current_node, errors)
|
@@ -57,7 +57,7 @@ module Kumi
|
|
57
57
|
first_decl = find_declaration_by_name(temp_marks.first || current_node)
|
58
58
|
location = first_decl&.loc
|
59
59
|
|
60
|
-
|
60
|
+
report_error(errors, "cycle detected: #{cycle_path}", location: location)
|
61
61
|
end
|
62
62
|
|
63
63
|
def find_declaration_by_name(name)
|
@@ -4,7 +4,7 @@ module Kumi
|
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
6
|
# RESPONSIBILITY: Validate function call arity and argument types against FunctionRegistry
|
7
|
-
# DEPENDENCIES:
|
7
|
+
# DEPENDENCIES: :decl_types from TypeInferencer
|
8
8
|
# PRODUCES: None (validation only)
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class TypeChecker < VisitorPass
|
@@ -12,6 +12,7 @@ module Kumi
|
|
12
12
|
visit_nodes_of_type(Expressions::CallExpression, errors: errors) do |node, _decl, errs|
|
13
13
|
validate_function_call(node, errs)
|
14
14
|
end
|
15
|
+
state
|
15
16
|
end
|
16
17
|
|
17
18
|
private
|
@@ -16,6 +16,7 @@ module Kumi
|
|
16
16
|
|
17
17
|
# Then check basic consistency (placeholder for now)
|
18
18
|
# In a full implementation, this would do sophisticated usage analysis
|
19
|
+
state
|
19
20
|
end
|
20
21
|
|
21
22
|
private
|
@@ -30,7 +31,7 @@ module Kumi
|
|
30
31
|
field_decl = find_input_field_declaration(field_name)
|
31
32
|
location = field_decl&.loc
|
32
33
|
|
33
|
-
|
34
|
+
report_type_error(errors, "Invalid type declaration for field :#{field_name}: #{declared_type.inspect}", location: location)
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
@@ -9,8 +9,6 @@ module Kumi
|
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class TypeInferencer < PassBase
|
11
11
|
def run(errors)
|
12
|
-
return if state[:decl_types] # Already run
|
13
|
-
|
14
12
|
types = {}
|
15
13
|
topo_order = get_state(:topo_order)
|
16
14
|
definitions = get_state(:definitions)
|
@@ -24,11 +22,11 @@ module Kumi
|
|
24
22
|
inferred_type = infer_expression_type(decl.expression, types)
|
25
23
|
types[name] = inferred_type
|
26
24
|
rescue StandardError => e
|
27
|
-
|
25
|
+
report_type_error(errors, "Type inference failed: #{e.message}", location: decl&.loc)
|
28
26
|
end
|
29
27
|
end
|
30
28
|
|
31
|
-
|
29
|
+
state.with(:decl_types, types)
|
32
30
|
end
|
33
31
|
|
34
32
|
private
|
@@ -11,6 +11,8 @@ module Kumi
|
|
11
11
|
|
12
12
|
def run(errors)
|
13
13
|
definitions = get_state(:definitions)
|
14
|
+
@input_meta = get_state(:input_meta) || {}
|
15
|
+
@definitions = definitions
|
14
16
|
@evaluator = ConstantEvaluator.new(definitions)
|
15
17
|
|
16
18
|
each_decl do |decl|
|
@@ -18,17 +20,64 @@ module Kumi
|
|
18
20
|
# Special handling for cascade expressions
|
19
21
|
check_cascade_expression(decl, definitions, errors)
|
20
22
|
else
|
21
|
-
#
|
22
|
-
|
23
|
-
|
23
|
+
# Check for OR expressions which need special disjunctive handling
|
24
|
+
if decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
|
25
|
+
impossible = check_or_expression(decl.expression, definitions, errors)
|
26
|
+
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
27
|
+
else
|
28
|
+
# Normal handling for non-cascade expressions
|
29
|
+
atoms = gather_atoms(decl.expression, definitions, Set.new)
|
30
|
+
next if atoms.empty?
|
24
31
|
|
25
|
-
|
32
|
+
# Use enhanced solver that can detect cross-variable mathematical constraints
|
33
|
+
impossible = if definitions && !definitions.empty?
|
34
|
+
Kumi::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
|
35
|
+
else
|
36
|
+
Kumi::AtomUnsatSolver.unsat?(atoms)
|
37
|
+
end
|
38
|
+
|
39
|
+
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
40
|
+
end
|
26
41
|
end
|
27
42
|
end
|
43
|
+
state
|
28
44
|
end
|
29
45
|
|
30
46
|
private
|
31
47
|
|
48
|
+
def check_or_expression(or_expr, definitions, errors)
|
49
|
+
# For OR expressions: A | B is impossible only if BOTH A AND B are impossible
|
50
|
+
# If either side is satisfiable, the OR is satisfiable
|
51
|
+
left_side, right_side = or_expr.args
|
52
|
+
|
53
|
+
# Check if left side is impossible
|
54
|
+
left_atoms = gather_atoms(left_side, definitions, Set.new)
|
55
|
+
left_impossible = if !left_atoms.empty?
|
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
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check if right side is impossible
|
66
|
+
right_atoms = gather_atoms(right_side, definitions, Set.new)
|
67
|
+
right_impossible = if !right_atoms.empty?
|
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
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
# OR is impossible only if BOTH sides are impossible
|
78
|
+
left_impossible && right_impossible
|
79
|
+
end
|
80
|
+
|
32
81
|
def gather_atoms(node, defs, visited, list = [])
|
33
82
|
return list unless node
|
34
83
|
|
@@ -41,7 +90,25 @@ module Kumi
|
|
41
90
|
|
42
91
|
if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
|
43
92
|
lhs, rhs = current.args
|
44
|
-
|
93
|
+
|
94
|
+
# Check for domain constraint violations before creating atom
|
95
|
+
list << if impossible_constraint?(lhs, rhs, current.fn_name)
|
96
|
+
# Create a special impossible atom that will always trigger unsat
|
97
|
+
Atom.new(:==, :__impossible__, true)
|
98
|
+
else
|
99
|
+
Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
|
100
|
+
end
|
101
|
+
elsif current.is_a?(CallExpression) && current.fn_name == :or
|
102
|
+
# Special handling for OR expressions - they are disjunctive, not conjunctive
|
103
|
+
# We should NOT add OR children to the stack as they would be treated as AND
|
104
|
+
# OR expressions need separate analysis in the main run() method
|
105
|
+
next
|
106
|
+
elsif current.is_a?(CallExpression) && current.fn_name == :all?
|
107
|
+
# For all? function, add all trait arguments to the stack
|
108
|
+
current.args.each { |arg| stack << arg }
|
109
|
+
elsif current.is_a?(ListExpression)
|
110
|
+
# For ListExpression, add all elements to the stack
|
111
|
+
current.elements.each { |elem| stack << elem }
|
45
112
|
elsif current.is_a?(Binding)
|
46
113
|
name = current.name
|
47
114
|
unless visited.include?(name)
|
@@ -51,7 +118,10 @@ module Kumi
|
|
51
118
|
end
|
52
119
|
|
53
120
|
# Add children to stack for processing
|
54
|
-
|
121
|
+
# IMPORTANT: Skip CascadeExpression children to avoid false positives
|
122
|
+
# Cascades are handled separately by check_cascade_expression() and are disjunctive,
|
123
|
+
# but gather_atoms() treats all collected atoms as conjunctive
|
124
|
+
current.children.each { |child| stack << child } if current.respond_to?(:children) && !current.is_a?(CascadeExpression)
|
55
125
|
end
|
56
126
|
|
57
127
|
list
|
@@ -66,27 +136,58 @@ module Kumi
|
|
66
136
|
# Skip the base case (it's typically a literal true condition)
|
67
137
|
next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
|
68
138
|
|
139
|
+
# Skip non-conjunctive conditions (any?, none?) as they are disjunctive
|
140
|
+
next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
|
141
|
+
|
69
142
|
# Skip single-trait 'on' branches: trait-level unsat detection covers these
|
70
143
|
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
71
|
-
|
72
|
-
|
144
|
+
# Handle both ListExpression (old format) and multiple args (new format)
|
145
|
+
if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ListExpression)
|
146
|
+
list = when_case.condition.args.first
|
147
|
+
next if list.elements.size == 1
|
148
|
+
elsif when_case.condition.args.size == 1
|
149
|
+
# Multiple args format
|
150
|
+
next
|
151
|
+
end
|
73
152
|
end
|
74
153
|
# Gather atoms from this individual condition only
|
75
154
|
condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
|
155
|
+
# DEBUG
|
156
|
+
# 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
|
+
# puts " Args: #{when_case.condition.args.inspect}"
|
159
|
+
# puts " Atoms found: #{condition_atoms.inspect}"
|
160
|
+
# end
|
76
161
|
|
77
162
|
# Only flag if this individual condition is impossible
|
78
|
-
|
163
|
+
# if !condition_atoms.empty?
|
164
|
+
# is_unsat = Kumi::AtomUnsatSolver.unsat?(condition_atoms)
|
165
|
+
# puts " Is unsat? #{is_unsat}"
|
166
|
+
# end
|
167
|
+
# Use enhanced solver for cascade conditions too
|
168
|
+
impossible = if definitions && !definitions.empty?
|
169
|
+
Kumi::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
|
170
|
+
else
|
171
|
+
Kumi::AtomUnsatSolver.unsat?(condition_atoms)
|
172
|
+
end
|
173
|
+
next unless !condition_atoms.empty? && impossible
|
79
174
|
|
80
175
|
# For multi-trait on-clauses, report the trait names rather than the value name
|
81
176
|
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
82
|
-
|
83
|
-
if
|
84
|
-
|
85
|
-
|
177
|
+
# Handle both ListExpression (old format) and multiple args (new format)
|
178
|
+
trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ListExpression)
|
179
|
+
when_case.condition.args.first.elements
|
180
|
+
else
|
181
|
+
when_case.condition.args
|
182
|
+
end
|
183
|
+
|
184
|
+
if trait_bindings.all?(Binding)
|
185
|
+
traits = trait_bindings.map(&:name).join(" AND ")
|
186
|
+
report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
|
86
187
|
next
|
87
188
|
end
|
88
189
|
end
|
89
|
-
|
190
|
+
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc)
|
90
191
|
end
|
91
192
|
end
|
92
193
|
|
@@ -101,6 +202,124 @@ module Kumi
|
|
101
202
|
:unknown
|
102
203
|
end
|
103
204
|
end
|
205
|
+
|
206
|
+
def check_domain_constraints(node, definitions, errors)
|
207
|
+
case node
|
208
|
+
when FieldRef
|
209
|
+
# Check if FieldRef points to a field with domain constraints
|
210
|
+
field_meta = @input_meta[node.name]
|
211
|
+
nil unless field_meta&.dig(:domain)
|
212
|
+
|
213
|
+
# For FieldRef, the constraint comes from trait conditions
|
214
|
+
# We don't flag here since the FieldRef itself is valid
|
215
|
+
when Binding
|
216
|
+
# Check if this binding evaluates to a value that violates domain constraints
|
217
|
+
definition = definitions[node.name]
|
218
|
+
return unless definition
|
219
|
+
|
220
|
+
if definition.expression.is_a?(Literal)
|
221
|
+
literal_value = definition.expression.value
|
222
|
+
check_value_against_domains(node.name, literal_value, errors, definition.loc)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def check_value_against_domains(_var_name, value, _errors, _location)
|
228
|
+
# Check if this value violates any input domain constraints
|
229
|
+
@input_meta.each_value do |field_meta|
|
230
|
+
domain = field_meta[:domain]
|
231
|
+
next unless domain
|
232
|
+
|
233
|
+
if violates_domain?(value, domain)
|
234
|
+
# This indicates a constraint that can never be satisfied
|
235
|
+
# Rather than flagging the cascade, flag the impossible condition
|
236
|
+
return true
|
237
|
+
end
|
238
|
+
end
|
239
|
+
false
|
240
|
+
end
|
241
|
+
|
242
|
+
def violates_domain?(value, domain)
|
243
|
+
case domain
|
244
|
+
when Range
|
245
|
+
!domain.include?(value)
|
246
|
+
when Array
|
247
|
+
!domain.include?(value)
|
248
|
+
when Proc
|
249
|
+
# For Proc domains, we can't statically analyze
|
250
|
+
false
|
251
|
+
else
|
252
|
+
false
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def impossible_constraint?(lhs, rhs, operator)
|
257
|
+
# Case 1: FieldRef compared against value outside its domain
|
258
|
+
if lhs.is_a?(FieldRef) && rhs.is_a?(Literal)
|
259
|
+
return field_literal_impossible?(lhs, rhs, operator)
|
260
|
+
elsif rhs.is_a?(FieldRef) && lhs.is_a?(Literal)
|
261
|
+
# Reverse case: literal compared to field
|
262
|
+
return field_literal_impossible?(rhs, lhs, flip_operator(operator))
|
263
|
+
end
|
264
|
+
|
265
|
+
# Case 2: Binding that evaluates to literal compared against impossible value
|
266
|
+
if lhs.is_a?(Binding) && rhs.is_a?(Literal)
|
267
|
+
return binding_literal_impossible?(lhs, rhs, operator)
|
268
|
+
elsif rhs.is_a?(Binding) && lhs.is_a?(Literal)
|
269
|
+
return binding_literal_impossible?(rhs, lhs, flip_operator(operator))
|
270
|
+
end
|
271
|
+
|
272
|
+
false
|
273
|
+
end
|
274
|
+
|
275
|
+
def field_literal_impossible?(field_ref, literal, operator)
|
276
|
+
field_meta = @input_meta[field_ref.name]
|
277
|
+
return false unless field_meta&.dig(:domain)
|
278
|
+
|
279
|
+
domain = field_meta[:domain]
|
280
|
+
literal_value = literal.value
|
281
|
+
|
282
|
+
case operator
|
283
|
+
when :==
|
284
|
+
# field == value where value is not in domain
|
285
|
+
violates_domain?(literal_value, domain)
|
286
|
+
when :!=
|
287
|
+
# field != value where value is not in domain is always true (not impossible)
|
288
|
+
false
|
289
|
+
else
|
290
|
+
# For other operators, we'd need more sophisticated analysis
|
291
|
+
false
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def binding_literal_impossible?(binding, literal, operator)
|
296
|
+
# Check if binding evaluates to a literal that conflicts with the comparison
|
297
|
+
evaluated_value = @evaluator.evaluate(binding)
|
298
|
+
return false if evaluated_value == :unknown
|
299
|
+
|
300
|
+
literal_value = literal.value
|
301
|
+
|
302
|
+
case operator
|
303
|
+
when :==
|
304
|
+
# binding == value where binding evaluates to different value
|
305
|
+
evaluated_value != literal_value
|
306
|
+
else
|
307
|
+
# For other operators, we could add more sophisticated checking
|
308
|
+
false
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def flip_operator(operator)
|
313
|
+
case operator
|
314
|
+
when :> then :<
|
315
|
+
when :>= then :<=
|
316
|
+
when :< then :>
|
317
|
+
when :<= then :>=
|
318
|
+
when :== then :==
|
319
|
+
when :!= then :!=
|
320
|
+
else operator
|
321
|
+
end
|
322
|
+
end
|
104
323
|
end
|
105
324
|
end
|
106
325
|
end
|
@@ -3,7 +3,8 @@
|
|
3
3
|
module Kumi
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
|
-
# Base class for analyzer passes that need to traverse the AST using the visitor pattern
|
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.
|
7
8
|
class VisitorPass < PassBase
|
8
9
|
# Visit a node and all its children using depth-first traversal
|
9
10
|
# @param node [Syntax::Node] The node to visit
|
data/lib/kumi/analyzer.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "analyzer/analysis_state"
|
4
|
+
|
3
5
|
module Kumi
|
4
6
|
module Analyzer
|
5
7
|
Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
|
@@ -7,40 +9,56 @@ module Kumi
|
|
7
9
|
module_function
|
8
10
|
|
9
11
|
DEFAULT_PASSES = [
|
10
|
-
Passes::NameIndexer,
|
11
|
-
Passes::InputCollector,
|
12
|
-
Passes::DefinitionValidator,
|
13
|
-
Passes::
|
14
|
-
Passes::
|
15
|
-
Passes::
|
16
|
-
Passes::
|
17
|
-
Passes::
|
18
|
-
Passes::
|
12
|
+
Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
|
13
|
+
Passes::InputCollector, # 2. Collects field metadata from input declarations.
|
14
|
+
Passes::DefinitionValidator, # 3. Checks the basic structure of each rule.
|
15
|
+
Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
|
16
|
+
Passes::DependencyResolver, # 5. Builds the dependency graph.
|
17
|
+
Passes::UnsatDetector, # 6. Detects unsatisfiable constraints in rules.
|
18
|
+
Passes::Toposorter, # 7. Creates the final evaluation order.
|
19
|
+
Passes::TypeInferencer, # 8. Infers types for all declarations (pure annotation).
|
20
|
+
Passes::TypeConsistencyChecker, # 9. Validates declared vs inferred type consistency.
|
21
|
+
Passes::TypeChecker # 10. Validates types using inferred information.
|
19
22
|
].freeze
|
20
23
|
|
21
24
|
def analyze!(schema, passes: DEFAULT_PASSES, **opts)
|
22
|
-
|
25
|
+
state = AnalysisState.new(opts)
|
23
26
|
errors = []
|
24
27
|
|
25
|
-
|
28
|
+
state = run_analysis_passes(schema, passes, state, errors)
|
29
|
+
handle_analysis_errors(errors) unless errors.empty?
|
30
|
+
create_analysis_result(state)
|
31
|
+
end
|
26
32
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
33
|
+
def self.run_analysis_passes(schema, passes, state, errors)
|
34
|
+
passes.each do |pass_class|
|
35
|
+
pass_instance = pass_class.new(schema, state)
|
36
|
+
begin
|
37
|
+
state = pass_instance.run(errors)
|
38
|
+
rescue StandardError => e
|
39
|
+
errors << ErrorReporter.create_error(e.message, location: nil, type: :semantic)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
state
|
43
|
+
end
|
31
44
|
|
32
|
-
|
45
|
+
def self.handle_analysis_errors(errors)
|
46
|
+
type_errors = errors.select { |e| e.type == :type }
|
47
|
+
first_error_location = errors.first.location
|
33
48
|
|
34
|
-
|
35
|
-
|
49
|
+
raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
|
50
|
+
|
51
|
+
raise Errors::SemanticError.new(format_errors(errors), first_error_location)
|
52
|
+
end
|
36
53
|
|
54
|
+
def self.create_analysis_result(state)
|
37
55
|
Result.new(
|
38
|
-
definitions:
|
39
|
-
dependency_graph:
|
40
|
-
leaf_map:
|
41
|
-
topo_order:
|
42
|
-
decl_types:
|
43
|
-
state:
|
56
|
+
definitions: state[:definitions],
|
57
|
+
dependency_graph: state[:dependency_graph],
|
58
|
+
leaf_map: state[:leaf_map],
|
59
|
+
topo_order: state[:topo_order],
|
60
|
+
decl_types: state[:decl_types],
|
61
|
+
state: state.to_h
|
44
62
|
)
|
45
63
|
end
|
46
64
|
|
@@ -38,6 +38,9 @@ module Kumi
|
|
38
38
|
# @param debug [Boolean] enable debug output
|
39
39
|
# @return [Boolean] true if constraints are unsatisfiable
|
40
40
|
def unsat?(atoms, debug: false)
|
41
|
+
# Pass 0: Check for special impossible atoms (domain violations, etc.)
|
42
|
+
return true if impossible_atoms_exist?(atoms, debug: debug)
|
43
|
+
|
41
44
|
# Pass 1: Check numerical bound contradictions (symbol vs numeric)
|
42
45
|
return true if numerical_contradiction?(atoms, debug: debug)
|
43
46
|
|
@@ -51,6 +54,21 @@ module Kumi
|
|
51
54
|
StrictInequalitySolver.cycle?(edges, debug: debug)
|
52
55
|
end
|
53
56
|
|
57
|
+
# Pass 0: Detects special impossible atoms (domain violations, etc.)
|
58
|
+
# These atoms are created by UnsatDetector when it finds statically impossible constraints
|
59
|
+
#
|
60
|
+
# @param atoms [Array<Atom>] constraint atoms
|
61
|
+
# @param debug [Boolean] enable debug output
|
62
|
+
# @return [Boolean] true if impossible atoms exist
|
63
|
+
def impossible_atoms_exist?(atoms, debug: false)
|
64
|
+
impossible_found = atoms.any? do |atom|
|
65
|
+
atom.lhs == :__impossible__ || atom.rhs == :__impossible__
|
66
|
+
end
|
67
|
+
|
68
|
+
puts "impossible atom detected (domain violation or static impossibility)" if impossible_found && debug
|
69
|
+
impossible_found
|
70
|
+
end
|
71
|
+
|
54
72
|
# Pass 1: Detects numerical bound contradictions using interval analysis
|
55
73
|
# Handles cases like x > 5 AND x < 3 (contradictory bounds)
|
56
74
|
# Also detects always-false comparisons like 100 < 100 or 5 > 5
|
@@ -99,6 +117,7 @@ module Kumi
|
|
99
117
|
equal_pairs, strict_pairs = collect_equality_pairs(atoms)
|
100
118
|
|
101
119
|
return true if direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
|
120
|
+
return true if conflicting_equalities?(atoms, debug: debug)
|
102
121
|
|
103
122
|
transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
|
104
123
|
end
|
@@ -210,6 +229,32 @@ module Kumi
|
|
210
229
|
true
|
211
230
|
end
|
212
231
|
|
232
|
+
# Checks for conflicting equalities (x == a AND x == b where a != b)
|
233
|
+
# @param atoms [Array<Atom>] constraint atoms
|
234
|
+
# @param debug [Boolean] enable debug output
|
235
|
+
# @return [Boolean] true if conflicting equalities exist
|
236
|
+
def conflicting_equalities?(atoms, debug: false)
|
237
|
+
equalities = atoms.select { |atom| atom.op == :== }
|
238
|
+
|
239
|
+
# Group equalities by their left-hand side
|
240
|
+
by_lhs = equalities.group_by(&:lhs)
|
241
|
+
|
242
|
+
# Check each variable for conflicting equality constraints
|
243
|
+
by_lhs.each do |lhs, atoms_for_lhs|
|
244
|
+
next if atoms_for_lhs.size < 2
|
245
|
+
|
246
|
+
# Get all values this variable is constrained to equal
|
247
|
+
values = atoms_for_lhs.map(&:rhs).uniq
|
248
|
+
|
249
|
+
if values.size > 1
|
250
|
+
puts "conflicting equalities detected: #{lhs} == #{values.join(" AND #{lhs} == ")}" if debug
|
251
|
+
return true
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
false
|
256
|
+
end
|
257
|
+
|
213
258
|
# Checks for transitive equality contradictions using union-find
|
214
259
|
# @param equal_pairs [Set] equality constraint pairs
|
215
260
|
# @param strict_pairs [Set] strict inequality pairs
|