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.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -1
  3. data/README.md +21 -5
  4. data/docs/AST.md +7 -0
  5. data/docs/features/README.md +7 -0
  6. data/docs/features/s-expression-printer.md +77 -0
  7. data/examples/game_of_life.rb +1 -1
  8. data/examples/static_analysis_errors.rb +7 -7
  9. data/lib/kumi/analyzer.rb +15 -15
  10. data/lib/kumi/compiler.rb +6 -6
  11. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  12. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  13. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  14. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  15. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  16. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  17. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  18. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  19. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  20. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  21. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  22. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  23. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  24. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  25. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  26. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  27. data/lib/kumi/core/compiled_schema.rb +43 -0
  28. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  29. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  30. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  31. data/lib/kumi/core/domain/validator.rb +82 -0
  32. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  33. data/lib/kumi/core/error_reporter.rb +166 -0
  34. data/lib/kumi/core/error_reporting.rb +97 -0
  35. data/lib/kumi/core/errors.rb +120 -0
  36. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  37. data/lib/kumi/core/explain.rb +295 -0
  38. data/lib/kumi/core/export/deserializer.rb +41 -0
  39. data/lib/kumi/core/export/errors.rb +14 -0
  40. data/lib/kumi/core/export/node_builders.rb +142 -0
  41. data/lib/kumi/core/export/node_registry.rb +54 -0
  42. data/lib/kumi/core/export/node_serializers.rb +158 -0
  43. data/lib/kumi/core/export/serializer.rb +25 -0
  44. data/lib/kumi/core/export.rb +35 -0
  45. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  46. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  47. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  48. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  49. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  50. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  51. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  52. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  53. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  54. data/lib/kumi/core/input/type_matcher.rb +97 -0
  55. data/lib/kumi/core/input/validator.rb +51 -0
  56. data/lib/kumi/core/input/violation_creator.rb +52 -0
  57. data/lib/kumi/core/json_schema/generator.rb +65 -0
  58. data/lib/kumi/core/json_schema/validator.rb +27 -0
  59. data/lib/kumi/core/json_schema.rb +16 -0
  60. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  61. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  62. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  63. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  64. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  65. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  66. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  67. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  68. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  69. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  70. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  71. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  72. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  73. data/lib/kumi/core/ruby_parser.rb +12 -0
  74. data/lib/kumi/core/schema_instance.rb +111 -0
  75. data/lib/kumi/core/types/builder.rb +23 -0
  76. data/lib/kumi/core/types/compatibility.rb +96 -0
  77. data/lib/kumi/core/types/formatter.rb +26 -0
  78. data/lib/kumi/core/types/inference.rb +42 -0
  79. data/lib/kumi/core/types/normalizer.rb +72 -0
  80. data/lib/kumi/core/types/validator.rb +37 -0
  81. data/lib/kumi/core/types.rb +66 -0
  82. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  83. data/lib/kumi/errors.rb +1 -112
  84. data/lib/kumi/registry.rb +37 -0
  85. data/lib/kumi/schema.rb +5 -5
  86. data/lib/kumi/schema_metadata.rb +3 -3
  87. data/lib/kumi/support/s_expression_printer.rb +161 -0
  88. data/lib/kumi/syntax/array_expression.rb +6 -6
  89. data/lib/kumi/syntax/call_expression.rb +4 -4
  90. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  91. data/lib/kumi/syntax/case_expression.rb +4 -4
  92. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  93. data/lib/kumi/syntax/hash_expression.rb +4 -4
  94. data/lib/kumi/syntax/input_declaration.rb +5 -5
  95. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  96. data/lib/kumi/syntax/input_reference.rb +5 -5
  97. data/lib/kumi/syntax/literal.rb +4 -4
  98. data/lib/kumi/syntax/node.rb +34 -34
  99. data/lib/kumi/syntax/root.rb +6 -6
  100. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  101. data/lib/kumi/syntax/value_declaration.rb +4 -4
  102. data/lib/kumi/version.rb +1 -1
  103. data/migrate_to_core_iterative.rb +938 -0
  104. data/scripts/generate_function_docs.rb +9 -9
  105. metadata +77 -72
  106. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  107. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  108. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
  109. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  110. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  111. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  112. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  113. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  114. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
  115. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  116. data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
  117. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  118. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  119. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
  120. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  121. data/lib/kumi/atom_unsat_solver.rb +0 -394
  122. data/lib/kumi/compiled_schema.rb +0 -41
  123. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  124. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  125. data/lib/kumi/domain/range_analyzer.rb +0 -83
  126. data/lib/kumi/domain/validator.rb +0 -80
  127. data/lib/kumi/domain/violation_formatter.rb +0 -40
  128. data/lib/kumi/error_reporter.rb +0 -164
  129. data/lib/kumi/error_reporting.rb +0 -95
  130. data/lib/kumi/evaluation_wrapper.rb +0 -38
  131. data/lib/kumi/explain.rb +0 -293
  132. data/lib/kumi/export/deserializer.rb +0 -39
  133. data/lib/kumi/export/errors.rb +0 -12
  134. data/lib/kumi/export/node_builders.rb +0 -140
  135. data/lib/kumi/export/node_registry.rb +0 -52
  136. data/lib/kumi/export/node_serializers.rb +0 -156
  137. data/lib/kumi/export/serializer.rb +0 -23
  138. data/lib/kumi/export.rb +0 -33
  139. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  140. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  141. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  142. data/lib/kumi/function_registry/function_builder.rb +0 -93
  143. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  144. data/lib/kumi/function_registry/math_functions.rb +0 -72
  145. data/lib/kumi/function_registry/string_functions.rb +0 -54
  146. data/lib/kumi/function_registry/type_functions.rb +0 -51
  147. data/lib/kumi/input/type_matcher.rb +0 -95
  148. data/lib/kumi/input/validator.rb +0 -49
  149. data/lib/kumi/input/violation_creator.rb +0 -50
  150. data/lib/kumi/json_schema/generator.rb +0 -63
  151. data/lib/kumi/json_schema/validator.rb +0 -25
  152. data/lib/kumi/json_schema.rb +0 -14
  153. data/lib/kumi/ruby_parser/build_context.rb +0 -25
  154. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
  155. data/lib/kumi/ruby_parser/dsl.rb +0 -12
  156. data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
  157. data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
  158. data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
  159. data/lib/kumi/ruby_parser/input_builder.rb +0 -125
  160. data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
  161. data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
  162. data/lib/kumi/ruby_parser/nested_input.rb +0 -15
  163. data/lib/kumi/ruby_parser/parser.rb +0 -69
  164. data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
  165. data/lib/kumi/ruby_parser/sugar.rb +0 -261
  166. data/lib/kumi/ruby_parser.rb +0 -10
  167. data/lib/kumi/schema_instance.rb +0 -109
  168. data/lib/kumi/types/builder.rb +0 -21
  169. data/lib/kumi/types/compatibility.rb +0 -94
  170. data/lib/kumi/types/formatter.rb +0 -24
  171. data/lib/kumi/types/inference.rb +0 -40
  172. data/lib/kumi/types/normalizer.rb +0 -70
  173. data/lib/kumi/types/validator.rb +0 -35
  174. data/lib/kumi/types.rb +0 -64
  175. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -0,0 +1,406 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
