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
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Propagate constraints forward and backward through operations
8
+ # DEPENDENCIES: :registry (function registry), constraint metadata from function specs
9
+ # INTERFACE: propagate_forward(constraint), propagate_reverse_through_operation(...)
10
+ #
11
+ # Implements formal constraint propagation rules based on function semantics.
12
+ # Forward: x == 5, y = x + 10 => y == 15
13
+ # Reverse: y == 15, y = x + 10 => x == 5
14
+ class FormalConstraintPropagator
15
+ def initialize(schema, state)
16
+ @schema = schema
17
+ @state = state
18
+ @registry = state[:registry]
19
+ end
20
+
21
+ # Forward propagate a constraint through a single operation
22
+ def propagate_forward_through_operation(constraint, operation_spec, operand_map)
23
+ case constraint[:op]
24
+ when :==
25
+ propagate_equality_forward(constraint, operation_spec, operand_map)
26
+ when :range
27
+ propagate_range_forward(constraint, operation_spec, operand_map)
28
+ else
29
+ nil
30
+ end
31
+ end
32
+
33
+ # Reverse propagate: derive input constraints from output constraints
34
+ def propagate_reverse_through_operation(constraint, operation_spec, operand_map)
35
+ case constraint[:op]
36
+ when :==
37
+ propagate_equality_reverse(constraint, operation_spec, operand_map)
38
+ when :range
39
+ propagate_range_reverse(constraint, operation_spec, operand_map)
40
+ else
41
+ nil
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # FORWARD PROPAGATION: Compute output value from input constraints
48
+ def propagate_equality_forward(constraint, operation_spec, operand_map)
49
+ result_var = operand_map[:result]
50
+ input_var = constraint[:variable]
51
+ input_value = constraint[:value]
52
+
53
+ case operation_spec.id
54
+ when "core.add"
55
+ # x == V, result = x + C => result == V + C
56
+ other_operand = get_other_operand_value(constraint, operand_map, "add")
57
+ return nil unless other_operand.is_a?(Numeric)
58
+
59
+ output_value = input_value + other_operand
60
+ { variable: result_var, op: :==, value: output_value }
61
+
62
+ when "core.mul"
63
+ # x == V, result = x * C => result == V * C
64
+ other_operand = get_other_operand_value(constraint, operand_map, "mul")
65
+ return nil unless other_operand.is_a?(Numeric)
66
+
67
+ output_value = input_value * other_operand
68
+ { variable: result_var, op: :==, value: output_value }
69
+
70
+ when "core.sub"
71
+ # x == V, result = x - C => result == V - C
72
+ other_operand = get_other_operand_value(constraint, operand_map, "sub")
73
+ return nil unless other_operand.is_a?(Numeric)
74
+
75
+ output_value = input_value - other_operand
76
+ { variable: result_var, op: :==, value: output_value }
77
+
78
+ else
79
+ nil
80
+ end
81
+ end
82
+
83
+ # FORWARD PROPAGATION: Compute output range from input range
84
+ def propagate_range_forward(constraint, operation_spec, operand_map)
85
+ result_var = operand_map[:result]
86
+ input_min = constraint[:min]
87
+ input_max = constraint[:max]
88
+
89
+ case operation_spec.id
90
+ when "core.add"
91
+ # x in [min, max], result = x + C => result in [min + C, max + C]
92
+ other = get_other_operand_value(constraint, operand_map, "add")
93
+ return nil unless other.is_a?(Numeric)
94
+
95
+ output_min = input_min + other
96
+ output_max = input_max + other
97
+ { variable: result_var, op: :range, min: output_min, max: output_max }
98
+
99
+ when "core.mul"
100
+ # x in [min, max], result = x * C => depends on sign of C
101
+ other = get_other_operand_value(constraint, operand_map, "mul")
102
+ return nil unless other.is_a?(Numeric)
103
+
104
+ if other > 0
105
+ output_min = input_min * other
106
+ output_max = input_max * other
107
+ elsif other < 0
108
+ output_min = input_max * other
109
+ output_max = input_min * other
110
+ else
111
+ output_min = 0
112
+ output_max = 0
113
+ end
114
+ { variable: result_var, op: :range, min: output_min, max: output_max }
115
+
116
+ when "core.sub"
117
+ # x in [min, max], result = x - C => result in [min - C, max - C]
118
+ other = get_other_operand_value(constraint, operand_map, "sub")
119
+ return nil unless other.is_a?(Numeric)
120
+
121
+ output_min = input_min - other
122
+ output_max = input_max - other
123
+ { variable: result_var, op: :range, min: output_min, max: output_max }
124
+
125
+ else
126
+ nil
127
+ end
128
+ end
129
+
130
+ # REVERSE PROPAGATION: Derive input equality from output equality
131
+ def propagate_equality_reverse(constraint, operation_spec, operand_map)
132
+ result_var = constraint[:variable]
133
+ result_value = constraint[:value]
134
+ left_var = operand_map[:left_operand]
135
+ right_var = operand_map[:right_operand]
136
+
137
+ case operation_spec.id
138
+ when "core.add"
139
+ # result == V, result = x + C => x == V - C
140
+ if left_var.is_a?(Symbol) && right_var.is_a?(Numeric)
141
+ { variable: left_var, op: :==, value: result_value - right_var }
142
+ elsif right_var.is_a?(Symbol) && left_var.is_a?(Numeric)
143
+ { variable: right_var, op: :==, value: result_value - left_var }
144
+ else
145
+ nil
146
+ end
147
+
148
+ when "core.mul"
149
+ # result == V, result = x * C => x == V / C (if C != 0)
150
+ if left_var.is_a?(Symbol) && right_var.is_a?(Numeric) && right_var != 0
151
+ return nil unless (result_value % right_var).zero?
152
+ { variable: left_var, op: :==, value: result_value / right_var }
153
+ elsif right_var.is_a?(Symbol) && left_var.is_a?(Numeric) && left_var != 0
154
+ return nil unless (result_value % left_var).zero?
155
+ { variable: right_var, op: :==, value: result_value / left_var }
156
+ else
157
+ nil
158
+ end
159
+
160
+ when "core.sub"
161
+ # result == V, result = x - C => x == V + C
162
+ if left_var.is_a?(Symbol) && right_var.is_a?(Numeric)
163
+ { variable: left_var, op: :==, value: result_value + right_var }
164
+ elsif right_var.is_a?(Symbol) && left_var.is_a?(Numeric)
165
+ { variable: right_var, op: :==, value: left_var - result_value }
166
+ else
167
+ nil
168
+ end
169
+
170
+ else
171
+ nil
172
+ end
173
+ end
174
+
175
+ # REVERSE PROPAGATION: Derive input range from output range
176
+ def propagate_range_reverse(constraint, operation_spec, operand_map)
177
+ result_min = constraint[:min]
178
+ result_max = constraint[:max]
179
+ left_var = operand_map[:left_operand]
180
+ right_var = operand_map[:right_operand]
181
+
182
+ case operation_spec.id
183
+ when "core.add"
184
+ # result in [min, max], result = x + C => x in [min - C, max - C]
185
+ if left_var.is_a?(Symbol) && right_var.is_a?(Numeric)
186
+ return { variable: left_var, op: :range, min: result_min - right_var, max: result_max - right_var }
187
+ elsif right_var.is_a?(Symbol) && left_var.is_a?(Numeric)
188
+ return { variable: right_var, op: :range, min: result_min - left_var, max: result_max - left_var }
189
+ end
190
+
191
+ when "core.mul"
192
+ # result in [min, max], result = x * C => x in [min/C, max/C] (depends on sign)
193
+ if left_var.is_a?(Symbol) && right_var.is_a?(Numeric) && right_var != 0
194
+ if right_var > 0
195
+ return { variable: left_var, op: :range, min: result_min / right_var, max: result_max / right_var }
196
+ else
197
+ return { variable: left_var, op: :range, min: result_max / right_var, max: result_min / right_var }
198
+ end
199
+ elsif right_var.is_a?(Symbol) && left_var.is_a?(Numeric) && left_var != 0
200
+ if left_var > 0
201
+ return { variable: right_var, op: :range, min: result_min / left_var, max: result_max / left_var }
202
+ else
203
+ return { variable: right_var, op: :range, min: result_max / left_var, max: result_min / left_var }
204
+ end
205
+ end
206
+
207
+ when "core.sub"
208
+ # result in [min, max], result = x - C => x in [min + C, max + C]
209
+ if left_var.is_a?(Symbol) && right_var.is_a?(Numeric)
210
+ return { variable: left_var, op: :range, min: result_min + right_var, max: result_max + right_var }
211
+ elsif right_var.is_a?(Symbol) && left_var.is_a?(Numeric)
212
+ return { variable: right_var, op: :range, min: left_var - result_max, max: left_var - result_min }
213
+ end
214
+ end
215
+
216
+ nil
217
+ end
218
+
219
+ def get_other_operand_value(constraint, operand_map, operation)
220
+ input_var = constraint[:variable]
221
+ left_var = operand_map[:left_operand] || operand_map.values[0]
222
+ right_var = operand_map[:right_operand] || operand_map.values[1]
223
+
224
+ if input_var == left_var && right_var.is_a?(Numeric)
225
+ right_var
226
+ elsif input_var == right_var && left_var.is_a?(Numeric)
227
+ left_var
228
+ else
229
+ nil
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -103,10 +103,28 @@ module Kumi
103
103
  end
