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
data/README.md
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Kumi is a Declarative logic and rules engine framework with static analysis for Ruby.
|
7
7
|
|
8
|
-
It
|
8
|
+
It handles complex, interdependent calculations with validation and consistency checking.
|
9
9
|
|
10
10
|
|
11
11
|
## What can you build?
|
@@ -59,14 +59,14 @@ result[:total_tax] # => 21,491.00
|
|
59
59
|
result[:after_tax] # => 78,509.00
|
60
60
|
```
|
61
61
|
|
62
|
-
Real tax calculation with brackets, deductions, and FICA caps.
|
62
|
+
Real tax calculation with brackets, deductions, and FICA caps.
|
63
63
|
|
64
|
-
|
64
|
+
Validation happens during schema definition.
|
65
65
|
|
66
66
|
## Installation
|
67
67
|
|
68
68
|
```bash
|
69
|
-
# Requires Ruby 3.
|
69
|
+
# Requires Ruby 3.1+
|
70
70
|
# No external dependencies
|
71
71
|
gem install kumi
|
72
72
|
```
|
@@ -74,7 +74,7 @@ gem install kumi
|
|
74
74
|
## Core Features
|
75
75
|
|
76
76
|
<details>
|
77
|
-
<summary><strong
|
77
|
+
<summary><strong>Schema Primitives</strong> - Four building blocks for business logic</summary>
|
78
78
|
|
79
79
|
### Schema Primitives
|
80
80
|
|
@@ -96,7 +96,7 @@ value :tax_rate, 0.08
|
|
96
96
|
value :tax_amount, subtotal * tax_rate
|
97
97
|
```
|
98
98
|
|
99
|
-
**Traits** are boolean conditions
|
99
|
+
**Traits** are boolean conditions for branching logic:
|
100
100
|
```ruby
|
101
101
|
trait :bulk_order, input.quantity >= 100
|
102
102
|
trait :premium_customer, input.tier == "premium"
|
@@ -109,7 +109,7 @@ value :discount do
|
|
109
109
|
end
|
110
110
|
```
|
111
111
|
|
112
|
-
**Functions**
|
112
|
+
**Functions** are computational building blocks:
|
113
113
|
|
114
114
|
```ruby
|
115
115
|
value :final_price, [subtotal - discount_amount, 0].max
|
@@ -120,9 +120,13 @@ Note: You can find a list all core functions in [docs/FUNCTIONS.md](docs/FUNCTIO
|
|
120
120
|
</details>
|
121
121
|
|
122
122
|
<details>
|
123
|
-
<summary><strong
|
123
|
+
<summary><strong>Analysis & Introspection</strong> - Static verification, runtime inspection, and metadata extraction</summary>
|
124
124
|
|
125
|
-
###
|
125
|
+
### Analysis & Introspection
|
126
|
+
|
127
|
+
Kumi provides comprehensive analysis capabilities - catching errors at definition time and exposing schema structure for tooling and debugging.
|
128
|
+
|
129
|
+
#### **Static Analysis: Catch Errors Early**
|
126
130
|
|
127
131
|
Kumi catches many types of business logic errors that cause runtime failures or silent bugs:
|
128
132
|
|
@@ -203,7 +207,7 @@ end
|
|
203
207
|
# ❌ Function arity error: divide expects 2 arguments, got 1
|
204
208
|
```
|
205
209
|
|
206
|
-
**
|
210
|
+
**Mutual Recursion**: Kumi supports mutual recursion when cascade conditions are mutually exclusive:
|
207
211
|
|
208
212
|
```ruby
|
209
213
|
trait :is_forward, input.operation == "forward"
|
@@ -212,7 +216,7 @@ trait :is_reverse, input.operation == "reverse"
|
|
212
216
|
# Safe mutual recursion - conditions are mutually exclusive
|
213
217
|
value :forward_processor do
|
214
218
|
on is_forward, input.value * 2 # Direct calculation
|
215
|
-
on is_reverse,
|
219
|
+
on is_reverse, reveAnalysisrse_processor + 10 # Delegates to reverse (safe)
|
216
220
|
base "invalid operation"
|
217
221
|
end
|
218
222
|
|
@@ -230,168 +234,225 @@ end
|
|
230
234
|
|
231
235
|
This compiles because `operation` can only be "forward" or "reverse", never both. Each recursion executes one step before hitting a direct calculation.
|
232
236
|
|
233
|
-
|
237
|
+
#### **Runtime Introspection: Debug and Understand**
|
234
238
|
|
235
|
-
|
236
|
-
<summary><strong>🔍 Array Broadcasting</strong> - Automatic vectorization over array fields</summary>
|
237
|
-
|
238
|
-
### Array Broadcasting
|
239
|
-
|
240
|
-
Kumi broadcasts operations over array fields for element-wise computation:
|
239
|
+
**Explainability**: Trace exactly how any value is computed, step-by-step. This is invaluable for debugging complex logic and auditing results.
|
241
240
|
|
242
241
|
```ruby
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
end
|
250
|
-
float :tax_rate
|
251
|
-
end
|
252
|
-
|
253
|
-
# Element-wise computation - broadcasts over each item
|
254
|
-
value :subtotals, input.line_items.price * input.line_items.quantity
|
255
|
-
|
256
|
-
# Element-wise traits - applied to each item
|
257
|
-
trait :is_taxable, (input.line_items.category != "digital")
|
258
|
-
|
259
|
-
# Aggregation operations - consume arrays to produce scalars
|
260
|
-
value :total_subtotal, fn(:sum, subtotals)
|
261
|
-
value :item_count, fn(:size, input.line_items)
|
262
|
-
end
|
242
|
+
Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_status: "single"})
|
243
|
+
# => fed_tax = fed_calc[0]
|
244
|
+
# = (fed_calc = piecewise_sum(taxable_income, fed_breaks, fed_rates)
|
245
|
+
# = piecewise_sum(85_400, [11_600, 47_150, ...], [0.10, 0.12, ...])
|
246
|
+
# = [15_099.50, 0.22])
|
247
|
+
# = 15_099.50
|
263
248
|
```
|
264
249
|
|
265
|
-
**
|
250
|
+
#### **Schema Metadata API: Build Tooling**
|
266
251
|
|
267
|
-
|
268
|
-
schema do
|
269
|
-
input do
|
270
|
-
array :items do
|
271
|
-
string :name
|
272
|
-
end
|
273
|
-
array :logs do
|
274
|
-
string :user_name
|
275
|
-
end
|
276
|
-
end
|
252
|
+
Programmatically access the analyzed structure of your schema to build tools like form generators, documentation sites, or custom validators.
|
277
253
|
|
278
|
-
|
279
|
-
|
280
|
-
end
|
254
|
+
```ruby
|
255
|
+
metadata = FederalTax2024.schema_metadata
|
281
256
|
|
282
|
-
#
|
283
|
-
#
|
284
|
-
#
|
285
|
-
#
|
286
|
-
# - Operand 2 resolves to array(string) from array 'logs'
|
287
|
-
# Direct operations on arrays from different sources is ambiguous and not supported.
|
288
|
-
```
|
257
|
+
# Processed, tool-friendly metadata
|
258
|
+
metadata.inputs # => { name: { type: :string, domain: ... } }
|
259
|
+
metadata.values # => { name: { dependencies: [...], expression: "..." } }
|
260
|
+
metadata.traits # => { name: { condition: "...", dependencies: [...] } }
|
289
261
|
|
290
|
-
|
262
|
+
# Raw analyzer state for deep analysis
|
263
|
+
metadata.dependencies # Dependency graph between all declarations
|
264
|
+
metadata.evaluation_order # Topologically sorted computation order
|
291
265
|
|
292
|
-
|
293
|
-
|
266
|
+
# Export to standard formats
|
267
|
+
metadata.to_h # => Serializable hash for JSON/APIs
|
268
|
+
metadata.to_json_schema # => JSON Schema for input validation
|
269
|
+
```
|
294
270
|
|
295
|
-
|
271
|
+
#### **AST Visualization: See the Structure**
|
296
272
|
|
297
|
-
|
273
|
+
For deep debugging, you can print the raw Abstract Syntax Tree (AST) of a schema.
|
298
274
|
|
299
275
|
```ruby
|
300
|
-
|
301
|
-
|
302
|
-
#
|
303
|
-
|
304
|
-
|
305
|
-
#
|
306
|
-
|
307
|
-
|
276
|
+
puts Kumi::Support::SExpressionPrinter.print(FederalTax2024.__syntax_tree__)
|
277
|
+
# => (Root
|
278
|
+
# inputs: [
|
279
|
+
# (InputDeclaration :income :float)
|
280
|
+
# (InputDeclaration :filing_status :string domain: ["single", "married_joint"])
|
281
|
+
# ]
|
282
|
+
# traits: [...]
|
283
|
+
# attributes: [...])
|
308
284
|
```
|
309
285
|
|
310
286
|
</details>
|
311
287
|
|
312
288
|
<details>
|
313
|
-
<summary><strong
|
289
|
+
<summary><strong>Hierarchical Broadcasting</strong> - Vectorization over nested data structures</summary>
|
290
|
+
|
291
|
+
### Hierarchical Broadcasting
|
292
|
+
|
293
|
+
Kumi broadcasts operations over hierarchical data structures with two complementary access modes for maximum flexibility.
|
314
294
|
|
315
|
-
|
295
|
+
See [docs/features/hierarchical-broadcasting.md](docs/features/hierarchical-broadcasting.md) for detailed documentation.
|
316
296
|
|
317
|
-
|
297
|
+
**Business Scenario**: E-commerce checkout with dynamic pricing rules
|
318
298
|
|
299
|
+
> **"As an e-commerce platform, I need to calculate order totals with complex discount rules:**
|
300
|
+
> - Premium members get 15% off electronics
|
301
|
+
> - Bulk orders (5+ items) get 10% off that item
|
302
|
+
> - Free shipping on orders over $100
|
303
|
+
> - Calculate: item subtotals, total discounts, shipping, final total
|
304
|
+
>
|
305
|
+
> **The challenge:** Each order has different items, quantities, categories, and customer tiers. The discount logic involves multiple conditions - some items qualify for multiple discounts, others for none. Traditional pricing code requires nested if-statements and manual calculations."
|
306
|
+
|
307
|
+
**Kumi Solution** (16 lines of declarative pricing logic):
|
319
308
|
```ruby
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
309
|
+
module OrderPricing
|
310
|
+
extend Kumi::Schema
|
311
|
+
|
312
|
+
schema do
|
313
|
+
input do
|
314
|
+
array :items do
|
315
|
+
float :price
|
316
|
+
integer :quantity
|
317
|
+
string :category
|
318
|
+
end
|
319
|
+
string :customer_tier
|
320
|
+
float :shipping_threshold
|
321
|
+
end
|
322
|
+
|
323
|
+
# Calculate item subtotals and discount eligibility
|
324
|
+
value :subtotals, input.items.price * input.items.quantity
|
325
|
+
trait :electronics, input.items.category == "electronics"
|
326
|
+
trait :bulk_item, input.items.quantity >= 5
|
327
|
+
trait :premium_customer, input.customer_tier == "premium"
|
328
|
+
|
329
|
+
# Apply layered discounts (premium + bulk can stack)
|
330
|
+
trait :premium_electronics, premium_customer & electronics
|
331
|
+
trait :stacked_discount, premium_electronics & bulk_item
|
332
|
+
|
333
|
+
value :discounted_prices do
|
334
|
+
on stacked_discount, input.items.price * 0.75 # 15% + 10% = 25% off
|
335
|
+
on premium_electronics, input.items.price * 0.85 # 15% off
|
336
|
+
on bulk_item, input.items.price * 0.90 # 10% off
|
337
|
+
base input.items.price # No discount
|
338
|
+
end
|
339
|
+
|
340
|
+
value :final_subtotals, discounted_prices * input.items.quantity
|
341
|
+
|
342
|
+
# Order totals and conditional shipping
|
343
|
+
value :subtotal, fn(:sum, final_subtotals)
|
344
|
+
value :total_savings, fn(:sum, subtotals) - subtotal
|
345
|
+
value :shipping, subtotal > input.shipping_threshold ? 0.0 : 9.99
|
346
|
+
value :total, subtotal + shipping
|
347
|
+
end
|
348
|
+
end
|
326
349
|
```
|
327
350
|
|
328
|
-
**
|
351
|
+
**Mixed Access Modes**: Both object and element access can be used together in the same schema:
|
329
352
|
|
330
353
|
```ruby
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
#
|
337
|
-
|
338
|
-
|
354
|
+
module UserAnalytics
|
355
|
+
extend Kumi::Schema
|
356
|
+
|
357
|
+
schema do
|
358
|
+
input do
|
359
|
+
# Object access mode - structured business data
|
360
|
+
array :users do
|
361
|
+
string :name
|
362
|
+
integer :age
|
363
|
+
end
|
364
|
+
|
365
|
+
# Element access mode - multi-dimensional raw arrays
|
366
|
+
array :recent_purchases do
|
367
|
+
element :integer, :days_ago
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Object access works normally
|
372
|
+
value :user_names, input.users.name
|
373
|
+
value :avg_age, fn(:avg, input.users.age)
|
374
|
+
|
375
|
+
# Element access handles nested arrays with progressive path traversal
|
376
|
+
value :all_purchase_days, fn(:flatten, input.recent_purchases.days_ago)
|
377
|
+
value :recent_flags, input.recent_purchases.days_ago < 5
|
378
|
+
trait :has_recent_purchase, fn(:any?, fn(:flatten, recent_flags))
|
379
|
+
|
380
|
+
# Progressive dimensional analysis - each path level goes deeper
|
381
|
+
value :purchase_dimensions, [
|
382
|
+
fn(:size, input.recent_purchases), # Number of purchase groups
|
383
|
+
fn(:size, input.recent_purchases.days_ago) # Total individual purchase days
|
384
|
+
]
|
385
|
+
|
386
|
+
# Mixed usage in conditions
|
387
|
+
trait :adult_users, input.users.age >= 18
|
388
|
+
value :adult_count, fn(:count_if, adult_users)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# Works with mixed data structures
|
393
|
+
result = UserAnalytics.from({
|
394
|
+
users: [{ name: "Alice", age: 25 }, { name: "Bob", age: 17 }],
|
395
|
+
recent_purchases: [[1, 3, 7], [2, 4], [10, 15]] # Nested arrays
|
396
|
+
})
|
397
|
+
|
398
|
+
result[:user_names] # => ["Alice", "Bob"]
|
399
|
+
result[:has_recent_purchase] # => true (some purchases < 5 days ago)
|
400
|
+
result[:adult_count] # => 1
|
401
|
+
result[:purchase_dimensions] # => [3, 8] (3 groups, 8 total days)
|
339
402
|
```
|
340
403
|
|
341
404
|
</details>
|
342
405
|
|
343
406
|
<details>
|
344
|
-
<summary><strong
|
407
|
+
<summary><strong>Memoization</strong> - Each value computed exactly once</summary>
|
345
408
|
|
346
|
-
###
|
409
|
+
### Memoization
|
347
410
|
|
348
|
-
|
411
|
+
Each value is computed exactly once:
|
349
412
|
|
350
413
|
```ruby
|
351
|
-
|
352
|
-
|
353
|
-
# Processed metadata (tool-friendly)
|
354
|
-
metadata.inputs # Input field types and domains
|
355
|
-
metadata.values # Value declarations with dependencies
|
356
|
-
metadata.traits # Trait conditions and metadata
|
357
|
-
metadata.functions # Function registry information
|
414
|
+
runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
|
358
415
|
|
359
|
-
#
|
360
|
-
|
361
|
-
metadata.evaluation_order # Topologically sorted computation order
|
362
|
-
metadata.inferred_types # Type inference results
|
363
|
-
metadata.declarations # Raw AST declaration nodes
|
416
|
+
# First access computes full dependency chain
|
417
|
+
runner[:total_tax] # => 53,155.20
|
364
418
|
|
365
|
-
#
|
366
|
-
|
367
|
-
|
419
|
+
# Subsequent access uses cached values
|
420
|
+
runner[:fed_tax] # => 39,077.00 (cached)
|
421
|
+
runner[:after_tax] # => 196,844.80 (cached)
|
368
422
|
```
|
369
|
-
|
370
|
-
The SchemaMetadata interface provides both processed metadata for tool development and raw analyzer state for advanced use cases. Complete documentation available in the SchemaMetadata class and [docs/schema_metadata.md](docs/schema_metadata.md).
|
371
|
-
|
372
423
|
</details>
|
373
424
|
|
374
|
-
## Beyond Rules: What the Metadata Unlocks
|
375
|
-
* **Auto-generated forms** – compile schema → field spec → React form
|
376
|
-
* **Scenario explorer** – derive all trait combinations, Monte Carlo outcomes
|
377
|
-
* **Coverage dashboard** – flag branches never hit in prod
|
378
|
-
* **Schema diff** – highlight behaviour changes across versions
|
379
|
-
|
380
425
|
## Usage
|
381
426
|
|
382
427
|
**Suitable for:**
|
383
428
|
- Complex interdependent business rules
|
384
429
|
- Tax calculation engines
|
385
430
|
- Insurance premium calculators
|
386
|
-
- Loan amortization schedules
|
387
431
|
- Commission structures with complex tiers
|
388
432
|
- Pricing engines with multiple discount rules
|
389
433
|
|
390
434
|
**Not suitable for:**
|
391
|
-
-
|
435
|
+
- Basic conditional statements
|
392
436
|
- Sequential procedural workflows
|
393
|
-
-
|
394
|
-
|
437
|
+
- High-frequency processing
|
438
|
+
|
439
|
+
## JavaScript Transpiler
|
440
|
+
|
441
|
+
Transpiles compiled schemas to standalone JavaScript code. See [docs/features/javascript-transpiler.md](docs/features/javascript-transpiler.md) for details.
|
442
|
+
|
443
|
+
```ruby
|
444
|
+
Kumi::Js.export_to_file(FederalTax2024, "federal-tax-2024.js")
|
445
|
+
```
|
446
|
+
|
447
|
+
```javascript
|
448
|
+
const { schema } = require('./federal-tax-2024.js');
|
449
|
+
const calculator = schema.from({ income: 100_000, filing_status: "single" });
|
450
|
+
console.log(calculator.fetch('total_tax')); // 21491
|
451
|
+
```
|
452
|
+
|
453
|
+
Generated JavaScript includes only functions used by the schema.
|
454
|
+
|
455
|
+
All tests run in dual mode to verify compiled schemas produce identical results in both Ruby and JavaScript.
|
395
456
|
|
396
457
|
## Performance
|
397
458
|
|
@@ -400,6 +461,12 @@ Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:
|
|
400
461
|
- 1,000 attributes: **131,000/sec** (analysis <50ms)
|
401
462
|
- 10,000 attributes: **14,200/sec** (analysis ~300ms)
|
402
463
|
|
464
|
+
See [docs/features/performance.md](docs/features/performance.md) for detailed benchmarks.
|
465
|
+
|
466
|
+
## What Kumi does not guarantee
|
467
|
+
|
468
|
+
Lambdas (e.g. -> inside the schema), external IO, floating-point vs bignum, JS transpiler edge cases, time-zone math differences, etc.
|
469
|
+
|
403
470
|
## Learn More
|
404
471
|
|
405
472
|
- [DSL Syntax Reference](docs/SYNTAX.md)
|
data/docs/AST.md
CHANGED
@@ -77,7 +77,7 @@ CaseExpression = Struct.new(:condition, :result)
|
|
77
77
|
```
|
78
78
|
|
79
79
|
**Case type mappings**:
|
80
|
-
- `on :a, :b, result` → `condition: fn(:
|
80
|
+
- `on :a, :b, result` → `condition: fn(:cascade_and, ref(:a), ref(:b))`
|
81
81
|
- `on_any :a, :b, result` → `condition: fn(:any?, ref(:a), ref(:b))`
|
82
82
|
- `base result` → `condition: literal(true)`
|
83
83
|
|
data/docs/FUNCTIONS.md
CHANGED
@@ -10,6 +10,8 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
|
|
10
10
|
* **Usage**: `fn(:and, boolean1, boolean2, ...)` → `boolean`
|
11
11
|
* **`any?`**: Check if any element in collection is truthy
|
12
12
|
* **Usage**: `fn(:any?, array(any) arg1)` → `boolean`
|
13
|
+
* **`cascade_and`**: Element-wise AND for arrays with same nested structure
|
14
|
+
* **Usage**: `fn(:cascade_and, boolean1, boolean2, ...)` → `boolean`
|
13
15
|
* **`none?`**: Check if no elements in collection are truthy
|
14
16
|
* **Usage**: `fn(:none?, array(any) arg1)` → `boolean`
|
15
17
|
* **`not`**: Logical NOT
|
@@ -52,14 +54,14 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
|
|
52
54
|
* **Usage**: `fn(:modulo, float arg1, float arg2)` → `float`
|
53
55
|
* **`multiply`**: Multiply two numbers
|
54
56
|
* **Usage**: `fn(:multiply, float arg1, float arg2)` → `float`
|
57
|
+
* **`piecewise_sum`**: Accumulate over tiered ranges; returns [sum, marginal_rate]
|
58
|
+
* **Usage**: `fn(:piecewise_sum, float arg1, array(float) arg2, array(float) arg3)` → `array(float)`
|
55
59
|
* **`power`**: Raise first number to power of second
|
56
60
|
* **Usage**: `fn(:power, float arg1, float arg2)` → `float`
|
57
61
|
* **`round`**: Round number to specified precision
|
58
62
|
* **Usage**: `fn(:round, float1, float2, ...)` → `float`
|
59
63
|
* **`subtract`**: Subtract second number from first
|
60
64
|
* **Usage**: `fn(:subtract, float arg1, float arg2)` → `float`
|
61
|
-
* **`piecewise_sum`**: Accumulate over tiered ranges; returns [sum, marginal_rate]
|
62
|
-
* **Usage**: `fn(:piecewise_sum, float arg1, array(float) arg2, array(float) arg3)` → `array(float)`
|
63
65
|
|
64
66
|
## String Functions
|
65
67
|
|
@@ -67,16 +69,22 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
|
|
67
69
|
* **Usage**: `fn(:capitalize, string arg1)` → `string`
|
68
70
|
* **`concat`**: Concatenate multiple strings
|
69
71
|
* **Usage**: `fn(:concat, string1, string2, ...)` → `string`
|
72
|
+
* **`contains?`**: Check if string contains substring
|
73
|
+
* **Usage**: `fn(:contains?, string arg1, string arg2)` → `boolean`
|
70
74
|
* **`downcase`**: Convert string to lowercase
|
71
75
|
* **Usage**: `fn(:downcase, string arg1)` → `string`
|
72
76
|
* **`end_with?`**: Check if string ends with suffix
|
73
77
|
* **Usage**: `fn(:end_with?, string arg1, string arg2)` → `boolean`
|
74
|
-
* **`
|
75
|
-
* **Usage**: `fn(:
|
76
|
-
* **`length`**: Get
|
77
|
-
* **Usage**: `fn(:length,
|
78
|
+
* **`includes?`**: Check if string contains substring
|
79
|
+
* **Usage**: `fn(:includes?, string arg1, string arg2)` → `boolean`
|
80
|
+
* **`length`**: Get string length
|
81
|
+
* **Usage**: `fn(:length, string arg1)` → `integer`
|
78
82
|
* **`start_with?`**: Check if string starts with prefix
|
79
83
|
* **Usage**: `fn(:start_with?, string arg1, string arg2)` → `boolean`
|
84
|
+
* **`string_include?`**: Check if string contains substring
|
85
|
+
* **Usage**: `fn(:string_include?, string arg1, string arg2)` → `boolean`
|
86
|
+
* **`string_length`**: Get string length
|
87
|
+
* **Usage**: `fn(:string_length, string arg1)` → `integer`
|
80
88
|
* **`strip`**: Remove leading and trailing whitespace
|
81
89
|
* **Usage**: `fn(:strip, string arg1)` → `string`
|
82
90
|
* **`upcase`**: Convert string to uppercase
|
@@ -84,20 +92,54 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
|
|
84
92
|
|
85
93
|
## Collection Functions
|
86
94
|
|
95
|
+
* **`all_across`**: Check if all elements are truthy across all nested levels
|
96
|
+
* **Usage**: `fn(:all_across, array(any) arg1)` → `boolean`
|
97
|
+
* **`any_across`**: Check if any element is truthy across all nested levels
|
98
|
+
* **Usage**: `fn(:any_across, array(any) arg1)` → `boolean`
|
99
|
+
* **`avg_if`**: Average values where corresponding condition is true
|
100
|
+
* **Usage**: `fn(:avg_if, array(float) arg1, array(boolean) arg2)` → `float`
|
101
|
+
* **`build_array`**: Build array of given size with index values
|
102
|
+
* **Usage**: `fn(:build_array, integer arg1)` → `array(any)`
|
103
|
+
* **`count_across`**: Count total elements across all nested levels
|
104
|
+
* **Usage**: `fn(:count_across, array(any) arg1)` → `integer`
|
105
|
+
* **`count_if`**: Count number of true values in boolean array
|
106
|
+
* **Usage**: `fn(:count_if, array(boolean) arg1)` → `integer`
|
107
|
+
* **`each_slice`**: Group array elements into subarrays of given size
|
108
|
+
* **Usage**: `fn(:each_slice, array arg1, integer arg2)` → `array(array)`
|
87
109
|
* **`empty?`**: Check if collection is empty
|
88
110
|
* **Usage**: `fn(:empty?, array(any) arg1)` → `boolean`
|
89
111
|
* **`first`**: Get first element of collection
|
90
112
|
* **Usage**: `fn(:first, array(any) arg1)` → `any`
|
113
|
+
* **`flatten`**: Flatten nested arrays into a single array
|
114
|
+
* **Usage**: `fn(:flatten, array(any) arg1)` → `array(any)`
|
115
|
+
* **`flatten_deep`**: Recursively flatten all nested arrays (alias for flatten)
|
116
|
+
* **Usage**: `fn(:flatten_deep, array(any) arg1)` → `array(any)`
|
117
|
+
* **`flatten_one`**: Flatten nested arrays by one level only
|
118
|
+
* **Usage**: `fn(:flatten_one, array(any) arg1)` → `array(any)`
|
91
119
|
* **`include?`**: Check if collection includes element
|
92
120
|
* **Usage**: `fn(:include?, array(any) arg1, any arg2)` → `boolean`
|
121
|
+
* **`indices`**: Generate array of indices for the collection
|
122
|
+
* **Usage**: `fn(:indices, array(any) arg1)` → `array(integer)`
|
123
|
+
* **`join`**: Join array elements into string with separator
|
124
|
+
* **Usage**: `fn(:join, array arg1, string arg2)` → `string`
|
93
125
|
* **`last`**: Get last element of collection
|
94
126
|
* **Usage**: `fn(:last, array(any) arg1)` → `any`
|
95
|
-
* **`
|
96
|
-
* **Usage**: `fn(:
|
127
|
+
* **`map_add`**: Add value to each element
|
128
|
+
* **Usage**: `fn(:map_add, array(float) arg1, float arg2)` → `array(float)`
|
129
|
+
* **`map_conditional`**: Transform elements based on condition: if element == condition_value then true_value else false_value
|
130
|
+
* **Usage**: `fn(:map_conditional, array arg1, any arg2, any arg3, any arg4)` → `array`
|
131
|
+
* **`map_join_rows`**: Join 2D array into string with row and column separators
|
132
|
+
* **Usage**: `fn(:map_join_rows, array(array) arg1, string arg2, string arg3)` → `string`
|
133
|
+
* **`map_multiply`**: Multiply each element by factor
|
134
|
+
* **Usage**: `fn(:map_multiply, array(float) arg1, float arg2)` → `array(float)`
|
135
|
+
* **`map_with_index`**: Map collection elements to [element, index] pairs
|
136
|
+
* **Usage**: `fn(:map_with_index, array(any) arg1)` → `array(any)`
|
97
137
|
* **`max`**: Find maximum value in numeric collection
|
98
138
|
* **Usage**: `fn(:max, array(float) arg1)` → `float`
|
99
139
|
* **`min`**: Find minimum value in numeric collection
|
100
140
|
* **Usage**: `fn(:min, array(float) arg1)` → `float`
|
141
|
+
* **`range`**: Generate range of integers from start to finish (exclusive)
|
142
|
+
* **Usage**: `fn(:range, integer arg1, integer arg2)` → `array(integer)`
|
101
143
|
* **`reverse`**: Reverse collection order
|
102
144
|
* **Usage**: `fn(:reverse, array(any) arg1)` → `array(any)`
|
103
145
|
* **`size`**: Get collection size
|
@@ -106,6 +148,8 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
|
|
106
148
|
* **Usage**: `fn(:sort, array(any) arg1)` → `array(any)`
|
107
149
|
* **`sum`**: Sum all numeric elements in collection
|
108
150
|
* **Usage**: `fn(:sum, array(float) arg1)` → `float`
|
151
|
+
* **`sum_if`**: Sum values where corresponding condition is true
|
152
|
+
* **Usage**: `fn(:sum_if, array(float) arg1, array(boolean) arg2)` → `float`
|
109
153
|
* **`unique`**: Remove duplicate elements from collection
|
110
154
|
* **Usage**: `fn(:unique, array(any) arg1)` → `array(any)`
|
111
155
|
|