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
@@ -5,38 +5,45 @@ require "date"
5
5
  module Kumi
6
6
  module Core
7
7
  module Types
8
- # Normalizes different type inputs to canonical forms
8
+ # Normalizes different type inputs to canonical Type objects
9
9
  class Normalizer
10
- # Type normalization - convert various inputs to canonical type symbols
10
+ # Type normalization - convert various inputs to Type objects
11
11
  def self.normalize(type_input)
12
12
  case type_input
13
+ when Type
14
+ # Already a Type object, return as-is
15
+ type_input
13
16
  when Symbol
14
- return type_input if Validator.valid_type?(type_input)
15
-
16
- raise ArgumentError, "Invalid type symbol: #{type_input}"
17
+ if Validator.valid_kind?(type_input)
18
+ Kumi::Core::Types.scalar(type_input)
19
+ else
20
+ raise ArgumentError, "Invalid type symbol: #{type_input}"
21
+ end
17
22
  when String
18
23
  symbol_type = type_input.to_sym
19
- return symbol_type if Validator.valid_type?(symbol_type)
20
-
21
- raise ArgumentError, "Invalid type string: #{type_input}"
24
+ if Validator.valid_kind?(symbol_type)
25
+ Kumi::Core::Types.scalar(symbol_type)
26
+ else
27
+ raise ArgumentError, "Invalid type string: #{type_input}"
28
+ end
22
29
  when Hash
23
- return type_input if Validator.valid_type?(type_input)
24
-
25
- raise ArgumentError, "Invalid type hash: #{type_input}"
30
+ raise ArgumentError, "Hash-based types no longer supported, use Type objects instead"
26
31
  when Class
27
32
  # Handle Ruby class inputs
28
- case type_input.name
29
- when "NilClass" then :null
30
- when "Integer" then :integer
31
- when "String" then :string
32
- when "Float" then :float
33
- when "Symbol" then :symbol
34
- when "TrueClass", "FalseClass" then :boolean
35
- when "Array" then raise ArgumentError, "Use array(:type) helper for array types"
36
- when "Hash" then raise ArgumentError, "Use hash(:key_type, :value_type) helper for hash types"
37
- else
38
- raise ArgumentError, "Unsupported class type: #{type_input}"
39
- end
33
+ kind = case type_input.name
34
+ when "NilClass" then :null
35
+ when "Integer" then :integer
36
+ when "String" then :string
37
+ when "Float" then :float
38
+ when "Decimal", "BigDecimal" then :decimal
39
+ when "Symbol" then :symbol
40
+ when "TrueClass", "FalseClass" then :boolean
41
+ when "Array" then raise ArgumentError, "Use array(:type) helper for array types"
42
+ when "Hash" then raise ArgumentError, "Use scalar(:hash) for hash type"
43
+ else
44
+ raise ArgumentError, "Unsupported class type: #{type_input}"
45
+ end
46
+ Kumi::Core::Types.scalar(kind)
40
47
  else
41
48
  case type_input
42
49
  when Integer, Float, Numeric
@@ -46,28 +53,6 @@ module Kumi
46
53
  end
47
54
  end
48
55
  end
49
-
50
- # Legacy compatibility - coerce old constants to symbols
51
- def self.coerce(type_input)
52
- # Handle legacy constant usage
53
- return type_input if type_input.is_a?(Symbol) && Validator.valid_type?(type_input)
54
-
55
- # Handle legacy constant objects
56
- case type_input
57
- when STRING then :string
58
- when INT then :integer
59
- when FLOAT, NUMERIC then :float # Both FLOAT and NUMERIC map to :float
60
- when BOOL then :boolean
61
- when ANY then :any
62
- when SYMBOL then :symbol
63
- when REGEXP then :regexp
64
- when TIME then :time
65
- when DATE then :date
66
- when DATETIME then :datetime
67
- else
68
- normalize(type_input)
69
- end
70
- end
71
56
  end
72
57
  end
73
58
  end
@@ -5,38 +5,27 @@ module Kumi
5
5
  module Types
6
6
  # Validates type definitions and structures
7
7
  class Validator
8
- VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array hash null].freeze
8
+ VALID_TYPES = %i[string integer float decimal boolean any symbol regexp time date datetime array hash null].freeze
9
9
 
