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,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Functions
6
+ # OverloadResolver handles type-aware function overload resolution
7
+ # Given a function alias/id and argument types, finds the best matching function
8
+ #
9
+ # Responsibilities:
10
+ # - Track all function overloads per alias
11
+ # - Match argument types against parameter constraints
12
+ # - Provide clear error messages when resolution fails
13
+ class OverloadResolver
14
+ def initialize(functions_by_id)
15
+ @functions = functions_by_id # "core.mul" => Function
16
+ @by_id = functions_by_id # Direct lookup
17
+ @alias_overloads = build_alias_overloads(functions_by_id)
18
+ end
19
+
20
+ # Resolve a function alias or ID to a specific function ID based on argument types
21
+ #
22
+ # @param alias_or_id [String, Symbol] Function alias or full function ID
23
+ # @param arg_types [Array<Symbol>] Inferred types of arguments
24
+ # @return [String] The resolved function_id
25
+ # @raise [ResolutionError] If function cannot be resolved
26
+ def resolve(alias_or_id, arg_types)
27
+ s = alias_or_id.to_s
28
+
29
+ # If it's already a full function ID, validate and return it
30
+ if @functions.key?(s)
31
+ validate_arity!(s, arg_types)
32
+ return s
33
+ end
34
+
35
+ # Get all candidate overloads for this alias
36
+ candidates = @alias_overloads[s]
37
+ raise ResolutionError, "unknown function #{alias_or_id}" if candidates.nil?
38
+
39
+ # Single overload - use it directly
40
+ if candidates.size == 1
41
+ validate_arity!(candidates.first, arg_types)
42
+ return candidates.first
43
+ end
44
+
45
+ # Multiple overloads - find best match by type constraints (prefer exact matches)
46
+ candidates_with_scores = candidates.map do |fn_id|
47
+ fn = @functions[fn_id]
48
+ score = match_score(fn.params, arg_types)
49
+ [fn_id, score]
50
+ end
51
+
52
+ best_match, score = candidates_with_scores.max_by { |_, s| s }
53
+
54
+ if score > 0
55
+ return best_match
56
+ end
57
+
58
+ # No match found - provide helpful error
59
+ available = candidates.map { |id| @functions[id].id }.join(", ")
60
+ raise ResolutionError,
61
+ "no overload of '#{alias_or_id}' matches argument types #{arg_types.inspect}. " \
62
+ "Available overloads: #{available}"
63
+ end
64
+
65
+ # Get function object by ID (already resolved)
66
+ def function(id)
67
+ @functions.fetch(id) do
68
+ raise ResolutionError, "unknown function #{id}"
69
+ end
70
+ end
71
+
72
+ # Check if a function exists
73
+ def exists?(id)
74
+ @functions.key?(id.to_s)
75
+ end
76
+
77
+ private
78
+
79
+ def build_alias_overloads(functions)
80
+ # Maps each alias to an array of all function_ids that have that alias
81
+ functions.values.each_with_object({}) do |func, acc|
82
+ func.aliases.each do |al|
83
+ acc[al] ||= []
84
+ acc[al] << func.id
85
+ end
86
+ end
87
+ end
88
+
89
+ def params_match?(params, arg_types)
90
+ # Check arity first
91
+ return false if params.size != arg_types.size
92
+
93
+ # Check each parameter constraint
94
+ params.zip(arg_types).all? do |param, arg_type|
95
+ param_dtype = param["dtype"]
96
+ param_dtype.nil? || type_compatible?(param_dtype, arg_type)
97
+ end
98
+ end
99
+
100
+ def match_score(params, arg_types)
101
+ # Returns match quality: higher is better
102
+ # 0 = no match, 1 = matches with unconstrained params, 2 = exact match
103
+ return 0 unless params_match?(params, arg_types)
104
+
105
+ # Count exact constraint matches (all arg_types are Type objects now)
106
+ exact_matches = params.zip(arg_types).count do |param, arg_type|
107
+ param_dtype = param["dtype"]
108
+ score_type_object_match(param_dtype, arg_type)
109
+ end
110
+
111
+ exact_matches
112
+ end
113
+
114
+ def score_type_object_match(param_dtype, type_obj)
115
+ case param_dtype&.to_s
116
+ when "string"
117
+ type_obj.is_a?(Kumi::Core::Types::ScalarType) && type_obj.kind == :string
118
+ when "array"
119
+ type_obj.is_a?(Kumi::Core::Types::ArrayType)
120
+ when "integer"
121
+ type_obj.is_a?(Kumi::Core::Types::ScalarType) && type_obj.kind == :integer
122
+ when "float"
123
+ type_obj.is_a?(Kumi::Core::Types::ScalarType) && type_obj.kind == :float
124
+ when "hash"
125
+ type_obj.is_a?(Kumi::Core::Types::ScalarType) && type_obj.kind == :hash
126
+ else
127
+ false
128
+ end
129
+ end
130
+
131
+ def type_compatible?(param_dtype_str, arg_type)
132
+ raise ArgumentError, "arg_type must be a Type object, got #{arg_type.inspect}" unless arg_type.is_a?(Kumi::Core::Types::Type)
133
+
134
+ case param_dtype_str
135
+ when "string"
136
+ arg_type.is_a?(Kumi::Core::Types::ScalarType) && arg_type.kind == :string
137
+ when "array"
138
+ arg_type.is_a?(Kumi::Core::Types::ArrayType)
139
+ when "integer"
140
+ arg_type.is_a?(Kumi::Core::Types::ScalarType) && arg_type.kind == :integer
141
+ when "float"
142
+ arg_type.is_a?(Kumi::Core::Types::ScalarType) && arg_type.kind == :float
143
+ when "hash"
144
+ arg_type.is_a?(Kumi::Core::Types::ScalarType) && arg_type.kind == :hash
145
+ else
146
+ # No constraint, any type matches
147
+ true
148
+ end
149
+ end
150
+
151
+ def validate_arity!(fn_id, arg_types)
152
+ fn = @functions[fn_id]
153
+ return if fn.params.size == arg_types.size
154
+
155
+ raise ResolutionError,
156
+ "function #{fn_id} expects #{fn.params.size} arguments, got #{arg_types.size}"
157
+ end
158
+
159
+ # Custom error for function resolution failures
160
+ class ResolutionError < StandardError; end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Functions
6
+ # TypeErrorReporter provides typed error reporting for function resolution and type checking
7
+ # Ensures all type errors have proper location information for better diagnostics
8
+ module TypeErrorReporter
9
+ # Report function overload resolution failure with proper location
10
+ #
11
+ # @param errors [Array] Error accumulator
12
+ # @param alias_or_id [String, Symbol] Function alias or ID that couldn't be resolved
13
+ # @param arg_types [Array<Symbol>] Argument types that didn't match any overload
14
+ # @param available_overloads [Array<String>] Available function overload IDs
15
+ # @param location [Syntax::Location, nil] Where the error occurred
16
+ def self.report_overload_resolution_error(errors, alias_or_id, arg_types, available_overloads, location)
17
+ message = format_overload_error(alias_or_id, arg_types, available_overloads)
18
+
19
+ error = Core::ErrorReporter.create_error(
20
+ message,
21
+ location: location,
22
+ type: :type,
23
+ context: {
24
+ alias: alias_or_id.to_s,
25
+ arg_types: arg_types,
26
+ candidates: available_overloads
27
+ }
28
+ )
29
+
30
+ errors << error
31
+ error
32
+ end
33
+
34
+ # Report arity mismatch (wrong number of arguments)
35
+ #
36
+ # @param errors [Array] Error accumulator
37
+ # @param fn_id [String] Full function ID
38
+ # @param expected [Integer] Expected number of arguments
39
+ # @param actual [Integer] Actual number of arguments provided
40
+ # @param location [Syntax::Location, nil] Where the error occurred
41
+ def self.report_arity_mismatch(errors, fn_id, expected, actual, location)
42
+ message = "function '#{fn_id}' expects #{expected} argument(s), got #{actual}"
43
+
44
+ error = Core::ErrorReporter.create_error(
45
+ message,
46
+ location: location,
47
+ type: :type,
48
+ context: {
49
+ function: fn_id.to_s,
50
+ expected: expected,
51
+ actual: actual
52
+ }
53
+ )
54
+
55
+ errors << error
56
+ error
57
+ end
58
+
59
+ # Report type constraint violation (parameter type doesn't match argument type)
60
+ #
61
+ # @param errors [Array] Error accumulator
62
+ # @param fn_id [String] Full function ID
63
+ # @param param_name [String] Parameter name
64
+ # @param expected_type [String] Expected type constraint
65
+ # @param actual_type [Symbol] Actual argument type
66
+ # @param location [Syntax::Location, nil] Where the error occurred
67
+ def self.report_type_constraint_violation(errors, fn_id, param_name, expected_type, actual_type, location)
68
+ message = "function '#{fn_id}' parameter '#{param_name}' expects type #{expected_type.inspect}, " \
69
+ "got #{actual_type.inspect}"
70
+
71
+ error = Core::ErrorReporter.create_error(
72
+ message,
73
+ location: location,
74
+ type: :type,
75
+ context: {
76
+ function: fn_id.to_s,
77
+ parameter: param_name.to_s,
78
+ expected_type: expected_type.to_s,
79
+ actual_type: actual_type.to_s
80
+ }
81
+ )
82
+
83
+ errors << error
84
+ error
85
+ end
86
+
87
+ # Report unknown function
88
+ #
89
+ # @param errors [Array] Error accumulator
90
+ # @param alias_or_id [String, Symbol] Function name/alias that doesn't exist
91
+ # @param location [Syntax::Location, nil] Where the error occurred
92
+ def self.report_unknown_function(errors, alias_or_id, location)
93
+ message = "unknown function '#{alias_or_id}'"
94
+
95
+ error = Core::ErrorReporter.create_error(
96
+ message,
97
+ location: location,
98
+ type: :semantic,
99
+ context: { function: alias_or_id.to_s }
100
+ )
101
+
102
+ errors << error
103
+ error
104
+ end
105
+
106
+ private
107
+
108
+ def self.format_overload_error(alias_or_id, arg_types, available_overloads)
109
+ arg_types_str = arg_types.map(&:inspect).join(", ")
110
+ available_str = available_overloads.map { |id| "'#{id}'" }.join(", ")
111
+
112
+ "no overload of '#{alias_or_id}' matches argument types (#{arg_types_str}). " \
113
+ "Available overloads: #{available_str}"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -6,17 +6,62 @@ module Kumi
6
6
  module TypeRules
