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.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -1
  3. data/README.md +8 -5
  4. data/examples/game_of_life.rb +1 -1
  5. data/examples/static_analysis_errors.rb +7 -7
  6. data/lib/kumi/analyzer.rb +15 -15
  7. data/lib/kumi/compiler.rb +6 -6
  8. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  9. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  10. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  11. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  13. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  14. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  15. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  16. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  17. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  18. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  19. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  20. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  21. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  22. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  23. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  24. data/lib/kumi/core/compiled_schema.rb +43 -0
  25. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  26. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  27. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  28. data/lib/kumi/core/domain/validator.rb +82 -0
  29. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  30. data/lib/kumi/core/error_reporter.rb +166 -0
  31. data/lib/kumi/core/error_reporting.rb +97 -0
  32. data/lib/kumi/core/errors.rb +120 -0
  33. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  34. data/lib/kumi/core/explain.rb +295 -0
  35. data/lib/kumi/core/export/deserializer.rb +41 -0
  36. data/lib/kumi/core/export/errors.rb +14 -0
  37. data/lib/kumi/core/export/node_builders.rb +142 -0
  38. data/lib/kumi/core/export/node_registry.rb +54 -0
  39. data/lib/kumi/core/export/node_serializers.rb +158 -0
  40. data/lib/kumi/core/export/serializer.rb +25 -0
  41. data/lib/kumi/core/export.rb +35 -0
  42. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  43. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  44. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  45. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  46. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  47. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  48. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  49. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  50. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  51. data/lib/kumi/core/input/type_matcher.rb +97 -0
  52. data/lib/kumi/core/input/validator.rb +51 -0
  53. data/lib/kumi/core/input/violation_creator.rb +52 -0
  54. data/lib/kumi/core/json_schema/generator.rb +65 -0
  55. data/lib/kumi/core/json_schema/validator.rb +27 -0
  56. data/lib/kumi/core/json_schema.rb +16 -0
  57. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  58. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  59. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  60. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  61. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  62. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  64. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  65. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  66. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  67. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  68. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  69. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  70. data/lib/kumi/core/ruby_parser.rb +12 -0
  71. data/lib/kumi/core/schema_instance.rb +111 -0
  72. data/lib/kumi/core/types/builder.rb +23 -0
  73. data/lib/kumi/core/types/compatibility.rb +96 -0
  74. data/lib/kumi/core/types/formatter.rb +26 -0
  75. data/lib/kumi/core/types/inference.rb +42 -0
  76. data/lib/kumi/core/types/normalizer.rb +72 -0
  77. data/lib/kumi/core/types/validator.rb +37 -0
  78. data/lib/kumi/core/types.rb +66 -0
  79. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  80. data/lib/kumi/errors.rb +1 -112
  81. data/lib/kumi/registry.rb +37 -0
  82. data/lib/kumi/schema.rb +5 -5
  83. data/lib/kumi/schema_metadata.rb +3 -3
  84. data/lib/kumi/syntax/array_expression.rb +6 -6
  85. data/lib/kumi/syntax/call_expression.rb +4 -4
  86. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  87. data/lib/kumi/syntax/case_expression.rb +4 -4
  88. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  89. data/lib/kumi/syntax/hash_expression.rb +4 -4
  90. data/lib/kumi/syntax/input_declaration.rb +5 -5
  91. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  92. data/lib/kumi/syntax/input_reference.rb +5 -5
  93. data/lib/kumi/syntax/literal.rb +4 -4
  94. data/lib/kumi/syntax/node.rb +34 -34
  95. data/lib/kumi/syntax/root.rb +6 -6
  96. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  97. data/lib/kumi/syntax/value_declaration.rb +4 -4
  98. data/lib/kumi/version.rb +1 -1
  99. data/migrate_to_core_iterative.rb +938 -0
  100. data/scripts/generate_function_docs.rb +9 -9
  101. metadata +75 -72
  102. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  103. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  104. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
  105. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  106. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  107. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  108. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  109. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  110. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
  111. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  112. data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
  113. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  114. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  115. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
  116. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  117. data/lib/kumi/atom_unsat_solver.rb +0 -394
  118. data/lib/kumi/compiled_schema.rb +0 -41
  119. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  120. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  121. data/lib/kumi/domain/range_analyzer.rb +0 -83
  122. data/lib/kumi/domain/validator.rb +0 -80
  123. data/lib/kumi/domain/violation_formatter.rb +0 -40
  124. data/lib/kumi/error_reporter.rb +0 -164
  125. data/lib/kumi/error_reporting.rb +0 -95
  126. data/lib/kumi/evaluation_wrapper.rb +0 -38
  127. data/lib/kumi/explain.rb +0 -293
  128. data/lib/kumi/export/deserializer.rb +0 -39
  129. data/lib/kumi/export/errors.rb +0 -12
  130. data/lib/kumi/export/node_builders.rb +0 -140
  131. data/lib/kumi/export/node_registry.rb +0 -52
  132. data/lib/kumi/export/node_serializers.rb +0 -156
  133. data/lib/kumi/export/serializer.rb +0 -23
  134. data/lib/kumi/export.rb +0 -33
  135. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  136. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  137. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  138. data/lib/kumi/function_registry/function_builder.rb +0 -93
  139. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  140. data/lib/kumi/function_registry/math_functions.rb +0 -72
  141. data/lib/kumi/function_registry/string_functions.rb +0 -54
  142. data/lib/kumi/function_registry/type_functions.rb +0 -51
  143. data/lib/kumi/input/type_matcher.rb +0 -95
  144. data/lib/kumi/input/validator.rb +0 -49
  145. data/lib/kumi/input/violation_creator.rb +0 -50
  146. data/lib/kumi/json_schema/generator.rb +0 -63
  147. data/lib/kumi/json_schema/validator.rb +0 -25
  148. data/lib/kumi/json_schema.rb +0 -14
  149. data/lib/kumi/ruby_parser/build_context.rb +0 -25
  150. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
  151. data/lib/kumi/ruby_parser/dsl.rb +0 -12
  152. data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
  153. data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
  154. data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
  155. data/lib/kumi/ruby_parser/input_builder.rb +0 -125
  156. data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
  157. data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
  158. data/lib/kumi/ruby_parser/nested_input.rb +0 -15
  159. data/lib/kumi/ruby_parser/parser.rb +0 -69
  160. data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
  161. data/lib/kumi/ruby_parser/sugar.rb +0 -261
  162. data/lib/kumi/ruby_parser.rb +0 -10
  163. data/lib/kumi/schema_instance.rb +0 -109
  164. data/lib/kumi/types/builder.rb +0 -21
  165. data/lib/kumi/types/compatibility.rb +0 -94
  166. data/lib/kumi/types/formatter.rb +0 -24
  167. data/lib/kumi/types/inference.rb +0 -40
  168. data/lib/kumi/types/normalizer.rb +0 -70
  169. data/lib/kumi/types/validator.rb +0 -35
  170. data/lib/kumi/types.rb +0 -64
  171. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ # AtomUnsatSolver detects logical contradictions in constraint systems using three analysis passes:
