kumi 0.0.4 → 0.0.6

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -3,6 +3,10 @@
3
3
  module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
+ # RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
7
+ # DEPENDENCIES: :definitions from NameIndexer, :input_meta from InputCollector
8
+ # PRODUCES: :cascade_metadata - Hash of cascade mutual exclusion analysis results
9
+ # INTERFACE: new(schema, state).run(errors)
6
10
  class UnsatDetector < VisitorPass
7
11
  include Syntax
8
12
 
@@ -11,24 +15,150 @@ module Kumi
11
15
 
12
16
  def run(errors)
13
17
  definitions = get_state(:definitions)
18
+ @input_meta = get_state(:input_meta) || {}
19
+ @definitions = definitions
14
20
  @evaluator = ConstantEvaluator.new(definitions)
15
21
 
22
+ # First pass: analyze cascade conditions for mutual exclusion
23
+ cascade_metadata = {}
24
+ each_decl do |decl|
25
+ cascade_metadata[decl.name] = analyze_cascade_mutual_exclusion(decl, definitions) if decl.expression.is_a?(CascadeExpression)
26
+ end
27
+
28
+ # Store cascade metadata for later passes
29
+
30
+ # Second pass: check for unsatisfiable constraints
16
31
  each_decl do |decl|
17
32
  if decl.expression.is_a?(CascadeExpression)
18
33
  # Special handling for cascade expressions
19
34
  check_cascade_expression(decl, definitions, errors)
35
+ elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
36
+ # Check for OR expressions which need special disjunctive handling
37
+ impossible = check_or_expression(decl.expression, definitions, errors)
38
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
20
39
  else
21
40
  # Normal handling for non-cascade expressions
22
41
  atoms = gather_atoms(decl.expression, definitions, Set.new)
23
42
  next if atoms.empty?
24
43
 
25
- add_error(errors, decl.loc, "conjunction `#{decl.name}` is impossible") if Kumi::AtomUnsatSolver.unsat?(atoms)
44
+ # Use enhanced solver that can detect cross-variable mathematical constraints
45
+ impossible = if definitions && !definitions.empty?
46
+ Kumi::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
47
+ else
48
+ Kumi::AtomUnsatSolver.unsat?(atoms)
49
+ end
50
+
51
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
26
52
  end
27
53
  end
54
+ state.with(:cascade_metadata, cascade_metadata)
28
55
  end
29
56
 
30
57
  private
31
58
 