104
104
 
105
105
  def kind_from_type(t)
106
- return :array if t == :array
107
- return :hash if t == :hash
108
-
109
- :scalar
106
+ # Handle both symbols (legacy) and Type objects (new)
107
+ case t
108
+ when Kumi::Core::Types::ArrayType
109
+ :array
110
+ when Kumi::Core::Types::TupleType
111
+ :array # Tuples behave like arrays for input access
112
+ when :array, Kumi::Core::Types::ScalarType
113
+ # Check if it's a hash scalar or :hash symbol
114
+ if t.is_a?(Kumi::Core::Types::ScalarType) && t.kind == :hash
115
+ :hash
116
+ elsif t == :hash
117
+ :hash
118
+ elsif t == :array
119
+ :array
120
+ else
121
+ :scalar
122
+ end
123
+ when :hash
124
+ :hash
125
+ else
126
+ :scalar
127
+ end
110
128
  end
111
129
  end
112
130
  end
@@ -41,22 +41,27 @@ module Kumi
41
41
  [fused, changed]
42
42
  end
43
43
 
44
+ # ---------------- core ----------------
45
+
46
+ Hoist = Struct.new(:ops, :target_depth, keyword_init: true)
47
+
44
48
  def inline_top_level_decl(ops)
45
49
  env = Env.new
