kumi 0.0.9 → 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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +28 -44
  3. data/README.md +187 -120
  4. data/docs/AST.md +1 -1
  5. data/docs/FUNCTIONS.md +52 -8
  6. data/docs/compiler_design_principles.md +86 -0
  7. data/docs/features/README.md +15 -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/schema_metadata.md +7 -7
  12. data/examples/game_of_life.rb +2 -4
  13. data/lib/kumi/analyzer.rb +0 -2
  14. data/lib/kumi/compiler.rb +6 -275
  15. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
  16. data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
  17. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
  18. data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
  19. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
  20. data/lib/kumi/core/cascade_executor_builder.rb +132 -0
  21. data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
  22. data/lib/kumi/core/compiler/function_invoker.rb +55 -0
  23. data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
  24. data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
  25. data/lib/kumi/core/compiler_base.rb +137 -0
  26. data/lib/kumi/core/explain.rb +2 -2
  27. data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
  28. data/lib/kumi/core/function_registry/function_builder.rb +5 -3
  29. data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
  30. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  31. data/lib/kumi/core/function_registry.rb +32 -10
  32. data/lib/kumi/core/nested_structure_utils.rb +78 -0
  33. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
  34. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  35. data/lib/kumi/core/schema_instance.rb +4 -0
  36. data/lib/kumi/core/vectorized_function_builder.rb +88 -0
  37. data/lib/kumi/errors.rb +2 -0
  38. data/lib/kumi/js/compiler.rb +878 -0
  39. data/lib/kumi/js/function_registry.rb +333 -0
  40. data/lib/kumi/js.rb +23 -0
  41. data/lib/kumi/registry.rb +61 -1
  42. data/lib/kumi/schema.rb +1 -1
  43. data/lib/kumi/support/s_expression_printer.rb +16 -15
  44. data/lib/kumi/syntax/array_expression.rb +6 -6
  45. data/lib/kumi/syntax/call_expression.rb +4 -4
  46. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  47. data/lib/kumi/syntax/case_expression.rb +4 -4
  48. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  49. data/lib/kumi/syntax/hash_expression.rb +4 -4
  50. data/lib/kumi/syntax/input_declaration.rb +6 -5
  51. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  52. data/lib/kumi/syntax/input_reference.rb +5 -5
  53. data/lib/kumi/syntax/literal.rb +4 -4
  54. data/lib/kumi/syntax/node.rb +34 -34
  55. data/lib/kumi/syntax/root.rb +6 -6
  56. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  57. data/lib/kumi/syntax/value_declaration.rb +4 -4
  58. data/lib/kumi/version.rb +1 -1
  59. data/lib/kumi.rb +1 -1
  60. data/scripts/analyze_broadcast_methods.rb +68 -0
  61. data/scripts/analyze_cascade_methods.rb +74 -0
  62. data/scripts/check_broadcasting_coverage.rb +51 -0
  63. data/scripts/find_dead_code.rb +114 -0
  64. metadata +20 -4
  65. data/docs/features/array-broadcasting.md +0 -170
  66. data/lib/kumi/cli.rb +0 -449
  67. data/lib/kumi/core/vectorization_metadata.rb +0 -110