59
+ def analyze_cascade_mutual_exclusion(decl, definitions)
60
+ conditions = []
61
+ condition_traits = []
62
+
63
+ # Extract all cascade conditions (except base case)
64
+ decl.expression.cases[0...-1].each do |when_case|
65
+ next unless when_case.condition
66
+
67
+ next unless when_case.condition.fn_name == :all?
68
+
69
+ when_case.condition.args.each do |arg|
70
+ next unless arg.is_a?(ArrayExpression)
71
+
72
+ arg.elements.each do |element|
73
+ next unless element.is_a?(DeclarationReference)
74
+
75
+ trait_name = element.name
76
+ trait = definitions[trait_name]
77
+ if trait
78
+ conditions << trait.expression
79
+ condition_traits << trait_name
80
+ end
81
+ end
82
+ end
83
+ # end
84
+ end
85
+
86
+ # Check mutual exclusion for all pairs
87
+ total_pairs = conditions.size * (conditions.size - 1) / 2
88
+ exclusive_pairs = 0
89
+
90
+ if conditions.size >= 2
91
+ conditions.combination(2).each do |cond1, cond2|
92
+ exclusive_pairs += 1 if conditions_mutually_exclusive?(cond1, cond2)
93
+ end
94
+ end
95
+
96
+ all_mutually_exclusive = (total_pairs > 0) && (exclusive_pairs == total_pairs)
97
+
98
+ {
99
+ condition_traits: condition_traits,
100
+ condition_count: conditions.size,
101
+ all_mutually_exclusive: all_mutually_exclusive,
102
+ exclusive_pairs: exclusive_pairs,
103
+ total_pairs: total_pairs
104
+ }
105
+ end
106
+
107
+ def conditions_mutually_exclusive?(cond1, cond2)
108
+ if cond1.is_a?(CallExpression) && cond1.fn_name == :== &&
109
+ cond2.is_a?(CallExpression) && cond2.fn_name == :==
110
+
111
+ c1_field, c1_value = cond1.args
112
+ c2_field, c2_value = cond2.args
113
+
114
+ # Same field, different values = mutually exclusive
115
+ return true if same_field?(c1_field, c2_field) && different_values?(c1_value, c2_value)
116
+ end
117
+
118
+ false
119
+ end
120
+
121
+ def same_field?(field1, field2)
122
+ return false unless field1.is_a?(InputReference) && field2.is_a?(InputReference)
123
+
124
+ field1.name == field2.name
125
+ end
126
+
127
+ def different_values?(val1, val2)
128
+ return false unless val1.is_a?(Literal) && val2.is_a?(Literal)
129
+
130
+ val1.value != val2.value
131
+ end
132
+
133
+ def check_or_expression(or_expr, definitions, errors)
134
+ # For OR expressions: A | B is impossible only if BOTH A AND B are impossible
135
+ # If either side is satisfiable, the OR is satisfiable
136
+ left_side, right_side = or_expr.args
137
+
138
+ # Check if left side is impossible
139
+ left_atoms = gather_atoms(left_side, definitions, Set.new)
140
+ left_impossible = if left_atoms.empty?
141
+ false
142
+ elsif definitions && !definitions.empty?
143
+ Kumi::ConstraintRelationshipSolver.unsat?(left_atoms, definitions, input_meta: @input_meta)
144
+ else
145
+ Kumi::AtomUnsatSolver.unsat?(left_atoms)
146
+ end
147
+
148
+ # Check if right side is impossible
149
+ right_atoms = gather_atoms(right_side, definitions, Set.new)
150
+ right_impossible = if right_atoms.empty?
151
+ false
152
+ elsif definitions && !definitions.empty?
153
+ Kumi::ConstraintRelationshipSolver.unsat?(right_atoms, definitions, input_meta: @input_meta)
154
+ else
155
+ Kumi::AtomUnsatSolver.unsat?(right_atoms)
156
+ end
157
+
158
+ # OR is impossible only if BOTH sides are impossible
159
+ left_impossible && right_impossible
160
+ end
161
+
32
162
  def gather_atoms(node, defs, visited, list = [])
33
163
  return list unless node
34
164
 
@@ -41,8 +171,26 @@ module Kumi
41
171
 
42
172
  if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
43
173
  lhs, rhs = current.args
44
- list << Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
45
- elsif current.is_a?(Binding)
174
+
175
+ # Check for domain constraint violations before creating atom
176
+ list << if impossible_constraint?(lhs, rhs, current.fn_name)
177
+ # Create a special impossible atom that will always trigger unsat
178
+ Atom.new(:==, :__impossible__, true)
179
+ else
180
+ Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
181
+ end
182
+ elsif current.is_a?(CallExpression) && current.fn_name == :or
183
+ # Special handling for OR expressions - they are disjunctive, not conjunctive
184
+ # We should NOT add OR children to the stack as they would be treated as AND
185
+ # OR expressions need separate analysis in the main run() method
186
+ next
187
+ elsif current.is_a?(CallExpression) && current.fn_name == :all?
188
+ # For all? function, add all trait arguments to the stack
189
+ current.args.each { |arg| stack << arg }
190
+ elsif current.is_a?(ArrayExpression)
191
+ # For ArrayExpression, add all elements to the stack
192
+ current.elements.each { |elem| stack << elem }
193
+ elsif current.is_a?(DeclarationReference)
46
194
  name = current.name
47
195
  unless visited.include?(name)
48
196
  visited << name
@@ -51,7 +199,10 @@ module Kumi
51
199
  end
52
200
 
53
201
  # Add children to stack for processing
54
- current.children.each { |child| stack << child } if current.respond_to?(:children)
202
+ # IMPORTANT: Skip CascadeExpression children to avoid false positives
203
+ # Cascades are handled separately by check_cascade_expression() and are disjunctive,
204
+ # but gather_atoms() treats all collected atoms as conjunctive
205
+ current.children.each { |child| stack << child } if current.respond_to?(:children) && !current.is_a?(CascadeExpression)
55
206
  end
