kumi 0.0.24 → 0.0.26

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 (234) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +70 -71
  4. data/data/functions/agg/boolean.yaml +6 -2
  5. data/data/functions/agg/numeric.yaml +32 -16
  6. data/data/functions/agg/string.yaml +4 -3
  7. data/data/functions/core/arithmetic.yaml +62 -14
  8. data/data/functions/core/boolean.yaml +12 -6
  9. data/data/functions/core/comparison.yaml +25 -13
  10. data/data/functions/core/constructor.yaml +16 -8
  11. data/data/functions/core/select.yaml +3 -1
  12. data/data/functions/core/stencil.yaml +14 -5
  13. data/data/functions/core/string.yaml +9 -4
  14. data/data/kernels/ruby/agg/numeric.yaml +1 -1
  15. data/docs/UNSAT_DETECTION.md +83 -0
  16. data/golden/array_element/expected/nast.txt +1 -1
  17. data/golden/array_element/expected/schema_ruby.rb +1 -1
  18. data/golden/array_index/expected/nast.txt +7 -7
  19. data/golden/array_index/expected/schema_ruby.rb +1 -1
  20. data/golden/array_operations/expected/nast.txt +2 -2
  21. data/golden/array_operations/expected/schema_ruby.rb +1 -1
  22. data/golden/array_operations/expected/snast.txt +3 -3
  23. data/golden/cascade_logic/expected/lir_02_inlined.txt +8 -8
  24. data/golden/cascade_logic/expected/schema_ruby.rb +1 -1
  25. data/golden/cascade_logic/expected/snast.txt +2 -2
  26. data/golden/chained_fusion/expected/lir_02_inlined.txt +36 -36
  27. data/golden/chained_fusion/expected/lir_03_cse.txt +23 -23
  28. data/golden/chained_fusion/expected/lir_04_1_loop_fusion.txt +25 -25
  29. data/golden/chained_fusion/expected/lir_04_loop_invcm.txt +23 -23
  30. data/golden/chained_fusion/expected/lir_06_const_prop.txt +23 -23
  31. data/golden/chained_fusion/expected/nast.txt +2 -2
  32. data/golden/chained_fusion/expected/schema_javascript.mjs +23 -23
  33. data/golden/chained_fusion/expected/schema_ruby.rb +28 -28
  34. data/golden/element_arrays/expected/nast.txt +2 -2
  35. data/golden/element_arrays/expected/schema_ruby.rb +1 -1
  36. data/golden/element_arrays/expected/snast.txt +1 -1
  37. data/golden/empty_and_null_inputs/expected/lir_02_inlined.txt +18 -18
  38. data/golden/empty_and_null_inputs/expected/lir_03_cse.txt +17 -17
  39. data/golden/empty_and_null_inputs/expected/lir_04_1_loop_fusion.txt +17 -17
  40. data/golden/empty_and_null_inputs/expected/lir_04_loop_invcm.txt +17 -17
  41. data/golden/empty_and_null_inputs/expected/lir_06_const_prop.txt +17 -17
  42. data/golden/empty_and_null_inputs/expected/nast.txt +3 -3
  43. data/golden/empty_and_null_inputs/expected/schema_javascript.mjs +13 -13
  44. data/golden/empty_and_null_inputs/expected/schema_ruby.rb +18 -18
  45. data/golden/function_overload/expected/ast.txt +29 -0
  46. data/golden/function_overload/expected/input_plan.txt +4 -0
  47. data/golden/function_overload/expected/lir_00_unoptimized.txt +18 -0
  48. data/golden/function_overload/expected/lir_01_hoist_scalar_references.txt +18 -0
  49. data/golden/function_overload/expected/lir_02_inlined.txt +20 -0
  50. data/golden/function_overload/expected/lir_03_cse.txt +20 -0
  51. data/golden/function_overload/expected/lir_04_1_loop_fusion.txt +20 -0
  52. data/golden/function_overload/expected/lir_04_loop_invcm.txt +20 -0
  53. data/golden/function_overload/expected/lir_06_const_prop.txt +20 -0
  54. data/golden/function_overload/expected/nast.txt +22 -0
  55. data/golden/function_overload/expected/schema_javascript.mjs +12 -0
  56. data/golden/function_overload/expected/schema_ruby.rb +39 -0
  57. data/golden/function_overload/expected/snast.txt +22 -0
  58. data/golden/function_overload/input.json +8 -0
  59. data/golden/function_overload/schema.kumi +19 -0
  60. data/golden/game_of_life/expected/lir_00_unoptimized.txt +4 -4
  61. data/golden/game_of_life/expected/lir_01_hoist_scalar_references.txt +4 -4
  62. data/golden/game_of_life/expected/lir_02_inlined.txt +1294 -1294
  63. data/golden/game_of_life/expected/lir_03_cse.txt +403 -399
  64. data/golden/game_of_life/expected/lir_04_1_loop_fusion.txt +403 -399
  65. data/golden/game_of_life/expected/lir_04_loop_invcm.txt +403 -399
  66. data/golden/game_of_life/expected/lir_06_const_prop.txt +403 -399
  67. data/golden/game_of_life/expected/nast.txt +4 -4
  68. data/golden/game_of_life/expected/schema_javascript.mjs +87 -85
  69. data/golden/game_of_life/expected/schema_ruby.rb +88 -86
  70. data/golden/game_of_life/expected/snast.txt +10 -10
  71. data/golden/hash_keys/expected/schema_ruby.rb +1 -1
  72. data/golden/hash_value/expected/nast.txt +1 -1
  73. data/golden/hash_value/expected/schema_ruby.rb +1 -1
  74. data/golden/hash_value/expected/snast.txt +1 -1
  75. data/golden/hierarchical_complex/expected/lir_02_inlined.txt +15 -15
  76. data/golden/hierarchical_complex/expected/lir_03_cse.txt +1 -1
  77. data/golden/hierarchical_complex/expected/lir_04_1_loop_fusion.txt +1 -1
  78. data/golden/hierarchical_complex/expected/lir_04_loop_invcm.txt +1 -1
  79. data/golden/hierarchical_complex/expected/lir_06_const_prop.txt +1 -1
  80. data/golden/hierarchical_complex/expected/nast.txt +3 -3
  81. data/golden/hierarchical_complex/expected/schema_javascript.mjs +1 -1
  82. data/golden/hierarchical_complex/expected/schema_ruby.rb +2 -2
  83. data/golden/hierarchical_complex/expected/snast.txt +3 -3
  84. data/golden/inline_rename_scope_leak/expected/nast.txt +3 -3
  85. data/golden/inline_rename_scope_leak/expected/schema_ruby.rb +1 -1
  86. data/golden/input_reference/expected/nast.txt +2 -2
  87. data/golden/input_reference/expected/schema_ruby.rb +1 -1
  88. data/golden/interleaved_fusion/expected/lir_02_inlined.txt +35 -35
  89. data/golden/interleaved_fusion/expected/lir_03_cse.txt +26 -26
  90. data/golden/interleaved_fusion/expected/lir_04_1_loop_fusion.txt +27 -26
  91. data/golden/interleaved_fusion/expected/lir_04_loop_invcm.txt +26 -26
  92. data/golden/interleaved_fusion/expected/lir_06_const_prop.txt +26 -26
  93. data/golden/interleaved_fusion/expected/nast.txt +2 -2
  94. data/golden/interleaved_fusion/expected/schema_javascript.mjs +23 -23
  95. data/golden/interleaved_fusion/expected/schema_ruby.rb +29 -29
  96. data/golden/let_inline/expected/nast.txt +4 -4
  97. data/golden/let_inline/expected/schema_ruby.rb +1 -1
  98. data/golden/loop_fusion/expected/lir_02_inlined.txt +17 -17
  99. data/golden/loop_fusion/expected/lir_03_cse.txt +14 -14
  100. data/golden/loop_fusion/expected/lir_04_1_loop_fusion.txt +14 -14
  101. data/golden/loop_fusion/expected/lir_04_loop_invcm.txt +14 -14
  102. data/golden/loop_fusion/expected/lir_06_const_prop.txt +14 -14
  103. data/golden/loop_fusion/expected/nast.txt +1 -1
  104. data/golden/loop_fusion/expected/schema_javascript.mjs +12 -12
  105. data/golden/loop_fusion/expected/schema_ruby.rb +16 -16
  106. data/golden/min_reduce_scope/expected/nast.txt +3 -3
  107. data/golden/min_reduce_scope/expected/schema_ruby.rb +1 -1
  108. data/golden/min_reduce_scope/expected/snast.txt +1 -1
  109. data/golden/mixed_dimensions/expected/lir_02_inlined.txt +5 -5
  110. data/golden/mixed_dimensions/expected/lir_03_cse.txt +5 -5
  111. data/golden/mixed_dimensions/expected/lir_04_1_loop_fusion.txt +5 -5
  112. data/golden/mixed_dimensions/expected/lir_04_loop_invcm.txt +5 -5
  113. data/golden/mixed_dimensions/expected/lir_06_const_prop.txt +5 -5
  114. data/golden/mixed_dimensions/expected/nast.txt +2 -2
  115. data/golden/mixed_dimensions/expected/schema_javascript.mjs +3 -3
  116. data/golden/mixed_dimensions/expected/schema_ruby.rb +6 -6
  117. data/golden/multirank_hoisting/expected/lir_02_inlined.txt +48 -48
  118. data/golden/multirank_hoisting/expected/lir_03_cse.txt +35 -35
  119. data/golden/multirank_hoisting/expected/lir_04_1_loop_fusion.txt +35 -35
  120. data/golden/multirank_hoisting/expected/lir_04_loop_invcm.txt +35 -35
  121. data/golden/multirank_hoisting/expected/lir_06_const_prop.txt +35 -35
  122. data/golden/multirank_hoisting/expected/nast.txt +7 -7
  123. data/golden/multirank_hoisting/expected/schema_javascript.mjs +34 -34
  124. data/golden/multirank_hoisting/expected/schema_ruby.rb +36 -36
  125. data/golden/nested_hash/expected/nast.txt +1 -1
  126. data/golden/nested_hash/expected/schema_ruby.rb +1 -1
  127. data/golden/reduction_broadcast/expected/lir_02_inlined.txt +30 -30
  128. data/golden/reduction_broadcast/expected/lir_03_cse.txt +22 -22
  129. data/golden/reduction_broadcast/expected/lir_04_1_loop_fusion.txt +22 -22
  130. data/golden/reduction_broadcast/expected/lir_04_loop_invcm.txt +22 -22
  131. data/golden/reduction_broadcast/expected/lir_06_const_prop.txt +22 -22
  132. data/golden/reduction_broadcast/expected/nast.txt +3 -3
  133. data/golden/reduction_broadcast/expected/schema_javascript.mjs +18 -18
  134. data/golden/reduction_broadcast/expected/schema_ruby.rb +23 -23
  135. data/golden/reduction_broadcast/expected/snast.txt +1 -1
  136. data/golden/roll/expected/schema_ruby.rb +1 -1
  137. data/golden/shift/expected/schema_ruby.rb +1 -1
  138. data/golden/shift_2d/expected/schema_ruby.rb +1 -1
  139. data/golden/simple_math/expected/lir_00_unoptimized.txt +1 -1
  140. data/golden/simple_math/expected/lir_01_hoist_scalar_references.txt +1 -1
  141. data/golden/simple_math/expected/lir_02_inlined.txt +1 -1
  142. data/golden/simple_math/expected/lir_03_cse.txt +1 -1
  143. data/golden/simple_math/expected/lir_04_1_loop_fusion.txt +1 -1
  144. data/golden/simple_math/expected/lir_04_loop_invcm.txt +1 -1
  145. data/golden/simple_math/expected/lir_06_const_prop.txt +1 -1
  146. data/golden/simple_math/expected/nast.txt +5 -5
  147. data/golden/simple_math/expected/schema_ruby.rb +1 -1
  148. data/golden/simple_math/expected/snast.txt +2 -2
  149. data/golden/streaming_basics/expected/lir_02_inlined.txt +25 -25
  150. data/golden/streaming_basics/expected/lir_03_cse.txt +13 -13
  151. data/golden/streaming_basics/expected/lir_04_1_loop_fusion.txt +13 -13
  152. data/golden/streaming_basics/expected/lir_04_loop_invcm.txt +13 -13
  153. data/golden/streaming_basics/expected/lir_06_const_prop.txt +13 -13
  154. data/golden/streaming_basics/expected/nast.txt +8 -8
  155. data/golden/streaming_basics/expected/schema_javascript.mjs +13 -13
  156. data/golden/streaming_basics/expected/schema_ruby.rb +14 -14
  157. data/golden/streaming_basics/expected/snast.txt +1 -1
  158. data/golden/tuples/expected/lir_00_unoptimized.txt +5 -5
  159. data/golden/tuples/expected/lir_01_hoist_scalar_references.txt +5 -5
  160. data/golden/tuples/expected/lir_02_inlined.txt +5 -5
  161. data/golden/tuples/expected/lir_03_cse.txt +5 -5
  162. data/golden/tuples/expected/lir_04_1_loop_fusion.txt +5 -5
  163. data/golden/tuples/expected/lir_04_loop_invcm.txt +5 -5
  164. data/golden/tuples/expected/lir_06_const_prop.txt +5 -5
  165. data/golden/tuples/expected/nast.txt +4 -4
  166. data/golden/tuples/expected/schema_ruby.rb +1 -1
  167. data/golden/tuples/expected/snast.txt +6 -6
  168. data/golden/tuples_and_arrays/expected/lir_00_unoptimized.txt +1 -1
  169. data/golden/tuples_and_arrays/expected/lir_01_hoist_scalar_references.txt +1 -1
  170. data/golden/tuples_and_arrays/expected/lir_02_inlined.txt +17 -17
  171. data/golden/tuples_and_arrays/expected/lir_03_cse.txt +13 -13
  172. data/golden/tuples_and_arrays/expected/lir_04_1_loop_fusion.txt +13 -13
  173. data/golden/tuples_and_arrays/expected/lir_04_loop_invcm.txt +13 -13
  174. data/golden/tuples_and_arrays/expected/lir_06_const_prop.txt +13 -13
  175. data/golden/tuples_and_arrays/expected/nast.txt +3 -3
  176. data/golden/tuples_and_arrays/expected/schema_javascript.mjs +13 -13
  177. data/golden/tuples_and_arrays/expected/schema_ruby.rb +14 -14
  178. data/golden/tuples_and_arrays/expected/snast.txt +2 -2
  179. data/golden/us_tax_2024/expected/ast.txt +63 -670
  180. data/golden/us_tax_2024/expected/input_plan.txt +8 -45
  181. data/golden/us_tax_2024/expected/lir_00_unoptimized.txt +253 -863
  182. data/golden/us_tax_2024/expected/lir_01_hoist_scalar_references.txt +253 -863
  183. data/golden/us_tax_2024/expected/lir_02_inlined.txt +1215 -5139
  184. data/golden/us_tax_2024/expected/lir_03_cse.txt +587 -2460
  185. data/golden/us_tax_2024/expected/lir_04_1_loop_fusion.txt +632 -2480
  186. data/golden/us_tax_2024/expected/lir_04_loop_invcm.txt +587 -2400
  187. data/golden/us_tax_2024/expected/lir_06_const_prop.txt +587 -2400
  188. data/golden/us_tax_2024/expected/nast.txt +123 -826
  189. data/golden/us_tax_2024/expected/schema_javascript.mjs +127 -581
  190. data/golden/us_tax_2024/expected/schema_ruby.rb +135 -610
  191. data/golden/us_tax_2024/expected/snast.txt +155 -858
  192. data/golden/us_tax_2024/expected.json +120 -1
  193. data/golden/us_tax_2024/input.json +18 -9
  194. data/golden/us_tax_2024/schema.kumi +48 -178
  195. data/golden/with_constants/expected/lir_00_unoptimized.txt +1 -1
  196. data/golden/with_constants/expected/lir_01_hoist_scalar_references.txt +1 -1
  197. data/golden/with_constants/expected/lir_02_inlined.txt +1 -1
  198. data/golden/with_constants/expected/lir_03_cse.txt +1 -1
  199. data/golden/with_constants/expected/lir_04_1_loop_fusion.txt +1 -1
  200. data/golden/with_constants/expected/lir_04_loop_invcm.txt +1 -1
  201. data/golden/with_constants/expected/lir_06_const_prop.txt +1 -1
  202. data/golden/with_constants/expected/nast.txt +2 -2
  203. data/golden/with_constants/expected/schema_ruby.rb +1 -1
  204. data/golden/with_constants/expected/snast.txt +2 -2
  205. data/lib/kumi/analyzer.rb +12 -12
  206. data/lib/kumi/core/analyzer/passes/formal_constraint_propagator.rb +236 -0
  207. data/lib/kumi/core/analyzer/passes/input_collector.rb +22 -4
  208. data/lib/kumi/core/analyzer/passes/lir/inline_declarations_pass.rb +118 -74
  209. data/lib/kumi/core/analyzer/passes/nast_dimensional_analyzer_pass.rb +64 -18
  210. data/lib/kumi/core/analyzer/passes/normalize_to_nast_pass.rb +9 -4
  211. data/lib/kumi/core/analyzer/passes/snast_pass.rb +3 -1
  212. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +172 -198
  213. data/lib/kumi/core/error_reporter.rb +36 -1
  214. data/lib/kumi/core/errors.rb +33 -1
  215. data/lib/kumi/core/functions/function_spec.rb +5 -4
  216. data/lib/kumi/core/functions/loader.rb +17 -1
  217. data/lib/kumi/core/functions/overload_resolver.rb +164 -0
  218. data/lib/kumi/core/functions/type_error_reporter.rb +118 -0
  219. data/lib/kumi/core/functions/type_rules.rb +155 -35
  220. data/lib/kumi/core/types/inference.rb +29 -22
  221. data/lib/kumi/core/types/normalizer.rb +29 -45
  222. data/lib/kumi/core/types/validator.rb +16 -27
  223. data/lib/kumi/core/types/value_objects.rb +116 -0
  224. data/lib/kumi/core/types.rb +45 -37
  225. data/lib/kumi/registry_v2/loader.rb +90 -0
  226. data/lib/kumi/registry_v2.rb +18 -1
  227. data/lib/kumi/version.rb +1 -1
  228. metadata +21 -7
  229. data/lib/kumi/core/analyzer/unsat_constant_evaluator.rb +0 -59
  230. data/lib/kumi/core/atom_unsat_solver.rb +0 -396
  231. data/lib/kumi/core/constraint_relationship_solver.rb +0 -641
  232. data/lib/kumi/core/types/builder.rb +0 -23
  233. data/lib/kumi/core/types/compatibility.rb +0 -96
  234. data/lib/kumi/core/types/formatter.rb +0 -26
