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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/CLAUDE.md +4 -0
  4. data/README.md +17 -8
  5. data/data/functions/core/conversion.yaml +32 -0
  6. data/data/kernels/javascript/core/coercion.yaml +20 -0
  7. data/data/kernels/ruby/core/coercion.yaml +20 -0
  8. data/docs/ARCHITECTURE.md +277 -0
  9. data/docs/DEVELOPMENT.md +62 -0
  10. data/docs/FUNCTIONS.md +955 -0
  11. data/docs/SYNTAX.md +8 -0
  12. data/docs/VSCODE_EXTENSION.md +114 -0
  13. data/docs/functions-reference.json +1821 -0
  14. data/golden/array_element/expected/schema_ruby.rb +1 -1
  15. data/golden/array_index/expected/schema_ruby.rb +1 -1
  16. data/golden/array_operations/expected/schema_ruby.rb +1 -1
  17. data/golden/cascade_logic/expected/schema_ruby.rb +1 -1
  18. data/golden/chained_fusion/expected/schema_ruby.rb +1 -1
  19. data/golden/decimal_explicit/expected/ast.txt +38 -0
  20. data/golden/decimal_explicit/expected/input_plan.txt +3 -0
  21. data/golden/decimal_explicit/expected/lir_00_unoptimized.txt +30 -0
  22. data/golden/decimal_explicit/expected/lir_01_hoist_scalar_references.txt +30 -0
  23. data/golden/decimal_explicit/expected/lir_02_inlined.txt +44 -0
  24. data/golden/decimal_explicit/expected/lir_03_cse.txt +40 -0
  25. data/golden/decimal_explicit/expected/lir_04_1_loop_fusion.txt +40 -0
  26. data/golden/decimal_explicit/expected/lir_04_loop_invcm.txt +40 -0
  27. data/golden/decimal_explicit/expected/lir_06_const_prop.txt +40 -0
  28. data/golden/decimal_explicit/expected/nast.txt +30 -0
  29. data/golden/decimal_explicit/expected/schema_javascript.mjs +31 -0
  30. data/golden/decimal_explicit/expected/schema_ruby.rb +57 -0
  31. data/golden/decimal_explicit/expected/snast.txt +30 -0
  32. data/golden/decimal_explicit/expected.json +1 -0
  33. data/golden/decimal_explicit/input.json +5 -0
  34. data/golden/decimal_explicit/schema.kumi +14 -0
  35. data/golden/element_arrays/expected/schema_ruby.rb +1 -1
  36. data/golden/empty_and_null_inputs/expected/schema_ruby.rb +1 -1
  37. data/golden/function_overload/expected/schema_ruby.rb +1 -1
  38. data/golden/game_of_life/expected/schema_ruby.rb +1 -1
  39. data/golden/hash_keys/expected/schema_ruby.rb +1 -1
  40. data/golden/hash_value/expected/schema_ruby.rb +1 -1
  41. data/golden/hierarchical_complex/expected/schema_ruby.rb +1 -1
  42. data/golden/inline_rename_scope_leak/expected/schema_ruby.rb +1 -1
  43. data/golden/input_reference/expected/schema_ruby.rb +1 -1
  44. data/golden/interleaved_fusion/expected/schema_ruby.rb +1 -1
  45. data/golden/let_inline/expected/schema_ruby.rb +1 -1
  46. data/golden/loop_fusion/expected/schema_ruby.rb +1 -1
  47. data/golden/min_reduce_scope/expected/schema_ruby.rb +1 -1
  48. data/golden/mixed_dimensions/expected/schema_ruby.rb +1 -1
  49. data/golden/multirank_hoisting/expected/schema_ruby.rb +1 -1
  50. data/golden/nested_hash/expected/schema_ruby.rb +1 -1
  51. data/golden/reduction_broadcast/expected/schema_ruby.rb +1 -1
  52. data/golden/roll/expected/schema_ruby.rb +1 -1
  53. data/golden/shift/expected/schema_ruby.rb +1 -1
  54. data/golden/shift_2d/expected/schema_ruby.rb +1 -1
  55. data/golden/simple_math/expected/schema_ruby.rb +1 -1
  56. data/golden/streaming_basics/expected/schema_ruby.rb +1 -1
  57. data/golden/tuples/expected/schema_ruby.rb +1 -1
  58. data/golden/tuples_and_arrays/expected/schema_ruby.rb +1 -1
  59. data/golden/us_tax_2024/expected/schema_ruby.rb +1 -1
  60. data/golden/with_constants/expected/schema_ruby.rb +1 -1
  61. data/lib/kumi/configuration.rb +6 -0
  62. data/lib/kumi/core/input/type_matcher.rb +8 -1
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +2 -2
  64. data/lib/kumi/core/types/normalizer.rb +1 -0
  65. data/lib/kumi/core/types/validator.rb +2 -2
  66. data/lib/kumi/core/types.rb +2 -2
  67. data/lib/kumi/dev/golden/reporter.rb +9 -0
  68. data/lib/kumi/dev/golden/result.rb +3 -1
  69. data/lib/kumi/dev/golden/runtime_test.rb +25 -0
  70. data/lib/kumi/dev/golden/suite.rb +4 -4
  71. data/lib/kumi/dev/golden/value_normalizer.rb +80 -0
  72. data/lib/kumi/dev/golden.rb +21 -12
  73. data/lib/kumi/doc_generator/formatters/json.rb +39 -0
  74. data/lib/kumi/doc_generator/formatters/markdown.rb +175 -0
  75. data/lib/kumi/doc_generator/loader.rb +37 -0
  76. data/lib/kumi/doc_generator/merger.rb +54 -0
  77. data/lib/kumi/doc_generator.rb +4 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/vscode-extension/.gitignore +4 -0
  80. data/vscode-extension/README.md +59 -0
  81. data/vscode-extension/TESTING.md +151 -0
  82. data/vscode-extension/package.json +51 -0
  83. data/vscode-extension/src/extension.ts +295 -0
  84. data/vscode-extension/tsconfig.json +15 -0
  85. metadata +37 -1