56
207
 
57
208
  list
@@ -66,33 +217,63 @@ module Kumi
66
217
  # Skip the base case (it's typically a literal true condition)
67
218
  next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
68
219
 
220
+ # Skip non-conjunctive conditions (any?, none?) as they are disjunctive
221
+ next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
222
+
69
223
  # Skip single-trait 'on' branches: trait-level unsat detection covers these
70
224
  if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
71
- list = when_case.condition.args.first
72
- next if list.is_a?(ListExpression) && list.elements.size < 2
225
+ # Handle both ArrayExpression (old format) and multiple args (new format)
226
+ if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
227
+ list = when_case.condition.args.first
228
+ next if list.elements.size == 1
229
+ elsif when_case.condition.args.size == 1
230
+ # Multiple args format
231
+ next
232
+ end
73
233
  end
74
234
  # Gather atoms from this individual condition only
75
235
  condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
236
+ # DEBUG
237
+ # if when_case.condition.is_a?(CallExpression) && [:all?, :any?, :none?].include?(when_case.condition.fn_name)
238
+ # puts " Args: #{when_case.condition.args.inspect}"
239
+ # puts " Atoms found: #{condition_atoms.inspect}"
240
+ # end
76
241
 
77
242
  # Only flag if this individual condition is impossible
78
- next unless !condition_atoms.empty? && Kumi::AtomUnsatSolver.unsat?(condition_atoms)
243
+ # if !condition_atoms.empty?
244
+ # is_unsat = Kumi::AtomUnsatSolver.unsat?(condition_atoms)
245
+ # puts " Is unsat? #{is_unsat}"
246
+ # end
247
+ # Use enhanced solver for cascade conditions too
248
+ impossible = if definitions && !definitions.empty?
249
+ Kumi::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
250
+ else
251
+ Kumi::AtomUnsatSolver.unsat?(condition_atoms)
252
+ end
253
+ next unless !condition_atoms.empty? && impossible
79
254
 
80
255
  # For multi-trait on-clauses, report the trait names rather than the value name
81
256
  if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
82
- list = when_case.condition.args.first
83
- if list.is_a?(ListExpression) && list.elements.all?(Binding)
84
- traits = list.elements.map(&:name).join(" AND ")
85
- add_error(errors, decl.loc, "conjunction `#{traits}` is impossible")
257
+ # Handle both ArrayExpression (old format) and multiple args (new format)
258
+ trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
259
+ when_case.condition.args.first.elements
260
+ else
261
+ when_case.condition.args
262
+ end
263
+
264
+ if trait_bindings.all?(DeclarationReference)
265
+ traits = trait_bindings.map(&:name).join(" AND ")
266
+ report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
86
267
  next
87
268
  end
88
269
  end
89
- add_error(errors, decl.loc, "conjunction `#{decl.name}` is impossible")
270
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc)
90
271
  end
91
272
  end
92
273
 
93
274
  def term(node, _defs)
94
275
  case node
95
- when FieldRef, Binding
276
+ when InputReference, DeclarationReference
96
277
  val = @evaluator.evaluate(node)
97
278
  val == :unknown ? node.name : val
98
279
  when Literal
@@ -101,6 +282,124 @@ module Kumi
101
282
  :unknown
102
283
  end
103
284
  end