@@ -75,14 +75,18 @@ module Kumi
75
75
 
76
76
  def normalize_call_expression(node, errors)
77
77
  begin
78
- fn_name = FnAliases::MAP[node.fn_name] || node.fn_name
79
- func = @registry.function(fn_name)
78
+ fn_alias = FnAliases::MAP[node.fn_name] || node.fn_name
79
+
80
+ # Try to get the function to check if it's expandable
81
+ # For expandable functions, we need to resolve now
82
+ # For regular functions, we defer resolution to NASTDimensionalAnalyzerPass
83
+ func = @registry.function(fn_alias) rescue nil
80
84
  rescue StandardError
81
85
  # puts "MISSING_FUNCTION: #{node.fn_name.inspect}"
82
86
  raise
83
87
  end
84
88
 
85
- if func.expand
89
+ if func && func.expand
86
90
  # 1. Normalize the arguments FIRST.
87
91
  normalized_args = node.args.map { |arg| normalize_expr(arg, errors) }
88
92
 
@@ -91,8 +95,9 @@ module Kumi
91
95
  MacroExpander.expand(func, normalized_args, node.loc, errors)
92
96
  else
93
97
  # Regular, non-expandable function call.
98
+ # Keep the alias, don't resolve yet - let NASTDimensionalAnalyzerPass handle overload resolution
94
99
  args = node.args.map { |a| normalize_expr(a, errors) }