7
7
  module_function
8
8
 
9
+ # Convert Type objects or symbols to Type objects
10
+ def to_type_object(type_input)
11
+ return type_input if type_input.is_a?(Kumi::Core::Types::Type)
12
+
13
+ # Convert symbol/string to Type object
14
+ case type_input
15
+ when :string
16
+ Kumi::Core::Types.scalar(:string)
17
+ when :integer
18
+ Kumi::Core::Types.scalar(:integer)
19
+ when :float
20
+ Kumi::Core::Types.scalar(:float)
21
+ when :boolean
22
+ Kumi::Core::Types.scalar(:boolean)
23
+ when :hash
24
+ Kumi::Core::Types.scalar(:hash)
25
+ when String
26
+ # Handle string type representations like "array<integer>" or "tuple<float, integer>"
27
+ parse_string_type(type_input)
28
+ else
29
+ # For any other type representation, normalize first
30
+ normalized = Kumi::Core::Types.normalize(type_input)
31
+ to_type_object(normalized)
32
+ end
33
+ end
34
+
35
+ def parse_string_type(str_type)
36
+ # Handle array types: "array<integer>"
37
+ if (m = /\Aarray<(.+)>\z/.match(str_type))
38
+ element_str = m[1]
39
+ element_type = to_type_object(element_str.to_sym)
40
+ return Kumi::Core::Types.array(element_type)
41
+ end
42
+
43
+ # Handle tuple types: "tuple<integer, float>"
44
+ if (m = /\Atuple<(.+)>\z/.match(str_type))
45
+ element_strs = m[1].split(",").map(&:strip)
46
+ element_types = element_strs.map { |s| to_type_object(s.to_sym) }
47
+ return Kumi::Core::Types.tuple(element_types)
48
+ end
49
+
50
+ # Try as symbol
51
+ to_type_object(str_type.to_sym)
52
+ end
53
+
9
54
  def normalize_type_symbol(type_symbol)