10
- def self.valid_type?(type)
11
- return true if !type.is_a?(Hash) && VALID_TYPES.include?(type.to_s.to_sym)
12
-
13
- return true if array_type?(type)
14
- return true if hash_type?(type)
15
-
16
- false
17
- end
18
-
19
- def self.array_type?(type)
20
- return true if type.is_a?(Hash) && type.keys == [:array] && valid_type?(type[:array])
10
+ # Validate scalar kinds (no :array or :hash)
11
+ VALID_KINDS = %i[string integer float decimal boolean any symbol regexp time date datetime null].freeze
21
12
 
22
- type = type.to_s
23
- type.is_a?(String) && type.match?(/^array<(.+)>$/) && type.scan(/(\w+)/)[1..-1].flatten.all? { |t| valid_type?(t) }
13
+ def self.valid_kind?(kind)
14
+ VALID_KINDS.include?(kind)
24
15
  end
25
16
 
26
- def item_types(type)
27
- end
28
-
29
- def self.hash_type?(type)
30
- type.is_a?(Hash) &&
31
- type.keys.sort == [:hash] &&
32
- type[:hash].is_a?(Array) &&
33
- type[:hash].size == 2 &&
34
- valid_type?(type[:hash][0]) &&
35
- valid_type?(type[:hash][1])
36
- end
37
-
38
- def self.primitive_type?(type)
39
- VALID_TYPES.include?(type)
17
+ def self.valid_type?(type)
18
+ # Support Type objects
19
+ case type
20
+ when ScalarType
21
+ valid_kind?(type.kind)
22
+ when ArrayType, TupleType
23
+ true # If constructed, it's valid
24
+ when Symbol
25
+ valid_kind?(type)
26
+ else
27
+ false
28
+ end
40
29
  end
41
30
  end
42
31
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Types
6
+ # Base class for all type objects
7
+ class Type
8
+ def scalar?
9
+ is_a?(ScalarType)
10
+ end
11
+
12
+ def array?
13
+ is_a?(ArrayType)
14
+ end
15
+
16
+ def tuple?
17
+ is_a?(TupleType)
18
+ end
19
+ end
20
+
21
+ # Represents scalar types: string, integer, float, boolean, hash
22
+ class ScalarType < Type
23
+ attr_reader :kind
24
+
25
+ def initialize(kind)
26
+ @kind = kind
27
+ end
28
+
29
+ def to_s
30
+ @kind.to_s
31
+ end
32
+
33
+ def inspect
34
+ "#<ScalarType:#{@kind}>"
35
+ end
36
+
37
+ def ==(other)
38
+ return false unless other.is_a?(ScalarType)
39
+ @kind == other.kind
40
+ end
41
+
42
+ def eql?(other)
43
+ self == other
44
+ end
45
+
46
+ def hash
47
+ [@kind].hash
48
+ end
49
+ end
50
+
51
+ # Represents array types with an element type
52
+ class ArrayType < Type
53
+ attr_reader :element_type
54
+
55
+ def initialize(element_type)
56
+ @element_type = element_type
57
+ end
58
+
59
+ def to_s
60
+ "array<#{@element_type}>"
61
+ end
62
+
63
+ def inspect
64
+ "#<ArrayType:#{to_s}>"
65
+ end
66
+
67
+ def ==(other)
68
+ return false unless other.is_a?(ArrayType)
69
+ @element_type == other.element_type
70
+ end
71
+
72
+ def eql?(other)
73
+ self == other
74
+ end
75
+
76
+ def hash
77
+ [@element_type].hash
78
+ end
79
+ end
80
+
81
+ # Represents tuple types with a list of element types
82
+ class TupleType < Type
83
+ attr_reader :element_types
84
+
85
+ def initialize(element_types)
86
+ @element_types = element_types
87
+ end
88
+
89
+ def to_s
90
+ "tuple<#{@element_types.join(', ')}>"
91
+ end
92
+
93
+ def inspect
94
+ "#<TupleType:#{to_s}>"
95
+ end
96
+
97
+ def ==(other)
98
+ return false unless other.is_a?(TupleType)
99
+ @element_types == other.element_types
100
+ end
101
+
102
+ def eql?(other)
103
+ self == other
104
+ end
105
+
106
+ def hash
107
+ @element_types.hash
108
+ end
109
+ end
110
+
111
+ # Namespace module for consistency with autoloader
112
+ module ValueObjects
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'types/value_objects'
4
+
3
5
  module Kumi
4
6
  module Core
5
7
  module Types
@@ -10,64 +12,70 @@ module Kumi
10
12
  tuple?(dtype) || array?(dtype)
11
13
  end
12
14
 