8
+ # DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
9
+ # PRODUCES: :cascades - Hash of cascade mutual exclusion analysis results
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ class UnsatDetector < VisitorPass
12
+ include Syntax
13
+
14
+ COMPARATORS = %i[> < >= <= == !=].freeze
15
+ Atom = Kumi::Core::AtomUnsatSolver::Atom
16
+
17
+ def run(errors)
18
+ definitions = get_state(:declarations)
19
+ @input_meta = get_state(:inputs) || {}
20
+ @definitions = definitions
21
+ @evaluator = ConstantEvaluator.new(definitions)
22
+
23
+ # First pass: analyze cascade conditions for mutual exclusion
24
+ cascades = {}
25
+ each_decl do |decl|
26
+ cascades[decl.name] = analyze_cascade_mutual_exclusion(decl, definitions) if decl.expression.is_a?(CascadeExpression)
27
+
28
+ # Store cascade metadata for later passes
29
+
30
+ # Second pass: check for unsatisfiable constraints
31
+ if decl.expression.is_a?(CascadeExpression)
32
+ # Special handling for cascade expressions
33
+ check_cascade_expression(decl, definitions, errors)
34
+ elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
35
+ # Check for OR expressions which need special disjunctive handling
36
+ impossible = check_or_expression(decl.expression, definitions, errors)
37
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
38
+ else
39
+ # Normal handling for non-cascade expressions
40
+ atoms = gather_atoms(decl.expression, definitions, Set.new)
41
+ next if atoms.empty?
42
+
43
+ # Use enhanced solver that can detect cross-variable mathematical constraints
44
+ impossible = if definitions && !definitions.empty?
45
+ Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
46
+ else
47
+ Kumi::Core::AtomUnsatSolver.unsat?(atoms)
48
+ end
49
+
50
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
51
+ end
52
+ end
53
+ state.with(:cascades, cascades)
54
+ end
55
+
56
+ private
57
+
58
+ def analyze_cascade_mutual_exclusion(decl, definitions)
59
+ conditions = []
60
+ condition_traits = []
61
+
62
+ # Extract all cascade conditions (except base case)
63
+ decl.expression.cases[0...-1].each do |when_case|
64
+ next unless when_case.condition
65
+
66
+ next unless when_case.condition.fn_name == :all?
67
+
68
+ when_case.condition.args.each do |arg|
69
+ next unless arg.is_a?(ArrayExpression)
70
+
71
+ arg.elements.each do |element|
72
+ next unless element.is_a?(DeclarationReference)
73
+
74
+ trait_name = element.name
75
+ trait = definitions[trait_name]
76
+ if trait
77
+ conditions << trait.expression
78
+ condition_traits << trait_name
79
+ end
80
+ end
81
+ end
82
+ # end
83
+ end
84
+
85
+ # Check mutual exclusion for all pairs
86
+ total_pairs = conditions.size * (conditions.size - 1) / 2
87
+ exclusive_pairs = 0
88
+
89
+ if conditions.size >= 2
90
+ conditions.combination(2).each do |cond1, cond2|
91
+ exclusive_pairs += 1 if conditions_mutually_exclusive?(cond1, cond2)
92
+ end
93
+ end
94
+
95
+ all_mutually_exclusive = total_pairs.positive? && (exclusive_pairs == total_pairs)
96
+
97
+ {
98
+ condition_traits: condition_traits,
99
+ condition_count: conditions.size,
100
+ all_mutually_exclusive: all_mutually_exclusive,
101
+ exclusive_pairs: exclusive_pairs,
102
+ total_pairs: total_pairs
103
+ }
104
+ end
105
+
106
+ def conditions_mutually_exclusive?(cond1, cond2)
107
+ if cond1.is_a?(CallExpression) && cond1.fn_name == :== &&
108
+ cond2.is_a?(CallExpression) && cond2.fn_name == :==
109
+
110
+ c1_field, c1_value = cond1.args
111
+ c2_field, c2_value = cond2.args
112
+
113
+ # Same field, different values = mutually exclusive
114
+ return true if same_field?(c1_field, c2_field) && different_values?(c1_value, c2_value)
115
+ end
116
+
117
+ false
118
+ end
119
+
120
+ def same_field?(field1, field2)
121
+ return false unless field1.is_a?(InputReference) && field2.is_a?(InputReference)
122
+
123
+ field1.name == field2.name
124
+ end
125
+
126
+ def different_values?(val1, val2)
127
+ return false unless val1.is_a?(Literal) && val2.is_a?(Literal)
128
+
129
+ val1.value != val2.value
130
+ end
131
+
132
+ def check_or_expression(or_expr, definitions, _errors)
133
+ # For OR expressions: A | B is impossible only if BOTH A AND B are impossible
134
+ # If either side is satisfiable, the OR is satisfiable
135
+ left_side, right_side = or_expr.args
136
+
137
+ # Check if left side is impossible
138
+ left_atoms = gather_atoms(left_side, definitions, Set.new)
139
+ left_impossible = if left_atoms.empty?
140
+ false
141
+ elsif definitions && !definitions.empty?
142
+ Kumi::Core::ConstraintRelationshipSolver.unsat?(left_atoms, definitions, input_meta: @input_meta)
143
+ else
144
+ Kumi::Core::AtomUnsatSolver.unsat?(left_atoms)
145
+ end
146
+
147
+ # Check if right side is impossible
148
+ right_atoms = gather_atoms(right_side, definitions, Set.new)
149
+ right_impossible = if right_atoms.empty?
150
+ false
151
+ elsif definitions && !definitions.empty?
152
+ Kumi::Core::ConstraintRelationshipSolver.unsat?(right_atoms, definitions, input_meta: @input_meta)
153
+ else
154
+ Kumi::Core::AtomUnsatSolver.unsat?(right_atoms)
155
+ end
156
+
157
+ # OR is impossible only if BOTH sides are impossible
158
+ left_impossible && right_impossible
159
+ end
160
+
161
+ def gather_atoms(node, defs, visited, list = [])
162
+ return list unless node
163
+
164
+ # Use iterative approach with stack to avoid SystemStackError on deep graphs
165
+ stack = [node]
166
+
167
+ until stack.empty?
168
+ current = stack.pop
169
+ next unless current
170
+
171
+ if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
172
+ lhs, rhs = current.args
173
+
174
+ # Check for domain constraint violations before creating atom
175
+ list << if impossible_constraint?(lhs, rhs, current.fn_name)
176
+ # Create a special impossible atom that will always trigger unsat
177
+ Atom.new(:==, :__impossible__, true)
178
+ else
179
+ Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
180
+ end
181
+ elsif current.is_a?(CallExpression) && current.fn_name == :or
182
+ # Special handling for OR expressions - they are disjunctive, not conjunctive
183
+ # We should NOT add OR children to the stack as they would be treated as AND
184
+ # OR expressions need separate analysis in the main run() method
185
+ next
186
+ elsif current.is_a?(CallExpression) && current.fn_name == :all?
187
+ # For all? function, add all trait arguments to the stack
188
+ current.args.each { |arg| stack << arg }
189
+ elsif current.is_a?(ArrayExpression)
190
+ # For ArrayExpression, add all elements to the stack
191
+ current.elements.each { |elem| stack << elem }
192
+ elsif current.is_a?(DeclarationReference)
193
+ name = current.name
194
+ unless visited.include?(name)
195
+ visited << name
196
+ stack << defs[name].expression if defs.key?(name)
197
+ end
198
+ end
199
+
200
+ # Add children to stack for processing
201
+ # IMPORTANT: Skip CascadeExpression children to avoid false positives
202
+ # Cascades are handled separately by check_cascade_expression() and are disjunctive,
203
+ # but gather_atoms() treats all collected atoms as conjunctive
204
+ current.children.each { |child| stack << child } if current.respond_to?(:children) && !current.is_a?(CascadeExpression)
205
+ end
206
+
207
+ list
208
+ end
209
+
210
+ def check_cascade_expression(decl, definitions, errors)
211
+ # Analyze each cascade branch condition independently
212
+ # This is the correct behavior: each 'on' condition should be checked separately
213
+ # since only ONE will be evaluated at runtime (they're mutually exclusive by design)
214
+
215
+ decl.expression.cases.each_with_index do |when_case, _index|
216
+ # Skip the base case (it's typically a literal true condition)
217
+ next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
218
+
219
+ # Skip non-conjunctive conditions (any?, none?) as they are disjunctive
220
+ next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
221
+
222
+ # Skip single-trait 'on' branches: trait-level unsat detection covers these
223
+ if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
224
+ # Handle both ArrayExpression (old format) and multiple args (new format)
225
+ if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
226
+ list = when_case.condition.args.first
227
+ next if list.elements.size == 1
228
+ elsif when_case.condition.args.size == 1
229
+ # Multiple args format
230
+ next
231
+ end
232
+ end
233
+ # Gather atoms from this individual condition only
234
+ condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
235
+ # DEBUG
236
+ # if when_case.condition.is_a?(CallExpression) && [:all?, :any?, :none?].include?(when_case.condition.fn_name)
237
+ # puts " Args: #{when_case.condition.args.inspect}"
238
+ # puts " Atoms found: #{condition_atoms.inspect}"
239
+ # end
240
+
241
+ # Only flag if this individual condition is impossible
242
+ # if !condition_atoms.empty?
243
+ # is_unsat = Kumi::Core::AtomUnsatSolver.unsat?(condition_atoms)
244
+ # puts " Is unsat? #{is_unsat}"
245
+ # end
246
+ # Use enhanced solver for cascade conditions too
247
+ impossible = if definitions && !definitions.empty?
248
+ Kumi::Core::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
249
+ else
250
+ Kumi::Core::AtomUnsatSolver.unsat?(condition_atoms)
251
+ end
252
+ next unless !condition_atoms.empty? && impossible
253
+
254
+ # For multi-trait on-clauses, report the trait names rather than the value name
255
+ if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
256
+ # Handle both ArrayExpression (old format) and multiple args (new format)
257
+ trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
258
+ when_case.condition.args.first.elements
259
+ else
260
+ when_case.condition.args
261
+ end
262
+
263
+ if trait_bindings.all?(DeclarationReference)
264
+ traits = trait_bindings.map(&:name).join(" AND ")
265
+ report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
266
+ next
267
+ end
268
+ end
269
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc)
270
+ end
271
+ end
272
+
273
+ def term(node, _defs)
274
+ case node
275
+ when InputReference, DeclarationReference
276
+ val = @evaluator.evaluate(node)
277
+ val == :unknown ? node.name : val
278
+ when Literal
279
+ node.value
280
+ else
281
+ :unknown
282
+ end
283
+ end
284
+
285
+ def check_domain_constraints(node, definitions, errors)
286
+ case node
287
+ when InputReference
288
+ # Check if InputReference points to a field with domain constraints
289
+ field_meta = @input_meta[node.name]
290
+ nil unless field_meta&.dig(:domain)
291
+
292
+ # For InputReference, the constraint comes from trait conditions
293
+ # We don't flag here since the InputReference itself is valid
294
+ when DeclarationReference
295
+ # Check if this binding evaluates to a value that violates domain constraints
296
+ definition = definitions[node.name]
297
+ return unless definition
298
+
299
+ if definition.expression.is_a?(Literal)
300
+ literal_value = definition.expression.value
301
+ check_value_against_domains(node.name, literal_value, errors, definition.loc)
302
+ end
303
+ end
304
+ end
305
+
306
+ def check_value_against_domains(_var_name, value, _errors, _location)
307
+ # Check if this value violates any input domain constraints
308
+ @input_meta.each_value do |field_meta|
309
+ domain = field_meta[:domain]
310
+ next unless domain
311
+
312
+ if violates_domain?(value, domain)
313
+ # This indicates a constraint that can never be satisfied
314
+ # Rather than flagging the cascade, flag the impossible condition
315
+ return true
316
+ end
317
+ end
318
+ false
319
+ end
320
+
321
+ def violates_domain?(value, domain)
322
+ case domain
323
+ when Range
324
+ !domain.include?(value)
325
+ when Array
326
+ !domain.include?(value)
327
+ when Proc
328
+ # For Proc domains, we can't statically analyze
329
+ false
330
+ else
331
+ false
332
+ end
333
+ end
334
+
335
+ def impossible_constraint?(lhs, rhs, operator)
336
+ # Case 1: InputReference compared against value outside its domain
337
+ if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
338
+ return field_literal_impossible?(lhs, rhs, operator)
339
+ elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
340
+ # Reverse case: literal compared to field
341
+ return field_literal_impossible?(rhs, lhs, flip_operator(operator))
342
+ end
343
+
344
+ # Case 2: DeclarationReference that evaluates to literal compared against impossible value
345
+ if lhs.is_a?(DeclarationReference) && rhs.is_a?(Literal)
346
+ return binding_literal_impossible?(lhs, rhs, operator)
347
+ elsif rhs.is_a?(DeclarationReference) && lhs.is_a?(Literal)
348
+ return binding_literal_impossible?(rhs, lhs, flip_operator(operator))
349
+ end
350
+
351
+ false
352
+ end
353
+
354
+ def field_literal_impossible?(field_ref, literal, operator)
355
+ field_meta = @input_meta[field_ref.name]
356
+ return false unless field_meta&.dig(:domain)
357
+
358
+ domain = field_meta[:domain]
359
+ literal_value = literal.value
360
+
361
+ case operator
362
+ when :==
363
+ # field == value where value is not in domain
364
+ violates_domain?(literal_value, domain)
365
+ when :!=
366
+ # field != value where value is not in domain is always true (not impossible)
367
+ false
368
+ else
369
+ # For other operators, we'd need more sophisticated analysis
370
+ false
371
+ end
372
+ end
373
+
374
+ def binding_literal_impossible?(binding, literal, operator)
375
+ # Check if binding evaluates to a literal that conflicts with the comparison
376
+ evaluated_value = @evaluator.evaluate(binding)
377
+ return false if evaluated_value == :unknown
378
+
379
+ literal_value = literal.value
380
+
381
+ case operator
382
+ when :==
383
+ # binding == value where binding evaluates to different value
384
+ evaluated_value != literal_value
385
+ else
386
+ # For other operators, we could add more sophisticated checking
387
+ false
388
+ end
389
+ end
390
+
391
+ def flip_operator(operator)
392
+ case operator
393
+ when :> then :<
394
+ when :>= then :<=
395
+ when :< then :>
396
+ when :<= then :>=
397
+ when :== then :==
398
+ when :!= then :!=
399
+ else operator
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # Base class for analyzer passes that need to traverse the AST using the visitor pattern.
8
+ # Inherits the new immutable state interface from PassBase.
9
+ class VisitorPass < PassBase
10
+ # Visit a node and all its children using depth-first traversal
11
+ # @param node [Syntax::Node] The node to visit
12
+ # @yield [Syntax::Node] Each node in the traversal
13
+ def visit(node, &block)
14
+ return unless node
15
+
16
+ yield(node)
17
+ node.children.each { |child| visit(child, &block) }
18
+ end
19
+
20
+ protected
21
+
22
+ # Helper to visit each declaration's expression tree
23
+ # @param errors [Array] Error accumulator
24
+ # @yield [Syntax::Node, Syntax::Base] Each node and its containing declaration
25
+ def visit_all_expressions(errors)
26
+ each_decl do |decl|
27
+ visit(decl.expression) { |node| yield(node, decl, errors) }
28
+ end
29
+ end
30
+
31
+ # Helper to visit only specific node types
32
+ # @param node_types [Array<Class>] Node types to match
33
+ # @param errors [Array] Error accumulator
34
+ # @yield [Syntax::Node, Syntax::Base] Matching nodes and their declarations
35
+ def visit_nodes_of_type(*node_types, errors:)
36
+ visit_all_expressions(errors) do |node, decl, errs|
37
+ yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end