46
50
  reg_map = {}
47
51
  rename_map = {}
48
- processed, hoisted = process_and_hoist_block(ops, env, reg_map, rename_map)
49
- raise "Orphaned code was hoisted to top level" unless hoisted.empty?
52
+ processed, hoist_pkgs = process_and_hoist_block(ops, env, reg_map, rename_map)
50
53
 
51
- processed
52
- end
54
+ top_emit, bubble = hoist_pkgs.partition { |p| p.target_depth == 0 }
55
+ raise "Orphaned code hoist with target depth(s): #{bubble.map(&:target_depth).uniq.inspect}" unless bubble.empty?
53
56
 
54
- # ---------------- core ----------------
57
+ top_emit.flat_map(&:ops) + processed
58
+ end
55
59
 
56
- # returns [processed_ops, hoisted_ops]
60
+ # returns [processed_ops, hoist_pkgs]
61
+ # returns [processed_ops, hoist_pkgs]
57
62
  def process_and_hoist_block(block_ops, env, reg_map, rename_map)
58
63
  out = []
59
- hoisted = []
64
+ hoisted_pkgs = []
60
65
  i = 0
61
66
  while i < block_ops.length
62
67
  ins = block_ops[i]
@@ -67,110 +72,144 @@ module Kumi
67
72
 
68
73
  env.push(ins)
69
74
  child_rename = {}
70
- processed_body, hoisted_from_child =
75
+ processed_body, child_hoists =
71
76
  process_and_hoist_block(loop_body, env, reg_map, child_rename)
77
+
78
+ depth_here = env.axes.length
79
+ child_el = ins.attributes[:as_element]
80
+ child_ix = ins.attributes[:as_index]
81
+
82
+ # Partition hoists: those that belong *inside* this loop vs bubble upward
83
+ inside_pkgs, bubble_pkgs = child_hoists.partition { |p| p.target_depth == depth_here }
84
+
85
+ # Renames: never let aliases to this loop's el/idx escape upward
86
+ safe_pairs = child_rename.reject { |_, v| v == child_el || v == child_ix }
72
87
  env.pop