@@ -0,0 +1,158 @@
1
+ module Kumi
2
+ module Core
3
+ module Compiler
4
+ module PathTraversalCompiler
5
+ private
6
+
7
+ def compile_element_field_reference(expr)
8
+ path = expr.path
9
+
10
+ # Check if we have nested paths metadata for this path
11
+ nested_paths = @analysis.state[:broadcasts]&.dig(:nested_paths)
12
+ unless nested_paths && nested_paths[path]
13
+ raise Errors::CompilationError, "Missing nested path metadata for #{path.inspect}. This indicates an analyzer bug."
14
+ end
15
+
16
+ # Determine operation mode based on context
17
+ operation_mode = determine_operation_mode_for_path(path)
18
+ path_metadata = nested_paths[path]
19
+ lambda do |ctx|
20
+ traverse_nested_path(ctx, path, operation_mode, path_metadata)
21
+ end
22
+
23
+ # ERROR: All nested paths should have metadata from the analyzer
24
+ # If we reach here, it means the BroadcastDetector didn't process this path
25
+ end
26
+
27
+ # Metadata-driven nested array traversal using the traversal algorithm from our design
28
+ def traverse_nested_path(data, path, operation_mode, path_metadata = nil)
29
+ access_mode = path_metadata&.dig(:access_mode) || :object
30
+
31
+ # Use specialized traversal for element access mode
32
+ result = if access_mode == :element
33
+ traverse_element_path(data, path, operation_mode)
34
+ else
35
+ traverse_path_recursive(data, path, operation_mode, access_mode)
36
+ end
37
+
38
+ # Post-process result based on operation mode
39
+ case operation_mode
40
+ when :flatten
41
+ # Completely flatten nested arrays for aggregation
42
+ flatten_completely(result)
43
+ else
44
+ result
45
+ end
46
+ end
47
+
48
+ # Specialized traversal for element access mode
49
+ # In element access, we need to extract the specific field from EvaluationWrapper
50
+ # then apply progressive traversal based on path depth
51
+ def traverse_element_path(data, path, _operation_mode)
52
+ # Handle EvaluationWrapper by extracting the specific field
53
+ if data.is_a?(Core::EvaluationWrapper)
54
+ field_name = path.first
55
+ array_data = data[field_name]
56
+
57
+ # Always apply progressive traversal based on path depth
58
+ # This gives us the structure at the correct nesting level for both
59
+ # broadcast operations and structure operations
60
+ if array_data.is_a?(Array) && path.length > 1
61
+ # Flatten exactly (path_depth - 1) levels to get the desired nesting level
62
+ array_data.flatten(path.length - 1)
63
+ else
64
+ array_data
65
+ end
66
+ else
67
+ data
68
+ end
69
+ end
70
+
71
+ def traverse_path_recursive(data, path, operation_mode, access_mode = :object, original_path_length = nil)
72
+ # Track original path length to determine traversal depth
73
+ original_path_length ||= path.length
74
+ current_depth = original_path_length - path.length
75
+
76
+ return data if path.empty?
77
+
78
+ field = path.first
79
+ remaining_path = path[1..]
80
+
81
+ if remaining_path.empty?
82
+ # Final field - extract based on operation mode
83
+ case operation_mode
84
+ when :broadcast, :flatten
85
+ # Extract field preserving array structure
86
+ extract_field_preserving_structure(data, field, access_mode, current_depth)
87
+ else
88
+ # Simple field access
89
+ if data.is_a?(Array)
90
+ data.map do |item|
91
+ access_field(item, field, access_mode, current_depth)
92
+ end
93
+ else
94
+ access_field(data, field, access_mode, current_depth)
95
+ end
96
+ end
97
+ elsif data.is_a?(Array)
98
+ # Intermediate step - traverse deeper
99
+ # Array of items - traverse each item
100
+ data.map do |item|
101
+ traverse_path_recursive(access_field(item, field, access_mode, current_depth), remaining_path, operation_mode, access_mode,
102
+ original_path_length)
103
+ end
104
+ else
105
+ # Single item - traverse directly
106
+ traverse_path_recursive(access_field(data, field, access_mode, current_depth), remaining_path, operation_mode, access_mode,
107
+ original_path_length)
108
+ end
109
+ end
110
+
111
+ def extract_field_preserving_structure(data, field, access_mode = :object, depth = 0)
112
+ if data.is_a?(Array)
113
+ data.map { |item| extract_field_preserving_structure(item, field, access_mode, depth) }
114
+ else
115
+ access_field(data, field, access_mode, depth)
116
+ end
117
+ end
118
+
119
+ def access_field(data, field, access_mode, _depth = 0)
120
+ case access_mode
121
+ when :element
122
+ # Element access mode - for nested arrays, we need to traverse one level deeper
123
+ # This enables progressive path traversal like input.cube.layer.row.value
124
+ if data.is_a?(Core::EvaluationWrapper)
125
+ data[field]
126
+ elsif data.is_a?(Array)
127
+ # For element access, flatten one level to traverse deeper into nested structure
128
+ data.flatten(1)
129
+ else
130
+ # If not an array, return as-is (leaf level)
131
+ data
132
+ end
133
+ when :object
134
+ # Object access mode - normal hash/object field access
135
+ data[field]
136
+ else
137
+ # Default to object access
138
+ data[field]
139
+ end
140
+ end
141
+
142
+ def flatten_completely(data)
143
+ result = []
144
+ flatten_recursive(data, result)
145
+ result
146
+ end
147
+
148
+ def flatten_recursive(data, result)
149
+ if data.is_a?(Array)
150
+ data.each { |item| flatten_recursive(item, result) }
151
+ else
152
+ result << data
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,46 @@
1
+ module Kumi
2
+ module Core
3
+ module Compiler
4
+ module ReferenceCompiler
5
+ private
6
+
7
+ def compile_literal(expr)
8
+ v = expr.value
9
+ ->(_ctx) { v }
10
+ end
11
+
12
+ def compile_field_node(expr)
13
+ compile_field(expr)
14
+ end
15
+
16
+ def compile_binding_node(expr)
17
+ name = expr.name
18
+ # Handle forward references in cycles by deferring binding lookup to runtime
19
+ lambda do |ctx|
20
+ fn = @bindings[name].last
21
+ fn.call(ctx)
22
+ end
23
+ end
24
+
25
+ def compile_field(node)
26
+ name = node.name
27
+ loc = node.loc
28
+ lambda do |ctx|
29
+ return ctx[name] if ctx.respond_to?(:key?) && ctx.key?(name)
30
+
31
+ raise Errors::RuntimeError,
32
+ "Key '#{name}' not found at #{loc}. Available: #{ctx.respond_to?(:keys) ? ctx.keys.join(', ') : 'N/A'}"
33
+ end
34
+ end
35
+
36
+ def compile_declaration(decl)
37
+ @current_declaration = decl.name
38
+ kind = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? :trait : :attr
39
+ fn = compile_expr(decl.expression)
40
+ @bindings[decl.name] = [kind, fn]
41
+ @current_declaration = nil
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ # Base compiler class with shared compilation logic between Ruby and JS compilers
6
+ class CompilerBase
7
+ # Map node classes to compiler methods
8
+ DISPATCH = {
9
+ Kumi::Syntax::Literal => :compile_literal,
10
+ Kumi::Syntax::InputReference => :compile_field_node,
11
+ Kumi::Syntax::InputElementReference => :compile_element_field_reference,
12
+ Kumi::Syntax::DeclarationReference => :compile_binding_node,
13
+ Kumi::Syntax::ArrayExpression => :compile_list,
14
+ Kumi::Syntax::CallExpression => :compile_call,
15
+ Kumi::Syntax::CascadeExpression => :compile_cascade
16
+ }.freeze
17
+
18
+ def initialize(syntax_tree, analyzer_result)
19
+ @schema = syntax_tree
20
+ @analysis = analyzer_result
21
+ end
22
+
23
+ # Shared compilation logic
24
+
25
+ def build_index
26
+ @index = {}
27
+ @schema.attributes.each { |a| @index[a.name] = a }
28
+ @schema.traits.each { |t| @index[t.name] = t }
29
+ end
30
+
31
+ def determine_operation_mode_for_path(_path)
32
+ # Use pre-computed operation mode from analysis
33
+ compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, @current_declaration)
34
+ compilation_meta&.dig(:operation_mode) || :broadcast
35
+ end
36
+
37
+ def vectorized_operation?(expr)
38
+ # Use pre-computed vectorization decision from analysis
39
+ compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, @current_declaration)
40
+ return false unless compilation_meta
41
+
42
+ # Check if current declaration is vectorized
43
+ if compilation_meta[:is_vectorized]
44
+ # For vectorized declarations, check if this specific operation should be vectorized
45
+ vectorized_ops = @analysis.state[:broadcasts][:vectorized_operations] || {}
46
+ current_decl_info = vectorized_ops[@current_declaration]
47
+
48
+ # For cascade declarations, check individual operations within them
49
+ return true if current_decl_info && current_decl_info[:operation] == expr.fn_name
50
+
51
+ # For cascade_with_vectorized_conditions_or_results, allow nested operations
52
+ return true if current_decl_info && current_decl_info[:source] == :cascade_with_vectorized_conditions_or_results
53
+
54
+ # Check if this is a direct vectorized operation
55
+ return true if current_decl_info && current_decl_info[:operation]
56
+ end
57
+
58
+ # Fallback: Reduction functions are NOT vectorized operations - they consume arrays
59
+ return false if Kumi::Registry.reducer?(expr.fn_name)
60
+
61
+ # Use pre-computed vectorization context for remaining cases
62
+ compilation_meta.dig(:vectorization_context, :needs_broadcasting) || false
63
+ end
64
+
65
+ def is_cascade_vectorized?(_expr)
66
+ # Use metadata to determine if this cascade is vectorized
67
+ broadcast_meta = @analysis.state[:broadcasts]
68
+ cascade_info = @current_declaration && broadcast_meta&.dig(:vectorized_operations, @current_declaration)
69
+ cascade_info && cascade_info[:source] == :cascade_with_vectorized_conditions_or_results
70
+ end
71
+
72
+ def get_cascade_compilation_metadata
73
+ compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, @current_declaration)
74
+ cascade_info = compilation_meta&.dig(:cascade_info) || {}
75
+ [compilation_meta, cascade_info]
76
+ end
77
+
78
+ def get_cascade_strategy
79
+ @analysis.state[:broadcasts][:cascade_strategies][@current_declaration]
80
+ end
81
+
82
+ def get_function_call_strategy
83
+ compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, @current_declaration)
84
+ compilation_meta&.dig(:function_call_strategy) || {}
85
+ end
86
+
87
+ def needs_flattening?
88
+ function_strategy = get_function_call_strategy
89
+ function_strategy[:flattening_required]
90
+ end
91
+
92
+ def get_flattening_info
93
+ @analysis.state[:broadcasts][:flattening_declarations][@current_declaration]
94
+ end
95
+
96
+ def get_flatten_argument_indices
97
+ compilation_meta = @analysis.state[:broadcasts]&.dig(:compilation_metadata, @current_declaration)
98
+ compilation_meta&.dig(:function_call_strategy, :flatten_argument_indices) || []
99
+ end
100
+
101
+ # Dispatch to the appropriate compile_* method
102
+ def compile_expr(expr)
103
+ method = DISPATCH.fetch(expr.class)
104
+ send(method, expr)
105
+ end
106
+
107
+ # Abstract methods to be implemented by subclasses
108
+ def compile_literal(expr)
109
+ raise NotImplementedError, "Subclasses must implement compile_literal"
110
+ end
111
+
112
+ def compile_field_node(expr)
113
+ raise NotImplementedError, "Subclasses must implement compile_field_node"
114
+ end
115
+
116
+ def compile_element_field_reference(expr)
117
+ raise NotImplementedError, "Subclasses must implement compile_element_field_reference"
118
+ end
119
+
120
+ def compile_binding_node(expr)
121
+ raise NotImplementedError, "Subclasses must implement compile_binding_node"
122
+ end
123
+
124
+ def compile_list(expr)
125
+ raise NotImplementedError, "Subclasses must implement compile_list"
126
+ end
127
+
128
+ def compile_call(expr)
129
+ raise NotImplementedError, "Subclasses must implement compile_call"
130
+ end
131
+
132
+ def compile_cascade(expr)
133
+ raise NotImplementedError, "Subclasses must implement compile_cascade"
134
+ end
135
+ end
136
+ end
137
+ end
@@ -8,11 +8,11 @@ module Kumi
8
8
  @analyzer_result = analyzer_result