13
- def self.tuple?(dtype) = dtype == :tuple || dtype.match?(/^tuple</)
14
- def self.array?(dtype) = dtype == :array || dtype.match?(/^array</)
15
+ def self.tuple?(dtype)
16
+ dtype.is_a?(TupleType)
17
+ end
18
+
19
+ def self.array?(dtype)
20
+ dtype.is_a?(ArrayType)
21
+ end
15
22
 
16
23
  # Validation methods
17
24
  def self.valid_type?(type)
18
25
  Validator.valid_type?(type)
19
26
  end
20
27
 
21
- # Type builders
22
- def self.array(elem_type)
23
- Builder.array(elem_type)
28
+ # Type value object constructors
29
+ def self.scalar(kind)
30
+ ScalarType.new(kind)
24
31
  end
25
32
 
26
- def self.hash(key_type, val_type)
27
- Builder.hash(key_type, val_type)
33
+ def self.array(element_type)
34
+ elem_obj = case element_type
35
+ when Type
36
+ element_type
37
+ when :string, :integer, :float, :decimal, :boolean, :hash, :any, :symbol, :regexp, :time, :date, :datetime, :null
38
+ scalar(element_type)
39
+ else
40
+ raise ArgumentError,
41
+ "array element must be Type object or scalar kind, got #{element_type.inspect}"
42
+ end
43
+ ArrayType.new(elem_obj)
28
44
  end
29
45
 
30
- # Normalization
31
- def self.normalize(type_input)
32
- Normalizer.normalize(type_input)
33
- end
46
+ def self.tuple(element_types)
47
+ unless element_types.is_a?(Array)
48
+ raise ArgumentError, "tuple expects array of Type objects, got #{element_types.class}"
49
+ end
34
50
 
35
- def self.coerce(type_input)
36
- Normalizer.coerce(type_input)
51
+ # Convert any non-Type elements to Type objects
52
+ converted = element_types.map do |t|
53
+ case t
54
+ when Type
55
+ t
56
+ when :string, :integer, :float, :decimal, :boolean, :hash, :any, :symbol, :regexp, :time, :date, :datetime, :null
57
+ scalar(t)
58
+ else
59
+ raise ArgumentError, "tuple element must be Type or scalar kind, got #{t.inspect}"
60
+ end
61
+ end
62
+
63
+ TupleType.new(converted)
37
64
  end
38
65
 
39
- # Compatibility and unification
40
- def self.compatible?(type1, type2)
41
- Compatibility.compatible?(type1, type2)
66
+ def self.hash(key_type, val_type)
67
+ raise NotImplementedError, "Use scalar(:hash) instead - Kumi treats hash as scalar, not key/value pair"
42
68
  end
43
69
 
44
- def self.unify(type1, type2)
45
- Compatibility.unify(type1, type2)
70
+ # Normalization
71
+ def self.normalize(type_input)
72
+ Normalizer.normalize(type_input)
46
73
  end
47
74
 
48
75
  # Type inference
49
76
  def self.infer_from_value(value)
50
77
  Inference.infer_from_value(value)
51
78
  end
52
-
53
- # Formatting
54
- def self.type_to_s(type)
55
- Formatter.type_to_s(type)
56
- end
57
-
58
- # Legacy compatibility constants (will be phased out)
59
- # These should be replaced with symbols in user code over time
60
- STRING = :string
61
- INT = :integer # NOTE: using :integer instead of :int for clarity
62
- FLOAT = :float
63
- BOOL = :boolean
64
- ANY = :any
65
- SYMBOL = :symbol
66
- REGEXP = :regexp
67
- TIME = :time
68
- DATE = :date
69
- DATETIME = :datetime
70
- NUMERIC = :float # Legacy: represents numeric compatibility
71
79
  end
72
80
  end
73
81
  end
@@ -40,12 +40,14 @@ module Kumi
40
40
 
41
41
  def report_verify(results_by_schema)
42
42
  success = true
43
+ results_presented = false
43
44
 
44
45
  results_by_schema.each do |schema_name, results|
45
46
  failed_reprs = results.select { |r| !r.passed? }
46
47
 
47
48
  if failed_reprs.empty?
48
49
  puts "✓ #{schema_name}"
50
+ results_presented = true
49
51
  else
50
52
  success = false
51
53
  failed_msgs = failed_reprs.map do |r|
@@ -61,9 +63,16 @@ module Kumi
61
63
  end
62
64
  end
63
65
  puts "✗ #{schema_name} (#{failed_msgs.join(', ')})"
66
+ results_presented = true
64
67
  end
65
68
  end
66
69
 
