kumi 0.0.26 → 0.0.28

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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/CLAUDE.md +4 -0
  4. data/README.md +36 -12
  5. data/data/functions/core/arithmetic.yaml +28 -8
  6. data/data/functions/core/boolean.yaml +8 -3
  7. data/data/functions/core/comparison.yaml +12 -4
  8. data/data/functions/core/conversion.yaml +32 -0
  9. data/data/kernels/javascript/core/arithmetic.yaml +6 -2
  10. data/data/kernels/javascript/core/coercion.yaml +20 -0
  11. data/data/kernels/ruby/core/arithmetic.yaml +7 -2
  12. data/data/kernels/ruby/core/coercion.yaml +20 -0
  13. data/docs/ARCHITECTURE.md +277 -0
  14. data/docs/DEVELOPMENT.md +62 -0
  15. data/docs/FUNCTIONS.md +955 -0
  16. data/docs/SYNTAX.md +8 -0
  17. data/docs/VSCODE_EXTENSION.md +114 -0
  18. data/docs/functions-reference.json +1821 -0
  19. data/golden/array_element/expected/schema_ruby.rb +1 -1
  20. data/golden/array_index/expected/lir_00_unoptimized.txt +2 -2
  21. data/golden/array_index/expected/lir_01_hoist_scalar_references.txt +2 -2
  22. data/golden/array_index/expected/lir_02_inlined.txt +2 -2
  23. data/golden/array_index/expected/lir_03_cse.txt +2 -2
  24. data/golden/array_index/expected/lir_04_1_loop_fusion.txt +2 -2
  25. data/golden/array_index/expected/lir_04_loop_invcm.txt +2 -2
  26. data/golden/array_index/expected/lir_06_const_prop.txt +2 -2
  27. data/golden/array_index/expected/schema_ruby.rb +1 -1
  28. data/golden/array_index/expected/snast.txt +2 -2
  29. data/golden/array_operations/expected/lir_00_unoptimized.txt +2 -2
  30. data/golden/array_operations/expected/lir_01_hoist_scalar_references.txt +2 -2
  31. data/golden/array_operations/expected/lir_02_inlined.txt +2 -2
  32. data/golden/array_operations/expected/lir_03_cse.txt +2 -2
  33. data/golden/array_operations/expected/lir_04_1_loop_fusion.txt +2 -2
  34. data/golden/array_operations/expected/lir_04_loop_invcm.txt +2 -2
  35. data/golden/array_operations/expected/lir_06_const_prop.txt +2 -2
  36. data/golden/array_operations/expected/schema_ruby.rb +1 -1
  37. data/golden/array_operations/expected/snast.txt +2 -2
  38. data/golden/cascade_logic/expected/schema_ruby.rb +1 -1
  39. data/golden/chained_fusion/expected/schema_ruby.rb +1 -1
  40. data/golden/decimal_explicit/expected/ast.txt +38 -0
  41. data/golden/decimal_explicit/expected/input_plan.txt +3 -0
  42. data/golden/decimal_explicit/expected/lir_00_unoptimized.txt +30 -0
  43. data/golden/decimal_explicit/expected/lir_01_hoist_scalar_references.txt +30 -0
  44. data/golden/decimal_explicit/expected/lir_02_inlined.txt +44 -0
  45. data/golden/decimal_explicit/expected/lir_03_cse.txt +40 -0
  46. data/golden/decimal_explicit/expected/lir_04_1_loop_fusion.txt +40 -0
  47. data/golden/decimal_explicit/expected/lir_04_loop_invcm.txt +40 -0
  48. data/golden/decimal_explicit/expected/lir_06_const_prop.txt +40 -0
  49. data/golden/decimal_explicit/expected/nast.txt +30 -0
  50. data/golden/decimal_explicit/expected/schema_javascript.mjs +31 -0
  51. data/golden/decimal_explicit/expected/schema_ruby.rb +57 -0
  52. data/golden/decimal_explicit/expected/snast.txt +30 -0
  53. data/golden/decimal_explicit/expected.json +1 -0
  54. data/golden/decimal_explicit/input.json +5 -0
  55. data/golden/decimal_explicit/schema.kumi +14 -0
  56. data/golden/element_arrays/expected/schema_ruby.rb +1 -1
  57. data/golden/empty_and_null_inputs/expected/schema_ruby.rb +1 -1
  58. data/golden/function_overload/expected/schema_ruby.rb +1 -1
  59. data/golden/game_of_life/expected/schema_ruby.rb +1 -1
  60. data/golden/hash_keys/expected/schema_ruby.rb +1 -1
  61. data/golden/hash_value/expected/schema_ruby.rb +1 -1
  62. data/golden/hierarchical_complex/expected/lir_00_unoptimized.txt +3 -3
  63. data/golden/hierarchical_complex/expected/lir_01_hoist_scalar_references.txt +3 -3
  64. data/golden/hierarchical_complex/expected/lir_02_inlined.txt +3 -3
  65. data/golden/hierarchical_complex/expected/lir_03_cse.txt +3 -3
  66. data/golden/hierarchical_complex/expected/lir_04_1_loop_fusion.txt +3 -3
  67. data/golden/hierarchical_complex/expected/lir_04_loop_invcm.txt +3 -3
  68. data/golden/hierarchical_complex/expected/lir_06_const_prop.txt +3 -3
  69. data/golden/hierarchical_complex/expected/schema_ruby.rb +1 -1
  70. data/golden/hierarchical_complex/expected/snast.txt +3 -3
  71. data/golden/inline_rename_scope_leak/expected/schema_ruby.rb +1 -1
  72. data/golden/input_reference/expected/schema_ruby.rb +1 -1
  73. data/golden/interleaved_fusion/expected/lir_00_unoptimized.txt +1 -1
  74. data/golden/interleaved_fusion/expected/lir_01_hoist_scalar_references.txt +1 -1
  75. data/golden/interleaved_fusion/expected/lir_02_inlined.txt +2 -2
  76. data/golden/interleaved_fusion/expected/lir_03_cse.txt +2 -2
  77. data/golden/interleaved_fusion/expected/lir_04_1_loop_fusion.txt +2 -2
  78. data/golden/interleaved_fusion/expected/lir_04_loop_invcm.txt +2 -2
  79. data/golden/interleaved_fusion/expected/lir_06_const_prop.txt +2 -2
  80. data/golden/interleaved_fusion/expected/schema_ruby.rb +1 -1
  81. data/golden/interleaved_fusion/expected/snast.txt +1 -1
  82. data/golden/let_inline/expected/lir_00_unoptimized.txt +2 -2
  83. data/golden/let_inline/expected/lir_01_hoist_scalar_references.txt +2 -2
  84. data/golden/let_inline/expected/lir_02_inlined.txt +6 -6
  85. data/golden/let_inline/expected/lir_03_cse.txt +6 -6
  86. data/golden/let_inline/expected/lir_04_1_loop_fusion.txt +6 -6
  87. data/golden/let_inline/expected/lir_04_loop_invcm.txt +6 -6
  88. data/golden/let_inline/expected/lir_06_const_prop.txt +6 -6
  89. data/golden/let_inline/expected/schema_ruby.rb +1 -1
  90. data/golden/let_inline/expected/snast.txt +2 -2
  91. data/golden/loop_fusion/expected/schema_ruby.rb +1 -1
  92. data/golden/min_reduce_scope/expected/schema_ruby.rb +1 -1
  93. data/golden/mixed_dimensions/expected/schema_ruby.rb +1 -1
  94. data/golden/multirank_hoisting/expected/lir_00_unoptimized.txt +2 -2
  95. data/golden/multirank_hoisting/expected/lir_01_hoist_scalar_references.txt +2 -2
  96. data/golden/multirank_hoisting/expected/lir_02_inlined.txt +7 -7
  97. data/golden/multirank_hoisting/expected/lir_03_cse.txt +7 -7
  98. data/golden/multirank_hoisting/expected/lir_04_1_loop_fusion.txt +7 -7
  99. data/golden/multirank_hoisting/expected/lir_04_loop_invcm.txt +7 -7
  100. data/golden/multirank_hoisting/expected/lir_06_const_prop.txt +7 -7
  101. data/golden/multirank_hoisting/expected/schema_ruby.rb +1 -1
  102. data/golden/multirank_hoisting/expected/snast.txt +2 -2
  103. data/golden/nested_hash/expected/lir_00_unoptimized.txt +1 -1
  104. data/golden/nested_hash/expected/lir_01_hoist_scalar_references.txt +1 -1
  105. data/golden/nested_hash/expected/lir_02_inlined.txt +1 -1
  106. data/golden/nested_hash/expected/lir_03_cse.txt +1 -1
  107. data/golden/nested_hash/expected/lir_04_1_loop_fusion.txt +1 -1
  108. data/golden/nested_hash/expected/lir_04_loop_invcm.txt +1 -1
  109. data/golden/nested_hash/expected/lir_06_const_prop.txt +1 -1
  110. data/golden/nested_hash/expected/schema_ruby.rb +1 -1
  111. data/golden/nested_hash/expected/snast.txt +1 -1
  112. data/golden/reduction_broadcast/expected/schema_ruby.rb +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 +2 -2
  117. data/golden/simple_math/expected/lir_01_hoist_scalar_references.txt +2 -2
  118. data/golden/simple_math/expected/lir_02_inlined.txt +2 -2
  119. data/golden/simple_math/expected/lir_03_cse.txt +2 -2
  120. data/golden/simple_math/expected/lir_04_1_loop_fusion.txt +2 -2
  121. data/golden/simple_math/expected/lir_04_loop_invcm.txt +2 -2
  122. data/golden/simple_math/expected/lir_06_const_prop.txt +2 -2
  123. data/golden/simple_math/expected/schema_ruby.rb +1 -1
  124. data/golden/simple_math/expected/snast.txt +2 -2
  125. data/golden/streaming_basics/expected/lir_00_unoptimized.txt +3 -3
  126. data/golden/streaming_basics/expected/lir_01_hoist_scalar_references.txt +3 -3
  127. data/golden/streaming_basics/expected/lir_02_inlined.txt +9 -9
  128. data/golden/streaming_basics/expected/lir_03_cse.txt +7 -7
  129. data/golden/streaming_basics/expected/lir_04_1_loop_fusion.txt +7 -7
  130. data/golden/streaming_basics/expected/lir_04_loop_invcm.txt +7 -7
  131. data/golden/streaming_basics/expected/lir_06_const_prop.txt +7 -7
  132. data/golden/streaming_basics/expected/schema_ruby.rb +1 -1
  133. data/golden/streaming_basics/expected/snast.txt +3 -3
  134. data/golden/tuples/expected/schema_ruby.rb +1 -1
  135. data/golden/tuples_and_arrays/expected/schema_ruby.rb +1 -1
  136. data/golden/us_tax_2024/expected/lir_00_unoptimized.txt +6 -6
  137. data/golden/us_tax_2024/expected/lir_01_hoist_scalar_references.txt +6 -6
  138. data/golden/us_tax_2024/expected/lir_02_inlined.txt +71 -71
  139. data/golden/us_tax_2024/expected/lir_03_cse.txt +43 -43
  140. data/golden/us_tax_2024/expected/lir_04_1_loop_fusion.txt +48 -48
  141. data/golden/us_tax_2024/expected/lir_04_loop_invcm.txt +43 -43
  142. data/golden/us_tax_2024/expected/lir_06_const_prop.txt +43 -43
  143. data/golden/us_tax_2024/expected/schema_ruby.rb +1 -1
  144. data/golden/us_tax_2024/expected/snast.txt +6 -6
  145. data/golden/with_constants/expected/schema_ruby.rb +1 -1
  146. data/lib/kumi/configuration.rb +6 -0
  147. data/lib/kumi/core/analyzer/passes/nast_dimensional_analyzer_pass.rb +1 -1
  148. data/lib/kumi/core/error_reporter.rb +1 -1
  149. data/lib/kumi/core/errors.rb +1 -1
  150. data/lib/kumi/core/functions/overload_resolver.rb +57 -11
  151. data/lib/kumi/core/functions/type_categories.rb +44 -0
  152. data/lib/kumi/core/input/type_matcher.rb +8 -1
  153. data/lib/kumi/core/ruby_parser/input_builder.rb +2 -2
  154. data/lib/kumi/core/types/normalizer.rb +1 -0
  155. data/lib/kumi/core/types/validator.rb +2 -2
  156. data/lib/kumi/core/types.rb +2 -2
  157. data/lib/kumi/dev/golden/reporter.rb +9 -0
  158. data/lib/kumi/dev/golden/result.rb +3 -1
  159. data/lib/kumi/dev/golden/runtime_test.rb +25 -0
  160. data/lib/kumi/dev/golden/suite.rb +4 -4
  161. data/lib/kumi/dev/golden/value_normalizer.rb +80 -0
  162. data/lib/kumi/dev/golden.rb +21 -12
  163. data/lib/kumi/doc_generator/formatters/json.rb +39 -0
  164. data/lib/kumi/doc_generator/formatters/markdown.rb +175 -0
  165. data/lib/kumi/doc_generator/loader.rb +37 -0
  166. data/lib/kumi/doc_generator/merger.rb +54 -0
  167. data/lib/kumi/doc_generator.rb +4 -0
  168. data/lib/kumi/frontends/text.rb +33 -5
  169. data/lib/kumi/syntax/location.rb +5 -1
  170. data/lib/kumi/version.rb +1 -1
  171. data/vscode-extension/.gitignore +4 -0
  172. data/vscode-extension/README.md +59 -0
  173. data/vscode-extension/TESTING.md +151 -0
  174. data/vscode-extension/package.json +51 -0
  175. data/vscode-extension/src/extension.ts +295 -0
  176. data/vscode-extension/tsconfig.json +15 -0
  177. metadata +38 -1