95
- NAST::Call.new(fn: func.id.to_sym, args: args, opts: node.opts, loc: node.loc)
100
+ NAST::Call.new(fn: fn_alias.to_sym, args: args, opts: node.opts, loc: node.loc)
96
101
  end
97
102
  end
98
103
 
@@ -174,7 +174,9 @@ module Kumi
174
174
  # regular elementwise
175
175
  args = n.args.map { _1.accept(self) }
176
176
  m = meta_for(n)
177
- out = n.class.new(id: n.id, fn: @registry.resolve_function(n.fn), args:, opts: n.opts, loc: n.loc)
177
+ # Use the function ID from metadata (already resolved with type awareness in NASTDimensionalAnalyzerPass)
178
+ fn_id = m[:function] || @registry.resolve_function(n.fn)
179
+ out = n.class.new(id: n.id, fn: fn_id.to_sym, args:, opts: n.opts, loc: n.loc)
178
180
  stamp!(out, m[:result_scope], m[:result_type])
179
181
  end
180
182
 
@@ -4,40 +4,41 @@ module Kumi
4
4
  module Core
5
5
  module Analyzer
6
6
  module Passes
7
- # RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
8
- # DEPENDENCIES: :declarations from NameIndexer, :input_metadata from InputCollector
7
+ # RESPONSIBILITY: Detect unsatisfiable constraints using formal constraint semantics
8
+ # DEPENDENCIES: :declarations, :input_metadata, :registry, SNAST representation
9
9
  # INTERFACE: new(schema, state).run(errors)
