kumi 0.0.8 → 0.0.10

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +28 -44
  3. data/README.md +188 -108
  4. data/docs/AST.md +8 -1
  5. data/docs/FUNCTIONS.md +52 -8
  6. data/docs/compiler_design_principles.md +86 -0
  7. data/docs/features/README.md +22 -2
  8. data/docs/features/hierarchical-broadcasting.md +349 -0
  9. data/docs/features/javascript-transpiler.md +148 -0
  10. data/docs/features/performance.md +1 -3
  11. data/docs/features/s-expression-printer.md +77 -0
  12. data/docs/schema_metadata.md +7 -7
  13. data/examples/game_of_life.rb +2 -4
  14. data/lib/kumi/analyzer.rb +0 -2
  15. data/lib/kumi/compiler.rb +6 -275
  16. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
  17. data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
  18. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
  19. data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
  20. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
  21. data/lib/kumi/core/cascade_executor_builder.rb +132 -0
  22. data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
  23. data/lib/kumi/core/compiler/function_invoker.rb +55 -0
  24. data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
  25. data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
  26. data/lib/kumi/core/compiler_base.rb +137 -0
  27. data/lib/kumi/core/explain.rb +2 -2
  28. data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
  29. data/lib/kumi/core/function_registry/function_builder.rb +5 -3
  30. data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
  31. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  32. data/lib/kumi/core/function_registry.rb +32 -10
  33. data/lib/kumi/core/nested_structure_utils.rb +78 -0
  34. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
  35. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  36. data/lib/kumi/core/schema_instance.rb +4 -0
  37. data/lib/kumi/core/vectorized_function_builder.rb +88 -0
  38. data/lib/kumi/errors.rb +2 -0
  39. data/lib/kumi/js/compiler.rb +878 -0
  40. data/lib/kumi/js/function_registry.rb +333 -0
  41. data/lib/kumi/js.rb +23 -0
  42. data/lib/kumi/registry.rb +61 -1
  43. data/lib/kumi/schema.rb +1 -1
  44. data/lib/kumi/support/s_expression_printer.rb +162 -0
  45. data/lib/kumi/syntax/array_expression.rb +6 -6
  46. data/lib/kumi/syntax/call_expression.rb +4 -4
  47. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  48. data/lib/kumi/syntax/case_expression.rb +4 -4
  49. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  50. data/lib/kumi/syntax/hash_expression.rb +4 -4
  51. data/lib/kumi/syntax/input_declaration.rb +6 -5
  52. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  53. data/lib/kumi/syntax/input_reference.rb +5 -5
  54. data/lib/kumi/syntax/literal.rb +4 -4
  55. data/lib/kumi/syntax/node.rb +34 -34
  56. data/lib/kumi/syntax/root.rb +6 -6
  57. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  58. data/lib/kumi/syntax/value_declaration.rb +4 -4
  59. data/lib/kumi/version.rb +1 -1
  60. data/lib/kumi.rb +1 -1
  61. data/scripts/analyze_broadcast_methods.rb +68 -0
  62. data/scripts/analyze_cascade_methods.rb +74 -0
  63. data/scripts/check_broadcasting_coverage.rb +51 -0
  64. data/scripts/find_dead_code.rb +114 -0
  65. metadata +22 -4
  66. data/docs/features/array-broadcasting.md +0 -170
  67. data/lib/kumi/cli.rb +0 -449
  68. data/lib/kumi/core/vectorization_metadata.rb +0 -110
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module FunctionRegistry
6
+ module StatFunctions
7
+ def self.definitions
8
+ {
9
+ # Statistical Functions
10
+ avg: FunctionBuilder::Entry.new(
11
+ fn: ->(array) { array.sum.to_f / array.size },
12
+ arity: 1,
13
+ param_types: [Kumi::Core::Types.array(:float)],
14
+ return_type: :float,
15
+ description: "Calculate arithmetic mean (average) of numeric collection",
16
+ reducer: true
17
+ ),
18
+
19
+ mean: FunctionBuilder::Entry.new(
20
+ fn: ->(array) { array.sum.to_f / array.size },
21
+ arity: 1,
22
+ param_types: [Kumi::Core::Types.array(:float)],
23
+ return_type: :float,
24
+ description: "Calculate arithmetic mean (average) of numeric collection (alias for avg)",
25
+ reducer: true
26
+ ),
27
+
28
+ median: FunctionBuilder::Entry.new(
29
+ fn: lambda do |array|
30
+ sorted = array.sort
31
+ len = sorted.length
32
+ if len.odd?
33
+ sorted[len / 2]
34
+ else
35
+ (sorted[(len / 2) - 1] + sorted[len / 2]) / 2.0
36
+ end
37
+ end,
38
+ arity: 1,
39
+ param_types: [Kumi::Core::Types.array(:float)],
40
+ return_type: :float,
41
+ description: "Calculate median (middle value) of numeric collection",
42
+ reducer: true
43
+ ),
44
+
45
+ variance: FunctionBuilder::Entry.new(
46
+ fn: lambda do |array|
47
+ mean = array.sum.to_f / array.size
48
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
49
+ sum_of_squares / array.size
50
+ end,
51
+ arity: 1,
52
+ param_types: [Kumi::Core::Types.array(:float)],
53
+ return_type: :float,
54
+ description: "Calculate population variance of numeric collection",
55
+ reducer: true
56
+ ),
57
+
58
+ stdev: FunctionBuilder::Entry.new(
59
+ fn: lambda do |array|
60
+ mean = array.sum.to_f / array.size
61
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
62
+ variance = sum_of_squares / array.size
63
+ Math.sqrt(variance)
64
+ end,
65
+ arity: 1,
66
+ param_types: [Kumi::Core::Types.array(:float)],
67
+ return_type: :float,
68
+ description: "Calculate population standard deviation of numeric collection",
69
+ reducer: true
70
+ ),
71
+
72
+ sample_variance: FunctionBuilder::Entry.new(
73
+ fn: lambda do |array|
74
+ return 0.0 if array.size <= 1
75
+
76
+ mean = array.sum.to_f / array.size
77
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
78
+ sum_of_squares / (array.size - 1)
79
+ end,
80
+ arity: 1,
81
+ param_types: [Kumi::Core::Types.array(:float)],
82
+ return_type: :float,
83
+ description: "Calculate sample variance of numeric collection",
84
+ reducer: true
85
+ ),
86
+
87
+ sample_stdev: FunctionBuilder::Entry.new(
88
+ fn: lambda do |array|
89
+ return 0.0 if array.size <= 1
90
+
91
+ mean = array.sum.to_f / array.size
92
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
93
+ variance = sum_of_squares / (array.size - 1)
94
+ Math.sqrt(variance)
95
+ end,
96
+ arity: 1,
97
+ param_types: [Kumi::Core::Types.array(:float)],
98
+ return_type: :float,
99
+ description: "Calculate sample standard deviation of numeric collection",
100
+ reducer: true
101
+ ),
102
+
103
+ # Convenience functions for flattened statistics
104
+ flat_size: FunctionBuilder::Entry.new(
105
+ fn: ->(nested_array) { nested_array.flatten.size },
106
+ arity: 1,
107
+ param_types: [Kumi::Core::Types.array(:any)],
108
+ return_type: :integer,
109
+ description: "Count total elements across all nested levels",
110
+ reducer: true
111
+ ),
112
+
113
+ flat_sum: FunctionBuilder::Entry.new(
114
+ fn: ->(nested_array) { nested_array.flatten.sum },
115
+ arity: 1,
116
+ param_types: [Kumi::Core::Types.array(:float)],
117
+ return_type: :float,
118
+ description: "Sum all numeric elements across all nested levels",
119
+ reducer: true
120
+ ),
121
+
122
+ flat_avg: FunctionBuilder::Entry.new(
123
+ fn: lambda do |nested_array|
124
+ flattened = nested_array.flatten
125
+ flattened.sum.to_f / flattened.size
126
+ end,
127
+ arity: 1,
128
+ param_types: [Kumi::Core::Types.array(:float)],
129
+ return_type: :float,
130
+ description: "Calculate average across all nested levels",
131
+ reducer: true
132
+ ),
133
+
134
+ flat_max: FunctionBuilder::Entry.new(
135
+ fn: ->(nested_array) { nested_array.flatten.max },
136
+ arity: 1,
137
+ param_types: [Kumi::Core::Types.array(:float)],
138
+ return_type: :float,
139
+ description: "Find maximum value across all nested levels",
140
+ reducer: true
141
+ ),
142
+
143
+ flat_min: FunctionBuilder::Entry.new(
144
+ fn: ->(nested_array) { nested_array.flatten.min },
145
+ arity: 1,
146
+ param_types: [Kumi::Core::Types.array(:float)],
147
+ return_type: :float,
148
+ description: "Find minimum value across all nested levels",
149
+ reducer: true
150
+ )
151
+ }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Kumi
4
4
  module Core