@@ -26,20 +26,37 @@ module Kumi
26
26
  def resolve(alias_or_id, arg_types)
27
27
  s = alias_or_id.to_s
28
28
 
29
- # If it's already a full function ID, validate and return it
29
+ # If it's already a full function ID, validate arity and type constraints
30
30
  if @functions.key?(s)
31
31
  validate_arity!(s, arg_types)
32
- return s
32
+ fn = @functions[s]
33
+ score = match_score(fn.params, arg_types)
34
+ if score > 0
35
+ return s
36
+ else
37
+ # Type constraints failed
38
+ raise ResolutionError,
39
+ "#{alias_or_id}(#{format_types(arg_types)}) - type mismatch"
40
+ end
33
41
  end
34
42
 
35
43
  # Get all candidate overloads for this alias
36
44
  candidates = @alias_overloads[s]
37
45
  raise ResolutionError, "unknown function #{alias_or_id}" if candidates.nil?
38
46
 
39
- # Single overload - use it directly
47
+ # Single overload - validate type constraints too
40
48
  if candidates.size == 1
41
- validate_arity!(candidates.first, arg_types)
42
- return candidates.first
49
+ fn_id = candidates.first
50
+ validate_arity!(fn_id, arg_types)
51
+ fn = @functions[fn_id]
52
+ score = match_score(fn.params, arg_types)
53
+ if score > 0
54
+ return fn_id
55
+ else
56
+ # Type constraints failed for the only overload
57
+ raise ResolutionError,
58
+ "#{alias_or_id}(#{format_types(arg_types)}) - type mismatch"
59
+ end
43
60
  end
