kumi 0.0.9 → 0.0.11
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/CHANGELOG.md +18 -0
- data/CLAUDE.md +18 -258
- data/README.md +188 -121
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/VECTOR_SEMANTICS.md +286 -0
- 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/features/s-expression-printer.md +2 -2
- data/docs/schema_metadata.md +7 -7
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +34 -14
- data/lib/kumi/compiler.rb +4 -283
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +137 -0
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -53
- data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +138 -98
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/errors.rb +2 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +17 -22
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +15 -4
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +17 -16
- 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/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +33 -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 +6 -15
- 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 +36 -9
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/schema_instance.rb +0 -111
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
- data/migrate_to_core_iterative.rb +0 -938
@@ -0,0 +1,349 @@
|
|
1
|
+
# Hierarchical Broadcasting
|
2
|
+
|
3
|
+
Automatic vectorization of operations over hierarchical data structures with two complementary access modes for different use cases.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The hierarchical broadcasting system enables natural field access syntax that automatically applies operations element-wise across complex nested data structures, with intelligent detection of map vs reduce operations and support for both structured objects and raw multi-dimensional arrays.
|
8
|
+
|
9
|
+
## Core Mechanism
|
10
|
+
|
11
|
+
The system uses a three-stage pipeline:
|
12
|
+
|
13
|
+
1. **Parser** - Creates InputElementReference AST nodes for nested field access
|
14
|
+
2. **BroadcastDetector** - Identifies which operations should be vectorized vs scalar
|
15
|
+
3. **Compiler** - Generates appropriate map/reduce functions based on usage context
|
16
|
+
|
17
|
+
## Access Modes
|
18
|
+
|
19
|
+
The system supports two complementary access modes that can be mixed within the same schema:
|
20
|
+
|
21
|
+
### Object Access Mode (Default)
|
22
|
+
For structured business data with named fields:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
input do
|
26
|
+
array :users do
|
27
|
+
string :name
|
28
|
+
integer :age
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
value :user_names, input.users.name # Broadcasts over structured objects
|
33
|
+
trait :adults, input.users.age >= 18 # Boolean array from age comparison
|
34
|
+
```
|
35
|
+
|
36
|
+
### Element Access Mode
|
37
|
+
For multi-dimensional raw arrays using `element` syntax with **progressive path traversal**:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
input do
|
41
|
+
array :cube do
|
42
|
+
element :array, :layer do
|
43
|
+
element :array, :row do
|
44
|
+
element :integer, :cell
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Progressive path traversal - each level goes deeper
|
51
|
+
value :dimensions, [
|
52
|
+
fn(:size, input.cube), # Number of layers
|
53
|
+
fn(:size, input.cube.layer), # Total matrices across all layers
|
54
|
+
fn(:size, input.cube.layer.row), # Total rows across all matrices
|
55
|
+
fn(:size, input.cube.layer.row.cell) # Total cells (leaf elements)
|
56
|
+
]
|
57
|
+
|
58
|
+
# Operations at different dimensional levels
|
59
|
+
value :layer_data, input.cube.layer # 3D matrices
|
60
|
+
value :row_data, input.cube.layer.row # 2D rows
|
61
|
+
value :cell_data, input.cube.layer.row.cell # 1D values
|
62
|
+
```
|
63
|
+
|
64
|
+
**Key Benefits of Progressive Path Traversal:**
|
65
|
+
- **Intuitive syntax**: `input.cube.layer.row` naturally represents "rows within layers within cube"
|
66
|
+
- **Direct dimensional access**: No need for complex flattening operations for basic dimensional analysis
|
67
|
+
- **Ranked polymorphism**: Same operations work across different dimensional arrays
|
68
|
+
- **Clean code**: `fn(:size, input.cube.layer.row)` instead of `fn(:size, fn(:flatten_one, input.cube.layer))`
|
69
|
+
|
70
|
+
## Business Use Cases
|
71
|
+
|
72
|
+
Element access mode is essential for common business scenarios involving simple nested arrays:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
# E-commerce: Product availability analysis
|
76
|
+
input do
|
77
|
+
array :products do
|
78
|
+
string :name
|
79
|
+
array :daily_sales do # Simple array: [12, 8, 15, 3]
|
80
|
+
element :integer, :units
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create flags for business rules
|
86
|
+
trait :had_slow_days, fn(:any?, input.products.daily_sales.units < 5)
|
87
|
+
trait :consistently_strong, fn(:all?, input.products.daily_sales.units >= 10)
|
88
|
+
|
89
|
+
# Progressive analysis
|
90
|
+
value :sales_summary, [
|
91
|
+
fn(:size, input.products), # Number of products
|
92
|
+
fn(:size, input.products.daily_sales.units), # Total sales data points
|
93
|
+
fn(:sum, fn(:flatten, input.products.daily_sales.units)) # Total units sold
|
94
|
+
]
|
95
|
+
```
|
96
|
+
|
97
|
+
### Mixed Access Modes
|
98
|
+
Both modes work together seamlessly:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
input do
|
102
|
+
array :users do # Object access
|
103
|
+
string :name
|
104
|
+
integer :age
|
105
|
+
end
|
106
|
+
array :activity_logs do # Element access
|
107
|
+
element :integer, :day
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
value :user_count, fn(:size, input.users)
|
112
|
+
value :total_activity_days, fn(:size, fn(:flatten, input.activity_logs.day))
|
113
|
+
```
|
114
|
+
|
115
|
+
## Basic Broadcasting (Object Access)
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
schema do
|
119
|
+
input do
|
120
|
+
array :line_items do
|
121
|
+
float :price
|
122
|
+
integer :quantity
|
123
|
+
string :category
|
124
|
+
end
|
125
|
+
float :tax_rate, type: :float
|
126
|
+
end
|
127
|
+
|
128
|
+
# Element-wise computation - broadcasts over each item
|
129
|
+
value :subtotals, input.line_items.price * input.line_items.quantity
|
130
|
+
|
131
|
+
# Element-wise traits - applied to each item
|
132
|
+
trait :is_taxable, (input.line_items.category != "digital")
|
133
|
+
|
134
|
+
# Conditional logic - element-wise evaluation
|
135
|
+
value :taxes, fn(:if, is_taxable, subtotals * input.tax_rate, 0.0)
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
## Aggregation Operations
|
140
|
+
|
141
|
+
Operations that consume arrays to produce scalars are automatically detected:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
schema do
|
145
|
+
# These aggregate the vectorized results
|
146
|
+
value :total_subtotal, fn(:sum, subtotals)
|
147
|
+
value :total_tax, fn(:sum, taxes)
|
148
|
+
value :grand_total, total_subtotal + total_tax
|
149
|
+
|
150
|
+
# Statistics over arrays
|
151
|
+
value :avg_price, fn(:avg, input.line_items.price)
|
152
|
+
value :max_quantity, fn(:max, input.line_items.quantity)
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
## Field Access Nesting
|
157
|
+
|
158
|
+
Supports arbitrary depth field access with path building:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
schema do
|
162
|
+
input do
|
163
|
+
array :orders do
|
164
|
+
array :items do
|
165
|
+
hash :product do
|
166
|
+
string :name
|
167
|
+
float :base_price
|
168
|
+
end
|
169
|
+
integer :quantity
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Deep field access - automatically broadcasts over nested arrays
|
175
|
+
value :all_product_names, input.orders.items.product.name
|
176
|
+
value :total_values, input.orders.items.product.base_price * input.orders.items.quantity
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
## Type Inference
|
181
|
+
|
182
|
+
The type system automatically infers appropriate types for broadcasted operations:
|
183
|
+
|
184
|
+
- `input.items.price` (float array) → inferred as `:float` per element
|
185
|
+
- `input.items.price * input.items.quantity` → element-wise `:float` result
|
186
|
+
- `fn(:sum, input.items.price)` → scalar `:float` result
|
187
|
+
|
188
|
+
## Implementation Details
|
189
|
+
|
190
|
+
### Parser Layer
|
191
|
+
- **InputFieldProxy** - Handles `input.field.subfield...` with path building
|
192
|
+
- **InputElementReference** - AST node representing array field access paths
|
193
|
+
|
194
|
+
### Analysis Layer
|
195
|
+
- **BroadcastDetector** - Identifies vectorized vs scalar operations
|
196
|
+
- **TypeInferencerPass** - Infers types for array element access patterns
|
197
|
+
|
198
|
+
### Compilation Layer
|
199
|
+
- **Automatic Dispatch** - Maps element-wise operations to array map functions
|
200
|
+
- **Reduction Detection** - Converts aggregation functions to array reduce operations
|
201
|
+
|
202
|
+
## Usage Patterns
|
203
|
+
|
204
|
+
### Element-wise Operations
|
205
|
+
```ruby
|
206
|
+
# All of these broadcast element-wise
|
207
|
+
value :discounted_prices, input.items.price * 0.9
|
208
|
+
trait :expensive, (input.items.price > 100.0)
|
209
|
+
value :categories, input.items.category
|
210
|
+
```
|
211
|
+
|
212
|
+
### Aggregation Operations
|
213
|
+
```ruby
|
214
|
+
# These consume arrays to produce scalars
|
215
|
+
value :item_count, fn(:size, input.items)
|
216
|
+
value :total_price, fn(:sum, input.items.price)
|
217
|
+
value :has_expensive, fn(:any?, expensive)
|
218
|
+
```
|
219
|
+
|
220
|
+
### Mixed Operations
|
221
|
+
```ruby
|
222
|
+
# Element-wise computation followed by aggregation
|
223
|
+
value :line_totals, input.items.price * input.items.quantity
|
224
|
+
value :order_total, fn(:sum, line_totals)
|
225
|
+
value :avg_line_total, fn(:avg, line_totals)
|
226
|
+
```
|
227
|
+
|
228
|
+
### Conditional Aggregation Functions
|
229
|
+
|
230
|
+
Kumi provides powerful conditional aggregation functions that make working with vectorized traits clean and intuitive:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
# Step 1: Create vectorized values and traits
|
234
|
+
value :revenues, input.sales.price * input.sales.quantity # → [3000.0, 2000.0, 1250.0]
|
235
|
+
trait :expensive, input.sales.price > 100 # → [true, true, false]
|
236
|
+
trait :bulk_order, input.sales.quantity >= 15 # → [false, false, true]
|
237
|
+
|
238
|
+
# Step 2: Use conditional aggregation functions (NEW - clean and readable)
|
239
|
+
value :expensive_count, fn(:count_if, expensive) # → 2
|
240
|
+
value :expensive_total, fn(:sum_if, revenues, expensive) # → 5000.0
|
241
|
+
value :expensive_average, fn(:avg_if, revenues, expensive) # → 2500.0
|
242
|
+
|
243
|
+
# Step 3: Combine conditions for complex filtering
|
244
|
+
trait :expensive_bulk, expensive & bulk_order # → [false, false, false]
|
245
|
+
value :expensive_bulk_total, fn(:sum_if, revenues, expensive_bulk) # → 0.0
|
246
|
+
```
|
247
|
+
|
248
|
+
**Old cascade pattern** (verbose, harder to read):
|
249
|
+
```ruby
|
250
|
+
# OLD WAY - required verbose cascade patterns
|
251
|
+
value :expensive_amounts do
|
252
|
+
on expensive, revenues
|
253
|
+
base 0.0
|
254
|
+
end
|
255
|
+
value :expensive_total, fn(:sum, expensive_amounts)
|
256
|
+
|
257
|
+
value :expensive_count_markers do
|
258
|
+
on expensive, 1.0
|
259
|
+
base 0.0
|
260
|
+
end
|
261
|
+
value :expensive_count, fn(:sum, expensive_count_markers)
|
262
|
+
```
|
263
|
+
|
264
|
+
**New conditional functions** (clean, direct):
|
265
|
+
```ruby
|
266
|
+
# NEW WAY - direct and readable
|
267
|
+
value :expensive_total, fn(:sum_if, revenues, expensive)
|
268
|
+
value :expensive_count, fn(:count_if, expensive)
|
269
|
+
value :expensive_average, fn(:avg_if, revenues, expensive)
|
270
|
+
```
|
271
|
+
|
272
|
+
The conditional aggregation functions are now the idiomatic way to work with boolean arrays in Kumi.
|
273
|
+
|
274
|
+
## Common Patterns and Gotchas
|
275
|
+
|
276
|
+
### Operator Precedence with Mixed Arithmetic
|
277
|
+
|
278
|
+
**Problem**: Complex arithmetic expressions with arrays can fail due to precedence parsing:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
# This fails with confusing error message
|
282
|
+
value :ones, input.items.price * 0 + 1
|
283
|
+
# Error: "no implicit conversion of Integer into Array"
|
284
|
+
|
285
|
+
# The expression is parsed as: (input.items.price * 0) + 1
|
286
|
+
# Which becomes: [0.0, 0.0, 0.0] + 1 (array + scalar in wrong context)
|
287
|
+
```
|
288
|
+
|
289
|
+
**Solutions**:
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
# Use explicit function calls
|
293
|
+
value :ones, fn(:add, input.items.price * 0, 1)
|
294
|
+
|
295
|
+
# Use cascade pattern (most idiomatic)
|
296
|
+
trait :always_true, input.items.price >= 0
|
297
|
+
value :ones do
|
298
|
+
on always_true, 1.0
|
299
|
+
base 0.0
|
300
|
+
end
|
301
|
+
|
302
|
+
# Use separate steps
|
303
|
+
value :zeros, input.items.price * 0
|
304
|
+
value :ones, zeros + 1
|
305
|
+
```
|
306
|
+
|
307
|
+
The cascade pattern is preferred as it's more explicit about intent and leverages Kumi's automatic scalar broadcasting.
|
308
|
+
|
309
|
+
## Error Handling
|
310
|
+
|
311
|
+
### Dimension Mismatch Detection
|
312
|
+
|
313
|
+
Array broadcasting operations are only valid within the same array source. Attempting to broadcast across different arrays generates detailed error messages:
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
schema do
|
317
|
+
input do
|
318
|
+
array :items do
|
319
|
+
string :name
|
320
|
+
end
|
321
|
+
array :logs do
|
322
|
+
string :user_name
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# This will generate a dimension mismatch error
|
327
|
+
trait :same_name, input.items.name == input.logs.user_name
|
328
|
+
end
|
329
|
+
|
330
|
+
# Error:
|
331
|
+
# Cannot broadcast operation across arrays from different sources: items, logs.
|
332
|
+
# Problem: Multiple operands are arrays from different sources:
|
333
|
+
# - Operand 1 resolves to array(string) from array 'items'
|
334
|
+
# - Operand 2 resolves to array(string) from array 'logs'
|
335
|
+
# Direct operations on arrays from different sources is ambiguous and not supported.
|
336
|
+
# Vectorized operations can only work on fields from the same array input.
|
337
|
+
```
|
338
|
+
|
339
|
+
The error messages provide:
|
340
|
+
- **Quick Summary**: Identifies the conflicting array sources
|
341
|
+
- **Type Information**: Shows the resolved types of each operand
|
342
|
+
- **Clear Explanation**: Why the operation is ambiguous and not supported
|
343
|
+
|
344
|
+
## Performance Characteristics
|
345
|
+
|
346
|
+
- **Single Pass** - Each array is traversed once per computation chain
|
347
|
+
- **Lazy Evaluation** - Operations are composed into pipelines
|
348
|
+
- **Memory Efficient** - No intermediate array allocations for simple operations
|
349
|
+
- **Type Safe** - Full compile-time type checking for array element operations
|
@@ -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
|
+
```
|
@@ -42,7 +42,7 @@ The printer produces indented S-expressions that clearly show the hierarchical s
|
|
42
42
|
(InputDeclaration :age :integer)
|
43
43
|
(InputDeclaration :name :string)
|
44
44
|
]
|
45
|
-
|
45
|
+
values: [
|
46
46
|
(ValueDeclaration :greeting
|
47
47
|
(CallExpression :concat
|
48
48
|
(Literal "Hello ")
|
@@ -65,7 +65,7 @@ The printer produces indented S-expressions that clearly show the hierarchical s
|
|
65
65
|
|
66
66
|
The printer handles all Kumi AST node types:
|
67
67
|
|
68
|
-
- **Root** - Schema container with inputs,
|
68
|
+
- **Root** - Schema container with inputs, values, and traits
|
69
69
|
- **Declarations** - InputDeclaration, ValueDeclaration, TraitDeclaration
|
70
70
|
- **Expressions** - CallExpression, ArrayExpression, CascadeExpression, CaseExpression
|
71
71
|
- **References** - InputReference, InputElementReference, DeclarationReference
|
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
|
```
|
@@ -86,21 +86,27 @@ puts
|
|
86
86
|
# ------------------------------------------------------------------
|
87
87
|
Benchmark.ips do |x|
|
88
88
|
schemas.each do |d, schema|
|
89
|
-
|
90
|
-
|
89
|
+
# 1) HOT (memoized): expect ~flat, nanosecond-level if cached
|
90
|
+
hot = schema.from(seed: 0)
|
91
|
+
x.report("HOT fetch #{d}-deep") do
|
92
|
+
hot[:final_result]
|
93
|
+
end
|
94
|
+
|
95
|
+
# 2) COLD via UPDATE (no memoized result): change a dependent input each iter
|
96
|
+
upd = schema.from(seed: 0)
|
97
|
+
i = 0
|
98
|
+
x.report("COLD update #{d}-deep") do
|
99
|
+
i += 1
|
100
|
+
upd.update(seed: i) # invalidates v0..vN; forces recompute
|
101
|
+
upd[:final_result]
|
102
|
+
end
|
103
|
+
|
104
|
+
# 3) COLD new runner (includes construction)
|
105
|
+
prng = Random.new(42)
|
106
|
+
x.report("COLD new #{d}-deep") do
|
107
|
+
r = schema.from(seed: prng.rand(1_000_000))
|
108
|
+
r[:final_result]
|
109
|
+
end
|
91
110
|
end
|
92
111
|
x.compare!
|
93
112
|
end
|
94
|
-
# Warming up --------------------------------------
|
95
|
-
# eval 50-deep 222.000 i/100ms
|
96
|
-
# eval 100-deep 57.000 i/100ms
|
97
|
-
# eval 150-deep 26.000 i/100ms
|
98
|
-
# Calculating -------------------------------------
|
99
|
-
# eval 50-deep 2.166k (± 1.9%) i/s (461.70 μs/i) - 10.878k in 5.024320s
|
100
|
-
# eval 100-deep 561.698 (± 1.4%) i/s (1.78 ms/i) - 2.850k in 5.075057s
|
101
|
-
# eval 150-deep 253.732 (± 0.8%) i/s (3.94 ms/i) - 1.274k in 5.021499s
|
102
|
-
|
103
|
-
# Comparison:
|
104
|
-
# eval 50-deep: 2165.9 i/s
|
105
|
-
# eval 100-deep: 561.7 i/s - 3.86x slower
|
106
|
-
# eval 150-deep: 253.7 i/s - 8.54x slower
|
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")
|