10
55
  Kumi::Core::Types.normalize(type_symbol)
11
56
  end
12
57
 
13
- # Minimal type promotion for NAST analysis
58
+ # Type promotion for NAST analysis - returns Type objects
14
59
  def promote_types(*input_types)
15
- normalized = input_types.flatten.compact.uniq
16
- return :float if normalized.include?(:float)
17
- return :integer if normalized.include?(:integer)
60
+ types = input_types.flatten.compact.uniq
61
+ return Kumi::Core::Types.scalar(:float) if types.any? { |t| float_type?(t) }
62
+ return Kumi::Core::Types.scalar(:integer) if types.any? { |t| integer_type?(t) }
18
63
 
19
- normalized.first
64
+ to_type_object(types.first)
20
65
  end
21
66
 
22
67
  def common_type(element_types)
@@ -26,81 +71,156 @@ module Kumi
26
71
  def unify_types(type1, type2)
27
72
  return type1 if type1 == type2
28
73
 
29
- promote_types(type1, type2) # Fall back to promotion for now
74
+ promote_types(type1, type2)
30
75
  end
31
76
 
32
- def same_type_as(reference_type_symbol)
33
- normalize_type_symbol(reference_type_symbol)
77
+ def same_type_as(reference_type)
78
+ to_type_object(reference_type)
34
79
  rescue StandardError