10
+ #
11
+ # Detects when constraints are clearly unsatisfiable using constraint propagation:
12
+ # 1. Same variable with contradicting equality values (v == 5 AND v == 10)
13
+ # 2. Values violating input domain constraints
14
+ # 3. Constraints derived through arithmetic operations that violate domains
10
15
  class UnsatDetector < VisitorPass
11
16
  include Syntax
12
17
 
13
18
  COMPARATORS = %i[> < >= <= == !=].freeze
14
- Atom = Kumi::Core::AtomUnsatSolver::Atom
15
19
 
16
20
  def run(errors)
17
- return state
18
21
  definitions = get_state(:declarations)
19
- @input_meta = get_state(:input_metadata) || {}
20
- @definitions = definitions
21
- @evaluator = UnsatConstantEvaluator.new(definitions)
22
+ input_meta = get_state(:input_metadata) || {}
23
+ registry = get_state(:registry)
24
+
25
+ @propagator = FormalConstraintPropagator.new(schema, state)
22
26
 
23
27
  each_decl do |decl|
24
- if decl.expression.is_a?(CascadeExpression)
25
- check_cascade_expression(decl, definitions, errors)
26
- elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
27
- impossible = check_or_expression(decl.expression, definitions, errors)
28
- report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
29
- else
30
- atoms = gather_atoms(decl.expression, definitions, Set.new)
31
- next if atoms.empty?
32
-
33
- result = if definitions && !definitions.empty?
34
- Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
35
- else
36
- Kumi::Core::AtomUnsatSolver.unsat?(atoms)
37
- end
38
- impossible = result
39
-
40
- report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
28
+ # Only check trait declarations for obvious contradictions
29
+ next unless decl.is_a?(TraitDeclaration)
30
+
31
+ atoms = extract_equality_atoms(decl.expression, definitions)
32
+ next if atoms.empty?
33
+
34
+ # Check for formal, obvious contradictions
35
+ if contradicting_equalities?(atoms) || domain_violations?(atoms, input_meta) ||
36
+ propagated_violations?(atoms, definitions, input_meta, registry)
37
+ report_error(
38
+ errors,
39
+ "conjunction `#{decl.name}` is impossible",
40
+ location: decl.loc
41
+ )
41
42
  end