73
88
 
74
- out.concat(hoisted_from_child) # emit hoists before loop
75
- rename_map.merge!(child_rename) # make child renames visible
89
+ # Merge only safe renames into outer scope
90
+ rename_map.merge!(safe_pairs)
91
+
92
+ # Emit loop shell
93
+ out << rewrite(ins, reg_map, rename_map)
94
+
95
+ # Local view inside loop: apply both local and outer renames, with local taking precedence
96
+ local_map = child_rename.merge(rename_map)
97
+
98
+ # Emit hoists that belong at this depth *inside* the loop, before the body
99
+ inside_ops = inside_pkgs.flat_map(&:ops)
100
+ out.concat(rewrite_block(inside_ops, local_map))
76
101
 
77
- out << rewrite(ins, reg_map, rename_map) # loop shell
78
- out.concat(processed_body)
102
+ # Emit rewritten body
103
+ out.concat(rewrite_block(processed_body, local_map))
104
+
105
+ # Close loop
79
106
  out << rewrite(block_ops[end_idx], reg_map, rename_map)
80
107
 
108
+ # Bubble remaining hoists to outer scopes
109
+ hoisted_pkgs.concat(bubble_pkgs)
110
+
111
+ # Extra safety: ensure no aliases to this loop's el/idx remain in outer map
112
+ rename_map.delete_if { |_, v| v == child_el || v == child_ix }
113
+
81
114
  i = end_idx
82
115
 
83
116
  when :LoadDeclaration
84
- inline_ops, hoist_ops =
85
- handle_load_declaration(ins, env, reg_map, rename_map)
117
+ inline_ops, new_pkgs = handle_load_declaration(ins, env, reg_map, rename_map)
86
118
  out.concat(inline_ops)
87
- hoisted.concat(hoist_ops)
119
+ hoisted_pkgs.concat(new_pkgs)
88
120
 
89
121
  else
90
122
  out << rewrite(ins, reg_map, rename_map)
91
123
  end
92
124
  i += 1
93
125
  end
94
- [out, hoisted]
126
+ [out, hoisted_pkgs]
95
127
  end
96
128
 
97
- # returns [inline_ops, hoisted_ops]
98
- def handle_load_declaration(call_ins, env, reg_map, outer_rename_map)
99
- raise "LoadDeclaration missing callee" unless call_ins.immediates&.first&.respond_to?(:value)
100
-
101
- callee = call_ins.immediates.first.value.to_sym
102
- raise "LoadDeclaration callee #{callee} not found" unless @ops_by_decl.key?(callee)
129
+ # returns [inline_ops, hoist_pkgs]
130
+ def handle_load_declaration(ins, env, _reg_map, rename_map)
131
+ callee = ins.immediates.first.value.to_sym
103
132
 
104
- decl_axes = fetch_decl_axes(callee, call_ins)
105
- site_axes = env.axes
133
+ # axes presence and agreement with callee gamma
134
+ decl_axes = ins.attributes.fetch(:axes) { raise "LoadDeclaration missing :axes for #{callee}" }
135
+ gamma_axes = @gamma.fetch(callee).axes
136
+ raise "axes mismatch for #{callee}: decl=#{decl_axes.inspect} gamma=#{gamma_axes.inspect}" unless decl_axes == gamma_axes
106
137
 
107
- body, yield_reg, callee_axis_regs = inline_callee_core(callee)
108
- axis_map = remap_axes(callee_axis_regs, env)
138
+ body, yield_reg, callee_regs = inline_callee_core(callee)
139
+ remap = remap_axes(callee_regs, env)
109
140
 
141
+ # per-callsite freshening
110
142
  local_reg_map = {}