9
9
  @inputs = EvaluationWrapper.new(inputs)
10
10
  @definitions = analyzer_result.definitions
11
- @compiled_schema = Compiler.compile(syntax_tree, analyzer: analyzer_result)
11
+ @compiled_schema = Kumi::Compiler.compile(syntax_tree, analyzer: analyzer_result)
12
12
 
13
13
  # TODO: REFACTOR QUICK!
14
14
  # Set up compiler once for expression evaluation
15
- @compiler = Compiler.new(syntax_tree, analyzer_result)
15
+ @compiler = Kumi::Compiler.new(syntax_tree, analyzer_result)
16
16
  @compiler.send(:build_index)
17
17
 
18
18
  # Populate bindings from the compiled schema
@@ -9,8 +9,8 @@ module Kumi
9
9
  {
10
10
  # Collection queries (these are reducers - they reduce arrays to scalars)
11
11
  empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty?, reducer: true),
12
- size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer, reducer: true),
13
- length: FunctionBuilder.collection_unary(:length, "Get collection length", :length, return_type: :integer, reducer: true),
12
+ size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer, reducer: false,
13
+ structure_function: true),
14
14
 
15
15
  # Element access
16
16
  first: FunctionBuilder::Entry.new(
@@ -98,7 +98,26 @@ module Kumi
98
98
  arity: 1,
99
99
  param_types: [Kumi::Core::Types.array(:any)],
100
100
  return_type: Kumi::Core::Types.array(:any),
101
- description: "Flatten nested arrays into a single array"
101
+ description: "Flatten nested arrays into a single array",
102
+ structure_function: true
103
+ ),
104
+
105
+ flatten_one: FunctionBuilder::Entry.new(
106
+ fn: ->(array) { array.flatten(1) },
107
+ arity: 1,
108
+ param_types: [Kumi::Core::Types.array(:any)],
109
+ return_type: Kumi::Core::Types.array(:any),
110
+ description: "Flatten nested arrays by one level only",
111
+ structure_function: true
112
+ ),
113
+
114
+ flatten_deep: FunctionBuilder::Entry.new(
115
+ fn: lambda(&:flatten),
116
+ arity: 1,
117
+ param_types: [Kumi::Core::Types.array(:any)],
118
+ return_type: Kumi::Core::Types.array(:any),
119
+ description: "Recursively flatten all nested arrays (alias for flatten)",
120
+ structure_function: true
102
121
  ),