35
- # binding.pry
36
- # raise
37
- # TODO CHECK HOW HANDLE THIS
80
+ to_type_object(reference_type)
38
81
  end
39
82
 
40
83
  def array_type(element_type)
41
- :"array<#{element_type}>"
84
+ element_obj = to_type_object(element_type)
85
+ Kumi::Core::Types.array(element_obj)
42
86
  end
43
87
 
44
88
  def tuple_type(*element_types)
45
- :"tuple<#{element_types.join(', ')}>"
89
+ element_objs = element_types.map { |t| to_type_object(t) }
90
+ Kumi::Core::Types.tuple(element_objs)
46
91
  end
47
92
 
48
- # Parses a collection type symbol to find its element type.
93
+ # Extract element type from collection Type objects
49
94
  def element_type_of(collection_type)
50
- str_type = collection_type.to_s
51
- if (m = /\Aarray<(.+)>\z/.match(str_type))
52
- return m[1].to_sym
95
+ type_obj = to_type_object(collection_type)
96
+
97
+ case type_obj
98
+ when Kumi::Core::Types::ArrayType
99
+ type_obj.element_type
100
+ when Kumi::Core::Types::TupleType
101
+ # Promote all element types to common type
102
+ promote_types(*type_obj.element_types)
103
+ else
104
+ type_obj
53
105
  end
106
+ end
54
107
 