111
- _acc, fresh_ops = freshen(body, local_reg_map, pre_map: axis_map)
112
-
113
- nested_rename = {}
114
- processed_inner, hoisted_inner =
115
- process_and_hoist_block(fresh_ops, env, {}, nested_rename)
116
-
117
- prelim = local_reg_map[yield_reg] || axis_map[yield_reg] || yield_reg
118
- mapped_yield = resolve_rename(prelim, nested_rename)
119
-
120
- # def/dominance guard
121
- defs_inline = defs_in(processed_inner)
122
- defs_hoisted = defs_in(hoisted_inner)
123
- unless defs_inline.include?(mapped_yield) || defs_hoisted.include?(mapped_yield)
124
- msg = [
125
- "inliner: mapped yield #{mapped_yield} has no def in emitted ops for #{callee}",
126
- " original yield: #{yield_reg}",
127
- " prelim mapping: #{prelim}",
128
- " nested_rename keys: #{nested_rename.keys.inspect}",
129
- " inline defs size: #{defs_inline.size}",
130
- " hoisted defs size: #{defs_hoisted.size}"
131
- ].join("\n")
132
- raise msg
143
+ _acc, fresh_ops = freshen(body, local_reg_map, pre_map: remap)
144
+
145
+ # recursively process nested calls
146
+ processed_inline, nested_pkgs = process_and_hoist_block(fresh_ops, env, {}, rename_map)
147
+
148
+ # compute yielded register mapping, then resolve through any renames created by nested inlines
149
+ mapped_yield =
150
+ local_reg_map[yield_reg] || remap[yield_reg] ||
151
+ (raise "inliner: yielded reg #{yield_reg} not produced in inlined body for #{callee}")
152
+ resolved_yield = resolve_rename(mapped_yield, rename_map)
153
+
154
+ # sanity: resolved_yield must be definable at site
155
+ emitted_defs = processed_inline.map(&:result_register).compact +
156
+ nested_pkgs.flat_map { |p| p.ops }.map(&:result_register).compact
157
+ unless emitted_defs.include?(resolved_yield) || env.ambient_regs.include?(resolved_yield)
158
+ raise "inliner: mapped yield #{resolved_yield} has no def in emitted ops for #{callee}\n" \
159
+ "original yield: #{yield_reg}\n" \
160
+ "inline defs size: #{processed_inline.count { |x| x.result_register }}\n" \
161
+ "nested hoist defs size: #{nested_pkgs.flat_map { |p| p.ops }.count { |x| x.result_register }}"
133
162
  end
134
163
 
135
- outer_rename_map[call_ins.result_register] = mapped_yield if call_ins.result_register
164
+ # final rename for call site result uses the resolved register
165
+ rename_map[ins.result_register] = resolved_yield
166
+
167
+ # decide placement by depth
168
+ site_depth = env.axes.length
169
+ callee_depth = decl_axes.length
170
+
171
+ if callee_depth < site_depth
172
+ forb = forbidden_ambient_after(callee_depth, env)
173
+ used = uses_of(processed_inline)
174
+ bad = used & forb
175
+ unless bad.empty?
176
+ raise "scope error: would hoist ops using deeper-axis regs #{bad.inspect} " \
177
+ "(callee_depth=#{callee_depth}, site_depth=#{site_depth})"
178
+ end
179
+ pkgs = nested_pkgs + [Hoist.new(ops: processed_inline, target_depth: callee_depth)]
180
+ [[], pkgs]
181
+
182
+ elsif callee_depth == site_depth
183
+ emit, bubble = nested_pkgs.partition { |p| p.target_depth == site_depth }
184
+ [(emit.flat_map(&:ops) + processed_inline), bubble]
136
185
 
137
- if prefix?(decl_axes, site_axes) && decl_axes.length < site_axes.length
138
- [[], hoisted_inner + processed_inner] # hoist
139
- elsif decl_axes == site_axes
140
- [hoisted_inner + processed_inner, []] # inline in place
141
186
  else
142
- [[rewrite(call_ins, reg_map, outer_rename_map)], []] # cannot inline
187
+ [[rewrite(ins, {}, rename_map)], []]
143
188
  end
144
189
  end
145
190
 
146
191
  # ---------------- helpers ----------------
192
+ def rewrite_block(ops, rename)
193
+ # Ensure late-added renames apply to a block we built earlier.
194
+ ops.map { |ins| rewrite(ins, {}, rename) }
195
+ end
147
196
 
148
- def resolve_rename(reg, rename, limit: 64)
197
+ def resolve_rename(reg, rename)
149
198
  seen = {}
150
199
  cur = reg
