kumi 0.0.26 → 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 +7 -0
- data/CLAUDE.md +4 -0
- data/README.md +17 -8
- data/data/functions/core/conversion.yaml +32 -0
- data/data/kernels/javascript/core/coercion.yaml +20 -0
- 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/schema_ruby.rb +1 -1
- data/golden/array_operations/expected/schema_ruby.rb +1 -1
- 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/schema_ruby.rb +1 -1
- 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/schema_ruby.rb +1 -1
- data/golden/let_inline/expected/schema_ruby.rb +1 -1
- 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/schema_ruby.rb +1 -1
- data/golden/nested_hash/expected/schema_ruby.rb +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/schema_ruby.rb +1 -1
- data/golden/streaming_basics/expected/schema_ruby.rb +1 -1
- 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/schema_ruby.rb +1 -1
- data/golden/with_constants/expected/schema_ruby.rb +1 -1
- data/lib/kumi/configuration.rb +6 -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/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 +37 -1
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_6acd7e7a86400b5713bb861b265bd71ca0409744e38f91750bf81b1c8cc7b4f7
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_bf0841514d27c68c3a5223b5c9e5625080c4342efd1141ccce1a07b41457e613
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_5e5c116e1acd27deccebcb9ab5ef133223c7005d86584465d3160f569d3c6a9b
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_949d31a31d5b9027826a4d9e0ac0f4f7cf7ec0cf26a2a52a7da7dc7b6de71a1f
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_01c25ad1ae3550b0c23581f73407635ef18b1b5947933fe2edfdcbcef3253d37
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_e1cf5a39d9cce72668a167abe6fc8f84c427d0b38cc7759c0447e7822c722a43
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_b939a357fe30dacac4c3f90205c870e934bd818e640e50a63e39d2c387541a60
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_b61e29419a3a15e4309079460b8a9fe1cb0cf4849c3f1d4264073c890a575c24
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_6af882f233afb29fd899c6f83f4ad3d325f35951d32ae3139f886b34916fa881
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Autogenerated by Kumi Codegen
|
2
|
-
module Kumi::Compiled::
|
2
|
+
module Kumi::Compiled::KUMI_67599b5d819e0f308bb60585f93878e89c60af8bea9e8006f92e07b1a479d336
|
3
3
|
def self.from(input_data = nil)
|
4
4
|
instance = Object.new
|
5
5
|
instance.extend(self)
|
data/lib/kumi/configuration.rb
CHANGED
@@ -22,11 +22,17 @@ module Kumi
|
|
22
22
|
# Useful for debugging the compiler itself.
|
23
23
|
attr_accessor :force_recompile
|
24
24
|
|
25
|
+
# Decimal coercion behavior for inputs declared as `decimal` type.
|
26
|
+
# :automatic (default): Automatically coerce inputs to BigDecimal in Ruby
|
27
|
+
# :explicit: User must explicitly call to_decimal() in the schema
|
28
|
+
attr_accessor :decimal_coercion_mode
|
29
|
+
|
25
30
|
def initialize
|
26
31
|
# Set smart, environment-aware defaults.
|
27
32
|
@cache_path = default_cache_path
|
28
33
|
@compilation_mode = default_compilation_mode
|
29
34
|
@force_recompile = false
|
35
|
+
@decimal_coercion_mode = :automatic
|
30
36
|
end
|
31
37
|
|
32
38
|
private
|
@@ -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
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Kumi
|
2
|
+
module DocGenerator
|
3
|
+
module Formatters
|
4
|
+
class Markdown
|
5
|
+
def initialize(docs)
|
6
|
+
@docs = docs
|
7
|
+
end
|
8
|
+
|
9
|
+
def format
|
10
|
+
lines = [
|
11
|
+
"# Kumi Function Reference",
|
12
|
+
"",
|
13
|
+
"Auto-generated documentation for Kumi functions and their kernels.",
|
14
|
+
""
|
15
|
+
]
|
16
|
+
|
17
|
+
grouped = group_by_id(@docs)
|
18
|
+
|
19
|
+
grouped.sort.each do |id, aliases|
|
20
|
+
entry = @docs[aliases.first]
|
21
|
+
lines.concat(format_function(id, entry, aliases))
|
22
|
+
end
|
23
|
+
|
24
|
+
lines.join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def group_by_id(docs)
|
30
|
+
result = {}
|
31
|
+
docs.each do |alias_name, entry|
|
32
|
+
id = entry['id']
|
33
|
+
result[id] ||= []
|
34
|
+
result[id] << alias_name
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_function(id, entry, aliases)
|
40
|
+
lines = [
|
41
|
+
"## `#{id}`",
|
42
|
+
""
|
43
|
+
]
|
44
|
+
|
45
|
+
if aliases.length > 1
|
46
|
+
lines << "**Aliases:** `#{aliases.sort.join('`, `')}`"
|
47
|
+
lines << ""
|
48
|
+
end
|
49
|
+
|
50
|
+
lines << "- **Arity:** #{entry['arity']}"
|
51
|
+
|
52
|
+
if entry['dtype']
|
53
|
+
dtype_str = format_dtype(entry['dtype'])
|
54
|
+
lines << "- **Type:** #{dtype_str}"
|
55
|
+
end
|
56
|
+
|
57
|
+
if is_reducer?(entry)
|
58
|
+
lines << "- **Behavior:** Reduces a dimension `[D] -> T`"
|
59
|
+
end
|
60
|
+
lines << ""
|
61
|
+
|
62
|
+
if entry['params'] && !entry['params'].empty?
|
63
|
+
lines << "### Parameters"
|
64
|
+
lines << ""
|
65
|
+
entry['params'].each do |param|
|
66
|
+
lines << "- `#{param['name']}`#{param['description'] ? ": #{param['description']}" : ""}"
|
67
|
+
end
|
68
|
+
lines << ""
|
69
|
+
end
|
70
|
+
|
71
|
+
if entry['kernels'] && !entry['kernels'].empty?
|
72
|
+
lines << "### Implementations"
|
73
|
+
lines << ""
|
74
|
+
entry['kernels'].each do |target, kernel|
|
75
|
+
lines.concat(format_kernel(target, kernel, entry['reduction_strategy']))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
lines
|
80
|
+
end
|
81
|
+
|
82
|
+
def format_kernel(target, kernel, reduction_strategy = nil)
|
83
|
+
lines = []
|
84
|
+
|
85
|
+
if kernel.is_a?(Hash)
|
86
|
+
lines << "#### #{target.capitalize}"
|
87
|
+
lines << ""
|
88
|
+
lines << "`#{kernel['id']}`"
|
89
|
+
lines << ""
|
90
|
+
|
91
|
+
has_identity = kernel['identity'] && !kernel['identity'].empty?
|
92
|
+
|
93
|
+
if kernel['inline'] && has_identity
|
94
|
+
lines << "**Inline:** `#{escape_backticks(kernel['inline'])}` (`$0` = accumulator, `$1` = element)"
|
95
|
+
lines << ""
|
96
|
+
end
|
97
|
+
|
98
|
+
if kernel['impl']
|
99
|
+
lines << "**Implementation:**"
|
100
|
+
lines << ""
|
101
|
+
lines << "```ruby"
|
102
|
+
lines << format_impl(kernel['impl'])
|
103
|
+
lines << "```"
|
104
|
+
lines << ""
|
105
|
+
end
|
106
|
+
|
107
|
+
if kernel['fold_inline']
|
108
|
+
lines << "**Fold:** `#{escape_backticks(kernel['fold_inline'])}`"
|
109
|
+
lines << ""
|
110
|
+
end
|
111
|
+
|
112
|
+
if has_identity
|
113
|
+
lines << "**Identity:**"
|
114
|
+
kernel['identity'].each do |type, value|
|
115
|
+
lines << "- #{type}: `#{value}`"
|
116
|
+
end
|
117
|
+
lines << ""
|
118
|
+
elsif kernel['inline']
|
119
|
+
lines << "_Note: No identity value. First element initializes accumulator._"
|
120
|
+
lines << ""
|
121
|
+
end
|
122
|
+
|
123
|
+
# Show reduction strategy if available
|
124
|
+
if reduction_strategy
|
125
|
+
case reduction_strategy
|
126
|
+
when 'identity'
|
127
|
+
lines << "**Reduction:** Monoid operation with identity element"
|
128
|
+
when 'first_element'
|
129
|
+
lines << "**Reduction:** First element is initial value (no identity)"
|
130
|
+
else
|
131
|
+
lines << "**Reduction:** #{reduction_strategy}"
|
132
|
+
end
|
133
|
+
lines << ""
|
134
|
+
end
|
135
|
+
else
|
136
|
+
lines << "- **#{target}:** `#{kernel}`"
|
137
|
+
end
|
138
|
+
|
139
|
+
lines
|
140
|
+
end
|
141
|
+
|
142
|
+
def format_dtype(dtype)
|
143
|
+
return "any" if dtype.nil?
|
144
|
+
|
145
|
+
case dtype['rule']
|
146
|
+
when 'same_as'
|
147
|
+
"same as `#{dtype['param']}`"
|
148
|
+
when 'scalar'
|
149
|
+
dtype['kind'] || 'scalar'
|
150
|
+
when 'promote'
|
151
|
+
params = Array(dtype['params']).join('`, `')
|
152
|
+
"promoted from `#{params}`"
|
153
|
+
when 'element_of'
|
154
|
+
"element of `#{dtype['param']}`"
|
155
|
+
else
|
156
|
+
dtype['rule']
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def format_impl(impl_str)
|
161
|
+
# Clean up multiline strings like "(a,b)\n a + b"
|
162
|
+
impl_str.gsub('\n', "\n").strip
|
163
|
+
end
|
164
|
+
|
165
|
+
def escape_backticks(str)
|
166
|
+
str.gsub('`', '\`')
|
167
|
+
end
|
168
|
+
|
169
|
+
def is_reducer?(entry)
|
170
|
+
entry['kind'] == 'reduce'
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|