55
- if (m = /\Atuple<(.+)>\z/.match(str_type))
56
- # The "element type" of a tuple is the common promoted type of its members.
57
- # e.g., tuple<integer, float> -> float
58
- member_types = m[1].split(",").map { |s| s.strip.to_sym }
59
- return promote_types(member_types)
108
+ # --- Typed Rule Builders (Direct Type Construction) ---
109
+
110
+ # Build rule: return the type of a specific parameter
111
+ def build_same_as(param_name)
112
+ ->(named) { same_type_as(named.fetch(param_name)) }
113
+ end
114
+
115
+ # Build rule: promote types of multiple parameters
116
+ def build_promote(*param_names)
117
+ ->(named) { promote_types(*param_names.map { |k| named.fetch(k) }) }
118
+ end
119
+
120
+ # Build rule: extract element type from a collection parameter
121
+ def build_element_of(param_name)
122
+ ->(named) { element_type_of(named.fetch(param_name)) }
123
+ end
124
+
125
+ # Build rule: unify types of two parameters
126
+ def build_unify(param_name1, param_name2)
127
+ ->(named) { unify_types(named.fetch(param_name1), named.fetch(param_name2)) }
128
+ end
129
+
130
+ # Build rule: common type among array elements
131
+ def build_common_type(param_name)
132
+ ->(named) { common_type(named.fetch(param_name)) }
133
+ end
134
+
135
+ # Build rule: array of a specific element type
136
+ def build_array(element_type_or_param_name)
137
+ # Check if it's a known scalar kind or Type object
138
+ if element_type_or_param_name.is_a?(Kumi::Core::Types::Type)
139
+ # Type object - use directly
140
+ type_obj = element_type_or_param_name
141
+ ->(_) { Kumi::Core::Types.array(type_obj) }
142
+ elsif element_type_or_param_name.is_a?(Symbol) && Kumi::Core::Types::Validator.valid_kind?(element_type_or_param_name)
143
+ # Known scalar kind - create Type and wrap
144
+ type_obj = to_type_object(element_type_or_param_name)
145
+ ->(_) { Kumi::Core::Types.array(type_obj) }
146
+ else
147
+ # Treat as parameter name reference
148
+ ->(named) { array_type(named.fetch(element_type_or_param_name)) }
60
149
  end
150
+ end
151
+
152
+ # Build rule: tuple of specific element types
153
+ def build_tuple(*element_types_or_param_names)
154
+ # If single symbol and NOT a known scalar kind, treat as parameter reference
155
+ if element_types_or_param_names.size == 1 && element_types_or_param_names[0].is_a?(Symbol)
156
+ sym = element_types_or_param_names[0]
157
+ unless Kumi::Core::Types::Validator.valid_kind?(sym)
158
+ # Not a known kind - treat as parameter name (holds array of types)
159
+ return ->(named) { tuple_type(*named.fetch(sym)) }
160
+ end
161
+ end
162
+
163
+ # Interpret as explicit types
164
+ type_objs = element_types_or_param_names.map { |t| to_type_object(t) }
165
+ ->(_) { Kumi::Core::Types.tuple(type_objs) }
166
+ end
61
167
 
62
- normalize_type_symbol(str_type)
168
+ # Build rule: constant scalar type
169
+ def build_scalar(kind)
170
+ ->(_) { to_type_object(kind) }
63
171
  end
64
172
 
65
- # Compile dtype rule string into callable
173
+ # --- Compile dtype rule string into callable (backward compatible) ---
66
174
  def compile_dtype_rule(rule_string, _parameter_names)
67
175
  rule = rule_string.to_s.strip
68
176
 
69
- # --- NEW: Handle the "element_of" rule ---
70
177
  if (m = /\Aelement_of\((.+)\)\z/.match(rule))
71
178
  key = m[1].strip.to_sym
72
- return ->(named) { element_type_of(named.fetch(key)) }
179
+ return build_element_of(key)
73
180
  end
74
181
 
75
- # Handle existing function-based rules
76
182
  if (m = /\Apromote\((.+)\)\z/.match(rule))
77
183
  keys = m[1].split(",").map { |s| s.strip.to_sym }
78
- return ->(named) { promote_types(*keys.map { |k| named.fetch(k) }) }
184
+ return build_promote(*keys)
79
185
  end
186
+
80
187
  if (m = /\Asame_as\((.+)\)\z/.match(rule))
81
188
  key = m[1].strip.to_sym
82
- return ->(named) { same_type_as(named.fetch(key)) }
189
+ return build_same_as(key)
83
190
  end
84
- if (m = /\Aunify\(([^,]+),\s*([^)]+)\)\z/.match(rule)) # TODO: - check if needed or is just the promote
191
+
192
+ if (m = /\Aunify\(([^,]+),\s*([^)]+)\)\z/.match(rule))
85
193
  k1 = m[1].strip.to_sym
86
194
  k2 = m[2].strip.to_sym
87
- return ->(named) { unify_types(named.fetch(k1), named.fetch(k2)) }
195
+ return build_unify(k1, k2)
88
196
  end