42
43
  end
43
44
 
@@ -46,221 +47,194 @@ module Kumi
46
47
 
47
48
  private
48
49
 
49
- def check_or_expression(or_expr, definitions, _errors)
50
- # For OR expressions: A | B is impossible only if BOTH A AND B are impossible
51
- # If either side is satisfiable, the OR is satisfiable
52
- left_side, right_side = or_expr.args
53
-
54
- # Check if left side is impossible
55
- left_atoms = gather_atoms(left_side, definitions, Set.new)
56
- left_impossible = if left_atoms.empty?
57
- false
58
- elsif definitions && !definitions.empty?
59
- Kumi::Core::ConstraintRelationshipSolver.unsat?(left_atoms, definitions, input_meta: @input_meta)
60
- else
61
- Kumi::Core::AtomUnsatSolver.unsat?(left_atoms)
62
- end
63
-
64
- # Check if right side is impossible
65
- right_atoms = gather_atoms(right_side, definitions, Set.new)
66
- right_impossible = if right_atoms.empty?
67
- false
68
- elsif definitions && !definitions.empty?
69
- Kumi::Core::ConstraintRelationshipSolver.unsat?(right_atoms, definitions, input_meta: @input_meta)
70
- else
71
- Kumi::Core::AtomUnsatSolver.unsat?(right_atoms)
72
- end
73
-
74
- # OR is impossible only if BOTH sides are impossible
75
- left_impossible && right_impossible
76
- end
77
-
78
- def gather_atoms(node, defs, visited, list = [])
79
- return list unless node
80
-
81
- # Use iterative approach with stack to avoid SystemStackError on deep graphs
82
- stack = [node]
50
+ # Extract equality constraints from expression
51
+ # Returns array of {op: :==, lhs: symbol, rhs: value} hashes
52
+ def extract_equality_atoms(expr, definitions)
53
+ atoms = []
54
+ stack = [expr]
55
+ visited = Set.new
83
56
 
84
57
  until stack.empty?
85
58
  current = stack.pop
86
59
  next unless current
87
60
 