285
+
286
+ def check_domain_constraints(node, definitions, errors)
287
+ case node
288
+ when InputReference
289
+ # Check if InputReference points to a field with domain constraints
290
+ field_meta = @input_meta[node.name]
291
+ nil unless field_meta&.dig(:domain)
292
+
293
+ # For InputReference, the constraint comes from trait conditions
294
+ # We don't flag here since the InputReference itself is valid
295
+ when DeclarationReference
296
+ # Check if this binding evaluates to a value that violates domain constraints
297
+ definition = definitions[node.name]
298
+ return unless definition
299
+
300
+ if definition.expression.is_a?(Literal)
301
+ literal_value = definition.expression.value
302
+ check_value_against_domains(node.name, literal_value, errors, definition.loc)
303
+ end
304
+ end
305
+ end
306
+
307
+ def check_value_against_domains(_var_name, value, _errors, _location)
308
+ # Check if this value violates any input domain constraints
309
+ @input_meta.each_value do |field_meta|
310
+ domain = field_meta[:domain]
311
+ next unless domain
312
+
313
+ if violates_domain?(value, domain)
314
+ # This indicates a constraint that can never be satisfied
315
+ # Rather than flagging the cascade, flag the impossible condition
316
+ return true
317
+ end
318
+ end
319
+ false
320
+ end
321
+
322
+ def violates_domain?(value, domain)
323
+ case domain
324
+ when Range
325
+ !domain.include?(value)
326
+ when Array
327
+ !domain.include?(value)
328
+ when Proc
329
+ # For Proc domains, we can't statically analyze
330
+ false
331
+ else
332
+ false
333
+ end
334
+ end
335
+
336
+ def impossible_constraint?(lhs, rhs, operator)
337
+ # Case 1: InputReference compared against value outside its domain
338
+ if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
339
+ return field_literal_impossible?(lhs, rhs, operator)
340
+ elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
341
+ # Reverse case: literal compared to field
342
+ return field_literal_impossible?(rhs, lhs, flip_operator(operator))
343
+ end
344
+
345
+ # Case 2: DeclarationReference that evaluates to literal compared against impossible value
346
+ if lhs.is_a?(DeclarationReference) && rhs.is_a?(Literal)
347
+ return binding_literal_impossible?(lhs, rhs, operator)
348
+ elsif rhs.is_a?(DeclarationReference) && lhs.is_a?(Literal)
349
+ return binding_literal_impossible?(rhs, lhs, flip_operator(operator))
350
+ end
351
+
352
+ false
353
+ end
354
+
355
+ def field_literal_impossible?(field_ref, literal, operator)
356
+ field_meta = @input_meta[field_ref.name]
357
+ return false unless field_meta&.dig(:domain)
358
+
359
+ domain = field_meta[:domain]
360
+ literal_value = literal.value
361
+
362
+ case operator
363
+ when :==
364
+ # field == value where value is not in domain
365
+ violates_domain?(literal_value, domain)
366
+ when :!=
367
+ # field != value where value is not in domain is always true (not impossible)
368
+ false
369
+ else
370
+ # For other operators, we'd need more sophisticated analysis
371
+ false
372
+ end
373
+ end
374
+
375
+ def binding_literal_impossible?(binding, literal, operator)
376
+ # Check if binding evaluates to a literal that conflicts with the comparison
377
+ evaluated_value = @evaluator.evaluate(binding)
378
+ return false if evaluated_value == :unknown
379
+
380
+ literal_value = literal.value
381
+
382
+ case operator
383
+ when :==
384
+ # binding == value where binding evaluates to different value
385
+ evaluated_value != literal_value
386
+ else
387
+ # For other operators, we could add more sophisticated checking
388
+ false
389
+ end
390
+ end
391
+
392
+ def flip_operator(operator)
393
+ case operator
394
+ when :> then :<
395
+ when :>= then :<=
396
+ when :< then :>
397
+ when :<= then :>=
398
+ when :== then :==
399
+ when :!= then :!=
400
+ else operator
401
+ end
402
+ end
104
403
  end
105
404
  end
106
405
  end
@@ -3,7 +3,8 @@
3
3
  module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
- # Base class for analyzer passes that need to traverse the AST using the visitor pattern
6
+ # Base class for analyzer passes that need to traverse the AST using the visitor pattern.
7
+ # Inherits the new immutable state interface from PassBase.
7
8
  class VisitorPass < PassBase
8
9
  # Visit a node and all its children using depth-first traversal
9
10
  # @param node [Syntax::Node] The node to visit
@@ -19,7 +20,7 @@ module Kumi
19
20
 
20
21
  # Helper to visit each declaration's expression tree
21
22
  # @param errors [Array] Error accumulator
22
- # @yield [Syntax::Node, Syntax::Declarations::Base] Each node and its containing declaration
23
+ # @yield [Syntax::Node, Syntax::Base] Each node and its containing declaration
23
24
  def visit_all_expressions(errors)
