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,148 @@
|
|
1
|
+
# JavaScript Transpiler
|
2
|
+
|
3
|
+
Transpiles compiled schemas to standalone JavaScript code.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
### Export Schema
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class TaxCalculator
|
11
|
+
extend Kumi::Schema
|
12
|
+
|
13
|
+
schema do
|
14
|
+
input do
|
15
|
+
float :income
|
16
|
+
string :filing_status
|
17
|
+
end
|
18
|
+
|
19
|
+
trait :single, input.filing_status == "single"
|
20
|
+
|
21
|
+
value :std_deduction do
|
22
|
+
on single, 14_600
|
23
|
+
base 29_200
|
24
|
+
end
|
25
|
+
|
26
|
+
value :taxable_income, fn(:max, [input.income - std_deduction, 0])
|
27
|
+
value :tax_owed, taxable_income * 0.22
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Kumi::Js.export_to_file(TaxCalculator, "tax-calculator.js")
|
32
|
+
```
|
33
|
+
|
34
|
+
### Use in JavaScript
|
35
|
+
|
36
|
+
```javascript
|
37
|
+
const { schema } = require('./tax-calculator.js');
|
38
|
+
|
39
|
+
const taxpayer = {
|
40
|
+
income: 75000,
|
41
|
+
filing_status: "single"
|
42
|
+
};
|
43
|
+
|
44
|
+
const calculator = schema.from(taxpayer);
|
45
|
+
console.log(calculator.fetch('tax_owed'));
|
46
|
+
|
47
|
+
const results = calculator.slice('taxable_income', 'tax_owed');
|
48
|
+
```
|
49
|
+
|
50
|
+
## Export Methods
|
51
|
+
|
52
|
+
### Command Line
|
53
|
+
|
54
|
+
```bash
|
55
|
+
bundle exec kumi --export-js output.js SchemaClass
|
56
|
+
```
|
57
|
+
|
58
|
+
### Programmatic
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
Kumi::Js.export_to_file(MySchema, "schema.js")
|
62
|
+
|
63
|
+
js_code = Kumi::Js.compile(MySchema)
|
64
|
+
File.write("output.js", js_code)
|
65
|
+
```
|
66
|
+
|
67
|
+
## JavaScript API
|
68
|
+
|
69
|
+
### schema.from(input)
|
70
|
+
|
71
|
+
Creates runner instance.
|
72
|
+
|
73
|
+
```javascript
|
74
|
+
const runner = schema.from({ income: 50000, status: "single" });
|
75
|
+
```
|
76
|
+
|
77
|
+
### runner.fetch(key)
|
78
|
+
|
79
|
+
Returns computed value. Results are cached.
|
80
|
+
|
81
|
+
```javascript
|
82
|
+
const tax = runner.fetch('tax_owed');
|
83
|
+
```
|
84
|
+
|
85
|
+
### runner.slice(...keys)
|
86
|
+
|
87
|
+
Returns multiple values.
|
88
|
+
|
89
|
+
```javascript
|
90
|
+
const results = runner.slice('taxable_income', 'tax_owed');
|
91
|
+
// Returns: { taxable_income: 35400, tax_owed: 7788 }
|
92
|
+
```
|
93
|
+
|
94
|
+
### runner.functionsUsed
|
95
|
+
|
96
|
+
Array of functions used by the schema.
|
97
|
+
|
98
|
+
```javascript
|
99
|
+
console.log(runner.functionsUsed); // ["max", "subtract", "multiply"]
|
100
|
+
```
|
101
|
+
|
102
|
+
## Function Optimization
|
103
|
+
|
104
|
+
The transpiler only includes functions actually used by the schema.
|
105
|
+
|
106
|
+
Example schema using 4 functions generates ~3 KB instead of ~8 KB with all 67 functions.
|
107
|
+
|
108
|
+
## Browser Compatibility
|
109
|
+
|
110
|
+
- ES6+ (Chrome 60+, Firefox 55+, Safari 10+)
|
111
|
+
- Modern bundlers (Webpack, Rollup, Vite)
|
112
|
+
- Node.js 12+
|
113
|
+
|
114
|
+
## Limitations
|
115
|
+
|
116
|
+
- No `explain()` method (Ruby only)
|
117
|
+
- Custom Ruby functions need JavaScript equivalents
|
118
|
+
|
119
|
+
## Module Formats
|
120
|
+
|
121
|
+
Generated JavaScript supports:
|
122
|
+
- CommonJS (`require()`)
|
123
|
+
- ES Modules (`import`)
|
124
|
+
- Global variables (browser)
|
125
|
+
|
126
|
+
## Minification
|
127
|
+
|
128
|
+
Use production minifiers like Terser or UglifyJS for smaller bundles.
|
129
|
+
|
130
|
+
## Dual Mode Validation
|
131
|
+
|
132
|
+
Set `KUMI_DUAL_MODE=true` to automatically execute both Ruby and JavaScript versions and validate they produce identical results:
|
133
|
+
|
134
|
+
```bash
|
135
|
+
KUMI_DUAL_MODE=true ruby my_script.rb
|
136
|
+
```
|
137
|
+
|
138
|
+
Every calculation is validated in real-time. Mismatches throw detailed error reports with both results for debugging.
|
139
|
+
|
140
|
+
## Error Handling
|
141
|
+
|
142
|
+
```javascript
|
143
|
+
try {
|
144
|
+
const runner = schema.from({ invalid: "data" });
|
145
|
+
} catch (error) {
|
146
|
+
console.error(error.message);
|
147
|
+
}
|
148
|
+
```
|
data/docs/schema_metadata.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Schema Metadata
|
2
2
|
|
3
|
-
Kumi's SchemaMetadata interface
|
3
|
+
Kumi's SchemaMetadata interface accesses analyzed schema information for building external tools like form generators, documentation systems, and analysis utilities.
|
4
4
|
|
5
5
|
## Primary Interface
|
6
6
|
|
@@ -10,7 +10,7 @@ SchemaMetadata is the main interface for extracting metadata from Kumi schemas:
|
|
10
10
|
metadata = MySchema.schema_metadata
|
11
11
|
```
|
12
12
|
|
13
|
-
See the
|
13
|
+
See the API documentation in the SchemaMetadata class for method documentation, examples, and usage patterns.
|
14
14
|
|
15
15
|
## Processed Metadata (Tool-Friendly)
|
16
16
|
|
@@ -83,24 +83,24 @@ metadata.values
|
|
83
83
|
# }
|
84
84
|
```
|
85
85
|
|
86
|
-
###
|
86
|
+
### Public Interface Examples
|
87
87
|
```ruby
|
88
|
-
# Processed dependency information
|
88
|
+
# Processed dependency information
|
89
89
|
metadata.dependencies
|
90
90
|
# => { :tax_amount => [{ to: :income, conditional: false }, { to: :tax_rate, conditional: false }] }
|
91
91
|
|
92
|
-
# Processed declaration metadata
|
92
|
+
# Processed declaration metadata
|
93
93
|
metadata.declarations
|
94
94
|
# => { :adult => { type: :trait, expression: ">=(input.age, 18)" }, :tax_amount => { type: :value, expression: "multiply(input.income, tax_rate)" } }
|
95
95
|
|
96
|
-
# Type inference results
|
96
|
+
# Type inference results
|
97
97
|
metadata.inferred_types
|
98
98
|
# => { :adult => :boolean, :tax_amount => :float, :item_totals => { array: :float } }
|
99
99
|
```
|
100
100
|
|
101
101
|
### Raw Analyzer State (Advanced Usage)
|
102
102
|
```ruby
|
103
|
-
#
|
103
|
+
# Raw state hash with internal objects (AST nodes, Edge objects)
|
104
104
|
metadata.analyzer_state
|
105
105
|
# => { declarations: {AST nodes...}, dependencies: {Edge objects...}, ... }
|
106
106
|
```
|
data/examples/game_of_life.rb
CHANGED
@@ -1,19 +1,17 @@
|
|
1
|
-
$LOAD_PATH.unshift(File.join(__dir__, "..", "lib"))
|
2
1
|
require "kumi"
|
3
2
|
|
4
|
-
NEIGHBOR_DELTAS = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
|
5
3
|
begin
|
6
4
|
# in a block so we dont define this globally
|
7
5
|
def neighbor_cells_sum_method(cells, row, col, height, width)
|
8
6
|
# Calculate neighbor indices with wraparound
|
9
|
-
|
7
|
+
[[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]].map do |dr, dc|
|
10
8
|
neighbor_row = (row + dr) % height
|
11
9
|
neighbor_col = (col + dc) % width
|
12
10
|
neighbor_index = (neighbor_row * width) + neighbor_col
|
13
11
|
cells[neighbor_index]
|
14
12
|
end.sum
|
15
13
|
end
|
16
|
-
Kumi::
|
14
|
+
Kumi::Registry.register_with_metadata(:neighbor_cells_sum, method(:neighbor_cells_sum_method),
|
17
15
|
return_type: :integer, arity: 5,
|
18
16
|
param_types: %i[array integer integer integer integer],
|
19
17
|
description: "Get neighbor cells for Conway's Game of Life")
|
data/lib/kumi/analyzer.rb
CHANGED
@@ -4,8 +4,6 @@ module Kumi
|
|
4
4
|
module Analyzer
|
5
5
|
Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
|
6
6
|
|
7
|
-
module_function
|
8
|
-
|
9
7
|
DEFAULT_PASSES = [
|
10
8
|
Core::Analyzer::Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
|
11
9
|
Core::Analyzer::Passes::InputCollector, # 2. Collects field metadata from input declarations.
|
data/lib/kumi/compiler.rb
CHANGED
@@ -2,152 +2,18 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
# Compiles an analyzed schema into executable lambdas
|
5
|
-
class Compiler
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
->(_ctx) { v }
|
11
|
-
end
|
12
|
-
|
13
|
-
def compile_field_node(expr)
|
14
|
-
compile_field(expr)
|
15
|
-
end
|
16
|
-
|
17
|
-
def compile_element_field_reference(expr)
|
18
|
-
path = expr.path
|
19
|
-
|
20
|
-
lambda do |ctx|
|
21
|
-
# Start with the top-level collection from the context.
|
22
|
-
collection = ctx[path.first]
|
23
|
-
|
24
|
-
# Recursively map over the nested collections.
|
25
|
-
# The `dig_and_map` helper will handle any level of nesting.
|
26
|
-
dig_and_map(collection, path[1..])
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def compile_binding_node(expr)
|
31
|
-
name = expr.name
|
32
|
-
# Handle forward references in cycles by deferring binding lookup to runtime
|
33
|
-
lambda do |ctx|
|
34
|
-
fn = @bindings[name].last
|
35
|
-
fn.call(ctx)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def compile_list(expr)
|
40
|
-
fns = expr.elements.map { |e| compile_expr(e) }
|
41
|
-
->(ctx) { fns.map { |fn| fn.call(ctx) } }
|
42
|
-
end
|
43
|
-
|
44
|
-
def compile_call(expr)
|
45
|
-
fn_name = expr.fn_name
|
46
|
-
arg_fns = expr.args.map { |a| compile_expr(a) }
|
47
|
-
|
48
|
-
# Check if this is a vectorized operation
|
49
|
-
if vectorized_operation?(expr)
|
50
|
-
->(ctx) { invoke_vectorized_function(fn_name, arg_fns, ctx, expr.loc) }
|
51
|
-
else
|
52
|
-
->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def compile_cascade(expr)
|
57
|
-
# Check if current declaration is vectorized
|
58
|
-
broadcast_meta = @analysis.state[:broadcasts]
|
59
|
-
is_vectorized = @current_declaration && broadcast_meta&.dig(:vectorized_operations, @current_declaration)
|
60
|
-
|
61
|
-
# For vectorized cascades, we need to transform conditions that use all?
|
62
|
-
pairs = if is_vectorized
|
63
|
-
expr.cases.map do |c|
|
64
|
-
condition_fn = transform_vectorized_condition(c.condition)
|
65
|
-
result_fn = compile_expr(c.result)
|
66
|
-
[condition_fn, result_fn]
|
67
|
-
end
|
68
|
-
else
|
69
|
-
expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
|
70
|
-
end
|
71
|
-
|
72
|
-
if is_vectorized
|
73
|
-
lambda do |ctx|
|
74
|
-
# This cascade can be vectorized - check if we actually need to at runtime
|
75
|
-
# Evaluate all conditions and results to check for arrays
|
76
|
-
cond_results = pairs.map { |cond, _res| cond.call(ctx) }
|
77
|
-
res_results = pairs.map { |_cond, res| res.call(ctx) }
|
78
|
-
|
79
|
-
# Check if any conditions or results are arrays (vectorized)
|
80
|
-
has_vectorized_data = (cond_results + res_results).any?(Array)
|
81
|
-
|
82
|
-
if has_vectorized_data
|
83
|
-
# Apply element-wise cascade evaluation
|
84
|
-
array_length = cond_results.find { |v| v.is_a?(Array) }&.length ||
|
85
|
-
res_results.find { |v| v.is_a?(Array) }&.length || 1
|
86
|
-
|
87
|
-
(0...array_length).map do |i|
|
88
|
-
pairs.each_with_index do |(_cond, _res), pair_idx|
|
89
|
-
cond_val = cond_results[pair_idx].is_a?(Array) ? cond_results[pair_idx][i] : cond_results[pair_idx]
|
90
|
-
|
91
|
-
if cond_val
|
92
|
-
res_val = res_results[pair_idx].is_a?(Array) ? res_results[pair_idx][i] : res_results[pair_idx]
|
93
|
-
break res_val
|
94
|
-
end
|
95
|
-
end || nil
|
96
|
-
end
|
97
|
-
else
|
98
|
-
# All data is scalar - use regular cascade evaluation
|
99
|
-
pairs.each_with_index do |(_cond, _res), pair_idx|
|
100
|
-
return res_results[pair_idx] if cond_results[pair_idx]
|
101
|
-
end
|
102
|
-
nil
|
103
|
-
end
|
104
|
-
end
|
105
|
-
else
|
106
|
-
lambda do |ctx|
|
107
|
-
pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
|
108
|
-
nil
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def transform_vectorized_condition(condition_expr)
|
114
|
-
# If this is fn(:all?, [trait_ref]), extract the trait_ref for vectorized cascades
|
115
|
-
if condition_expr.is_a?(Kumi::Syntax::CallExpression) &&
|
116
|
-
condition_expr.fn_name == :all? &&
|
117
|
-
condition_expr.args.length == 1
|
118
|
-
|
119
|
-
arg = condition_expr.args.first
|
120
|
-
if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
|
121
|
-
trait_ref = arg.elements.first
|
122
|
-
return compile_expr(trait_ref)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# Otherwise compile normally
|
127
|
-
compile_expr(condition_expr)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
include ExprCompilers
|
132
|
-
|
133
|
-
# Map node classes to compiler methods
|
134
|
-
DISPATCH = {
|
135
|
-
Kumi::Syntax::Literal => :compile_literal,
|
136
|
-
Kumi::Syntax::InputReference => :compile_field_node,
|
137
|
-
Kumi::Syntax::InputElementReference => :compile_element_field_reference,
|
138
|
-
Kumi::Syntax::DeclarationReference => :compile_binding_node,
|
139
|
-
Kumi::Syntax::ArrayExpression => :compile_list,
|
140
|
-
Kumi::Syntax::CallExpression => :compile_call,
|
141
|
-
Kumi::Syntax::CascadeExpression => :compile_cascade
|
142
|
-
}.freeze
|
5
|
+
class Compiler < Core::CompilerBase
|
6
|
+
include Kumi::Core::Compiler::ReferenceCompiler
|
7
|
+
include Kumi::Core::Compiler::PathTraversalCompiler
|
8
|
+
include Kumi::Core::Compiler::ExpressionCompiler
|
9
|
+
include Kumi::Core::Compiler::FunctionInvoker
|
143
10
|
|
144
11
|
def self.compile(schema, analyzer:)
|
145
12
|
new(schema, analyzer).compile
|
146
13
|
end
|
147
14
|
|
148
15
|
def initialize(schema, analyzer)
|
149
|
-
|
150
|
-
@analysis = analyzer
|
16
|
+
super
|
151
17
|
@bindings = {}
|
152
18
|
end
|
153
19
|
|
@@ -160,140 +26,5 @@ module Kumi
|
|
160
26
|
|
161
27
|
Core::CompiledSchema.new(@bindings.freeze)
|
162
28
|
end
|
163
|
-
|
164
|
-
private
|
165
|
-
|
166
|
-
def build_index
|
167
|
-
@index = {}
|
168
|
-
@schema.attributes.each { |a| @index[a.name] = a }
|
169
|
-
@schema.traits.each { |t| @index[t.name] = t }
|
170
|
-
end
|
171
|
-
|
172
|
-
def dig_and_map(collection, path_segments)
|
173
|
-
return collection unless collection.is_a?(Array)
|
174
|
-
|
175
|
-
current_segment = path_segments.first
|
176
|
-
remaining_segments = path_segments[1..]
|
177
|
-
|
178
|
-
collection.map do |element|
|
179
|
-
value = element[current_segment]
|
180
|
-
|
181
|
-
# If there are more segments, recurse. Otherwise, return the value.
|
182
|
-
if remaining_segments.empty?
|
183
|
-
value
|
184
|
-
else
|
185
|
-
dig_and_map(value, remaining_segments)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def compile_declaration(decl)
|
191
|
-
@current_declaration = decl.name
|
192
|
-
kind = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? :trait : :attr
|
193
|
-
fn = compile_expr(decl.expression)
|
194
|
-
@bindings[decl.name] = [kind, fn]
|
195
|
-
@current_declaration = nil
|
196
|
-
end
|
197
|
-
|
198
|
-
# Dispatch to the appropriate compile_* method
|
199
|
-
def compile_expr(expr)
|
200
|
-
method = DISPATCH.fetch(expr.class)
|
201
|
-
send(method, expr)
|
202
|
-
end
|
203
|
-
|
204
|
-
def compile_field(node)
|
205
|
-
name = node.name
|
206
|
-
loc = node.loc
|
207
|
-
lambda do |ctx|
|
208
|
-
return ctx[name] if ctx.respond_to?(:key?) && ctx.key?(name)
|
209
|
-
|
210
|
-
raise Errors::RuntimeError,
|
211
|
-
"Key '#{name}' not found at #{loc}. Available: #{ctx.respond_to?(:keys) ? ctx.keys.join(', ') : 'N/A'}"
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
def vectorized_operation?(expr)
|
216
|
-
# Check if this operation uses vectorized inputs
|
217
|
-
broadcast_meta = @analysis.state[:broadcasts]
|
218
|
-
return false unless broadcast_meta
|
219
|
-
|
220
|
-
# Reduction functions are NOT vectorized operations - they consume arrays
|
221
|
-
return false if Kumi::Registry.reducer?(expr.fn_name)
|
222
|
-
|
223
|
-
expr.args.any? do |arg|
|
224
|
-
case arg
|
225
|
-
when Kumi::Syntax::InputElementReference
|
226
|
-
broadcast_meta[:array_fields]&.key?(arg.path.first)
|
227
|
-
when Kumi::Syntax::DeclarationReference
|
228
|
-
broadcast_meta[:vectorized_operations]&.key?(arg.name)
|
229
|
-
else
|
230
|
-
false
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
def invoke_vectorized_function(name, arg_fns, ctx, loc)
|
236
|
-
# Evaluate arguments
|
237
|
-
values = arg_fns.map { |fn| fn.call(ctx) }
|
238
|
-
|
239
|
-
# Check if any argument is vectorized (array)
|
240
|
-
has_vectorized_args = values.any?(Array)
|
241
|
-
|
242
|
-
if has_vectorized_args
|
243
|
-
# Apply function with broadcasting to all vectorized arguments
|
244
|
-
vectorized_function_call(name, values)
|
245
|
-
else
|
246
|
-
# All arguments are scalars - regular function call
|
247
|
-
fn = Kumi::Registry.fetch(name)
|
248
|
-
fn.call(*values)
|
249
|
-
end
|
250
|
-
rescue StandardError => e
|
251
|
-
enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
|
252
|
-
runtime_error = Errors::RuntimeError.new(enhanced_message)
|
253
|
-
runtime_error.set_backtrace(e.backtrace)
|
254
|
-
runtime_error.define_singleton_method(:cause) { e }
|
255
|
-
raise runtime_error
|
256
|
-
end
|
257
|
-
|
258
|
-
def vectorized_function_call(fn_name, values)
|
259
|
-
# Get the function from registry
|
260
|
-
fn = Kumi::Registry.fetch(fn_name)
|
261
|
-
|
262
|
-
# Find array dimensions for broadcasting
|
263
|
-
array_values = values.select { |v| v.is_a?(Array) }
|
264
|
-
return fn.call(*values) if array_values.empty?
|
265
|
-
|
266
|
-
# All arrays should have the same length (validation could be added)
|
267
|
-
array_length = array_values.first.size
|
268
|
-
|
269
|
-
# Broadcast and apply function element-wise
|
270
|
-
(0...array_length).map do |i|
|
271
|
-
element_args = values.map do |v|
|
272
|
-
v.is_a?(Array) ? v[i] : v # Broadcast scalars
|
273
|
-
end
|
274
|
-
fn.call(*element_args)
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
def invoke_function(name, arg_fns, ctx, loc)
|
279
|
-
fn = Kumi::Registry.fetch(name)
|
280
|
-
values = arg_fns.map { |fn| fn.call(ctx) }
|
281
|
-
fn.call(*values)
|
282
|
-
rescue StandardError => e
|
283
|
-
# Preserve original error class and backtrace while adding context
|
284
|
-
enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
|
285
|
-
|
286
|
-
if e.is_a?(Kumi::Core::Errors::Error)
|
287
|
-
# Re-raise Kumi errors with enhanced message but preserve type
|
288
|
-
e.define_singleton_method(:message) { enhanced_message }
|
289
|
-
raise e
|
290
|
-
else
|
291
|
-
# For non-Kumi errors, wrap in RuntimeError but preserve original error info
|
292
|
-
runtime_error = Errors::RuntimeError.new(enhanced_message)
|
293
|
-
runtime_error.set_backtrace(e.backtrace)
|
294
|
-
runtime_error.define_singleton_method(:cause) { e }
|
295
|
-
raise runtime_error
|
296
|
-
end
|
297
|
-
end
|
298
29
|
end
|
299
30
|
end
|