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
@@ -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
@@ -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