5
- # Registry for functions that can be used in Kumi schemas
6
- # This is the public interface for registering custom functions
5
+ # Internal function registry implementation
6
+ # Use Kumi::Registry for the public interface to register custom functions
7
7
  module FunctionRegistry
8
8
  # Re-export the Entry struct from FunctionBuilder for compatibility
9
9
  Entry = FunctionBuilder::Entry
@@ -13,20 +13,33 @@ module Kumi
13
13
 
14
14
  # Build the complete function registry by combining all categories
15
15
  CORE_FUNCTIONS = {}.tap do |registry|
16
- registry.merge!(ComparisonFunctions.definitions)
17
- registry.merge!(MathFunctions.definitions)
18
- registry.merge!(StringFunctions.definitions)
19
- registry.merge!(LogicalFunctions.definitions)
20
- registry.merge!(CollectionFunctions.definitions)
21
- registry.merge!(ConditionalFunctions.definitions)
22
- registry.merge!(TypeFunctions.definitions)
16
+ function_modules = [
17
+ ComparisonFunctions.definitions,
18
+ MathFunctions.definitions,
19
+ StringFunctions.definitions,
20
+ LogicalFunctions.definitions,
21
+ CollectionFunctions.definitions,
22
+ ConditionalFunctions.definitions,
23
+ TypeFunctions.definitions,
24
+ StatFunctions.definitions
25
+ ]
26
+
27
+ function_modules.each do |module_definitions|
28
+ module_definitions.each do |name, definition|
29
+ if registry.key?(name)
30
+ raise ArgumentError, "Function #{name.inspect} is already registered. Found duplicate in function registry."
31
+ end
32
+
33
+ registry[name] = definition
34
+ end
35
+ end
23
36
  end.freeze