@@ -1,5 +1,5 @@
1
1
  # Autogenerated by Kumi Codegen
2
- module Kumi::Compiled::KUMI_d4065d9b2c1fa95582e236ac4c79f6623745ec731b5961da26df7c2f9e2a9cb6
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::KUMI_ee08a4690485a37a0f18de249ead6563ffb9cf9625312459a6000d00cad9f29c
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::KUMI_c72cd3609d08e6946eb6c74183f400d8c0678040a863711e4b82a63da1b63319
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::KUMI_1049f6474f9d41f05da4c2b6445c374c1eab40b0ff3089ecb80195a4550bceb8
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::KUMI_52c858dd08717cf97edaff33a54004b4af6c064f32453eb33012e14cd18d48c2
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::KUMI_1d3b2fb0b1b00e7730f48d49c8c33acdea2ee603018ffacfc1349df73a8dd4ee
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::KUMI_2a2d7ca421c364fcc8994d8ea79ac793a5bfe18cd756cea122fc6ea42d8f6e9a
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::KUMI_ada1b474771f2aaf5e63976adaf0402190d8744885036ff2a695e5bf8c93f65e
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::KUMI_c9608112fc116466a01eeee25616ed8f4e011b48728f579b0bff1761f145e9ba
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::KUMI_57ccd5ab1fc1187ce0abd20aa81d60fe4c33ce2438cccd991cd4fbd32e5a67e8
2
+ module Kumi::Compiled::KUMI_67599b5d819e0f308bb60585f93878e89c60af8bea9e8006f92e07b1a479d336
3
3
  def self.from(input_data = nil)
4
4
  instance = Object.new
5
5
  instance.extend(self)
@@ -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 :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
@@ -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