70
+ # If nothing was shown, report that results were empty
71
+ unless results_presented
72
+ puts "⚠ No test results to report - check that schema files exist"
73
+ success = false
74
+ end
75
+
67
76
  success
68
77
  end
69
78
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'value_normalizer'
4
+
3
5
  module Kumi
4
6
  module Dev
5
7
  module Golden
@@ -60,7 +62,7 @@ module Kumi
60
62
  end
61
63
 
62
64
  def passed?
63
- actual == expected
65
+ ValueNormalizer.values_equal?(actual, expected, language: language)
64
66
  end
65
67
 
66
68
  def failed?
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "json"
4
4
  require "open3"
5
+ require "bigdecimal"
6
+ require_relative "value_normalizer"
5
7
 
6
8
  module Kumi
7
9
  module Dev
@@ -64,6 +66,9 @@ module Kumi
64
66
  code = File.read(code_file)
65
67
  input_data = JSON.parse(File.read(input_file))
66
68
 
69
+ # Convert decimal string inputs to BigDecimal
70
+ input_data = convert_decimal_strings(input_data)
71
+
67
72
  module_name = code.match(/module (Kumi::Compiled::\S+)/)[1]
68
73
  eval(code)
69
74
  module_const = Object.const_get(module_name)
@@ -72,6 +77,26 @@ module Kumi
72
77
  decl_names.to_h { |name| [name, instance[name.to_sym]] }
73
78
  end
74
79
 
80
+ private
81
+
82
+ def convert_decimal_strings(value)
83
+ case value
84
+ when Hash
85
+ value.transform_values { |v| convert_decimal_strings(v) }
86
+ when Array
87
+ value.map { |v| convert_decimal_strings(v) }
88
+ when String
89
+ # Convert decimal-like strings to BigDecimal
90
+ if value.match?(/\A-?\d+(\.\d+)?\z/)
91
+ BigDecimal(value)
92
+ else
93
+ value
94
+ end
95
+ else
96
+ value
97
+ end
98
+ end
99
+
75
100
  def execute_javascript(base_dir, decl_names)
76
101
  runner_path = File.expand_path("../support/kumi_runner.mjs", __dir__)
77
102
  raise "JS test runner not found at #{runner_path}" unless File.exist?(runner_path)
@@ -16,25 +16,25 @@ module Kumi
16
16
  end
17
17
 
18
18
  def update(name = nil)
19
- names = name ? [name] : schema_names
19
+ names = name ? (name.is_a?(Array) ? name : [name]) : schema_names
20
20
  results = update_schemas(names)
21
21
  Reporter.new.report_update(results)
22
22
  end
23
23
 
24
24
  def verify(name = nil)
25
- names = name ? [name] : schema_names
25
+ names = name ? (name.is_a?(Array) ? name : [name]) : schema_names
26
26
  results = verify_schemas(names)
27
27
  Reporter.new.report_verify(results)
28
28
  end
29
29
 
30
30
  def diff(name = nil)
31
- names = name ? [name] : schema_names
31
+ names = name ? (name.is_a?(Array) ? name : [name]) : schema_names
32
32
  results = diff_schemas(names)
33
33
  Reporter.new.report_diff(results)
34
34
  end
35
35
 
36
36
  def test(name = nil)
37
- names = name ? [name] : schema_names
37
+ names = name ? (name.is_a?(Array) ? name : [name]) : schema_names
38
38
 
39
39
  update_schemas(names)