88
- if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
89
- lhs, rhs = current.args
90
-
91
- list << if impossible_constraint?(lhs, rhs, current.fn_name)
92
- Atom.new(:==, :__impossible__, true)
93
- else
94
- Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
95
- end
96
- elsif current.is_a?(CallExpression) && current.fn_name == :or
97
- next
98
- elsif current.is_a?(CallExpression) && current.fn_name == :cascade_and
99
- current.args.each { |arg| stack << arg }
100
- elsif current.is_a?(ArrayExpression)
101
- current.elements.each { |elem| stack << elem }
102
- elsif current.is_a?(DeclarationReference)
103
- name = current.name
104
- unless visited.include?(name)
105
- visited << name
106
- stack << defs[name].expression if defs.key?(name)
61
+ case current
62
+ when CallExpression
63
+ if current.fn_name == :==
64
+ lhs, rhs = current.args
65
+ lhs_val = extract_term(lhs, definitions)
66
+ rhs_val = extract_term(rhs, definitions)
67
+ atoms << { op: :==, lhs: lhs_val, rhs: rhs_val } if lhs_val && rhs_val
68
+ elsif current.fn_name == :and
69
+ current.args.each { |arg| stack << arg }
70
+ end
71
+ when DeclarationReference
72
+ unless visited.include?(current.name)
73
+ visited << current.name
74
+ stack << definitions[current.name]&.expression
107
75
  end
108
76
  end
109
-
110
- current.children.each { |child| stack << child } if current.respond_to?(:children) && !current.is_a?(CascadeExpression)
111
77
  end
112
78
 
113
- list
79
+ atoms
114
80
  end
115
81
 
116
- def check_cascade_expression(decl, definitions, errors)
117
- # Analyze each cascade branch condition independently
118
- # This is the correct behavior: each 'on' condition should be checked separately
119
- # since only ONE will be evaluated at runtime (they're mutually exclusive by design)
120
-
121
- # DEBUG: Add detailed logging for hierarchical broadcasting debugging
122
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
123
- puts "DEBUG UNSAT CASCADE: Checking cascade '#{decl.name}' at #{decl.loc}"
124
- puts " Total cases: #{decl.expression.cases.length}"
82
+ # Extract value from AST node
83
+ # Returns symbol for variables, literal values for constants
84
+ def extract_term(node, definitions)
85
+ case node
86
+ when Literal
87
+ node.value
88
+ when DeclarationReference
89
+ val = evaluate_to_literal(node, definitions)
90
+ val == :unknown ? node.name : val
91
+ when InputReference
92
+ node.name
93
+ else
94
+ nil
125
95
  end
96
+ end
126
97
 
127
- decl.expression.cases.each_with_index do |when_case, index|
128
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
129
- puts " Case #{index}: condition=#{when_case.condition.inspect}"
130
- end
98
+ # Try to evaluate a declaration to a literal constant
99
+ def evaluate_to_literal(decl_ref, definitions)
100
+ definition = definitions[decl_ref.name]
101
+ return :unknown unless definition&.expression.is_a?(Literal)
131
102
 
132
- next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
103
+ definition.expression.value
104
+ end
133
105
 
134
- next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
106
+ # FORMAL RULE 1: Contradicting equalities
107
+ # IF: same_variable == value1 AND same_variable == value2 AND value1 != value2
108
+ # THEN: unsatisfiable
109
+ def contradicting_equalities?(atoms)
110
+ by_lhs = atoms.group_by { |a| a[:lhs] }
135
111
 
136
- if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and && (when_case.condition.args.size == 1)
137
- next
138
- end
112
+ by_lhs.each do |_lhs, constraints|
113
+ rhs_values = constraints.map { |c| c[:rhs] }.uniq
114
+ return true if rhs_values.size > 1
115
+ end
139
116
 
140
- condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
117
+ false
118
+ end
141
119
 
142
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
143
- puts " Condition atoms: #{condition_atoms.map(&:inspect)}"
144
- end
120
+ # FORMAL RULE 2: Domain violations
121
+ # IF: variable is input field AND domain constraint exists AND value outside domain
122
+ # THEN: unsatisfiable
123
+ def domain_violations?(atoms, input_meta)
124
+ atoms.each do |atom|
125
+ variable = atom[:lhs]
126
+ value = atom[:rhs]
145
127
 
146
- if definitions && !definitions.empty?
147
- result = Kumi::Core::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
148
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
149
- puts " Enhanced solver result: #{result}"
150
- end
151
- else
152
- result = Kumi::Core::AtomUnsatSolver.unsat?(condition_atoms)
153
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
154
- puts " Basic solver result: #{result}"
155
- end
156
- end
157
- impossible = result
158
- next unless !condition_atoms.empty? && impossible
159
-
160
- if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and
161
- trait_bindings = when_case.condition.args
162
-
163
- if trait_bindings.all?(DeclarationReference)
164
- traits = trait_bindings.map(&:name).join(" AND ")
165
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
166
- puts " -> FLAGGING AS IMPOSSIBLE CASCADE CONDITION: #{traits}"
167
- end
168
- report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
169
- next
170
- end
171
- end
172
- if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
173
- puts " -> FLAGGING AS IMPOSSIBLE CASCADE: #{decl.name}"
174
- end
175
- report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc)
176
- end
177
- end
128
+ next unless value.is_a?(Numeric)
178
129
 
