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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +4 -0
- data/README.md +86 -78
- data/data/functions/agg/boolean.yaml +6 -2
- data/data/functions/agg/numeric.yaml +32 -16
- data/data/functions/agg/string.yaml +4 -3
- data/data/functions/core/arithmetic.yaml +62 -14
- data/data/functions/core/boolean.yaml +12 -6
- data/data/functions/core/comparison.yaml +25 -13
- data/data/functions/core/constructor.yaml +16 -8
- data/data/functions/core/conversion.yaml +32 -0
- data/data/functions/core/select.yaml +3 -1
- data/data/functions/core/stencil.yaml +14 -5
- data/data/functions/core/string.yaml +9 -4
- data/data/kernels/javascript/core/coercion.yaml +20 -0
- data/data/kernels/ruby/agg/numeric.yaml +1 -1
- 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/UNSAT_DETECTION.md +83 -0
- data/docs/VSCODE_EXTENSION.md +114 -0
- data/docs/functions-reference.json +1821 -0
- data/golden/array_element/expected/nast.txt +1 -1
- data/golden/array_element/expected/schema_ruby.rb +1 -1
- data/golden/array_index/expected/nast.txt +7 -7
- data/golden/array_index/expected/schema_ruby.rb +1 -1
- data/golden/array_operations/expected/nast.txt +2 -2
- data/golden/array_operations/expected/schema_ruby.rb +1 -1
- data/golden/array_operations/expected/snast.txt +3 -3
- data/golden/cascade_logic/expected/schema_ruby.rb +1 -1
- data/golden/cascade_logic/expected/snast.txt +2 -2
- data/golden/chained_fusion/expected/nast.txt +2 -2
- 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/nast.txt +2 -2
- data/golden/element_arrays/expected/schema_ruby.rb +1 -1
- data/golden/element_arrays/expected/snast.txt +1 -1
- data/golden/empty_and_null_inputs/expected/nast.txt +3 -3
- data/golden/empty_and_null_inputs/expected/schema_ruby.rb +1 -1
- data/golden/function_overload/expected/ast.txt +29 -0
- data/golden/function_overload/expected/input_plan.txt +4 -0
- data/golden/function_overload/expected/lir_00_unoptimized.txt +18 -0
- data/golden/function_overload/expected/lir_01_hoist_scalar_references.txt +18 -0
- data/golden/function_overload/expected/lir_02_inlined.txt +20 -0
- data/golden/function_overload/expected/lir_03_cse.txt +20 -0
- data/golden/function_overload/expected/lir_04_1_loop_fusion.txt +20 -0
- data/golden/function_overload/expected/lir_04_loop_invcm.txt +20 -0
- data/golden/function_overload/expected/lir_06_const_prop.txt +20 -0
- data/golden/function_overload/expected/nast.txt +22 -0
- data/golden/function_overload/expected/schema_javascript.mjs +12 -0
- data/golden/function_overload/expected/schema_ruby.rb +39 -0
- data/golden/function_overload/expected/snast.txt +22 -0
- data/golden/function_overload/input.json +8 -0
- data/golden/function_overload/schema.kumi +19 -0
- data/golden/game_of_life/expected/lir_00_unoptimized.txt +4 -4
- data/golden/game_of_life/expected/lir_01_hoist_scalar_references.txt +4 -4
- data/golden/game_of_life/expected/lir_02_inlined.txt +16 -16
- data/golden/game_of_life/expected/lir_03_cse.txt +20 -16
- data/golden/game_of_life/expected/lir_04_1_loop_fusion.txt +20 -16
- data/golden/game_of_life/expected/lir_04_loop_invcm.txt +20 -16
- data/golden/game_of_life/expected/lir_06_const_prop.txt +20 -16
- data/golden/game_of_life/expected/nast.txt +4 -4
- data/golden/game_of_life/expected/schema_javascript.mjs +4 -2
- data/golden/game_of_life/expected/schema_ruby.rb +5 -3
- data/golden/game_of_life/expected/snast.txt +10 -10
- data/golden/hash_keys/expected/schema_ruby.rb +1 -1
- data/golden/hash_value/expected/nast.txt +1 -1
- data/golden/hash_value/expected/schema_ruby.rb +1 -1
- data/golden/hash_value/expected/snast.txt +1 -1
- data/golden/hierarchical_complex/expected/nast.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/nast.txt +3 -3
- data/golden/inline_rename_scope_leak/expected/schema_ruby.rb +1 -1
- data/golden/input_reference/expected/nast.txt +2 -2
- data/golden/input_reference/expected/schema_ruby.rb +1 -1
- data/golden/interleaved_fusion/expected/nast.txt +2 -2
- data/golden/interleaved_fusion/expected/schema_ruby.rb +1 -1
- data/golden/let_inline/expected/nast.txt +4 -4
- data/golden/let_inline/expected/schema_ruby.rb +1 -1
- data/golden/loop_fusion/expected/nast.txt +1 -1
- data/golden/loop_fusion/expected/schema_ruby.rb +1 -1
- data/golden/min_reduce_scope/expected/nast.txt +3 -3
- data/golden/min_reduce_scope/expected/schema_ruby.rb +1 -1
- data/golden/min_reduce_scope/expected/snast.txt +1 -1
- data/golden/mixed_dimensions/expected/nast.txt +2 -2
- data/golden/mixed_dimensions/expected/schema_ruby.rb +1 -1
- data/golden/multirank_hoisting/expected/nast.txt +7 -7
- data/golden/multirank_hoisting/expected/schema_ruby.rb +1 -1
- data/golden/nested_hash/expected/nast.txt +1 -1
- data/golden/nested_hash/expected/schema_ruby.rb +1 -1
- data/golden/reduction_broadcast/expected/nast.txt +3 -3
- data/golden/reduction_broadcast/expected/schema_ruby.rb +1 -1
- data/golden/reduction_broadcast/expected/snast.txt +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 +1 -1
- data/golden/simple_math/expected/lir_01_hoist_scalar_references.txt +1 -1
- data/golden/simple_math/expected/lir_02_inlined.txt +1 -1
- data/golden/simple_math/expected/lir_03_cse.txt +1 -1
- data/golden/simple_math/expected/lir_04_1_loop_fusion.txt +1 -1
- data/golden/simple_math/expected/lir_04_loop_invcm.txt +1 -1
- data/golden/simple_math/expected/lir_06_const_prop.txt +1 -1
- data/golden/simple_math/expected/nast.txt +5 -5
- data/golden/simple_math/expected/schema_ruby.rb +1 -1
- data/golden/simple_math/expected/snast.txt +2 -2
- data/golden/streaming_basics/expected/nast.txt +8 -8
- data/golden/streaming_basics/expected/schema_ruby.rb +1 -1
- data/golden/streaming_basics/expected/snast.txt +1 -1
- data/golden/tuples/expected/lir_00_unoptimized.txt +5 -5
- data/golden/tuples/expected/lir_01_hoist_scalar_references.txt +5 -5
- data/golden/tuples/expected/lir_02_inlined.txt +5 -5
- data/golden/tuples/expected/lir_03_cse.txt +5 -5
- data/golden/tuples/expected/lir_04_1_loop_fusion.txt +5 -5
- data/golden/tuples/expected/lir_04_loop_invcm.txt +5 -5
- data/golden/tuples/expected/lir_06_const_prop.txt +5 -5
- data/golden/tuples/expected/nast.txt +4 -4
- data/golden/tuples/expected/schema_ruby.rb +1 -1
- data/golden/tuples/expected/snast.txt +6 -6
- data/golden/tuples_and_arrays/expected/lir_00_unoptimized.txt +1 -1
- data/golden/tuples_and_arrays/expected/lir_01_hoist_scalar_references.txt +1 -1
- data/golden/tuples_and_arrays/expected/lir_02_inlined.txt +2 -2
- data/golden/tuples_and_arrays/expected/lir_03_cse.txt +2 -2
- data/golden/tuples_and_arrays/expected/lir_04_1_loop_fusion.txt +2 -2
- data/golden/tuples_and_arrays/expected/lir_04_loop_invcm.txt +2 -2
- data/golden/tuples_and_arrays/expected/lir_06_const_prop.txt +2 -2
- data/golden/tuples_and_arrays/expected/nast.txt +3 -3
- data/golden/tuples_and_arrays/expected/schema_ruby.rb +1 -1
- data/golden/tuples_and_arrays/expected/snast.txt +2 -2
- data/golden/us_tax_2024/expected/ast.txt +63 -670
- data/golden/us_tax_2024/expected/input_plan.txt +8 -45
- data/golden/us_tax_2024/expected/lir_00_unoptimized.txt +253 -863
- data/golden/us_tax_2024/expected/lir_01_hoist_scalar_references.txt +253 -863
- data/golden/us_tax_2024/expected/lir_02_inlined.txt +1215 -5139
- data/golden/us_tax_2024/expected/lir_03_cse.txt +587 -2460
- data/golden/us_tax_2024/expected/lir_04_1_loop_fusion.txt +632 -2480
- data/golden/us_tax_2024/expected/lir_04_loop_invcm.txt +587 -2460
- data/golden/us_tax_2024/expected/lir_06_const_prop.txt +587 -2460
- data/golden/us_tax_2024/expected/nast.txt +123 -826
- data/golden/us_tax_2024/expected/schema_javascript.mjs +127 -581
- data/golden/us_tax_2024/expected/schema_ruby.rb +135 -610
- data/golden/us_tax_2024/expected/snast.txt +155 -858
- data/golden/us_tax_2024/expected.json +120 -1
- data/golden/us_tax_2024/input.json +18 -9
- data/golden/us_tax_2024/schema.kumi +48 -178
- data/golden/with_constants/expected/lir_00_unoptimized.txt +1 -1
- data/golden/with_constants/expected/lir_01_hoist_scalar_references.txt +1 -1
- data/golden/with_constants/expected/lir_02_inlined.txt +1 -1
- data/golden/with_constants/expected/lir_03_cse.txt +1 -1
- data/golden/with_constants/expected/lir_04_1_loop_fusion.txt +1 -1
- data/golden/with_constants/expected/lir_04_loop_invcm.txt +1 -1
- data/golden/with_constants/expected/lir_06_const_prop.txt +1 -1
- data/golden/with_constants/expected/nast.txt +2 -2
- data/golden/with_constants/expected/schema_ruby.rb +1 -1
- data/golden/with_constants/expected/snast.txt +2 -2
- data/lib/kumi/analyzer.rb +12 -12
- data/lib/kumi/configuration.rb +6 -0
- data/lib/kumi/core/analyzer/passes/formal_constraint_propagator.rb +236 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +22 -4
- data/lib/kumi/core/analyzer/passes/nast_dimensional_analyzer_pass.rb +64 -18
- data/lib/kumi/core/analyzer/passes/normalize_to_nast_pass.rb +9 -4
- data/lib/kumi/core/analyzer/passes/snast_pass.rb +3 -1
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +172 -198
- data/lib/kumi/core/error_reporter.rb +36 -1
- data/lib/kumi/core/errors.rb +33 -1
- data/lib/kumi/core/functions/function_spec.rb +5 -4
- data/lib/kumi/core/functions/loader.rb +17 -1
- data/lib/kumi/core/functions/overload_resolver.rb +164 -0
- data/lib/kumi/core/functions/type_error_reporter.rb +118 -0
- data/lib/kumi/core/functions/type_rules.rb +155 -35
- 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/inference.rb +29 -22
- data/lib/kumi/core/types/normalizer.rb +30 -45
- data/lib/kumi/core/types/validator.rb +17 -28
- data/lib/kumi/core/types/value_objects.rb +116 -0
- data/lib/kumi/core/types.rb +45 -37
- 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/registry_v2/loader.rb +90 -0
- data/lib/kumi/registry_v2.rb +18 -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 +57 -7
- data/lib/kumi/core/analyzer/unsat_constant_evaluator.rb +0 -59
- data/lib/kumi/core/atom_unsat_solver.rb +0 -396
- data/lib/kumi/core/constraint_relationship_solver.rb +0 -641
- data/lib/kumi/core/types/builder.rb +0 -23
- data/lib/kumi/core/types/compatibility.rb +0 -96
- 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
|
8
|
+
# Normalizes different type inputs to canonical Type objects
|
9
9
|
class Normalizer
|
10
|
-
# Type normalization - convert various inputs to
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
23
|
-
|
13
|
+
def self.valid_kind?(kind)
|
14
|
+
VALID_KINDS.include?(kind)
|
24
15
|
end
|
25
16
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
data/lib/kumi/core/types.rb
CHANGED
@@ -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)
|
14
|
-
|
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
|
22
|
-
def self.
|
23
|
-
|
28
|
+
# Type value object constructors
|
29
|
+
def self.scalar(kind)
|
30
|
+
ScalarType.new(kind)
|
24
31
|
end
|
25
32
|
|
26
|
-
def self.
|
27
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
45
|
-
|
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
|
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))
|