197
+
89
198
  if (m = /\Acommon_type\((.+)\)\z/.match(rule))
90
199
  param_name = m[1].strip.to_sym
91
- return ->(named) { common_type(named.fetch(param_name)) }
200
+ return build_common_type(param_name)
92
201
  end
202
+
93
203
  if (m = /\Aarray\((.+)\)\z/.match(rule))
94
204
  inner_rule = m[1].strip
95
- inner_compiled = compile_dtype_rule(inner_rule, []) # param_names not needed here
205
+ inner_compiled = compile_dtype_rule(inner_rule, [])
96
206
  return ->(named) { array_type(inner_compiled.call(named)) }
97
207
  end
208
+
98
209
  if (m = /\Atuple\(types\((.+)\)\)\z/.match(rule))
99
210
  param_name = m[1].strip.to_sym
100
- return ->(named) { tuple_type(*named.fetch(param_name)) }
211
+ return build_tuple(param_name)
101
212
  end
102
213
 
103
- ->(_) { normalize_type_symbol(rule.to_sym) }
214
+ # Constant scalar type
215
+ build_scalar(rule.to_sym)
216
+ end
217
+
218
+ def float_type?(t)
219
+ t.is_a?(Kumi::Core::Types::ScalarType) ? t.kind == :float : t == :float
220
+ end
221
+
222
+ def integer_type?(t)
223
+ t.is_a?(Kumi::Core::Types::ScalarType) ? t.kind == :integer : t == :integer
104
224
  end
105
225
  end
106
226
  end
@@ -9,31 +9,38 @@ module Kumi
9
9
  class Inference
10
10
  def self.infer_from_value(value)
11
11
  case value
12
- when String then :string
13
- when Integer then :integer
14
- when Float then :float
15
- when TrueClass, FalseClass then :boolean
16
- when Symbol then :symbol
17
- when Regexp then :regexp
18
- when Time then :time
19
- when DateTime then :datetime
20
- when Date then :date
12
+ when String
13
+ Kumi::Core::Types.scalar(:string)
14
+ when Integer
15
+ Kumi::Core::Types.scalar(:integer)
16
+ when Float
17
+ Kumi::Core::Types.scalar(:float)
18
+ when TrueClass, FalseClass
19
+ Kumi::Core::Types.scalar(:boolean)
20
+ when Symbol
21
+ Kumi::Core::Types.scalar(:symbol)
22
+ when Regexp
23
+ Kumi::Core::Types.scalar(:regexp)
24
+ when Time
25
+ Kumi::Core::Types.scalar(:time)
26
+ when DateTime
27
+ Kumi::Core::Types.scalar(:datetime)
28
+ when Date
29
+ Kumi::Core::Types.scalar(:date)
21
30
  when Array
22
- return Kumi::Core::Types.array(:any) if value.empty?
23
-
24
- # Infer element type from first element (simple heuristic)
25
- first_elem_type = infer_from_value(value.first)
26
- Kumi::Core::Types.array(first_elem_type)
31
+ if value.empty?
32
+ Kumi::Core::Types.array(Kumi::Core::Types.scalar(:any))
33
+ else
34
+ # Infer element type from first element (simple heuristic)
35
+ elem_type = infer_from_value(value.first)
36
+ Kumi::Core::Types.array(elem_type)
37
+ end
27
38
  when Hash
28
- return Kumi::Core::Types.hash(:any, :any) if value.empty?
29
-
30
- # Infer key/value types from first pair (simple heuristic)
31
- first_key, first_value = value.first
32
- key_type = infer_from_value(first_key)
33
- value_type = infer_from_value(first_value)
34
- Kumi::Core::Types.hash(key_type, value_type)
39
+ # Kumi treats hash as scalar, not key/value pair type
40
+ # So we just return scalar(:hash) regardless of contents
41
+ Kumi::Core::Types.scalar(:hash)
35
42
  else
36
- :any
43
+ Kumi::Core::Types.scalar(:any)
37
44
  end
38
45
  end
39
46
  end