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
@@ -1,394 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kumi
|
4
|
-
# AtomUnsatSolver detects logical contradictions in constraint systems using three analysis passes:
|
5
|
-
# 1. Numerical bounds checking for symbol-numeric inequalities (e.g. x > 5, x < 3)
|
6
|
-
# 2. Equality contradiction detection for same-type comparisons
|
7
|
-
# 3. Strict inequality cycle detection using Kahn's topological sort (stack-safe)
|
8
|
-
#
|
9
|
-
# @example Basic usage
|
10
|
-
# atoms = [Atom.new(:>, :x, 5), Atom.new(:<, :x, 3)]
|
11
|
-
# AtomUnsatSolver.unsat?(atoms) #=> true (contradiction: x > 5 AND x < 3)
|
12
|
-
#
|
13
|
-
# @example Cycle detection
|
14
|
-
# atoms = [Atom.new(:<, :x, :y), Atom.new(:<, :y, :z), Atom.new(:<, :z, :x)]
|
15
|
-
# AtomUnsatSolver.unsat?(atoms) #=> true (cycle: x < y < z < x)
|
16
|
-
module AtomUnsatSolver
|
17
|
-
# Represents a constraint atom with operator and operands
|
18
|
-
# @!attribute [r] op
|
19
|
-
# @return [Symbol] comparison operator (:>, :<, :>=, :<=, :==)
|
20
|
-
# @!attribute [r] lhs
|
21
|
-
# @return [Object] left-hand side operand
|
22
|
-
# @!attribute [r] rhs
|
23
|
-
# @return [Object] right-hand side operand
|
24
|
-
Atom = Struct.new(:op, :lhs, :rhs)
|
25
|
-
|
26
|
-
# Represents a directed edge in the strict inequality graph
|
27
|
-
# @!attribute [r] from
|
28
|
-
# @return [Symbol] source vertex
|
29
|
-
# @!attribute [r] to
|
30
|
-
# @return [Symbol] target vertex
|
31
|
-
Edge = Struct.new(:from, :to)
|
32
|
-
|
33
|
-
module_function
|
34
|
-
|
35
|
-
# Main entry point: checks if the given constraint atoms are unsatisfiable
|
36
|
-
#
|
37
|
-
# @param atoms [Array<Atom>] constraint atoms to analyze
|
38
|
-
# @param debug [Boolean] enable debug output
|
39
|
-
# @return [Boolean] true if constraints are unsatisfiable
|
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
|
-
|
44
|
-
# Pass 1: Check numerical bound contradictions (symbol vs numeric)
|
45
|
-
return true if numerical_contradiction?(atoms, debug: debug)
|
46
|
-
|
47
|
-
# Pass 2: Check equality contradictions (same-type comparisons)
|
48
|
-
return true if equality_contradiction?(atoms, debug: debug)
|
49
|
-
|
50
|
-
# Pass 3: Check strict inequality cycles using stack-safe Kahn's algorithm
|
51
|
-
edges = build_strict_inequality_edges(atoms)
|
52
|
-
puts "edges: #{edges.map { |e| "#{e.from}→#{e.to}" }.join(', ')}" if debug
|
53
|
-
|
54
|
-
StrictInequalitySolver.cycle?(edges, debug: debug)
|
55
|
-
end
|
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
|
-
|
72
|
-
# Pass 1: Detects numerical bound contradictions using interval analysis
|
73
|
-
# Handles cases like x > 5 AND x < 3 (contradictory bounds)
|
74
|
-
# Also detects always-false comparisons like 100 < 100 or 5 > 5
|
75
|
-
#
|
76
|
-
# @param atoms [Array<Atom>] constraint atoms
|
77
|
-
# @param debug [Boolean] enable debug output
|
78
|
-
# @return [Boolean] true if numerical contradiction exists
|
79
|
-
def numerical_contradiction?(atoms, debug: false)
|
80
|
-
return true if always_false_constraints_exist?(atoms, debug)
|
81
|
-
|
82
|
-
check_bound_contradictions(atoms, debug)
|
83
|
-
end
|
84
|
-
|
85
|
-
def always_false_constraints_exist?(atoms, debug)
|
86
|
-
atoms.any? do |atom|
|
87
|
-
next false unless always_false_comparison?(atom)
|
88
|
-
|
89
|
-
puts "always-false comparison detected: #{atom.lhs} #{atom.op} #{atom.rhs}" if debug
|
90
|
-
true
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def check_bound_contradictions(atoms, debug)
|
95
|
-
lowers = Hash.new(-Float::INFINITY)
|
96
|
-
uppers = Hash.new(Float::INFINITY)
|
97
|
-
|
98
|
-
atoms.each do |atom|
|
99
|
-
sym, num, op = extract_symbol_numeric_pair(atom)
|
100
|
-
next unless sym
|
101
|
-
|
102
|
-
update_bounds(lowers, uppers, sym, num, op)
|
103
|
-
end
|
104
|
-
|
105
|
-
contradiction = uppers.any? { |sym, hi| hi < lowers[sym] }
|
106
|
-
puts "numerical contradiction detected" if contradiction && debug
|
107
|
-
contradiction
|
108
|
-
end
|
109
|
-
|
110
|
-
# Pass 2: Detects equality contradictions using union-find equivalence classes
|
111
|
-
# Handles cases like x == y AND x > y (equality vs strict inequality)
|
112
|
-
#
|
113
|
-
# @param atoms [Array<Atom>] constraint atoms
|
114
|
-
# @param debug [Boolean] enable debug output
|
115
|
-
# @return [Boolean] true if equality contradiction exists
|
116
|
-
def equality_contradiction?(atoms, debug: false)
|
117
|
-
equal_pairs, strict_pairs = collect_equality_pairs(atoms)
|
118
|
-
|
119
|
-
return true if direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
|
120
|
-
return true if conflicting_equalities?(atoms, debug: debug)
|
121
|
-
|
122
|
-
transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
|
123
|
-
end
|
124
|
-
|
125
|
-
# Extracts symbol-numeric pairs and normalizes operator direction
|
126
|
-
# @param atom [Atom] constraint atom
|
127
|
-
# @return [Array(Symbol, Numeric, Symbol)] normalized [symbol, number, operator] or [nil, nil, nil]
|
128
|
-
def extract_symbol_numeric_pair(atom)
|
129
|
-
if atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Numeric)
|
130
|
-
[atom.lhs, atom.rhs, atom.op]
|
131
|
-
elsif atom.rhs.is_a?(Symbol) && atom.lhs.is_a?(Numeric)
|
132
|
-
[atom.rhs, atom.lhs, flip_operator(atom.op)]
|
133
|
-
else
|
134
|
-
[nil, nil, nil]
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
# Updates variable bounds based on constraint
|
139
|
-
# @param lowers [Hash] lower bounds by symbol
|
140
|
-
# @param uppers [Hash] upper bounds by symbol
|
141
|
-
# @param sym [Symbol] variable symbol
|
142
|
-
# @param num [Numeric] constraint value
|
143
|
-
# @param operator [Symbol] constraint operator
|
144
|
-
def update_bounds(lowers, uppers, sym, num, operator)
|
145
|
-
case operator
|
146
|
-
when :> then lowers[sym] = [lowers[sym], num + 1].max # x > 5 means x >= 6
|
147
|
-
when :>= then lowers[sym] = [lowers[sym], num].max # x >= 5 means x >= 5
|
148
|
-
when :< then uppers[sym] = [uppers[sym], num - 1].min # x < 5 means x <= 4
|
149
|
-
when :<= then uppers[sym] = [uppers[sym], num].min # x <= 5 means x <= 5
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
# Flips comparison operator for normalization
|
154
|
-
# @param operator [Symbol] original operator
|
155
|
-
# @return [Symbol] flipped operator
|
156
|
-
def flip_operator(operator)
|
157
|
-
{ :> => :<, :>= => :<=, :< => :>, :<= => :>= }[operator]
|
158
|
-
end
|
159
|
-
|
160
|
-
# Detects always-false comparisons like 5 > 5, 100 < 100, etc.
|
161
|
-
# These represent impossible conditions since they can never be true
|
162
|
-
# @param atom [Atom] constraint atom to check
|
163
|
-
# @return [Boolean] true if comparison is always false
|
164
|
-
def always_false_comparison?(atom)
|
165
|
-
return false unless atom.lhs.is_a?(Numeric) && atom.rhs.is_a?(Numeric)
|
166
|
-
|
167
|
-
lhs = atom.lhs
|
168
|
-
rhs = atom.rhs
|
169
|
-
case atom.op
|
170
|
-
when :> then lhs <= rhs # 5 > 5 is always false
|
171
|
-
when :< then lhs >= rhs # 5 < 5 is always false
|
172
|
-
when :>= then lhs < rhs # 5 >= 6 is always false
|
173
|
-
when :<= then lhs > rhs # 6 <= 5 is always false
|
174
|
-
when :== then lhs != rhs # 5 == 6 is always false
|
175
|
-
when :!= then lhs == rhs # 5 != 5 is always false
|
176
|
-
else false
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# Builds directed edges for strict inequality cycle detection
|
181
|
-
# Only creates edges when both endpoints are symbols (variables)
|
182
|
-
# Filters out symbol-numeric pairs (handled by numerical_contradiction?)
|
183
|
-
#
|
184
|
-
# @param atoms [Array<Atom>] constraint atoms
|
185
|
-
# @return [Array<Edge>] directed edges for cycle detection
|
186
|
-
def build_strict_inequality_edges(atoms)
|
187
|
-
atoms.filter_map do |atom|
|
188
|
-
next unless atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Symbol)
|
189
|
-
|
190
|
-
case atom.op
|
191
|
-
when :> then Edge.new(atom.rhs, atom.lhs) # x > y ⇒ edge y → x
|
192
|
-
when :< then Edge.new(atom.lhs, atom.rhs) # x < y ⇒ edge x → y
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
# Collects equality and strict inequality pairs for same-type operands
|
198
|
-
# @param atoms [Array<Atom>] constraint atoms
|
199
|
-
# @return [Array(Set, Set)] [equality pairs, strict inequality pairs]
|
200
|
-
def collect_equality_pairs(atoms)
|
201
|
-
equal_pairs = Set.new
|
202
|
-
strict_pairs = Set.new
|
203
|
-
|
204
|
-
atoms.each do |atom|
|
205
|
-
next unless atom.lhs.instance_of?(atom.rhs.class)
|
206
|
-
|
207
|
-
pair = [atom.lhs, atom.rhs].sort
|
208
|
-
case atom.op
|
209
|
-
when :==
|
210
|
-
equal_pairs << pair
|
211
|
-
when :>, :<
|
212
|
-
strict_pairs << pair
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
[equal_pairs, strict_pairs]
|
217
|
-
end
|
218
|
-
|
219
|
-
# Checks for direct equality contradictions (x == y AND x > y)
|
220
|
-
# @param equal_pairs [Set] equality constraint pairs
|
221
|
-
# @param strict_pairs [Set] strict inequality pairs
|
222
|
-
# @param debug [Boolean] enable debug output
|
223
|
-
# @return [Boolean] true if direct contradiction exists
|
224
|
-
def direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
|
225
|
-
conflicting_pairs = equal_pairs & strict_pairs
|
226
|
-
return false unless conflicting_pairs.any?
|
227
|
-
|
228
|
-
puts "equality contradiction detected" if debug
|
229
|
-
true
|
230
|
-
end
|
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
|
-
|
258
|
-
# Checks for transitive equality contradictions using union-find
|
259
|
-
# @param equal_pairs [Set] equality constraint pairs
|
260
|
-
# @param strict_pairs [Set] strict inequality pairs
|
261
|
-
# @param debug [Boolean] enable debug output
|
262
|
-
# @return [Boolean] true if transitive contradiction exists
|
263
|
-
def transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
|
264
|
-
equiv_classes = build_equivalence_classes(equal_pairs)
|
265
|
-
equiv_classes.each do |equiv_class|
|
266
|
-
equiv_class.combination(2).each do |var1, var2|
|
267
|
-
pair = [var1, var2].sort
|
268
|
-
next unless strict_pairs.include?(pair)
|
269
|
-
|
270
|
-
puts "transitive equality contradiction detected" if debug
|
271
|
-
return true
|
272
|
-
end
|
273
|
-
end
|
274
|
-
false
|
275
|
-
end
|
276
|
-
|
277
|
-
# Builds equivalence classes using union-find algorithm
|
278
|
-
# @param equal_pairs [Set] equality constraint pairs
|
279
|
-
# @return [Array<Array>] equivalence classes (groups of equal variables)
|
280
|
-
def build_equivalence_classes(equal_pairs)
|
281
|
-
parent = Hash.new { |h, k| h[k] = k }
|
282
|
-
|
283
|
-
equal_pairs.each do |pair|
|
284
|
-
root1 = find_root(pair[0], parent)
|
285
|
-
root2 = find_root(pair[1], parent)
|
286
|
-
parent[root1] = root2
|
287
|
-
end
|
288
|
-
|
289
|
-
group_variables_by_root(parent)
|
290
|
-
end
|
291
|
-
|
292
|
-
# Finds root of equivalence class with path compression
|
293
|
-
# @param element [Object] element to find root for
|
294
|
-
# @param parent [Hash] parent pointers for union-find
|
295
|
-
# @return [Object] root element of equivalence class
|
296
|
-
def find_root(element, parent)
|
297
|
-
return element if parent[element] == element
|
298
|
-
|
299
|
-
parent[element] = find_root(parent[element], parent)
|
300
|
-
parent[element]
|
301
|
-
end
|
302
|
-
|
303
|
-
# Groups variables by their equivalence class root
|
304
|
-
# @param parent [Hash] parent pointers for union-find
|
305
|
-
# @return [Array<Array>] equivalence classes with multiple elements
|
306
|
-
def group_variables_by_root(parent)
|
307
|
-
groups = Hash.new { |h, k| h[k] = [] }
|
308
|
-
parent.each_key do |var|
|
309
|
-
groups[find_root(var, parent)] << var
|
310
|
-
end
|
311
|
-
groups.values.select { |group| group.size > 1 }
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
# Stack-safe strict inequality cycle detector using Kahn's topological sort algorithm
|
316
|
-
#
|
317
|
-
# This module implements iterative cycle detection to avoid SystemStackError on deep graphs.
|
318
|
-
# Uses Kahn's algorithm: if topological sort cannot order all vertices, a cycle exists.
|
319
|
-
module StrictInequalitySolver
|
320
|
-
module_function
|
321
|
-
|
322
|
-
# Detects cycles in directed graph using stack-safe Kahn's topological sort
|
323
|
-
#
|
324
|
-
# @param edges [Array<Edge>] directed edges representing strict inequalities
|
325
|
-
# @param debug [Boolean] enable debug output
|
326
|
-
# @return [Boolean] true if cycle exists
|
327
|
-
def cycle?(edges, debug: false)
|
328
|
-
return false if edges.empty?
|
329
|
-
|
330
|
-
graph, in_degree = build_graph_with_degrees(edges)
|
331
|
-
processed_count = kahns_algorithm(graph, in_degree)
|
332
|
-
|
333
|
-
detect_cycle_from_processing_count(processed_count, graph.size, debug)
|
334
|
-
end
|
335
|
-
|
336
|
-
def kahns_algorithm(graph, in_degree)
|
337
|
-
queue = graph.keys.select { |v| in_degree[v].zero? }
|
338
|
-
processed_count = 0
|
339
|
-
|
340
|
-
until queue.empty?
|
341
|
-
vertex = queue.shift
|
342
|
-
processed_count += 1
|
343
|
-
|
344
|
-
graph[vertex].each do |neighbor|
|
345
|
-
in_degree[neighbor] -= 1
|
346
|
-
queue << neighbor if in_degree[neighbor].zero?
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
processed_count
|
351
|
-
end
|
352
|
-
|
353
|
-
def detect_cycle_from_processing_count(processed_count, total_vertices, debug)
|
354
|
-
has_cycle = processed_count < total_vertices
|
355
|
-
puts "cycle detected in strict inequality graph" if has_cycle && debug
|
356
|
-
has_cycle
|
357
|
-
end
|
358
|
-
|
359
|
-
# Builds adjacency list graph and in-degree counts from edges
|
360
|
-
# Pre-populates all vertices (including those with no outgoing edges) to avoid mutation during iteration
|
361
|
-
#
|
362
|
-
# @param edges [Array<Edge>] directed edges
|
363
|
-
# @return [Array(Hash, Hash)] [adjacency_list, in_degree_counts]
|
364
|
-
def build_graph_with_degrees(edges)
|
365
|
-
vertices = collect_all_vertices(edges)
|
366
|
-
graph, in_degree = initialize_graph_structures(vertices)
|
367
|
-
populate_graph_data(edges, graph, in_degree)
|
368
|
-
[graph, in_degree]
|
369
|
-
end
|
370
|
-
|
371
|
-
def collect_all_vertices(edges)
|
372
|
-
vertices = Set.new
|
373
|
-
edges.each { |e| vertices << e.from << e.to }
|
374
|
-
vertices
|
375
|
-
end
|
376
|
-
|
377
|
-
def initialize_graph_structures(vertices)
|
378
|
-
graph = Hash.new { |h, k| h[k] = [] }
|
379
|
-
in_degree = Hash.new(0)
|
380
|
-
vertices.each do |v|
|
381
|
-
graph[v]
|
382
|
-
in_degree[v] = 0
|
383
|
-
end
|
384
|
-
[graph, in_degree]
|
385
|
-
end
|
386
|
-
|
387
|
-
def populate_graph_data(edges, graph, in_degree)
|
388
|
-
edges.each do |edge|
|
389
|
-
graph[edge.from] << edge.to
|
390
|
-
in_degree[edge.to] += 1
|
391
|
-
end
|
392
|
-
end
|
393
|
-
end
|
394
|
-
end
|
data/lib/kumi/compiled_schema.rb
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kumi
|
4
|
-
class CompiledSchema
|
5
|
-
attr_reader :bindings
|
6
|
-
|
7
|
-
def initialize(bindings)
|
8
|
-
@bindings = bindings.freeze
|
9
|
-
end
|
10
|
-
|
11
|
-
def evaluate(ctx, *key_names)
|
12
|
-
target_keys = key_names.empty? ? @bindings.keys : validate_keys(key_names)
|
13
|
-
|
14
|
-
target_keys.each_with_object({}) do |key, result|
|
15
|
-
result[key] = evaluate_binding(key, ctx)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def evaluate_binding(key, ctx)
|
20
|
-
memo = ctx.instance_variable_get(:@__schema_cache__)
|
21
|
-
return memo[key] if memo&.key?(key)
|
22
|
-
|
23
|
-
value = @bindings[key][1].call(ctx)
|
24
|
-
memo[key] = value if memo
|
25
|
-
value
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def hash_like?(obj)
|
31
|
-
obj.respond_to?(:key?) && obj.respond_to?(:[])
|
32
|
-
end
|
33
|
-
|
34
|
-
def validate_keys(keys)
|
35
|
-
unknown_keys = keys - @bindings.keys
|
36
|
-
return keys if unknown_keys.empty?
|
37
|
-
|
38
|
-
raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|