24
25
  each_decl do |decl|
25
26
  visit(decl.expression) { |node| yield(node, decl, errors) }
@@ -29,7 +30,7 @@ module Kumi
29
30
  # Helper to visit only specific node types
30
31
  # @param node_types [Array<Class>] Node types to match
31
32
  # @param errors [Array] Error accumulator
32
- # @yield [Syntax::Node, Syntax::Declarations::Base] Matching nodes and their declarations
33
+ # @yield [Syntax::Node, Syntax::Base] Matching nodes and their declarations
33
34
  def visit_nodes_of_type(*node_types, errors:)
34
35
  visit_all_expressions(errors) do |node, decl, errs|
35
36
  yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
data/lib/kumi/analyzer.rb CHANGED
@@ -7,40 +7,57 @@ module Kumi
7
7
  module_function
8
8
 
9
9
  DEFAULT_PASSES = [
10
- Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
11
- Passes::InputCollector, # 2. Collects field metadata from input declarations.
12
- Passes::DefinitionValidator, # 3. Checks the basic structure of each rule.
13
- Passes::DependencyResolver, # 4. Builds the dependency graph.
14
- Passes::UnsatDetector, # 5. Detects unsatisfiable constraints in rules.
15
- Passes::Toposorter, # 6. Creates the final evaluation order.
16
- Passes::TypeInferencer, # 7. Infers types for all declarations (pure annotation).
17
- Passes::TypeConsistencyChecker, # 8. Validates declared vs inferred type consistency.
18
- Passes::TypeChecker # 9. Validates types using inferred information.
10
+ Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
11
+ Passes::InputCollector, # 2. Collects field metadata from input declarations.
12
+ Passes::DeclarationValidator, # 3. Checks the basic structure of each rule.
13
+ Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
14
+ Passes::DependencyResolver, # 5. Builds the dependency graph with conditional dependencies.
15
+ Passes::UnsatDetector, # 6. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
16
+ Passes::Toposorter, # 7. Creates the final evaluation order, allowing safe cycles.
17
+ Passes::BroadcastDetector, # 8. Detects which operations should be broadcast over arrays (must run before type inference).
18
+ Passes::TypeInferencer, # 9. Infers types for all declarations (uses vectorization metadata).
19
+ Passes::TypeConsistencyChecker, # 10. Validates declared vs inferred type consistency.
20
+ Passes::TypeChecker # 11. Validates types using inferred information.
19
21
  ].freeze
20
22
 
21
23
  def analyze!(schema, passes: DEFAULT_PASSES, **opts)
22
- analysis_state = { opts: opts }
24
+ state = AnalysisState.new(opts)
23
25
  errors = []
24
26
 
25
- passes.each { |klass| klass.new(schema, analysis_state).run(errors) }
27
+ state = run_analysis_passes(schema, passes, state, errors)
28
+ handle_analysis_errors(errors) unless errors.empty?
29
+ create_analysis_result(state)
30
+ end
31
+
32
+ def self.run_analysis_passes(schema, passes, state, errors)
33
+ passes.each do |pass_class|
34
+ pass_instance = pass_class.new(schema, state)
35
+ begin
36
+ state = pass_instance.run(errors)
37
+ rescue StandardError => e
38
+ errors << ErrorReporter.create_error(e.message, location: nil, type: :semantic)
39
+ end
40
+ end
41
+ state
42
+ end
26
43
 
27
- unless errors.empty?
28
- # Check if we have type-specific errors to raise more specific exception
29
- type_errors = errors.select { |e| e.type == :type }
30
- first_error_location = errors.first.location
44
+ def self.handle_analysis_errors(errors)
45
+ type_errors = errors.select { |e| e.type == :type }
46
+ first_error_location = errors.first.location
31
47
 
32
- raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
48
+ raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
33
49
 
34
- raise Errors::SemanticError.new(format_errors(errors), first_error_location)
35
- end
50
+ raise Errors::SemanticError.new(format_errors(errors), first_error_location)
51
+ end
36
52
 
