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.
- checksums.yaml +4 -4
- data/CLAUDE.md +28 -44
- data/README.md +187 -120
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/schema_metadata.md +7 -7
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +0 -2
- data/lib/kumi/compiler.rb +6 -275
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
- data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
- data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
- data/lib/kumi/core/cascade_executor_builder.rb +132 -0
- data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
- data/lib/kumi/core/compiler/function_invoker.rb +55 -0
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
- data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
- data/lib/kumi/core/compiler_base.rb +137 -0
- data/lib/kumi/core/explain.rb +2 -2
- data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
- data/lib/kumi/core/function_registry/function_builder.rb +5 -3
- data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +32 -10
- data/lib/kumi/core/nested_structure_utils.rb +78 -0
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- data/lib/kumi/core/schema_instance.rb +4 -0
- data/lib/kumi/core/vectorized_function_builder.rb +88 -0
- data/lib/kumi/errors.rb +2 -0
- data/lib/kumi/js/compiler.rb +878 -0
- data/lib/kumi/js/function_registry.rb +333 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +61 -1
- data/lib/kumi/schema.rb +1 -1
- data/lib/kumi/support/s_expression_printer.rb +16 -15
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/node.rb +34 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +1 -1
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +20 -4
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- 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
|
data/lib/kumi/core/explain.rb
CHANGED
@@ -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:
|
13
|
-
|
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,
|
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
|