179
- def term(node, _defs)
180
- case node
181
- when InputReference, DeclarationReference
182
- val = @evaluator.evaluate(node)
183
- val == :unknown ? node.name : val
184
- when InputElementReference
185
- path_identifier = node.path.join(".").to_s
186
- path_identifier.to_sym
187
- when Literal
188
- node.value
189
- else
190
- :unknown
130
+ metadata = input_meta[variable]
131
+ next unless metadata&.dig(:domain)
132
+
133
+ domain = metadata[:domain]
134
+ return true unless domain.include?(value)
191
135
  end
136
+
137
+ false
192
138
  end
193
139
 
194
- def violates_domain?(value, domain)
195
- case domain
196
- when Range, Array
197
- !domain.include?(value)
198
- else
199
- false
200
- end
140
+ # FORMAL RULE 3: Propagated constraint violations
141
+ # Propagate constraints through operations to derive hidden impossibilities
142
+ def propagated_violations?(atoms, definitions, input_meta, registry)
143
+ propagated = propagate_constraints(atoms, definitions, registry)
144
+ return false if propagated.empty?
145
+
146
+ propagated_domain_violations?(propagated, input_meta)
201
147
  end
202
148
 
203
- def impossible_constraint?(lhs, rhs, operator)
204
- # Case 1: InputReference compared against value outside its domain
205
- if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
206
- return field_literal_impossible?(lhs, rhs, operator)
207
- elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
208
- return field_literal_impossible?(rhs, lhs, flip_operator(operator))
209
- end
149
+ # Propagate constraints through operation definitions
150
+ def propagate_constraints(atoms, definitions, registry)
151
+ propagated = []
210
152
 
211
- # Case 2: DeclarationReference that evaluates to literal compared against impossible value
212
- if lhs.is_a?(DeclarationReference) && rhs.is_a?(Literal)
213
- return binding_literal_impossible?(lhs, rhs, operator)
214
- elsif rhs.is_a?(DeclarationReference) && lhs.is_a?(Literal)
215
- return binding_literal_impossible?(rhs, lhs, flip_operator(operator))
153
+ atoms.each do |atom|
154
+ variable = atom[:lhs]
155
+ value = atom[:rhs]
156
+
157
+ next unless value.is_a?(Numeric)
158
+
159
+ definition = definitions[variable]
160
+ next unless definition&.is_a?(ValueDeclaration)
161
+
162
+ propagated.concat(propagate_through_operation(definition, variable, value, registry, definitions))
216
163
  end
217
164
 
218
- false
165
+ propagated
219
166
  end
220
167
 
221
- def field_literal_impossible?(field_ref, literal, operator)
222
- field_meta = @input_meta[field_ref.name]
223
- return false unless field_meta&.dig(:domain)
168
+ # Propagate a single constraint through an operation
169
+ def propagate_through_operation(decl, variable, value, registry, definitions)
170
+ expr = decl.expression
171
+ return [] unless expr.is_a?(CallExpression)
224
172
 
225
- domain = field_meta[:domain]
226
- literal_value = literal.value
173
+ fn_id = registry.resolve_function(expr.fn_name)
174
+ return [] unless fn_id
227
175
 
228
- case operator
229
- when :==
230
- violates_domain?(literal_value, domain)
231
- when :!=
232
- false
233
- else
234
- false
235
- end
176
+ operation = registry.function(fn_id)
177
+ return [] unless operation
178
+
179
+ operand_map = build_operand_map(expr, definitions)
180
+ constraint = { variable: variable, op: :==, value: value }
181
+
182
+ propagated_constraint = @propagator.propagate_reverse_through_operation(
183
+ constraint,
184
+ operation,
185
+ operand_map
186
+ )
187
+
188
+ propagated_constraint ? [propagated_constraint] : []
236
189
  end
237
190
 
238
- def binding_literal_impossible?(binding, literal, operator)
239
- # Check if binding evaluates to a literal that conflicts with the comparison
240
- evaluated_value = @evaluator.evaluate(binding)
241
- return false if evaluated_value == :unknown
191
+ # Build operand map for an operation expression
192
+ def build_operand_map(expr, definitions)
193
+ args = expr.args
194
+ map = {}
242
195
 
243
- literal_value = literal.value
196
+ if args.size >= 2
197
+ left_val = extract_operand_value(args[0], definitions)
198
+ right_val = extract_operand_value(args[1], definitions)
244
199
 
245
- case operator
246
- when :==
247
- evaluated_value != literal_value
248
- else
249
- false
200
+ map[:left_operand] = left_val
201
+ map[:right_operand] = right_val
250
202
  end
203
+
204
+ map
251
205
  end
252
206
 
253
- def flip_operator(operator)
254
- case operator
255
- when :> then :<
256
- when :>= then :<=
257
- when :< then :>
258
- when :<= then :>=
259
- when :== then :==
260
- when :!= then :!=
261
- else operator
207
+ # Extract operand value (symbol for variable, numeric for constant)
208
+ def extract_operand_value(node, _definitions)
209
+ case node
210
+ when Literal
211
+ node.value
212
+ when DeclarationReference
213
+ node.name
214
+ when InputReference
215
+ node.name
262
216
  end
263
217
  end
218
+
219
+ # Check if propagated constraints violate input domain
220
+ def propagated_domain_violations?(propagated, input_meta)
221
+ propagated.each do |constraint|
222
+ next unless constraint[:op] == :==
223
+
224
+ variable = constraint[:variable]
225
+ value = constraint[:value]
226
+
227
+ next unless value.is_a?(Numeric)
228
+
229
+ metadata = input_meta[variable]
230
+ next unless metadata&.dig(:domain)
231
+
232
+ domain = metadata[:domain]
233
+ return true unless domain.include?(value)
234
+ end
235
+
236
+ false
237
+ end
264
238
  end