44
61
 
45
62
  # Multiple overloads - find best match by type constraints (prefer exact matches)
@@ -56,10 +73,8 @@ module Kumi
56
73
  end
57
74
 
58
75
  # No match found - provide helpful error
59
- available = candidates.map { |id| @functions[id].id }.join(", ")
60
76
  raise ResolutionError,
61
- "no overload of '#{alias_or_id}' matches argument types #{arg_types.inspect}. " \
62
- "Available overloads: #{available}"
77
+ "#{alias_or_id}(#{format_types(arg_types)}) - type mismatch"
63
78
  end
64
79
 
65
80
  # Get function object by ID (already resolved)
@@ -99,7 +114,7 @@ module Kumi
99
114
 
100
115
  def match_score(params, arg_types)
101
116
  # Returns match quality: higher is better
102
- # 0 = no match, 1 = matches with unconstrained params, 2 = exact match
117
+ # 0 = no match, 1+ = match (1 for unconstrained params, higher for exact matches)
103
118
  return 0 unless params_match?(params, arg_types)
104
119
 
105
120
  # Count exact constraint matches (all arg_types are Type objects now)
@@ -108,11 +123,22 @@ module Kumi
108
123
  score_type_object_match(param_dtype, arg_type)
109
124
  end
110
125
 