24
37
 
25
38
  @functions = CORE_FUNCTIONS.dup
26
39
  @frozen = false
27
40
 
28
41
  # class << self
29
- # Public interface for registering custom functions
42
+ # Internal interface for registering custom functions
30
43
  def register(name, &block)
31
44
  raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
32
45
 
@@ -97,6 +110,11 @@ module Kumi
97
110
  entry.reducer || false
98
111
  end
99
112
 
113
+ def structure_function?(name)
114
+ entry = @functions.fetch(name) { return false }
115
+ entry.structure_function || false
116
+ end
117
+
100
118
  # Alias for compatibility
101
119
  def all
102
120
  @functions.keys
@@ -130,6 +148,10 @@ module Kumi
130
148
  def type_operations
131
149
  TypeFunctions.definitions.keys
132
150
  end
151
+
152
+ def stat_operations
153
+ StatFunctions.definitions.keys
154
+ end
133
155
  end
134
156
  end
135
157
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ # Shared utilities for working with nested array structures
6
+ module NestedStructureUtils
7
+ def calculate_array_depth(arr)
8
+ return 0 unless arr.is_a?(Array)
9
+ return 1 if arr.empty? || !arr.first.is_a?(Array)
10
+
11
+ 1 + calculate_array_depth(arr.first)
12
+ end
13
+
14
+ def map_nested_structure(structure, indices = [], &block)
15
+ if structure.is_a?(Array) && structure.first.is_a?(Array)
16
+ # Still nested - recurse deeper
17
+ structure.map.with_index do |sub_structure, i|
18
+ map_nested_structure(sub_structure, indices + [i], &block)
19
+ end
20
+ elsif structure.is_a?(Array)
21
+ # Leaf array level - apply function to elements
22
+ structure.map.with_index do |_element, i|
23
+ yield(*(indices + [i]))
24
+ end
25
+ else
26
+ # Single element - apply function
27
+ yield(*indices)
28
+ end
29
+ end
30
+
31
+ def navigate_nested_indices(structure, indices)
32
+ indices.reduce(structure) do |current, index|
33
+ if current.is_a?(Array)
34
+ current[index]
35
+ else
36
+ # If we hit a non-array during navigation, it means we're dealing with
37
+ # mixed nesting levels - return the current value
38
+ current
39
+ end
40
+ end
41
+ end
42
+
43
+ def navigate_with_hierarchical_broadcasting(value, indices, template)
44
+ # Navigate through value with hierarchical broadcasting to match template structure
45
+ value_depth = calculate_array_depth(value)
46
+ template_depth = calculate_array_depth(template)
47
+
48
+ if value_depth < template_depth
49
+ # Value is at parent level - broadcast to child level by using fewer indices
50
+ parent_indices = indices[0, value_depth]
51
+ navigate_nested_indices(value, parent_indices)
52
+ else
53
+ # Same or deeper level - navigate normally
54
+ navigate_nested_indices(value, indices)
55
+ end
56
+ end
57
+
58
+ def find_structure_template(all_results)
59
+ # Find the first array to use as structure template
60
+ all_results.find { |v| v.is_a?(Array) }
61
+ end
62
+
63
+ def determine_array_length(arrays)
64
+ # Find the first array and use its length
65
+ first_array = arrays.find { |v| v.is_a?(Array) }
66
+ first_array ? first_array.length : 1
67
+ end
68
+
69
+ def extract_element_at_index(value, index)
70
+ if value.is_a?(Array)
71
+ index < value.length ? value[index] : nil
72
+ else
73
+ value # Scalar value - same for all indices
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -22,7 +22,7 @@ module Kumi
22
22
  expr = args.last