151
- limit.times do
152
- nxt = rename[cur]
153
- break unless nxt
154
- raise "inliner: rename cycle at #{cur}" if seen[nxt]
155
-
156
- seen[nxt] = true
157
- cur = nxt
200
+ while (n = rename[cur]) && !seen[n]
201
+ seen[cur] = true
202
+ cur = n
158
203
  end
159
204
  cur
160
205
  end
161
206
 
162
- def defs_in(ops)
163
- ops.each_with_object(Set.new) { |ins, s| s << ins.result_register if ins.result_register }
207
+ def uses_of(ops)
208
+ ops.flat_map { |x| Array(x.inputs) }.compact
164
209
  end
165
210
 
166
- def fetch_decl_axes(callee, call_ins)
167
- attr_axes = call_ins.attributes && call_ins.attributes[:axes]
168
- gamma_axes = Array(@gamma.fetch(callee)&.axes || [])
169
- ax = attr_axes.nil? ? gamma_axes : Array(attr_axes)
170
- raise "LoadDeclaration missing :axes" if ax.nil?
171
- raise "inliner: non-array axes for #{callee}: #{ax.inspect}" unless ax.is_a?(Array)
172
-
173
- ax
211
+ def forbidden_ambient_after(depth, env)
212
+ env.frames_after(depth).flat_map { |f| [f[:el], f[:idx]] }
174
213
  end
175
214
 
176
215
  def find_matching_loop_end(ops, start_index)
@@ -195,6 +234,7 @@ module Kumi
195
234
  class Env
196
235
  def initialize = @frames = []
197
236
  def axes = @frames.map { _1[:axis] }
237
+ def ambient_regs = @frames.flat_map { |f| [f[:el], f[:idx]] }
198
238
 
199
239
  def push(loop_ins)