111
- exact_matches
126
+ # Return exact_matches + 1 so that: unconstrained=1, one exact=2, all exact=N+1
127
+ exact_matches + 1
112
128
  end
113
129
 
114
130
  def score_type_object_match(param_dtype, type_obj)
115
- case param_dtype&.to_s
131
+ constraint = param_dtype&.to_s
132
+ return false unless constraint
133
+
134
+ # Check if it's a type category
135
+ if TypeCategories.category?(constraint)
136
+ return false unless type_obj.is_a?(Kumi::Core::Types::ScalarType)
137
+ return TypeCategories.includes?(constraint, type_obj.kind)
138
+ end
139
+
140
+ # Individual scalar type constraints
141
+ case constraint
116
142
  when "string"
117
143
  type_obj.is_a?(Kumi::Core::Types::ScalarType) && type_obj.kind == :string
118
144
  when "array"
@@ -131,6 +157,13 @@ module Kumi
131
157
  def type_compatible?(param_dtype_str, arg_type)
132
158
  raise ArgumentError, "arg_type must be a Type object, got #{arg_type.inspect}" unless arg_type.is_a?(Kumi::Core::Types::Type)
133
159
 
160
+ # Check if it's a type category
161
+ if TypeCategories.category?(param_dtype_str)
162
+ return false unless arg_type.is_a?(Kumi::Core::Types::ScalarType)
163
+ return TypeCategories.includes?(param_dtype_str, arg_type.kind)
164
+ end
165
+
166
+ # Individual scalar type constraints
134
167
  case param_dtype_str