40
40
 
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+
5
+ module Kumi
6
+ module Dev
7
+ module Golden
8
+ # Normalizes values for test comparisons, handling decimal precision
9
+ class ValueNormalizer
10
+ def self.normalize(value, language: :ruby)
11
+ case value
12
+ when Hash
13
+ value.transform_values { |v| normalize(v, language: language) }
14
+ when Array
15
+ value.map { |v| normalize(v, language: language) }
16
+ when String
17
+ # Try to parse as decimal if it looks like one
18
+ if decimal_string?(value)
19
+ language == :ruby ? BigDecimal(value) : value
20
+ else
21
+ value
22
+ end
23
+ else
24
+ value
25
+ end
26
+ end
27
+
28
+ def self.values_equal?(actual, expected, language: :ruby)
29
+ norm_actual = normalize(actual, language: language)
30
+ norm_expected = normalize(expected, language: language)
31
+
32
+ compare_values(norm_actual, norm_expected, language: language)
33
+ end
34
+
35
+ private
36
+
37
+ def self.decimal_string?(str)
38
+ # Match decimal number strings like "10.50", "123", "-45.67"
39
+ str.match?(/\A-?\d+(\.\d+)?\z/)
40
+ end
41
+
42
+ def self.compare_values(actual, expected, language:)
43
+ # Handle decimal comparisons with tolerance for floating-point errors
44
+ case [actual, expected]
45
+ in [Array, Array]
46
+ actual.length == expected.length &&
47
+ actual.zip(expected).all? { |a, e| compare_values(a, e, language: language) }
48
+ in [Hash, Hash]
49
+ actual.keys == expected.keys &&
50
+ actual.all? { |k, v| compare_values(v, expected[k], language: language) }
51
+ in [BigDecimal, BigDecimal]
52
+ actual == expected
53
+ in [BigDecimal, (Integer | Float)]
54
+ BigDecimal(actual.to_s) == BigDecimal(expected.to_s)
55
+ in [(Integer | Float), BigDecimal]
56
+ BigDecimal(actual.to_s) == BigDecimal(expected.to_s)
57
+ in [(Integer | Float), String] | [String, (Integer | Float)]
58
+ # Compare number with decimal string (e.g., JavaScript number vs expected string)
59
+ actual_bd = BigDecimal(actual.to_s)
60
+ expected_bd = BigDecimal(expected.to_s)
61
+ # Allow small floating-point differences (within 1e-10)
62
+ (actual_bd - expected_bd).abs < BigDecimal("1e-10")
63
+ in [String, String]
64
+ # Both strings - try to parse as decimals and compare
65
+ begin
66
+ actual_bd = BigDecimal(actual)
67
+ expected_bd = BigDecimal(expected)
68
+ (actual_bd - expected_bd).abs < BigDecimal("1e-10")
69
+ rescue ArgumentError
70
+ # If not valid decimals, compare as strings
71
+ actual == expected
72
+ end
73
+ else
74
+ actual == expected
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -21,20 +21,27 @@ module Kumi
21
21
  suite.list
22
22
  end
23
23
 
24
- def update!(name = nil)
25
- suite.update(name)
24
+ def update!(*names)
25
+ names = [names].flatten.compact
26
+ names = nil if names.empty?
27
+ suite.update(names)
26
28
  end
27
29
 
28
- def verify!(name = nil)
29
- suite.verify(name)
30
+ def verify!(*names)
31
+ names = [names].flatten.compact
32
+ names = nil if names.empty?
33
+ suite.verify(names)
30
34
  end
31
35
 
32
- def diff!(name = nil)
33
- suite.diff(name)
36
+ def diff!(*names)
37
+ names = [names].flatten.compact
38
+ names = nil if names.empty?
39
+ suite.diff(names)
34
40
  end
35
41
 
36
- def test_all_codegen!(name = nil)
37
- names = name ? [name] : suite.send(:schema_names)
42
+ def test_all_codegen!(*names_arg)
43
+ names_arg = [names_arg].flatten.compact
44
+ names = names_arg.any? ? names_arg : suite.send(:schema_names)
38
45
 
39
46
  ruby_names = suite.send(:filter_testable_schemas, names, :ruby)
40
47
  ruby_results = ruby_names.map do |schema_name|
@@ -49,8 +56,9 @@ module Kumi
49
56
  Reporter.new.report_runtime_tests(ruby: ruby_results, javascript: js_results)
50
57
  end
51
58
 
52
- def test_codegen!(name = nil)
53
- names = name ? [name] : suite.send(:schema_names)
59
+ def test_codegen!(*names_arg)
60
+ names_arg = [names_arg].flatten.compact
61
+ names = names_arg.any? ? names_arg : suite.send(:schema_names)
54
62
  testable_names = suite.send(:filter_testable_schemas, names, :ruby)
55
63
  results = testable_names.map do |schema_name|
56
64
  RuntimeTest.new(schema_name, :ruby).run(suite.send(:schema_dir, schema_name))
@@ -58,8 +66,9 @@ module Kumi
58
66
  Reporter.new.report_runtime_tests(ruby: results)
59
67
  end
60
68
 
61
- def test_js_codegen!(name = nil)
62
- names = name ? [name] : suite.send(:schema_names)
69
+ def test_js_codegen!(*names_arg)
70
+ names_arg = [names_arg].flatten.compact
71
+ names = names_arg.any? ? names_arg : suite.send(:schema_names)
63
72
  testable_names = suite.send(:filter_testable_schemas, names, :javascript)
64
73
  results = testable_names.map do |schema_name|
65
74
  RuntimeTest.new(schema_name, :javascript).run(suite.send(:schema_dir, schema_name))