103
122
 
104
123
  # Mathematical transformation functions
@@ -193,6 +212,70 @@ module Kumi
193
212
  param_types: [Kumi::Core::Types.array(:any)],
194
213
  return_type: Kumi::Core::Types.array(:integer),
195
214
  description: "Generate array of indices for the collection"
215
+ ),
216
+
217
+ # Conditional aggregation functions
218
+ count_if: FunctionBuilder::Entry.new(
219
+ fn: ->(condition_array) { condition_array.count(true) },
220
+ arity: 1,
221
+ param_types: [Kumi::Core::Types.array(:boolean)],
222
+ return_type: :integer,
223
+ description: "Count number of true values in boolean array",
224
+ reducer: true
225
+ ),
226
+
227
+ sum_if: FunctionBuilder::Entry.new(
228
+ fn: lambda { |value_array, condition_array|
229
+ value_array.zip(condition_array).sum { |value, condition| condition ? value : 0 }
230
+ },
231
+ arity: 2,
232
+ param_types: [Kumi::Core::Types.array(:float), Kumi::Core::Types.array(:boolean)],
233
+ return_type: :float,
234
+ description: "Sum values where corresponding condition is true",
235
+ reducer: true
236
+ ),
237
+
238
+ avg_if: FunctionBuilder::Entry.new(
239
+ fn: lambda { |value_array, condition_array|
240
+ pairs = value_array.zip(condition_array)
241
+ true_values = pairs.filter_map { |value, condition| value if condition }
242
+ return 0.0 if true_values.empty?
243
+
244
+ true_values.sum.to_f / true_values.size
245
+ },
246
+ arity: 2,
247
+ param_types: [Kumi::Core::Types.array(:float), Kumi::Core::Types.array(:boolean)],
248
+ return_type: :float,
249
+ description: "Average values where corresponding condition is true",
250
+ reducer: true
251
+ ),
252
+
253
+ # Flattening utilities for hierarchical data
254
+ any_across: FunctionBuilder::Entry.new(
255
+ fn: ->(nested_array) { nested_array.flatten.any? },
256
+ arity: 1,
257
+ param_types: [Kumi::Core::Types.array(:any)],
258
+ return_type: :boolean,
259
+ description: "Check if any element is truthy across all nested levels",
260
+ reducer: true
261
+ ),
262
+
263
+ all_across: FunctionBuilder::Entry.new(
264
+ fn: ->(nested_array) { nested_array.flatten.all? },
265
+ arity: 1,
266
+ param_types: [Kumi::Core::Types.array(:any)],
267
+ return_type: :boolean,
268
+ description: "Check if all elements are truthy across all nested levels",
269
+ reducer: true
270
+ ),
271
+
272
+ count_across: FunctionBuilder::Entry.new(
273
+ fn: ->(nested_array) { nested_array.flatten.size },
274
+ arity: 1,
275
+ param_types: [Kumi::Core::Types.array(:any)],
276
+ return_type: :integer,
277
+ description: "Count total elements across all nested levels",
278
+ reducer: true
196
279
  )