135
168
  when "string"
136
169
  arg_type.is_a?(Kumi::Core::Types::ScalarType) && arg_type.kind == :string
@@ -156,6 +189,19 @@ module Kumi
156
189
  "function #{fn_id} expects #{fn.params.size} arguments, got #{arg_types.size}"
157
190
  end
158
191
 
192
+ private
193
+
194
+ def format_types(arg_types)
195
+ arg_types.map(&:to_s).join(", ")
196
+ end
197
+
198
+ def format_param_constraints(params)
199
+ params.map do |param|
200
+ dtype = param["dtype"]
201
+ dtype || "any"
202
+ end.join(", ")
203
+ end
204
+
159
205
  # Custom error for function resolution failures
160
206
  class ResolutionError < StandardError; end
161
207
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Functions
6
+ # Type categories define reusable type constraints
7
+ # Instead of hardcoding type checks scattered throughout the codebase,
8
+ # we define categories once and reference them in function definitions
9
+ class TypeCategories
10
+ # Define type categories as unions of scalar kinds
11
+ CATEGORIES = {
12
+ numeric: [:integer, :float, :decimal],
13
+ comparable: [:integer, :float, :decimal, :string],
14
+ boolean: [:boolean],
15
+ stringable: [:string],
16
+ orderable: [:integer, :float, :decimal, :string]
17
+ }.freeze
18
+
19
+ def self.expand(dtype_constraint)
20
+ return dtype_constraint unless dtype_constraint.is_a?(String)
21
+
22
+ category = dtype_constraint.to_sym
23
+ CATEGORIES[category] || dtype_constraint
24
+ end
25
+
26
+ def self.includes?(dtype_constraint, kind)
27
+ kinds = expand(dtype_constraint)
28
+ return kinds.include?(kind) if kinds.is_a?(Array)
29
+
30
+ # Fall back to string comparison for uncategorized constraints
31
+ kinds == kind.to_s
32
+ end
33
+
34
+ def self.category?(name)
35
+ CATEGORIES.key?(name.to_sym)
36
+ end
37
+
38
+ def self.categories
39
+ CATEGORIES
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bigdecimal'
4
+
3
5
  module Kumi
4
6
  module Core
5
7
  module Input
@@ -10,6 +12,8 @@ module Kumi
10
12
  value.is_a?(Integer)
11
13
  when :float
12
14
  value.is_a?(Float) || value.is_a?(Integer) # Allow integer for float
15
+ when :decimal
16
+ value.is_a?(BigDecimal) || value.is_a?(Float) || value.is_a?(Integer)
13
17
  when :string
14
18
  value.is_a?(String)
15
19
  when :boolean
@@ -36,7 +40,10 @@ module Kumi
36
40
  when Symbol then :symbol
37
41
  when Array then { array: :mixed }