6
+ # 1. Numerical bounds checking for symbol-numeric inequalities (e.g. x > 5, x < 3)
7
+ # 2. Equality contradiction detection for same-type comparisons
8
+ # 3. Strict inequality cycle detection using Kahn's topological sort (stack-safe)
9
+ #
10
+ # @example Basic usage
11
+ # atoms = [Atom.new(:>, :x, 5), Atom.new(:<, :x, 3)]
12
+ # AtomUnsatSolver.unsat?(atoms) #=> true (contradiction: x > 5 AND x < 3)
13
+ #
14
+ # @example Cycle detection
15
+ # atoms = [Atom.new(:<, :x, :y), Atom.new(:<, :y, :z), Atom.new(:<, :z, :x)]
16
+ # AtomUnsatSolver.unsat?(atoms) #=> true (cycle: x < y < z < x)
17
+ module AtomUnsatSolver
18
+ # Represents a constraint atom with operator and operands
19
+ # @!attribute [r] op
20
+ # @return [Symbol] comparison operator (:>, :<, :>=, :<=, :==)
21
+ # @!attribute [r] lhs
22
+ # @return [Object] left-hand side operand
23
+ # @!attribute [r] rhs
24
+ # @return [Object] right-hand side operand
25
+ Atom = Struct.new(:op, :lhs, :rhs)
26
+
27
+ # Represents a directed edge in the strict inequality graph
28
+ # @!attribute [r] from
29
+ # @return [Symbol] source vertex
30
+ # @!attribute [r] to
31
+ # @return [Symbol] target vertex
32
+ Edge = Struct.new(:from, :to)
33
+
34
+ module_function
35
+
36
+ # Main entry point: checks if the given constraint atoms are unsatisfiable
37
+ #
38
+ # @param atoms [Array<Atom>] constraint atoms to analyze
39
+ # @param debug [Boolean] enable debug output
40
+ # @return [Boolean] true if constraints are unsatisfiable
41
+ def unsat?(atoms, debug: false)
42
+ # Pass 0: Check for special impossible atoms (domain violations, etc.)
43
+ return true if impossible_atoms_exist?(atoms, debug: debug)
44
+
45
+ # Pass 1: Check numerical bound contradictions (symbol vs numeric)
46
+ return true if numerical_contradiction?(atoms, debug: debug)
47
+
48
+ # Pass 2: Check equality contradictions (same-type comparisons)
49
+ return true if equality_contradiction?(atoms, debug: debug)
50
+
51
+ # Pass 3: Check strict inequality cycles using stack-safe Kahn's algorithm
52
+ edges = build_strict_inequality_edges(atoms)
53
+ puts "edges: #{edges.map { |e| "#{e.from}→#{e.to}" }.join(', ')}" if debug
54
+
55
+ StrictInequalitySolver.cycle?(edges, debug: debug)
56
+ end
57
+
58
+ # Pass 0: Detects special impossible atoms (domain violations, etc.)
59
+ # These atoms are created by UnsatDetector when it finds statically impossible constraints
60
+ #
61
+ # @param atoms [Array<Atom>] constraint atoms
62
+ # @param debug [Boolean] enable debug output
63
+ # @return [Boolean] true if impossible atoms exist
64
+ def impossible_atoms_exist?(atoms, debug: false)
65
+ impossible_found = atoms.any? do |atom|
66
+ atom.lhs == :__impossible__ || atom.rhs == :__impossible__
67
+ end
68
+
69
+ puts "impossible atom detected (domain violation or static impossibility)" if impossible_found && debug
70
+ impossible_found
71
+ end
72
+
73
+ # Pass 1: Detects numerical bound contradictions using interval analysis
74
+ # Handles cases like x > 5 AND x < 3 (contradictory bounds)
75
+ # Also detects always-false comparisons like 100 < 100 or 5 > 5
76
+ #
77
+ # @param atoms [Array<Atom>] constraint atoms
78
+ # @param debug [Boolean] enable debug output
79
+ # @return [Boolean] true if numerical contradiction exists
80
+ def numerical_contradiction?(atoms, debug: false)
81
+ return true if always_false_constraints_exist?(atoms, debug)
82
+
83
+ check_bound_contradictions(atoms, debug)
84
+ end
85
+
86
+ def always_false_constraints_exist?(atoms, debug)
87
+ atoms.any? do |atom|
88
+ next false unless always_false_comparison?(atom)
89
+
90
+ puts "always-false comparison detected: #{atom.lhs} #{atom.op} #{atom.rhs}" if debug
91
+ true
92
+ end
93
+ end
94
+
95
+ def check_bound_contradictions(atoms, debug)
96
+ lowers = Hash.new(-Float::INFINITY)
97
+ uppers = Hash.new(Float::INFINITY)
98
+
99
+ atoms.each do |atom|
100
+ sym, num, op = extract_symbol_numeric_pair(atom)
101
+ next unless sym
102
+
103
+ update_bounds(lowers, uppers, sym, num, op)
104
+ end
105
+
106
+ contradiction = uppers.any? { |sym, hi| hi < lowers[sym] }
107
+ puts "numerical contradiction detected" if contradiction && debug
108
+ contradiction
109
+ end
110
+
111
+ # Pass 2: Detects equality contradictions using union-find equivalence classes
112
+ # Handles cases like x == y AND x > y (equality vs strict inequality)
113
+ #
114
+ # @param atoms [Array<Atom>] constraint atoms
115
+ # @param debug [Boolean] enable debug output
116
+ # @return [Boolean] true if equality contradiction exists
117
+ def equality_contradiction?(atoms, debug: false)
118
+ equal_pairs, strict_pairs = collect_equality_pairs(atoms)
119
+
120
+ return true if direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
121
+ return true if conflicting_equalities?(atoms, debug: debug)
122
+
123
+ transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
124
+ end
125
+
126
+ # Extracts symbol-numeric pairs and normalizes operator direction
127
+ # @param atom [Atom] constraint atom
128
+ # @return [Array(Symbol, Numeric, Symbol)] normalized [symbol, number, operator] or [nil, nil, nil]
129
+ def extract_symbol_numeric_pair(atom)
130
+ if atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Numeric)
131
+ [atom.lhs, atom.rhs, atom.op]
132
+ elsif atom.rhs.is_a?(Symbol) && atom.lhs.is_a?(Numeric)
133
+ [atom.rhs, atom.lhs, flip_operator(atom.op)]
134
+ else
135
+ [nil, nil, nil]
136
+ end
137
+ end
138
+
139
+ # Updates variable bounds based on constraint
140
+ # @param lowers [Hash] lower bounds by symbol
141
+ # @param uppers [Hash] upper bounds by symbol
142
+ # @param sym [Symbol] variable symbol
143
+ # @param num [Numeric] constraint value
144
+ # @param operator [Symbol] constraint operator
145
+ def update_bounds(lowers, uppers, sym, num, operator)
146
+ case operator
147
+ when :> then lowers[sym] = [lowers[sym], num + 1].max # x > 5 means x >= 6
148
+ when :>= then lowers[sym] = [lowers[sym], num].max # x >= 5 means x >= 5
149
+ when :< then uppers[sym] = [uppers[sym], num - 1].min # x < 5 means x <= 4
150
+ when :<= then uppers[sym] = [uppers[sym], num].min # x <= 5 means x <= 5
151
+ end
152
+ end
153
+
154
+ # Flips comparison operator for normalization
155
+ # @param operator [Symbol] original operator
156
+ # @return [Symbol] flipped operator
157
+ def flip_operator(operator)
158
+ { :> => :<, :>= => :<=, :< => :>, :<= => :>= }[operator]
159
+ end
160
+
161
+ # Detects always-false comparisons like 5 > 5, 100 < 100, etc.
162
+ # These represent impossible conditions since they can never be true
163
+ # @param atom [Atom] constraint atom to check
164
+ # @return [Boolean] true if comparison is always false
165
+ def always_false_comparison?(atom)
166
+ return false unless atom.lhs.is_a?(Numeric) && atom.rhs.is_a?(Numeric)
167
+
168
+ lhs = atom.lhs
169
+ rhs = atom.rhs
170
+ case atom.op
171
+ when :> then lhs <= rhs # 5 > 5 is always false
172
+ when :< then lhs >= rhs # 5 < 5 is always false
173
+ when :>= then lhs < rhs # 5 >= 6 is always false
174
+ when :<= then lhs > rhs # 6 <= 5 is always false
175
+ when :== then lhs != rhs # 5 == 6 is always false
176
+ when :!= then lhs == rhs # 5 != 5 is always false
177
+ else false
178
+ end
179
+ end
180
+
181
+ # Builds directed edges for strict inequality cycle detection
182
+ # Only creates edges when both endpoints are symbols (variables)
183
+ # Filters out symbol-numeric pairs (handled by numerical_contradiction?)
184
+ #
185
+ # @param atoms [Array<Atom>] constraint atoms
186
+ # @return [Array<Edge>] directed edges for cycle detection
187
+ def build_strict_inequality_edges(atoms)
188
+ atoms.filter_map do |atom|
189
+ next unless atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Symbol)
190
+
191
+ case atom.op
192
+ when :> then Edge.new(atom.rhs, atom.lhs) # x > y ⇒ edge y → x
193
+ when :< then Edge.new(atom.lhs, atom.rhs) # x < y ⇒ edge x → y
194
+ end
195
+ end
196
+ end
197
+
198
+ # Collects equality and strict inequality pairs for same-type operands
199
+ # @param atoms [Array<Atom>] constraint atoms
200
+ # @return [Array(Set, Set)] [equality pairs, strict inequality pairs]
201
+ def collect_equality_pairs(atoms)
202
+ equal_pairs = Set.new
203
+ strict_pairs = Set.new
204
+
205
+ atoms.each do |atom|
206
+ next unless atom.lhs.instance_of?(atom.rhs.class)
207
+
208
+ pair = [atom.lhs, atom.rhs].sort
209
+ case atom.op
210
+ when :==
211
+ equal_pairs << pair
212
+ when :>, :<
213
+ strict_pairs << pair
214
+ end
215
+ end
216
+
217
+ [equal_pairs, strict_pairs]
218
+ end
219
+
220
+ # Checks for direct equality contradictions (x == y AND x > y)
221
+ # @param equal_pairs [Set] equality constraint pairs
222
+ # @param strict_pairs [Set] strict inequality pairs
223
+ # @param debug [Boolean] enable debug output
224
+ # @return [Boolean] true if direct contradiction exists
225
+ def direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
226
+ conflicting_pairs = equal_pairs & strict_pairs
227
+ return false unless conflicting_pairs.any?
228
+
229
+ puts "equality contradiction detected" if debug
230
+ true
231
+ end
232
+
233
+ # Checks for conflicting equalities (x == a AND x == b where a != b)
234
+ # @param atoms [Array<Atom>] constraint atoms
235
+ # @param debug [Boolean] enable debug output
236
+ # @return [Boolean] true if conflicting equalities exist
237
+ def conflicting_equalities?(atoms, debug: false)
238
+ equalities = atoms.select { |atom| atom.op == :== }
239
+
240
+ # Group equalities by their left-hand side
241
+ by_lhs = equalities.group_by(&:lhs)
242
+
243
+ # Check each variable for conflicting equality constraints
244
+ by_lhs.each do |lhs, atoms_for_lhs|
245
+ next if atoms_for_lhs.size < 2
246
+
247
+ # Get all values this variable is constrained to equal
248
+ values = atoms_for_lhs.map(&:rhs).uniq
249
+
250
+ if values.size > 1
251
+ puts "conflicting equalities detected: #{lhs} == #{values.join(" AND #{lhs} == ")}" if debug
252
+ return true
253
+ end
254
+ end
255
+
256
+ false
257
+ end
258
+
259
+ # Checks for transitive equality contradictions using union-find
260
+ # @param equal_pairs [Set] equality constraint pairs
261
+ # @param strict_pairs [Set] strict inequality pairs
262
+ # @param debug [Boolean] enable debug output
263
+ # @return [Boolean] true if transitive contradiction exists
264
+ def transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
265
+ equiv_classes = build_equivalence_classes(equal_pairs)
266
+ equiv_classes.each do |equiv_class|
267
+ equiv_class.combination(2).each do |var1, var2|
268
+ pair = [var1, var2].sort
269
+ next unless strict_pairs.include?(pair)
270
+
271
+ puts "transitive equality contradiction detected" if debug
272
+ return true
273
+ end
274
+ end
275
+ false
276
+ end
277
+
278
+ # Builds equivalence classes using union-find algorithm
279
+ # @param equal_pairs [Set] equality constraint pairs
280
+ # @return [Array<Array>] equivalence classes (groups of equal variables)
281
+ def build_equivalence_classes(equal_pairs)
282
+ parent = Hash.new { |h, k| h[k] = k }
283
+
284
+ equal_pairs.each do |pair|
285
+ root1 = find_root(pair[0], parent)
286
+ root2 = find_root(pair[1], parent)
287
+ parent[root1] = root2
288
+ end
289
+
290
+ group_variables_by_root(parent)
291
+ end
292
+
293
+ # Finds root of equivalence class with path compression
294
+ # @param element [Object] element to find root for
295
+ # @param parent [Hash] parent pointers for union-find
296
+ # @return [Object] root element of equivalence class
297
+ def find_root(element, parent)
298
+ return element if parent[element] == element
299
+
300
+ parent[element] = find_root(parent[element], parent)
301
+ parent[element]
302
+ end
303
+
304
+ # Groups variables by their equivalence class root
305
+ # @param parent [Hash] parent pointers for union-find
306
+ # @return [Array<Array>] equivalence classes with multiple elements
307
+ def group_variables_by_root(parent)
308
+ groups = Hash.new { |h, k| h[k] = [] }
309
+ parent.each_key do |var|
310
+ groups[find_root(var, parent)] << var
311
+ end
312
+ groups.values.select { |group| group.size > 1 }
313
+ end
314
+ end
315
+
316
+ # Stack-safe strict inequality cycle detector using Kahn's topological sort algorithm
317
+ #
318
+ # This module implements iterative cycle detection to avoid SystemStackError on deep graphs.
319
+ # Uses Kahn's algorithm: if topological sort cannot order all vertices, a cycle exists.
320
+ module StrictInequalitySolver
321
+ module_function
322
+
323
+ # Detects cycles in directed graph using stack-safe Kahn's topological sort
324
+ #
325
+ # @param edges [Array<Edge>] directed edges representing strict inequalities
326
+ # @param debug [Boolean] enable debug output
327
+ # @return [Boolean] true if cycle exists
328
+ def cycle?(edges, debug: false)
329
+ return false if edges.empty?
330
+
331
+ graph, in_degree = build_graph_with_degrees(edges)
332
+ processed_count = kahns_algorithm(graph, in_degree)
333
+
334
+ detect_cycle_from_processing_count(processed_count, graph.size, debug)
335
+ end
336
+
337
+ def kahns_algorithm(graph, in_degree)
338
+ queue = graph.keys.select { |v| in_degree[v].zero? }
339
+ processed_count = 0
340
+
341
+ until queue.empty?
342
+ vertex = queue.shift
343
+ processed_count += 1
344
+
345
+ graph[vertex].each do |neighbor|
346
+ in_degree[neighbor] -= 1
347
+ queue << neighbor if in_degree[neighbor].zero?
348
+ end
349
+ end
350
+
351
+ processed_count
352
+ end
353
+
354
+ def detect_cycle_from_processing_count(processed_count, total_vertices, debug)
355
+ has_cycle = processed_count < total_vertices
356
+ puts "cycle detected in strict inequality graph" if has_cycle && debug
357
+ has_cycle
358
+ end
359
+
360
+ # Builds adjacency list graph and in-degree counts from edges
361
+ # Pre-populates all vertices (including those with no outgoing edges) to avoid mutation during iteration
362
+ #
363
+ # @param edges [Array<Edge>] directed edges
364
+ # @return [Array(Hash, Hash)] [adjacency_list, in_degree_counts]
365
+ def build_graph_with_degrees(edges)
366
+ vertices = collect_all_vertices(edges)
367
+ graph, in_degree = initialize_graph_structures(vertices)
368
+ populate_graph_data(edges, graph, in_degree)
369
+ [graph, in_degree]
370
+ end
371
+
372
+ def collect_all_vertices(edges)
373
+ vertices = Set.new
374
+ edges.each { |e| vertices << e.from << e.to }
375
+ vertices
376
+ end
377
+
378
+ def initialize_graph_structures(vertices)
379
+ graph = Hash.new { |h, k| h[k] = [] }
380
+ in_degree = Hash.new(0)
381
+ vertices.each do |v|
382
+ graph[v]
383
+ in_degree[v] = 0
384
+ end
385
+ [graph, in_degree]
386
+ end
387
+
388
+ def populate_graph_data(edges, graph, in_degree)
389
+ edges.each do |edge|
390
+ graph[edge.from] << edge.to
391
+ in_degree[edge.to] += 1
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ class CompiledSchema
6
+ attr_reader :bindings
7
+
8
+ def initialize(bindings)
9
+ @bindings = bindings.freeze
10
+ end
11
+
12
+ def evaluate(ctx, *key_names)
13
+ target_keys = key_names.empty? ? @bindings.keys : validate_keys(key_names)
14
+
15
+ target_keys.each_with_object({}) do |key, result|
16
+ result[key] = evaluate_binding(key, ctx)
17
+ end
18
+ end
19
+
20
+ def evaluate_binding(key, ctx)
21
+ memo = ctx.instance_variable_get(:@__schema_cache__)
22
+ return memo[key] if memo&.key?(key)
23
+
24
+ value = @bindings[key][1].call(ctx)
25
+ memo[key] = value if memo
26
+ value
27
+ end
28
+
29
+ private
30
+
31
+ def hash_like?(obj)
32
+ obj.respond_to?(:key?) && obj.respond_to?(:[])
33
+ end
34
+
35
+ def validate_keys(keys)
36
+ unknown_keys = keys - @bindings.keys
37
+ return keys if unknown_keys.empty?
38
+
39
+ raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
40
+ end
41
+ end
42
+ end
43
+ end