23
23
 
24
24
  trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
25
- condition = create_fn(:all?, trait_bindings)
25
+ condition = @context.fn(:cascade_and, *trait_bindings)
26
26
  result = ensure_syntax(expr)
27
27
  add_case(condition, result)
28
28
  end
@@ -92,7 +92,7 @@ module Kumi
92
92
  when DeclarationReference
93
93
  name # Already a binding from method_missing
94
94
  else
95
- raise_error("trait reference must be a symbol or bare identifier, got #{name.class}", location)
95
+ raise_error("trait reference must be a bare trait identifier, got #{name.class}", location)
96
96
  end
97
97
  end
98
98
  end
@@ -13,13 +13,13 @@ module Kumi
13
13
 
14
14
  def key(name, type: :any, domain: nil)
15
15
  normalized_type = normalize_type(type, name)
16
- @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], loc: @context.current_location)
16
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], nil, loc: @context.current_location)
17
17
  end
18
18
 
19
19
  %i[integer float 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
- @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], loc: @context.current_location)
22
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], nil, loc: @context.current_location)
23
23
  end
24
24
  end
25
25
 
@@ -40,7 +40,7 @@ module Kumi
40
40
  end
41
41
 
42
42
  def method_missing(method_name, *_args)
43
- allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', and 'hash'"
43
+ allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', 'hash', and 'element'"
44
44
  raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
45
45
  location: @context.current_location)
46
46
  end
@@ -63,7 +63,7 @@ module Kumi
63
63
  elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
64
64
 
65
65
  array_type = create_array_type(field_name, elem_type)
66
- @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], loc: @context.current_location)
66
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], :object, loc: @context.current_location)
67
67
  end
68
68
 
69
69
  def create_array_type(field_name, elem_type)
@@ -81,7 +81,7 @@ module Kumi
81
81
  val_type = extract_type(val_spec)
82
82
 
83
83
  hash_type = create_hash_type(field_name, key_type, val_type)
84
- @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], loc: @context.current_location)
84
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], nil, loc: @context.current_location)
85
85
  end
86
86
 
87
87
  def extract_type(spec)
@@ -98,14 +98,16 @@ module Kumi
98
98
  domain = options[:domain]
99
99
 
100
100
  # Collect children by creating a nested context
101
- children = collect_array_children(&block)
101
+ children, _, using_elements = collect_array_children(&block)
102
102
 
103
- # Create the InputDeclaration with children
103
+ # Create the InputDeclaration with children and access_mode
104
+ access_mode = using_elements ? :element : :object
104
105
  @context.inputs << Kumi::Syntax::InputDeclaration.new(
105
106
  field_name,
106
107
  domain,
107
108
  :array,
108
109
  children,
110
+ access_mode,
109
111
  loc: @context.current_location
110
112
  )
111
113
  end
@@ -119,7 +121,58 @@ module Kumi
119
121
  # Execute the block in the nested context
120
122
  nested_builder.instance_eval(&block)
121
123
 