200
240
  @frames << {
@@ -210,6 +250,10 @@ module Kumi
210
250
  @frames.reverse.find { _1[:axis] == axis } ||
211
251
  raise("no element for axis=#{axis.inspect}")
212
252
  end
253
+
254
+ def frames_after(depth)
255
+ @frames[depth..] || []
256
+ end
213
257
  end
214
258
 
215
259
  def detect_all_gammas(ops_by_decl)
@@ -240,10 +284,10 @@ module Kumi
240
284
  end
241
285
 
242
286
  def inline_callee_core(callee_name)
243
- ops = Array(@ops_by_decl.fetch(callee_name)[:operations])
287
+ ops = Array(@ops_by_decl.fetch(callee_name)[:operations])
244
288
  info = @gamma.fetch(callee_name)
245
289
  axes = info.axes
246
- k = axes.length
290
+ k = axes.length
247
291
 
248
292
  yi = ops.rindex { |x| x.opcode == :Yield } or raise "callee #{callee_name} has no Yield"
249
293
  yielded_reg = Array(ops[yi].inputs).first
@@ -68,18 +68,61 @@ module Kumi
68
68
  end
69
69
 
70
70
  def analyze_call_expression(call, errors)
71
- function_spec = @registry.function(call.fn.to_s)
72
-
71
+ # Step 1: Analyze arguments to get their types and scopes
73
72
  arg_metadata = call.args.map { |arg| analyze_expression(arg, errors) }
74
73
  arg_types = arg_metadata.map { |m| m[:type] }
75
74
  arg_scopes = arg_metadata.map { |m| m[:scope] }
76
75
 
77
- debug " Call #{call.fn}: arg_scopes=#{arg_scopes.inspect}"
76
+ # Ensure all arg_types are Type objects (defensive programming)
77
+ arg_types = arg_types.map do |t|
78
+ case t
79
+ when Types::Type
80
+ t
81
+ when :array
82
+ # :array is actually an ArrayType marker, not a scalar kind
83
+ Types.array(Types.scalar(:any))
84
+ when :hash
85
+ Types.scalar(:hash)
86
+ when Symbol
87
+ # Try to normalize as scalar kind
88
+ Types.normalize(t)
89
+ else
90
+ # Already a Type object or unknown format
91
+ t
92
+ end
93
+ end
78
94
 
79
- if ENV["DEBUG_NAST_DIMENSIONAL_ANALYZER"] == "1" && function_spec.param_names.size != arg_types.size
80
- puts "[NASTDimensionalAnalyzer] WARNING: #{call.fn} expects #{function_spec.param_names.size} args, got #{arg_types.size}"
95
+ debug " Call #{call.fn}: arg_scopes=#{arg_scopes.inspect}, arg_types=#{arg_types.inspect}"
96
+
97
+ # Step 2: Resolve function using type-aware overload resolution
98
+ begin
99
+ resolved_fn_id = @registry.resolve_function_with_types(call.fn.to_s, arg_types)
100
+ function_spec = @registry.function(resolved_fn_id)
101
+ debug " Resolved '#{call.fn}' with types #{arg_types.inspect} to #{resolved_fn_id}"
102
+ rescue Core::Functions::OverloadResolver::ResolutionError => e
103
+ # Type-aware overload resolution failed - report with location
104
+ report_type_error(
105
+ errors,
106
+ e.message,
107
+ location: call.loc,
108
+ context: {
109
+ function: call.fn.to_s,
110
+ arg_types: arg_types
111
+ }
112
+ )
113
+ raise Kumi::Core::Errors::TypeError, e.message
114
+ rescue StandardError => e
115
+ # Other function resolution errors
116
+ report_semantic_error(
117
+ errors,
118
+ "Function resolution error for '#{call.fn}': #{e.message}",
119
+ location: call.loc,
120
+ context: { function: call.fn.to_s }
121
+ )
122
+ raise Kumi::Core::Errors::SemanticError, e.message
81
123
  end
82
124
 
125
+ # Step 3: Compute result type
83
126
  named_types =
84
127
  if function_spec.params.size == arg_types.size
85
128
  Hash[function_spec.param_names.zip(arg_types)]
@@ -89,11 +132,17 @@ module Kumi
89
132
 
90
133
  begin
91
134
  result_type = function_spec.dtype_rule.call(named_types)
92
- rescue StandardError
93
- # Maybe we have the wrong function, lets try to see if another function with same name works
94
- # TODO: Fix this hack
95
-
96
- raise
135
+ rescue StandardError => e
136
+ report_type_error(
137
+ errors,
138
+ "Type rule evaluation failed for #{function_spec.id}: #{e.message}",
139
+ location: call.loc,
140
+ context: {
141
+ function: function_spec.id,
142
+ arg_types: arg_types
143
+ }
144
+ )
145
+ raise Kumi::Core::Errors::TypeError, "Type rule failed for #{function_spec.id}: #{e.message}"
97
146
  end
98
147
 
99
148
  over_collection = arg_types.size == 1 && Types.collection?(arg_types[0])
@@ -120,11 +169,8 @@ module Kumi
120
169
  element_scopes = elems.map { |m| m[:scope] }
121
170
  result_scope = lub_by_prefix(element_scopes)
122
171
 
123
- result_type = if element_types.uniq.size == 1
124
- "tuple<#{element_types.uniq[0]}>"
125
- else
126
- "tuple<#{element_types.join(', ')}>"
127
- end
172
+ # Create TupleType from element Types
173
+ result_type = Types.tuple(element_types)
128
174
 
129
175
  @metadata_table[node_id(node)] = {
130
176
  parameter_names: [],
@@ -143,7 +189,7 @@ module Kumi
143
189
  fields = node.pairs.map { |e| analyze_expression(e, errors) }
144
190
  fields_scopes = fields.map { |m| m[:scope] }
145
191
  scope = lub_by_prefix(fields_scopes)
146
- dtype = :hash
192
+ dtype = Types.scalar(:hash)
147
193
 
148
194
  @metadata_table[node_id(node)] = {
149
195
  type: dtype,
@@ -153,7 +199,7 @@ module Kumi
153
199
 
154
200
  def analyze_pair(node, errors)
155
201
  value_node = analyze_expression(node.value, errors)
156
- dtype = :pair
202
+ dtype = Types.scalar(:pair)
157
203
 
158
204
  @metadata_table[node_id(node)] = {
159
205
  type: dtype,
@@ -182,7 +228,7 @@ module Kumi
182
228
  def analyze_index_ref(node, _errors)
183
229
  meta = @input_table.find { _1.path_fqn == node.input_fqn } or raise "Index plan found: #{n.name.inspect}"
184
230
  axes = Array(meta[:axes])
185
- type = :integer
231
+ type = Types.scalar(:integer)
186
232
 
187
233
  debug " IndexRef #{node.name}: input_fqn=#{node.input_fqn}, axes=#{axes.inspect}"
188
234