197
280
  }
198
281
  end
@@ -5,7 +5,8 @@ module Kumi
5
5
  module FunctionRegistry
6
6
  # Utility class to reduce repetition in function definitions
7
7
  class FunctionBuilder
8
- Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, :reducer, keyword_init: true)
8
+ Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, :reducer, :structure_function,
9
+ keyword_init: true)
9
10
 
10
11
  def self.comparison(_name, description, operation)
11
12
  Entry.new(
@@ -79,14 +80,15 @@ module Kumi
79
80
  )
80
81
  end
81
82
 
82
- def self.collection_unary(_name, description, operation, return_type: :boolean, reducer: false)
83
+ def self.collection_unary(_name, description, operation, return_type: :boolean, reducer: false, structure_function: false)
83
84
  Entry.new(
84
85
  fn: proc(&operation),
85
86
  arity: 1,
86
87
  param_types: [Kumi::Core::Types.array(:any)],
87
88
  return_type: return_type,
88
89
  description: description,
89
- reducer: reducer
90
+ reducer: reducer,
91
+ structure_function: structure_function
90
92
  )
91
93
  end
92
94
  end
@@ -5,6 +5,146 @@ module Kumi
5
5
  module FunctionRegistry
6
6
  # Logical operations and boolean functions
7
7
  module LogicalFunctions