122
- nested_inputs
124
+ # Determine element type based on what was declared
125
+ elem_type = determine_element_type(nested_builder, nested_inputs)
126
+
127
+ # Check if element() was used
128
+ using_elements = nested_builder.instance_variable_get(:@using_elements) || false
129
+
130
+ [nested_inputs, elem_type, using_elements]
131
+ end
132
+
133
+ def determine_element_type(_builder, inputs)
134
+ # Since element() always creates named children now,
135
+ # we just use the standard logic
136
+ if inputs.any?
137
+ # If fields were declared, it's a hash/object structure
138
+ :hash
139
+ else
140
+ # No fields declared, default to :any
141
+ :any
142
+ end
143
+ end
144
+
145
+ def primitive_element_type?(elem_type)
146
+ %i[string integer float boolean bool any symbol].include?(elem_type)
147
+ end
148
+
149
+ # New method: element() declaration - always requires a name
150
+ def element(type_spec, name, &block)
151
+ @using_elements = true
152
+ if block_given?
153
+ # Named element with nested structure: element(:array, :rows) do ... end
154
+ # These DO set @using_elements to enable element access mode for multi-dimensional arrays
155
+ case type_spec
156
+ when :array
157
+ create_array_field_with_block(name, {}, &block)
158
+ when :object
159
+ # Create nested object structure
160
+ create_object_element(name, &block)
161
+ else
162
+ raise_syntax_error("element(#{type_spec.inspect}, #{name.inspect}) with block only supports :array or :object types",
163
+ location: @context.current_location)
164
+ end
165
+ else
166
+ # Named primitive element: element(:boolean, :active)
167
+ # Only primitive elements mark the parent as using element access
168
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, type_spec, [], nil, loc: @context.current_location)
169
+ end
170
+ end
171
+
172
+ def create_object_element(name, &block)
173
+ # Similar to create_array_field_with_block but for objects
174
+ children, = collect_array_children(&block)
175
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, :object, children, nil, loc: @context.current_location)
123
176
  end
124
177
  end
125
178
  end
@@ -39,6 +39,10 @@ module Kumi
39
39
  evaluate(key_name)[key_name]
40
40
  end
41
41
 
42
+ def functions_used
43
+ @metadata[:functions_required].to_a
44
+ end
45
+
42
46
  # Update input values and clear affected cached computations
43
47
  def update(**changes)
44
48
  changes.each do |field, value|
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ # Builds vectorized function execution lambdas from analysis metadata
6
+ class VectorizedFunctionBuilder
7
+ include NestedStructureUtils
8
+
9
+ def self.build_executor(fn_name, compilation_meta, analysis_state)
10
+ new(fn_name, compilation_meta, analysis_state).build
11
+ end
12
+
13
+ def initialize(fn_name, compilation_meta, analysis_state)
14
+ @fn_name = fn_name
15
+ @compilation_meta = compilation_meta
16
+ @analysis_state = analysis_state
17
+ end
18
+
19
+ def build
20
+ # Get the function from registry
21
+ fn = Kumi::Registry.fetch(@fn_name)
22
+
23
+ lambda do |arg_values, loc|
24
+ # Check if any argument is vectorized (array)
25
+ has_vectorized_args = arg_values.any?(Array)
26
+
27
+ if has_vectorized_args
28
+ # Apply function with broadcasting to all vectorized arguments
29
+ vectorized_function_call(fn, arg_values)
30
+ else
31
+ # All arguments are scalars - regular function call
32
+ fn.call(*arg_values)
33
+ end
34
+ rescue StandardError => e
35
+ enhanced_message = "Error calling fn(:#{@fn_name}) at #{loc}: #{e.message}"
36
+ runtime_error = Errors::RuntimeError.new(enhanced_message)
37
+ runtime_error.set_backtrace(e.backtrace)
38
+ runtime_error.define_singleton_method(:cause) { e }
39
+ raise runtime_error
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def vectorized_function_call(fn, values)
46
+ # Find array dimensions for broadcasting
47
+ array_values = values.select { |v| v.is_a?(Array) }
48
+ return fn.call(*values) if array_values.empty?
49
+
50
+ # Check if we have deeply nested arrays (arrays containing arrays)
51
+ has_nested_arrays = array_values.any? { |arr| arr.is_a?(Array) && arr.first.is_a?(Array) }
52
+
53
+ if has_nested_arrays
54
+ # Use recursive element-wise operation for nested arrays
55
+ apply_function_to_nested_structure(fn, values)
56
+ else
57
+ # Original flat array logic
58
+ array_length = array_values.first.size
59
+ (0...array_length).map do |i|
60
+ element_args = values.map do |v|
61
+ v.is_a?(Array) ? v[i] : v # Broadcast scalars
62
+ end
63
+ fn.call(*element_args)
64
+ end
65
+ end
66
+ end
67
+
68
+ def apply_function_to_nested_structure(fn, values)
69
+ # Find the first array to determine structure
70
+ array_value = values.find { |v| v.is_a?(Array) }
71
+
72
+ # Apply function element-wise, preserving nested structure
73
+ map_nested_structure(array_value) do |*indices|
74
+ element_args = values.map do |value|
75
+ if value.is_a?(Array)
76
+ # Navigate to the corresponding element in nested structure
77
+ navigate_nested_indices(value, indices)
78
+ else
79
+ # Scalar - broadcast to all positions
80
+ value
81
+ end
82
+ end
83
+ fn.call(*element_args)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
data/lib/kumi/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kumi
2
4
  module Errors
3
5
  include Core::Errors