kumi 0.0.25 → 0.0.27

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 (223) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/CLAUDE.md +4 -0
  4. data/README.md +86 -78
  5. data/data/functions/agg/boolean.yaml +6 -2
  6. data/data/functions/agg/numeric.yaml +32 -16
  7. data/data/functions/agg/string.yaml +4 -3
  8. data/data/functions/core/arithmetic.yaml +62 -14
  9. data/data/functions/core/boolean.yaml +12 -6
  10. data/data/functions/core/comparison.yaml +25 -13
  11. data/data/functions/core/constructor.yaml +16 -8
  12. data/data/functions/core/conversion.yaml +32 -0
  13. data/data/functions/core/select.yaml +3 -1
  14. data/data/functions/core/stencil.yaml +14 -5
  15. data/data/functions/core/string.yaml +9 -4
  16. data/data/kernels/javascript/core/coercion.yaml +20 -0
  17. data/data/kernels/ruby/agg/numeric.yaml +1 -1
  18. data/data/kernels/ruby/core/coercion.yaml +20 -0
  19. data/docs/ARCHITECTURE.md +277 -0
  20. data/docs/DEVELOPMENT.md +62 -0
  21. data/docs/FUNCTIONS.md +955 -0
  22. data/docs/SYNTAX.md +8 -0
  23. data/docs/UNSAT_DETECTION.md +83 -0
  24. data/docs/VSCODE_EXTENSION.md +114 -0
  25. data/docs/functions-reference.json +1821 -0
  26. data/golden/array_element/expected/nast.txt +1 -1
  27. data/golden/array_element/expected/schema_ruby.rb +1 -1
  28. data/golden/array_index/expected/nast.txt +7 -7
  29. data/golden/array_index/expected/schema_ruby.rb +1 -1
  30. data/golden/array_operations/expected/nast.txt +2 -2
  31. data/golden/array_operations/expected/schema_ruby.rb +1 -1
  32. data/golden/array_operations/expected/snast.txt +3 -3
  33. data/golden/cascade_logic/expected/schema_ruby.rb +1 -1
  34. data/golden/cascade_logic/expected/snast.txt +2 -2
  35. data/golden/chained_fusion/expected/nast.txt +2 -2
  36. data/golden/chained_fusion/expected/schema_ruby.rb +1 -1
  37. data/golden/decimal_explicit/expected/ast.txt +38 -0
  38. data/golden/decimal_explicit/expected/input_plan.txt +3 -0
  39. data/golden/decimal_explicit/expected/lir_00_unoptimized.txt +30 -0
  40. data/golden/decimal_explicit/expected/lir_01_hoist_scalar_references.txt +30 -0
  41. data/golden/decimal_explicit/expected/lir_02_inlined.txt +44 -0
  42. data/golden/decimal_explicit/expected/lir_03_cse.txt +40 -0
  43. data/golden/decimal_explicit/expected/lir_04_1_loop_fusion.txt +40 -0
  44. data/golden/decimal_explicit/expected/lir_04_loop_invcm.txt +40 -0
  45. data/golden/decimal_explicit/expected/lir_06_const_prop.txt +40 -0
  46. data/golden/decimal_explicit/expected/nast.txt +30 -0
  47. data/golden/decimal_explicit/expected/schema_javascript.mjs +31 -0
  48. data/golden/decimal_explicit/expected/schema_ruby.rb +57 -0
  49. data/golden/decimal_explicit/expected/snast.txt +30 -0
  50. data/golden/decimal_explicit/expected.json +1 -0
  51. data/golden/decimal_explicit/input.json +5 -0
  52. data/golden/decimal_explicit/schema.kumi +14 -0
  53. data/golden/element_arrays/expected/nast.txt +2 -2
  54. data/golden/element_arrays/expected/schema_ruby.rb +1 -1
  55. data/golden/element_arrays/expected/snast.txt +1 -1
  56. data/golden/empty_and_null_inputs/expected/nast.txt +3 -3
  57. data/golden/empty_and_null_inputs/expected/schema_ruby.rb +1 -1
  58. data/golden/function_overload/expected/ast.txt +29 -0
  59. data/golden/function_overload/expected/input_plan.txt +4 -0
  60. data/golden/function_overload/expected/lir_00_unoptimized.txt +18 -0
  61. data/golden/function_overload/expected/lir_01_hoist_scalar_references.txt +18 -0
  62. data/golden/function_overload/expected/lir_02_inlined.txt +20 -0
  63. data/golden/function_overload/expected/lir_03_cse.txt +20 -0
  64. data/golden/function_overload/expected/lir_04_1_loop_fusion.txt +20 -0
  65. data/golden/function_overload/expected/lir_04_loop_invcm.txt +20 -0
  66. data/golden/function_overload/expected/lir_06_const_prop.txt +20 -0
  67. data/golden/function_overload/expected/nast.txt +22 -0
  68. data/golden/function_overload/expected/schema_javascript.mjs +12 -0
  69. data/golden/function_overload/expected/schema_ruby.rb +39 -0
  70. data/golden/function_overload/expected/snast.txt +22 -0
  71. data/golden/function_overload/input.json +8 -0
  72. data/golden/function_overload/schema.kumi +19 -0
  73. data/golden/game_of_life/expected/lir_00_unoptimized.txt +4 -4
  74. data/golden/game_of_life/expected/lir_01_hoist_scalar_references.txt +4 -4
  75. data/golden/game_of_life/expected/lir_02_inlined.txt +16 -16
  76. data/golden/game_of_life/expected/lir_03_cse.txt +20 -16
  77. data/golden/game_of_life/expected/lir_04_1_loop_fusion.txt +20 -16
  78. data/golden/game_of_life/expected/lir_04_loop_invcm.txt +20 -16
  79. data/golden/game_of_life/expected/lir_06_const_prop.txt +20 -16
  80. data/golden/game_of_life/expected/nast.txt +4 -4
  81. data/golden/game_of_life/expected/schema_javascript.mjs +4 -2
  82. data/golden/game_of_life/expected/schema_ruby.rb +5 -3
  83. data/golden/game_of_life/expected/snast.txt +10 -10
  84. data/golden/hash_keys/expected/schema_ruby.rb +1 -1
  85. data/golden/hash_value/expected/nast.txt +1 -1
  86. data/golden/hash_value/expected/schema_ruby.rb +1 -1
  87. data/golden/hash_value/expected/snast.txt +1 -1
  88. data/golden/hierarchical_complex/expected/nast.txt +3 -3
  89. data/golden/hierarchical_complex/expected/schema_ruby.rb +1 -1
  90. data/golden/hierarchical_complex/expected/snast.txt +3 -3
  91. data/golden/inline_rename_scope_leak/expected/nast.txt +3 -3
  92. data/golden/inline_rename_scope_leak/expected/schema_ruby.rb +1 -1
  93. data/golden/input_reference/expected/nast.txt +2 -2
  94. data/golden/input_reference/expected/schema_ruby.rb +1 -1
  95. data/golden/interleaved_fusion/expected/nast.txt +2 -2
  96. data/golden/interleaved_fusion/expected/schema_ruby.rb +1 -1
  97. data/golden/let_inline/expected/nast.txt +4 -4
  98. data/golden/let_inline/expected/schema_ruby.rb +1 -1
  99. data/golden/loop_fusion/expected/nast.txt +1 -1
  100. data/golden/loop_fusion/expected/schema_ruby.rb +1 -1
  101. data/golden/min_reduce_scope/expected/nast.txt +3 -3
  102. data/golden/min_reduce_scope/expected/schema_ruby.rb +1 -1
  103. data/golden/min_reduce_scope/expected/snast.txt +1 -1
  104. data/golden/mixed_dimensions/expected/nast.txt +2 -2
  105. data/golden/mixed_dimensions/expected/schema_ruby.rb +1 -1
  106. data/golden/multirank_hoisting/expected/nast.txt +7 -7
  107. data/golden/multirank_hoisting/expected/schema_ruby.rb +1 -1
  108. data/golden/nested_hash/expected/nast.txt +1 -1
  109. data/golden/nested_hash/expected/schema_ruby.rb +1 -1
  110. data/golden/reduction_broadcast/expected/nast.txt +3 -3
  111. data/golden/reduction_broadcast/expected/schema_ruby.rb +1 -1
  112. data/golden/reduction_broadcast/expected/snast.txt +1 -1
  113. data/golden/roll/expected/schema_ruby.rb +1 -1
  114. data/golden/shift/expected/schema_ruby.rb +1 -1
  115. data/golden/shift_2d/expected/schema_ruby.rb +1 -1
  116. data/golden/simple_math/expected/lir_00_unoptimized.txt +1 -1
  117. data/golden/simple_math/expected/lir_01_hoist_scalar_references.txt +1 -1
  118. data/golden/simple_math/expected/lir_02_inlined.txt +1 -1
  119. data/golden/simple_math/expected/lir_03_cse.txt +1 -1
  120. data/golden/simple_math/expected/lir_04_1_loop_fusion.txt +1 -1
  121. data/golden/simple_math/expected/lir_04_loop_invcm.txt +1 -1
  122. data/golden/simple_math/expected/lir_06_const_prop.txt +1 -1
  123. data/golden/simple_math/expected/nast.txt +5 -5
  124. data/golden/simple_math/expected/schema_ruby.rb +1 -1
  125. data/golden/simple_math/expected/snast.txt +2 -2
  126. data/golden/streaming_basics/expected/nast.txt +8 -8
  127. data/golden/streaming_basics/expected/schema_ruby.rb +1 -1
  128. data/golden/streaming_basics/expected/snast.txt +1 -1
  129. data/golden/tuples/expected/lir_00_unoptimized.txt +5 -5
  130. data/golden/tuples/expected/lir_01_hoist_scalar_references.txt +5 -5
  131. data/golden/tuples/expected/lir_02_inlined.txt +5 -5
  132. data/golden/tuples/expected/lir_03_cse.txt +5 -5
  133. data/golden/tuples/expected/lir_04_1_loop_fusion.txt +5 -5
  134. data/golden/tuples/expected/lir_04_loop_invcm.txt +5 -5
  135. data/golden/tuples/expected/lir_06_const_prop.txt +5 -5
  136. data/golden/tuples/expected/nast.txt +4 -4
  137. data/golden/tuples/expected/schema_ruby.rb +1 -1
  138. data/golden/tuples/expected/snast.txt +6 -6
  139. data/golden/tuples_and_arrays/expected/lir_00_unoptimized.txt +1 -1
  140. data/golden/tuples_and_arrays/expected/lir_01_hoist_scalar_references.txt +1 -1
  141. data/golden/tuples_and_arrays/expected/lir_02_inlined.txt +2 -2
  142. data/golden/tuples_and_arrays/expected/lir_03_cse.txt +2 -2
  143. data/golden/tuples_and_arrays/expected/lir_04_1_loop_fusion.txt +2 -2
  144. data/golden/tuples_and_arrays/expected/lir_04_loop_invcm.txt +2 -2
  145. data/golden/tuples_and_arrays/expected/lir_06_const_prop.txt +2 -2
  146. data/golden/tuples_and_arrays/expected/nast.txt +3 -3
  147. data/golden/tuples_and_arrays/expected/schema_ruby.rb +1 -1
  148. data/golden/tuples_and_arrays/expected/snast.txt +2 -2
  149. data/golden/us_tax_2024/expected/ast.txt +63 -670
  150. data/golden/us_tax_2024/expected/input_plan.txt +8 -45
  151. data/golden/us_tax_2024/expected/lir_00_unoptimized.txt +253 -863
  152. data/golden/us_tax_2024/expected/lir_01_hoist_scalar_references.txt +253 -863
  153. data/golden/us_tax_2024/expected/lir_02_inlined.txt +1215 -5139
  154. data/golden/us_tax_2024/expected/lir_03_cse.txt +587 -2460
  155. data/golden/us_tax_2024/expected/lir_04_1_loop_fusion.txt +632 -2480
  156. data/golden/us_tax_2024/expected/lir_04_loop_invcm.txt +587 -2460
  157. data/golden/us_tax_2024/expected/lir_06_const_prop.txt +587 -2460
  158. data/golden/us_tax_2024/expected/nast.txt +123 -826
  159. data/golden/us_tax_2024/expected/schema_javascript.mjs +127 -581
  160. data/golden/us_tax_2024/expected/schema_ruby.rb +135 -610
  161. data/golden/us_tax_2024/expected/snast.txt +155 -858
  162. data/golden/us_tax_2024/expected.json +120 -1
  163. data/golden/us_tax_2024/input.json +18 -9
  164. data/golden/us_tax_2024/schema.kumi +48 -178
  165. data/golden/with_constants/expected/lir_00_unoptimized.txt +1 -1
  166. data/golden/with_constants/expected/lir_01_hoist_scalar_references.txt +1 -1
  167. data/golden/with_constants/expected/lir_02_inlined.txt +1 -1
  168. data/golden/with_constants/expected/lir_03_cse.txt +1 -1
  169. data/golden/with_constants/expected/lir_04_1_loop_fusion.txt +1 -1
  170. data/golden/with_constants/expected/lir_04_loop_invcm.txt +1 -1
  171. data/golden/with_constants/expected/lir_06_const_prop.txt +1 -1
  172. data/golden/with_constants/expected/nast.txt +2 -2
  173. data/golden/with_constants/expected/schema_ruby.rb +1 -1
  174. data/golden/with_constants/expected/snast.txt +2 -2
  175. data/lib/kumi/analyzer.rb +12 -12
  176. data/lib/kumi/configuration.rb +6 -0
  177. data/lib/kumi/core/analyzer/passes/formal_constraint_propagator.rb +236 -0
  178. data/lib/kumi/core/analyzer/passes/input_collector.rb +22 -4
  179. data/lib/kumi/core/analyzer/passes/nast_dimensional_analyzer_pass.rb +64 -18
  180. data/lib/kumi/core/analyzer/passes/normalize_to_nast_pass.rb +9 -4
  181. data/lib/kumi/core/analyzer/passes/snast_pass.rb +3 -1
  182. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +172 -198
  183. data/lib/kumi/core/error_reporter.rb +36 -1
  184. data/lib/kumi/core/errors.rb +33 -1
  185. data/lib/kumi/core/functions/function_spec.rb +5 -4
  186. data/lib/kumi/core/functions/loader.rb +17 -1
  187. data/lib/kumi/core/functions/overload_resolver.rb +164 -0
  188. data/lib/kumi/core/functions/type_error_reporter.rb +118 -0
  189. data/lib/kumi/core/functions/type_rules.rb +155 -35
  190. data/lib/kumi/core/input/type_matcher.rb +8 -1
  191. data/lib/kumi/core/ruby_parser/input_builder.rb +2 -2
  192. data/lib/kumi/core/types/inference.rb +29 -22
  193. data/lib/kumi/core/types/normalizer.rb +30 -45
  194. data/lib/kumi/core/types/validator.rb +17 -28
  195. data/lib/kumi/core/types/value_objects.rb +116 -0
  196. data/lib/kumi/core/types.rb +45 -37
  197. data/lib/kumi/dev/golden/reporter.rb +9 -0
  198. data/lib/kumi/dev/golden/result.rb +3 -1
  199. data/lib/kumi/dev/golden/runtime_test.rb +25 -0
  200. data/lib/kumi/dev/golden/suite.rb +4 -4
  201. data/lib/kumi/dev/golden/value_normalizer.rb +80 -0
  202. data/lib/kumi/dev/golden.rb +21 -12
  203. data/lib/kumi/doc_generator/formatters/json.rb +39 -0
  204. data/lib/kumi/doc_generator/formatters/markdown.rb +175 -0
  205. data/lib/kumi/doc_generator/loader.rb +37 -0
  206. data/lib/kumi/doc_generator/merger.rb +54 -0
  207. data/lib/kumi/doc_generator.rb +4 -0
  208. data/lib/kumi/registry_v2/loader.rb +90 -0
  209. data/lib/kumi/registry_v2.rb +18 -1
  210. data/lib/kumi/version.rb +1 -1
  211. data/vscode-extension/.gitignore +4 -0
  212. data/vscode-extension/README.md +59 -0
  213. data/vscode-extension/TESTING.md +151 -0
  214. data/vscode-extension/package.json +51 -0
  215. data/vscode-extension/src/extension.ts +295 -0
  216. data/vscode-extension/tsconfig.json +15 -0
  217. metadata +57 -7
  218. data/lib/kumi/core/analyzer/unsat_constant_evaluator.rb +0 -59
  219. data/lib/kumi/core/atom_unsat_solver.rb +0 -396
  220. data/lib/kumi/core/constraint_relationship_solver.rb +0 -641
  221. data/lib/kumi/core/types/builder.rb +0 -23
  222. data/lib/kumi/core/types/compatibility.rb +0 -96
  223. 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
@@ -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
 
@@ -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