38
42
  when Hash then { hash: %i[mixed mixed] }
39
- else :unknown
43
+ else
44
+ return :decimal if value.is_a?(BigDecimal)
45
+
46
+ :unknown
40
47
  end
41
48
  end
42
49
 
@@ -16,7 +16,7 @@ module Kumi
16
16
  @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], nil, loc: @context.current_location)
17
17
  end
18
18
 
19
- %i[integer float string boolean any scalar].each do |type_name|
19
+ %i[integer float decimal string boolean any scalar].each do |type_name|
20
20
  define_method(type_name) do |name, type: nil, domain: nil|
21
21
  actual_type = type || (type_name == :scalar ? :any : type_name)
22
22
  @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], nil, loc: @context.current_location)
@@ -44,7 +44,7 @@ module Kumi
44
44
  end
45
45
 
46
46
  def method_missing(method_name, *_args)
47
- allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', 'hash', and 'element'"
47
+ allowed_methods = "'key', 'integer', 'float', 'decimal', 'string', 'boolean', 'any', 'scalar', 'array', 'hash', and 'element'"
48
48
  raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
49
49
  location: @context.current_location)
50
50
  end
@@ -35,6 +35,7 @@ module Kumi
35
35
  when "Integer" then :integer
36
36
  when "String" then :string
37
37
  when "Float" then :float
38
+ when "Decimal", "BigDecimal" then :decimal
38
39
  when "Symbol" then :symbol
39
40
  when "TrueClass", "FalseClass" then :boolean
40
41
  when "Array" then raise ArgumentError, "Use array(:type) helper for array types"
@@ -5,10 +5,10 @@ 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
10
  # Validate scalar kinds (no :array or :hash)
11
- VALID_KINDS = %i[string integer float boolean any symbol regexp time date datetime null].freeze
11
+ VALID_KINDS = %i[string integer float decimal boolean any symbol regexp time date datetime null].freeze
12
12
 
13
13
  def self.valid_kind?(kind)
14
14
  VALID_KINDS.include?(kind)
@@ -34,7 +34,7 @@ module Kumi
34
34
  elem_obj = case element_type
35
35
  when Type
36
36
  element_type
37
- when :string, :integer, :float, :boolean, :hash, :any, :symbol, :regexp, :time, :date, :datetime, :null
37
+ when :string, :integer, :float, :decimal, :boolean, :hash, :any, :symbol, :regexp, :time, :date, :datetime, :null
38
38
  scalar(element_type)
39
39
  else
40
40
  raise ArgumentError,
@@ -53,7 +53,7 @@ module Kumi
53
53
  case t
54
54
  when Type
55
55
  t
56
- when :string, :integer, :float, :boolean, :hash, :any, :symbol, :regexp, :time, :date, :datetime, :null
56
+ when :string, :integer, :float, :decimal, :boolean, :hash, :any, :symbol, :regexp, :time, :date, :datetime, :null
57
57
  scalar(t)
58
58
  else
59
59
  raise ArgumentError, "tuple element must be Type or scalar kind, got #{t.inspect}"
@@ -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))
@@ -0,0 +1,39 @@
1
+ require 'json'
2
+
3
+ module Kumi
4
+ module DocGenerator
5
+ module Formatters
6
+ class Json
7
+ def initialize(docs)
8
+ @docs = docs
9
+ end
10
+
11
+ def format
12
+ enriched = @docs.each_with_object({}) do |(alias_name, entry), acc|
13
+ kernel_ids = extract_kernel_ids(entry['kernels'])
14
+ acc[alias_name] = {
15
+ 'id' => entry['id'],
16
+ 'kind' => entry['kind'],
17
+ 'arity' => entry['arity'],
18
+ 'params' => entry['params'],
19
+ 'kernels' => kernel_ids,
20
+ 'dtype' => entry['dtype'],
21
+ 'aliases' => entry['aliases'],
22
+ 'reduction_strategy' => entry['reduction_strategy']
23
+ }
24
+ end
25
+
26
+ JSON.pretty_generate(enriched)
27
+ end
28
+
29
+ private
30
+
31
+ def extract_kernel_ids(kernels)
32
+ kernels.each_with_object({}) do |(target, kernel), acc|
33
+ acc[target] = kernel.is_a?(Hash) ? kernel['id'] : kernel
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end