265
239
  end
266
240
  end
@@ -12,16 +12,51 @@ module Kumi
12
12
  # 4. Support both immediate raising and error accumulation patterns
13
13
  module ErrorReporter
14
14
  # Standard error structure for internal use
15
- ErrorEntry = Struct.new(:location, :message, :type, :context, :backtrace, keyword_init: true) do
15
+ # Provides clean access to location components (file, line, column)
16
+ class ErrorEntry
17
+ attr_reader :location, :message, :type, :context, :backtrace
18
+
19
+ def initialize(location:, message:, type: :semantic, context: {}, backtrace: nil)
20
+ @location = location
21
+ @message = message
22
+ @type = type
23
+ @context = context
24
+ @backtrace = backtrace
25
+ end
26
+
16
27
  def to_s
17
28
  location_str = format_location(location)
18
29
  "#{location_str}: #{message}"
19
30
  end
20
31
 
32
+ # Check if location information is present
21
33
  def location?
22
34
  location && !location.is_a?(Symbol)
23
35
  end
24
36
 
37
+ # Extract location components cleanly
38
+ def file
39
+ location.is_a?(Syntax::Location) ? location.file : nil
40
+ end
41
+
42
+ def line
43
+ location.is_a?(Syntax::Location) ? location.line : nil
44
+ end
45
+
46
+ def column
47
+ location.is_a?(Syntax::Location) ? location.column : nil
48
+ end
49
+
50
+ # Alias for consistency
51
+ def path
52
+ file
53
+ end
54
+
55
+ # Check if location is valid (has file and line)
56
+ def valid_location?
57
+ !!(location? && file && line && line > 0)
58
+ end
59
+
25
60
  private
26
61
 
27
62
  def format_location(loc)
@@ -13,9 +13,41 @@ module Kumi
13
13
  @location = location
14
14
  end
15
15
 
16
+ # Extract location components cleanly
17
+ def location_file
18
+ @location&.file
19
+ end
20
+
21
+ def location_line
22
+ @location&.line
23
+ end
24
+
25
+ def location_column
26
+ @location&.column
27
+ end
28
+
29
+ # Aliases for convenient access
30
+ alias path location_file
31
+ alias line location_line
32
+ alias column location_column
33
+
34
+ # Check if location information is present and valid
35
+ def has_location?
36
+ @location && @location.file && @location.line && @location.line > 0
37
+ end
38
+
39
+ # Format location for error messages
40
+ def format_location
41
+ if @location
42
+ "at #{@location.file}:#{@location.line}:#{@location.column}"
43
+ else
44
+ "at ?"
45
+ end
46
+ end
47
+
16
48
  def to_s
17
49
  if @location
18
- "#{super} at #{@location.file}:#{@location.line}:#{@location.column}"
50
+ "#{super} #{format_location}"
19
51
  else
20
52
  super
21
53
  end
@@ -5,10 +5,11 @@ module Kumi
5
5
  module Functions
6
6
  # Minimal function specification for NAST dimensional analysis
7
7
  FunctionSpec = Struct.new(
8
- :id, # "core.add"
9
- :kind, # :elementwise, :reduce, :constructor
10
- :parameter_names, # [:left_operand, :right_operand]
11
- :dtype_rule, # "promote(left_operand,right_operand)" or proc
8
+ :id, # "core.add"
9
+ :kind, # :elementwise, :reduce, :constructor
10
+ :parameter_names, # [:left_operand, :right_operand]
11
+ :dtype_rule, # "promote(left_operand,right_operand)" or proc
12
+ :constraint_semantics, # formal constraint metadata (hash or nil)
12
13
  keyword_init: true
13
14
  )
14
15
  end
@@ -33,14 +33,30 @@ module Kumi
33
33
  function_kind = fn_hash.fetch("kind").to_sym
34
34
  parameter_names = (fn_hash["params"] || []).map { |p| p["name"].to_sym }
35
35
  dtype_rule_fn = TypeRules.compile_dtype_rule(fn_hash.fetch("dtype"), parameter_names)
36
+ constraint_semantics = parse_constraint_semantics(fn_hash["constraint_semantics"])
36
37
 
37
38
  Functions::FunctionSpec.new(
38
39
  id: function_id,
39
40
  kind: function_kind,
40
41
  parameter_names: parameter_names,
41
- dtype_rule: dtype_rule_fn
42
+ dtype_rule: dtype_rule_fn,
43
+ constraint_semantics: constraint_semantics
42
44
  )
43
45
  end
46
+
47
+ def parse_constraint_semantics(semantics_hash)
48
+ return nil unless semantics_hash
49
+
50
+ {
51
+ domain_effect: semantics_hash["domain_effect"]&.to_sym,
52
+ pure_combiner: semantics_hash["pure_combiner"],
53
+ commutativity: semantics_hash["commutativity"],
54
+ associativity: semantics_hash["associativity"],
55
+ identity: semantics_hash["identity"],
56
+ forward_propagation: semantics_hash["forward_propagation"],
57
+ reverse_propagation: semantics_hash["reverse_propagation"]
58
+ }
59
+ end
44
60
  end
45
61
  end
46
62
  end