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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/CLAUDE.md +4 -0
- data/README.md +36 -12
- data/data/functions/core/arithmetic.yaml +28 -8
- data/data/functions/core/boolean.yaml +8 -3
- data/data/functions/core/comparison.yaml +12 -4
- data/data/functions/core/conversion.yaml +32 -0
- data/data/kernels/javascript/core/arithmetic.yaml +6 -2
- data/data/kernels/javascript/core/coercion.yaml +20 -0
- data/data/kernels/ruby/core/arithmetic.yaml +7 -2
- data/data/kernels/ruby/core/coercion.yaml +20 -0
- data/docs/ARCHITECTURE.md +277 -0
- data/docs/DEVELOPMENT.md +62 -0
- data/docs/FUNCTIONS.md +955 -0
- data/docs/SYNTAX.md +8 -0
- data/docs/VSCODE_EXTENSION.md +114 -0
- data/docs/functions-reference.json +1821 -0
- data/golden/array_element/expected/schema_ruby.rb +1 -1
- data/golden/array_index/expected/lir_00_unoptimized.txt +2 -2
- data/golden/array_index/expected/lir_01_hoist_scalar_references.txt +2 -2
- data/golden/array_index/expected/lir_02_inlined.txt +2 -2
- data/golden/array_index/expected/lir_03_cse.txt +2 -2
- data/golden/array_index/expected/lir_04_1_loop_fusion.txt +2 -2
- data/golden/array_index/expected/lir_04_loop_invcm.txt +2 -2
- data/golden/array_index/expected/lir_06_const_prop.txt +2 -2
- data/golden/array_index/expected/schema_ruby.rb +1 -1
- data/golden/array_index/expected/snast.txt +2 -2
- data/golden/array_operations/expected/lir_00_unoptimized.txt +2 -2
- data/golden/array_operations/expected/lir_01_hoist_scalar_references.txt +2 -2
- data/golden/array_operations/expected/lir_02_inlined.txt +2 -2
- data/golden/array_operations/expected/lir_03_cse.txt +2 -2
- data/golden/array_operations/expected/lir_04_1_loop_fusion.txt +2 -2
- data/golden/array_operations/expected/lir_04_loop_invcm.txt +2 -2
- data/golden/array_operations/expected/lir_06_const_prop.txt +2 -2
- data/golden/array_operations/expected/schema_ruby.rb +1 -1
- data/golden/array_operations/expected/snast.txt +2 -2
- data/golden/cascade_logic/expected/schema_ruby.rb +1 -1
- data/golden/chained_fusion/expected/schema_ruby.rb +1 -1
- data/golden/decimal_explicit/expected/ast.txt +38 -0
- data/golden/decimal_explicit/expected/input_plan.txt +3 -0
- data/golden/decimal_explicit/expected/lir_00_unoptimized.txt +30 -0
- data/golden/decimal_explicit/expected/lir_01_hoist_scalar_references.txt +30 -0
- data/golden/decimal_explicit/expected/lir_02_inlined.txt +44 -0
- data/golden/decimal_explicit/expected/lir_03_cse.txt +40 -0
- data/golden/decimal_explicit/expected/lir_04_1_loop_fusion.txt +40 -0
- data/golden/decimal_explicit/expected/lir_04_loop_invcm.txt +40 -0
- data/golden/decimal_explicit/expected/lir_06_const_prop.txt +40 -0
- data/golden/decimal_explicit/expected/nast.txt +30 -0
- data/golden/decimal_explicit/expected/schema_javascript.mjs +31 -0
- data/golden/decimal_explicit/expected/schema_ruby.rb +57 -0
- data/golden/decimal_explicit/expected/snast.txt +30 -0
- data/golden/decimal_explicit/expected.json +1 -0
- data/golden/decimal_explicit/input.json +5 -0
- data/golden/decimal_explicit/schema.kumi +14 -0
- data/golden/element_arrays/expected/schema_ruby.rb +1 -1
- data/golden/empty_and_null_inputs/expected/schema_ruby.rb +1 -1
- data/golden/function_overload/expected/schema_ruby.rb +1 -1
- data/golden/game_of_life/expected/schema_ruby.rb +1 -1
- data/golden/hash_keys/expected/schema_ruby.rb +1 -1
- data/golden/hash_value/expected/schema_ruby.rb +1 -1
- data/golden/hierarchical_complex/expected/lir_00_unoptimized.txt +3 -3
- data/golden/hierarchical_complex/expected/lir_01_hoist_scalar_references.txt +3 -3
- data/golden/hierarchical_complex/expected/lir_02_inlined.txt +3 -3
- data/golden/hierarchical_complex/expected/lir_03_cse.txt +3 -3
- data/golden/hierarchical_complex/expected/lir_04_1_loop_fusion.txt +3 -3
- data/golden/hierarchical_complex/expected/lir_04_loop_invcm.txt +3 -3
- data/golden/hierarchical_complex/expected/lir_06_const_prop.txt +3 -3
- data/golden/hierarchical_complex/expected/schema_ruby.rb +1 -1
- data/golden/hierarchical_complex/expected/snast.txt +3 -3
- data/golden/inline_rename_scope_leak/expected/schema_ruby.rb +1 -1
- data/golden/input_reference/expected/schema_ruby.rb +1 -1
- data/golden/interleaved_fusion/expected/lir_00_unoptimized.txt +1 -1
- data/golden/interleaved_fusion/expected/lir_01_hoist_scalar_references.txt +1 -1
- data/golden/interleaved_fusion/expected/lir_02_inlined.txt +2 -2
- data/golden/interleaved_fusion/expected/lir_03_cse.txt +2 -2
- data/golden/interleaved_fusion/expected/lir_04_1_loop_fusion.txt +2 -2
- data/golden/interleaved_fusion/expected/lir_04_loop_invcm.txt +2 -2
- data/golden/interleaved_fusion/expected/lir_06_const_prop.txt +2 -2
- data/golden/interleaved_fusion/expected/schema_ruby.rb +1 -1
- data/golden/interleaved_fusion/expected/snast.txt +1 -1
- data/golden/let_inline/expected/lir_00_unoptimized.txt +2 -2
- data/golden/let_inline/expected/lir_01_hoist_scalar_references.txt +2 -2
- data/golden/let_inline/expected/lir_02_inlined.txt +6 -6
- data/golden/let_inline/expected/lir_03_cse.txt +6 -6
- data/golden/let_inline/expected/lir_04_1_loop_fusion.txt +6 -6
- data/golden/let_inline/expected/lir_04_loop_invcm.txt +6 -6
- data/golden/let_inline/expected/lir_06_const_prop.txt +6 -6
- data/golden/let_inline/expected/schema_ruby.rb +1 -1
- data/golden/let_inline/expected/snast.txt +2 -2
- data/golden/loop_fusion/expected/schema_ruby.rb +1 -1
- data/golden/min_reduce_scope/expected/schema_ruby.rb +1 -1
- data/golden/mixed_dimensions/expected/schema_ruby.rb +1 -1
- data/golden/multirank_hoisting/expected/lir_00_unoptimized.txt +2 -2
- data/golden/multirank_hoisting/expected/lir_01_hoist_scalar_references.txt +2 -2
- data/golden/multirank_hoisting/expected/lir_02_inlined.txt +7 -7
- data/golden/multirank_hoisting/expected/lir_03_cse.txt +7 -7
- data/golden/multirank_hoisting/expected/lir_04_1_loop_fusion.txt +7 -7
- data/golden/multirank_hoisting/expected/lir_04_loop_invcm.txt +7 -7
- data/golden/multirank_hoisting/expected/lir_06_const_prop.txt +7 -7
- data/golden/multirank_hoisting/expected/schema_ruby.rb +1 -1
- data/golden/multirank_hoisting/expected/snast.txt +2 -2
- data/golden/nested_hash/expected/lir_00_unoptimized.txt +1 -1
- data/golden/nested_hash/expected/lir_01_hoist_scalar_references.txt +1 -1
- data/golden/nested_hash/expected/lir_02_inlined.txt +1 -1
- data/golden/nested_hash/expected/lir_03_cse.txt +1 -1
- data/golden/nested_hash/expected/lir_04_1_loop_fusion.txt +1 -1
- data/golden/nested_hash/expected/lir_04_loop_invcm.txt +1 -1
- data/golden/nested_hash/expected/lir_06_const_prop.txt +1 -1
- data/golden/nested_hash/expected/schema_ruby.rb +1 -1
- data/golden/nested_hash/expected/snast.txt +1 -1
- data/golden/reduction_broadcast/expected/schema_ruby.rb +1 -1
- data/golden/roll/expected/schema_ruby.rb +1 -1
- data/golden/shift/expected/schema_ruby.rb +1 -1
- data/golden/shift_2d/expected/schema_ruby.rb +1 -1
- data/golden/simple_math/expected/lir_00_unoptimized.txt +2 -2
- data/golden/simple_math/expected/lir_01_hoist_scalar_references.txt +2 -2
- data/golden/simple_math/expected/lir_02_inlined.txt +2 -2
- data/golden/simple_math/expected/lir_03_cse.txt +2 -2
- data/golden/simple_math/expected/lir_04_1_loop_fusion.txt +2 -2
- data/golden/simple_math/expected/lir_04_loop_invcm.txt +2 -2
- data/golden/simple_math/expected/lir_06_const_prop.txt +2 -2
- data/golden/simple_math/expected/schema_ruby.rb +1 -1
- data/golden/simple_math/expected/snast.txt +2 -2
- data/golden/streaming_basics/expected/lir_00_unoptimized.txt +3 -3
- data/golden/streaming_basics/expected/lir_01_hoist_scalar_references.txt +3 -3
- data/golden/streaming_basics/expected/lir_02_inlined.txt +9 -9
- data/golden/streaming_basics/expected/lir_03_cse.txt +7 -7
- data/golden/streaming_basics/expected/lir_04_1_loop_fusion.txt +7 -7
- data/golden/streaming_basics/expected/lir_04_loop_invcm.txt +7 -7
- data/golden/streaming_basics/expected/lir_06_const_prop.txt +7 -7
- data/golden/streaming_basics/expected/schema_ruby.rb +1 -1
- data/golden/streaming_basics/expected/snast.txt +3 -3
- data/golden/tuples/expected/schema_ruby.rb +1 -1
- data/golden/tuples_and_arrays/expected/schema_ruby.rb +1 -1
- data/golden/us_tax_2024/expected/lir_00_unoptimized.txt +6 -6
- data/golden/us_tax_2024/expected/lir_01_hoist_scalar_references.txt +6 -6
- data/golden/us_tax_2024/expected/lir_02_inlined.txt +71 -71
- data/golden/us_tax_2024/expected/lir_03_cse.txt +43 -43
- data/golden/us_tax_2024/expected/lir_04_1_loop_fusion.txt +48 -48
- data/golden/us_tax_2024/expected/lir_04_loop_invcm.txt +43 -43
- data/golden/us_tax_2024/expected/lir_06_const_prop.txt +43 -43
- data/golden/us_tax_2024/expected/schema_ruby.rb +1 -1
- data/golden/us_tax_2024/expected/snast.txt +6 -6
- data/golden/with_constants/expected/schema_ruby.rb +1 -1
- data/lib/kumi/configuration.rb +6 -0
- data/lib/kumi/core/analyzer/passes/nast_dimensional_analyzer_pass.rb +1 -1
- data/lib/kumi/core/error_reporter.rb +1 -1
- data/lib/kumi/core/errors.rb +1 -1
- data/lib/kumi/core/functions/overload_resolver.rb +57 -11
- data/lib/kumi/core/functions/type_categories.rb +44 -0
- data/lib/kumi/core/input/type_matcher.rb +8 -1
- data/lib/kumi/core/ruby_parser/input_builder.rb +2 -2
- data/lib/kumi/core/types/normalizer.rb +1 -0
- data/lib/kumi/core/types/validator.rb +2 -2
- data/lib/kumi/core/types.rb +2 -2
- data/lib/kumi/dev/golden/reporter.rb +9 -0
- data/lib/kumi/dev/golden/result.rb +3 -1
- data/lib/kumi/dev/golden/runtime_test.rb +25 -0
- data/lib/kumi/dev/golden/suite.rb +4 -4
- data/lib/kumi/dev/golden/value_normalizer.rb +80 -0
- data/lib/kumi/dev/golden.rb +21 -12
- data/lib/kumi/doc_generator/formatters/json.rb +39 -0
- data/lib/kumi/doc_generator/formatters/markdown.rb +175 -0
- data/lib/kumi/doc_generator/loader.rb +37 -0
- data/lib/kumi/doc_generator/merger.rb +54 -0
- data/lib/kumi/doc_generator.rb +4 -0
- data/lib/kumi/frontends/text.rb +33 -5
- data/lib/kumi/syntax/location.rb +5 -1
- data/lib/kumi/version.rb +1 -1
- data/vscode-extension/.gitignore +4 -0
- data/vscode-extension/README.md +59 -0
- data/vscode-extension/TESTING.md +151 -0
- data/vscode-extension/package.json +51 -0
- data/vscode-extension/src/extension.ts +295 -0
- data/vscode-extension/tsconfig.json +15 -0
- 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
|
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
|
-
|
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 -
|
47
|
+
# Single overload - validate type constraints too
|
40
48
|
if candidates.size == 1
|
41
|
-
|
42
|
-
|
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
|
-
"
|
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 =
|
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
|
-
|
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
|
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)
|
data/lib/kumi/core/types.rb
CHANGED
@@ -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
|
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
|
data/lib/kumi/dev/golden.rb
CHANGED
@@ -21,20 +21,27 @@ module Kumi
|
|
21
21
|
suite.list
|
22
22
|
end
|
23
23
|
|
24
|
-
def update!(
|
25
|
-
|
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!(
|
29
|
-
|
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!(
|
33
|
-
|
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!(
|
37
|
-
|
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!(
|
53
|
-
|
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!(
|
62
|
-
|
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
|