8
+ def self.element_wise_and(a, b)
9
+ if ENV["DEBUG_CASCADE"]
10
+ puts "DEBUG element_wise_and called with:"
11
+ puts " a: #{a.inspect} (depth: #{array_depth(a)})"
12
+ puts " b: #{b.inspect} (depth: #{array_depth(b)})"
13
+ end
14
+
15
+ case [a.class, b.class]
16
+ when [Array, Array]
17
+ # Both are arrays - handle hierarchical broadcasting
18
+ if hierarchical_broadcasting_needed?(a, b)
19
+ puts " -> Using hierarchical broadcasting" if ENV["DEBUG_CASCADE"]
20
+ result = perform_hierarchical_and(a, b)
21
+ puts " -> Hierarchical result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
22
+ else
23
+ # Same structure - use zip for element-wise operations
24
+ puts " -> Using same-structure zip" if ENV["DEBUG_CASCADE"]
25
+ result = a.zip(b).map { |elem_a, elem_b| element_wise_and(elem_a, elem_b) }
26
+ puts " -> Zip result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
27
+ end
28
+ result
29
+ when [Array, Object], [Object, Array]
30
+ # One is array, one is scalar - broadcast scalar
31
+ puts " -> Broadcasting scalar to array" if ENV["DEBUG_CASCADE"]
32
+ result = if a.is_a?(Array)
33
+ a.map { |elem| element_wise_and(elem, b) }
34
+ else
35
+ b.map { |elem| element_wise_and(a, elem) }
36
+ end
37
+ puts " -> Broadcast result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
38
+ result
39
+ else
40
+ # Both are scalars - simple AND
41
+ puts " -> Simple scalar AND: #{a} && #{b} = #{a && b}" if ENV["DEBUG_CASCADE"]
42
+ a && b
43
+ end
44
+ end
45
+
46
+ def self.hierarchical_broadcasting_needed?(a, b)
47
+ # Check if arrays have different nesting depths (hierarchical broadcasting)
48
+ depth_a = array_depth(a)
49
+ depth_b = array_depth(b)
50
+ depth_a != depth_b
51
+ end
52
+
53
+ def self.array_depth(arr)
54
+ return 0 unless arr.is_a?(Array)
55
+ return 1 if arr.empty? || !arr.first.is_a?(Array)
56
+
57
+ 1 + array_depth(arr.first)
58
+ end
59
+
60
+ def self.perform_hierarchical_and(a, b)
61
+ # Determine which is the higher dimension and which is lower
62
+ depth_a = array_depth(a)
63
+ depth_b = array_depth(b)
64
+
65
+ puts " perform_hierarchical_and: depth_a=#{depth_a}, depth_b=#{depth_b}" if ENV["DEBUG_CASCADE"]
66
+
67
+ if depth_a > depth_b
68
+ # a is deeper (child level), b is shallower (parent level)
69
+ # Broadcast b values to match a's structure - PRESERVE a's structure
70
+ puts " -> Broadcasting b (parent) to match a (child) structure" if ENV["DEBUG_CASCADE"]
71
+ broadcast_parent_to_child_structure(a, b)
72
+ else
73
+ # b is deeper (child level), a is shallower (parent level)
74
+ # Broadcast a values to match b's structure - PRESERVE b's structure
75
+ puts " -> Broadcasting a (parent) to match b (child) structure" if ENV["DEBUG_CASCADE"]
76
+ broadcast_parent_to_child_structure(b, a)
77
+ end
78
+ end
79
+
80
+ def self.broadcast_parent_to_child_structure(child_array, parent_array)
81
+ # Broadcast parent array values to match child array structure, preserving child structure
82
+ if ENV["DEBUG_CASCADE"]
83
+ puts " broadcast_parent_to_child_structure:"
84
+ puts " child_array: #{child_array.inspect}"
85
+ puts " parent_array: #{parent_array.inspect}"
86
+ puts " child depth: #{array_depth(child_array)}, parent depth: #{array_depth(parent_array)}"
87
+ end
88
+
89
+ # Use child array structure as template and broadcast parent values
90
+ map_with_parent_broadcasting(child_array, parent_array, [])
91
+ end
92
+
93
+ def self.map_with_parent_broadcasting(child_structure, parent_structure, indices)
94
+ if child_structure.is_a?(Array)
95
+ child_structure.map.with_index do |child_elem, index|
96
+ new_indices = indices + [index]
97
+
98
+ # Navigate parent structure with fewer indices (broadcasting)
99
+ parent_depth = array_depth(parent_structure)
100
+ parent_indices = new_indices[0, parent_depth]
101
+ parent_value = navigate_indices(parent_structure, parent_indices)
102
+
103
+ if child_elem.is_a?(Array)
104
+ # Recurse deeper into child structure
105
+ map_with_parent_broadcasting(child_elem, parent_structure, new_indices)
106
+ else
107
+ # Leaf level - apply AND operation
108
+ result = child_elem && parent_value
109
+ if ENV["DEBUG_CASCADE"]
110
+ puts " Leaf: child=#{child_elem}, parent=#{parent_value} (indices #{new_indices.inspect}) -> #{result}"
111
+ end
112
+ result
113
+ end
114
+ end
115
+ else
116
+ # Non-array child - just AND with parent
117
+ child_structure && parent_structure
118
+ end
119
+ end
120
+
121
+ def self.navigate_indices(structure, indices)
122
+ return structure if indices.empty?
123
+ return structure unless structure.is_a?(Array)
124
+ return nil if indices.first >= structure.length
125
+
126
+ navigate_indices(structure[indices.first], indices[1..])
127
+ end
128
+
129
+ def self.broadcast_to_match_structure(child_array, parent_array)
130
+ # Legacy method - keeping for backward compatibility
131
+ if ENV["DEBUG_CASCADE"]
132
+ puts " broadcast_to_match_structure (LEGACY):"
133
+ puts " child_array: #{child_array.inspect}"
134
+ puts " parent_array: #{parent_array.inspect}"
135
+ puts " child_array.length: #{child_array.length}"
136
+ puts " parent_array.length: #{parent_array.length}"
137
+ end
138
+
139
+ result = child_array.zip(parent_array).map do |child_elem, parent_elem|
140
+ puts " Combining child_elem: #{child_elem.inspect} with parent_elem: #{parent_elem.inspect}" if ENV["DEBUG_CASCADE"]
141
+ element_wise_and(child_elem, parent_elem)
142
+ end
143
+
144
+ puts " broadcast result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
145
+ result
146
+ end
147
+
8
148
  def self.definitions