53
+ def self.create_analysis_result(state)
37
54
  Result.new(
38
- definitions: analysis_state[:definitions].freeze,
39
- dependency_graph: analysis_state[:dependency_graph].freeze,
40
- leaf_map: analysis_state[:leaf_map].freeze,
41
- topo_order: analysis_state[:topo_order].freeze,
42
- decl_types: analysis_state[:decl_types].freeze,
43
- state: analysis_state.freeze
55
+ definitions: state[:definitions],
56
+ dependency_graph: state[:dependency_graph],
57
+ leaf_map: state[:leaf_map],
58
+ topo_order: state[:topo_order],
59
+ decl_types: state[:decl_types],
60
+ state: state.to_h
44
61
  )
45
62
  end
46
63
 
@@ -38,6 +38,9 @@ module Kumi
38
38
  # @param debug [Boolean] enable debug output
39
39
  # @return [Boolean] true if constraints are unsatisfiable
40
40
  def unsat?(atoms, debug: false)
41
+ # Pass 0: Check for special impossible atoms (domain violations, etc.)
42
+ return true if impossible_atoms_exist?(atoms, debug: debug)
43
+
41
44
  # Pass 1: Check numerical bound contradictions (symbol vs numeric)
42
45
  return true if numerical_contradiction?(atoms, debug: debug)
43
46
 
@@ -51,6 +54,21 @@ module Kumi
51
54
  StrictInequalitySolver.cycle?(edges, debug: debug)
52
55
  end
53
56
 
57
+ # Pass 0: Detects special impossible atoms (domain violations, etc.)
58
+ # These atoms are created by UnsatDetector when it finds statically impossible constraints
59
+ #
60
+ # @param atoms [Array<Atom>] constraint atoms
61
+ # @param debug [Boolean] enable debug output
62
+ # @return [Boolean] true if impossible atoms exist
63
+ def impossible_atoms_exist?(atoms, debug: false)
64
+ impossible_found = atoms.any? do |atom|
65
+ atom.lhs == :__impossible__ || atom.rhs == :__impossible__
66
+ end
67
+
68
+ puts "impossible atom detected (domain violation or static impossibility)" if impossible_found && debug
69
+ impossible_found
70
+ end
71
+
54
72
  # Pass 1: Detects numerical bound contradictions using interval analysis
55
73
  # Handles cases like x > 5 AND x < 3 (contradictory bounds)
56
74
  # Also detects always-false comparisons like 100 < 100 or 5 > 5
@@ -99,6 +117,7 @@ module Kumi
99
117
  equal_pairs, strict_pairs = collect_equality_pairs(atoms)
100
118
 
101
119
  return true if direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
120
+ return true if conflicting_equalities?(atoms, debug: debug)
102
121
 
103
122
  transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
104
123
  end
@@ -210,6 +229,32 @@ module Kumi
210
229
  true
211
230
  end
212
231
 
232
+ # Checks for conflicting equalities (x == a AND x == b where a != b)
233
+ # @param atoms [Array<Atom>] constraint atoms
234
+ # @param debug [Boolean] enable debug output
235
+ # @return [Boolean] true if conflicting equalities exist
236
+ def conflicting_equalities?(atoms, debug: false)
237
+ equalities = atoms.select { |atom| atom.op == :== }
238
+
239
+ # Group equalities by their left-hand side
240
+ by_lhs = equalities.group_by(&:lhs)
241
+
242
+ # Check each variable for conflicting equality constraints
243
+ by_lhs.each do |lhs, atoms_for_lhs|
244
+ next if atoms_for_lhs.size < 2
245
+
246
+ # Get all values this variable is constrained to equal
247
+ values = atoms_for_lhs.map(&:rhs).uniq
248
+
249
+ if values.size > 1
250
+ puts "conflicting equalities detected: #{lhs} == #{values.join(" AND #{lhs} == ")}" if debug
251
+ return true
252
+ end
253
+ end
254
+
255
+ false
256
+ end
257
+
213
258
  # Checks for transitive equality contradictions using union-find
214
259
  # @param equal_pairs [Set] equality constraint pairs
215
260
  # @param strict_pairs [Set] strict inequality pairs