9
149
  {
10
150
  # Basic logical operations
@@ -35,7 +175,37 @@ module Kumi
35
175
  # Collection logical operations
36
176
  all?: FunctionBuilder.collection_unary(:all?, "Check if all elements in collection are truthy", :all?),
37
177
  any?: FunctionBuilder.collection_unary(:any?, "Check if any element in collection is truthy", :any?),
38
- none?: FunctionBuilder.collection_unary(:none?, "Check if no elements in collection are truthy", :none?)
178
+ none?: FunctionBuilder.collection_unary(:none?, "Check if no elements in collection are truthy", :none?),
179
+
180
+ # Element-wise AND for cascades - works on arrays with same structure
181
+ cascade_and: FunctionBuilder::Entry.new(
182
+ fn: lambda do |*conditions|
183
+ if ENV["DEBUG_CASCADE"]
184
+ puts "DEBUG cascade_and called with #{conditions.length} conditions:"
185
+ conditions.each_with_index do |cond, i|
186
+ puts " condition[#{i}]: #{cond.inspect}"
187
+ end
188
+ end
189
+
190
+ return false if conditions.empty?
191
+ return conditions.first if conditions.length == 1
192
+
193
+ # Element-wise AND for arrays with same nested structure
194
+ result = conditions.first
195
+ conditions[1..].each_with_index do |condition, i|
196
+ puts " Combining result with condition[#{i + 1}]" if ENV["DEBUG_CASCADE"]
197
+ result = LogicalFunctions.element_wise_and(result, condition)
198
+ puts " Result after combining: #{result.inspect}" if ENV["DEBUG_CASCADE"]
199
+ end
200
+
201
+ puts " Final cascade_and result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
202
+ result
203
+ end,
204
+ arity: -1,
205
+ param_types: [:boolean],
206
+ return_type: :boolean,
207
+ description: "Element-wise AND for arrays with same nested structure"
208
+ )
39
209
  }
40
210
  end
41
211
  end