kumi 0.0.8 → 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 +188 -108
- data/docs/AST.md +8 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +22 -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 +77 -0
- 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 +162 -0
- 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 +22 -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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98308b4c27cb40488f14215c8da9700c62a51fc54d11b9d822a1eb25d396a724
|
4
|
+
data.tar.gz: 3fbbb50dc0c74ba14d83fbcf71c5866efa814b71b4d2679c45b57d59a1f778da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4453ea76de50c433696c3ae5ebe4d39ae049cb35e40f34cf9a3cce0e52e5d0967a8ccfd9ca7627eafe33444ea4da186015a59796a59e022fdf5b5edc04c50444
|
7
|
+
data.tar.gz: dfdb9f118ee7c0c16fa30392b3591b88c8320a2d19ff331253a4344e49fa773967e6b8d2451c0fd490f200171846fec2b408ecd000e6ad111e3c2d89bd92c000
|
data/CLAUDE.md
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# CLAUDE.md
|
2
2
|
|
3
|
+
!! Important:
|
3
4
|
!! Remember, this gem is not on production yet, so no backward compatilibity is necessary. But do not change the public interfaces (e.g. DSL, Schema) without explicitly requested or demanded.
|
4
5
|
!! We are using zeitwerk, i.e.: no requires
|
5
|
-
!!
|
6
|
-
!!
|
6
|
+
!! Disregard linting or coverage issues unless asked to do so.
|
7
|
+
!! Communication style - Write direct, factual statements. Avoid promotional language, unnecessary claims, or marketing speak. State what the system does, not what benefits it provides. Use TODOs for missing information rather than placeholder claims.
|
8
|
+
!! See all Available Functions in docs/FUNCTIONS.md
|
7
9
|
|
8
10
|
## Project Overview
|
9
11
|
|
@@ -27,7 +29,7 @@ Kumi is a Declarative logic and rules engine framework with static analysis for
|
|
27
29
|
|
28
30
|
**Schema System** (`lib/kumi/schema.rb`):
|
29
31
|
- Entry point that ties together parsing, analysis, and compilation
|
30
|
-
-
|
32
|
+
- DSL method `schema(&block)` builds the syntax tree, runs analysis, and compiles to executable form
|
31
33
|
- Generates a `Runner` instance for executing queries against input data
|
32
34
|
|
33
35
|
**Parser** (`lib/kumi/ruby_parser{/*,.rb}`):
|
@@ -60,25 +62,19 @@ Kumi is a Declarative logic and rules engine framework with static analysis for
|
|
60
62
|
- **Pass 8**: `type_checker.rb` - Validate function types and compatibility using inferred types
|
61
63
|
|
62
64
|
**Compiler** (`lib/kumi/compiler.rb`):
|
63
|
-
-
|
65
|
+
- Compiles analyzed syntax tree into executable lambda functions
|
64
66
|
- Maps each expression type to a compilation method
|
65
67
|
- Handles function calls via `Kumi::Registry`
|
66
68
|
- Produces `CompiledSchema` with executable bindings
|
67
69
|
|
68
70
|
**Function Registry** (`lib/kumi/function_registry.rb`):
|
69
71
|
- Registry of available functions (operators, math, string, logical, collection operations)
|
70
|
-
- Supports custom function registration with
|
72
|
+
- Supports custom function registration with type metadata
|
71
73
|
- Each function includes param_types, return_type, arity, and description
|
72
74
|
- Core functions include: `==`, `>`, `<`, `add`, `multiply`, `and`, `or`, `clamp`, etc.
|
73
75
|
- Maintains backward compatibility with legacy type checking system
|
74
76
|
- Function documents are generated by the script ./scripts/generate_function_docs.rb
|
75
77
|
|
76
|
-
**Runner** (`lib/kumi/runner.rb`):
|
77
|
-
- Executes compiled schemas against input data
|
78
|
-
- Provides `fetch(key)` for individual value retrieval with caching
|
79
|
-
- Provides `slice(*keys)` for batch evaluation
|
80
|
-
- Provides `explain(key)` for detailed execution tracing
|
81
|
-
|
82
78
|
**Input Validation System** (`lib/kumi/input/` and `lib/kumi/domain/`):
|
83
79
|
- `input/validator.rb` - Main validation coordinator for type and domain checking
|
84
80
|
- `input/type_matcher.rb` - Type validation logic for primitive and complex types
|
@@ -92,7 +88,7 @@ Kumi is a Declarative logic and rules engine framework with static analysis for
|
|
92
88
|
|
93
89
|
### Critical Syntax Rules
|
94
90
|
|
95
|
-
**Module Definition Structure**
|
91
|
+
**Module Definition Structure**
|
96
92
|
```ruby
|
97
93
|
# CORRECT - CLI can find and load this
|
98
94
|
module SchemaName
|
@@ -102,11 +98,6 @@ module SchemaName
|
|
102
98
|
# schema definition here
|
103
99
|
end
|
104
100
|
end
|
105
|
-
|
106
|
-
# INCORRECT - CLI cannot load standalone schemas
|
107
|
-
schema do # This won't work with CLI
|
108
|
-
# schema definition
|
109
|
-
end
|
110
101
|
```
|
111
102
|
|
112
103
|
**Function Call Syntax**:
|
@@ -115,7 +106,7 @@ end
|
|
115
106
|
**Arithmetic Operations**:
|
116
107
|
- **Sugar Syntax**: `input.field1 + input.field2` - Works for input fields and value references
|
117
108
|
- **Function Syntax**: `fn(:add, input.field1, input.field2)` - Always works, more explicit
|
118
|
-
- **Mixed**:
|
109
|
+
- **Mixed**: Sugar syntax for basic operations, function syntax for complex ones
|
119
110
|
|
120
111
|
**Cascade Condition Syntax**:
|
121
112
|
```ruby
|
@@ -159,13 +150,6 @@ end
|
|
159
150
|
|
160
151
|
**IMPORTANT CASCADE CONDITION SYNTAX:**
|
161
152
|
In cascade expressions (`value :name do ... end`), trait references use bare identifiers:
|
162
|
-
```ruby
|
163
|
-
value :status do
|
164
|
-
on adult, "Adult Status"
|
165
|
-
on verified, "Verified User"
|
166
|
-
base "Unverified"
|
167
|
-
end
|
168
|
-
```
|
169
153
|
|
170
154
|
**Input Block System**:
|
171
155
|
- **Required**: All schemas must have an `input` block declaring expected fields
|
@@ -190,9 +174,9 @@ end
|
|
190
174
|
4. Execute with Runner
|
191
175
|
|
192
176
|
**Type System** (`lib/kumi/types.rb`):
|
193
|
-
-
|
177
|
+
- Symbol-based type system
|
194
178
|
- **Dual Type System**: Declared types (from input blocks) and inferred types (from expressions)
|
195
|
-
-
|
179
|
+
- Type inference for all declarations based on expression analysis
|
196
180
|
- Type primitives: `:string`, `:integer`, `:float`, `:boolean`, `:any`, `:symbol`, `:regexp`, `:time`, `:date`, `:datetime`
|
197
181
|
- Collection types: `array(:element_type)` and `hash(:key_type, :value_type)` helper functions
|
198
182
|
- Type compatibility checking and unification algorithms for numeric types
|
@@ -201,13 +185,13 @@ end
|
|
201
185
|
|
202
186
|
### Examples Directory
|
203
187
|
|
204
|
-
The `examples/` directory contains
|
188
|
+
The `examples/` directory contains examples showing Kumi usage patterns:
|
205
189
|
- `cascade_demonstration.rb` - Demonstrates cascade logic with UnsatDetector fixes (working)
|
206
|
-
- `working_comprehensive_schema.rb` -
|
190
|
+
- `working_comprehensive_schema.rb` - Feature showcase (current best practices, working)
|
207
191
|
- Mathematical predicate examples - Safe mutual recursion patterns using cascade mutual exclusion
|
208
192
|
- `federal_tax_calculator_2024.rb` - Real-world tax calculation example (working)
|
209
|
-
- `tax_2024.rb` -
|
210
|
-
- `wide_schema_compilation_and_evaluation_benchmark.rb` -
|
193
|
+
- `tax_2024.rb` - Tax example with explain functionality (working)
|
194
|
+
- `wide_schema_compilation_and_evaluation_benchmark.rb` - Benchmark for wide schemas (compilation and evaluation)
|
211
195
|
- `deep_schema_compilation_and_evaluation_benchmark.rb` - Performance benchmark for deep dependency chains (stack-safe evaluation)
|
212
196
|
- `comprehensive_god_schema.rb` - Complex example (currently has UnsatDetector semantic errors)
|
213
197
|
|
@@ -223,11 +207,11 @@ The `examples/` directory contains comprehensive examples showing Kumi usage pat
|
|
223
207
|
## Files for Understanding
|
224
208
|
|
225
209
|
. `docs/*` - Documents about Kumi, its features, DSL syntax, ...
|
226
|
-
- `examples/*` Random examples of
|
210
|
+
- `examples/*` Random examples of diverse contexts.
|
227
211
|
|
228
212
|
### Troubleshooting Schema Issues
|
229
213
|
- **Parse Errors**: Check function syntax (avoid empty `fn()` calls)
|
230
|
-
- **Module Not Found**:
|
214
|
+
- **Module Not Found**: Check module structure and naming, see examples
|
231
215
|
- **UnsatDetector Errors**: Review trait logic for contradictions, add debugs!
|
232
216
|
- **Type Errors**: Check input block type declarations match usage, add debugs!
|
233
217
|
- **Runtime Errors**: Use explain to trace computation dependencies, add debugs!
|
@@ -237,7 +221,7 @@ The `examples/` directory contains comprehensive examples showing Kumi usage pat
|
|
237
221
|
### Required Input Blocks
|
238
222
|
- **All schemas must have an input block** -
|
239
223
|
- Input blocks declare expected fields with optional type and domain constraints
|
240
|
-
- **Empty input blocks are allowed** -`input {}` Even if
|
224
|
+
- **Empty input blocks are allowed** -`input {}` Even if not useful.
|
241
225
|
- Fields are accessed via `input.field_name` or `input.field.nested_field.nested_nested_field` which
|
242
226
|
works for referencing nested array input declarations.
|
243
227
|
|
@@ -263,13 +247,13 @@ input do
|
|
263
247
|
hash :metadata, key: { type: :string }, val: { type: :any }
|
264
248
|
|
265
249
|
#generic type
|
266
|
-
any :misc # this
|
250
|
+
any :misc # this reduces Kumi's analyze/inference capabilities
|
267
251
|
end
|
268
252
|
```
|
269
253
|
|
270
254
|
### Array Broadcasting System
|
271
255
|
|
272
|
-
**
|
256
|
+
**Vectorization**: Field access on array inputs (`input.items.price`) applies operations element-wise with map/reduce detection.
|
273
257
|
|
274
258
|
**Basic Broadcasting**:
|
275
259
|
```ruby
|
@@ -286,7 +270,7 @@ value :subtotals, input.line_items.price * input.line_items.quantity
|
|
286
270
|
trait :is_taxable, (input.line_items.category != "digital")
|
287
271
|
```
|
288
272
|
|
289
|
-
**Aggregation Operations**: Functions consuming arrays
|
273
|
+
**Aggregation Operations**: Functions consuming arrays are detected:
|
290
274
|
```ruby
|
291
275
|
value :total_subtotal, fn(:sum, subtotals)
|
292
276
|
value :avg_price, fn(:avg, input.line_items.price)
|
@@ -297,7 +281,7 @@ value :max_quantity, fn(:max, input.line_items.quantity)
|
|
297
281
|
- **InputElementReference** AST nodes for nested field access paths
|
298
282
|
- **BroadcastDetector** analyzer pass identifies vectorized vs scalar operations
|
299
283
|
- **Compiler** generates appropriate map/reduce functions based on usage context
|
300
|
-
- **Type Inference**
|
284
|
+
- **Type Inference** infers types for array element operations
|
301
285
|
- Supports arbitrary depth field access with nested arrays and hashes
|
302
286
|
|
303
287
|
### Trait Syntax Evolution
|
@@ -330,7 +314,7 @@ trait :qualified, input.age, :>=, 21, input.score # OLD - shows deprecation war
|
|
330
314
|
```
|
331
315
|
|
332
316
|
**Key Changes**:
|
333
|
-
- **NEW**: Bare identifier syntax
|
317
|
+
- **NEW**: Bare identifier syntax for direct trait reference: `adult` instead of `ref(:adult)`
|
334
318
|
- New syntax uses parenthesized expressions: `trait :name, (expression)`
|
335
319
|
- FieldRef nodes have operator methods that create CallExpression nodes
|
336
320
|
- Logical AND chaining via `&` operator (Ruby limitation prevents `&&`)
|
@@ -350,9 +334,9 @@ trait :qualified, input.age, :>=, 21, input.score # OLD - shows deprecation war
|
|
350
334
|
|
351
335
|
- **Multi-pass Analysis**: Each analysis pass has a single responsibility and builds on previous passes
|
352
336
|
- **Immutable Syntax Tree**: AST nodes are immutable; analysis results stored separately in analyzer state
|
353
|
-
- **Dependency-driven Evaluation**: All computation follows dependency graph
|
354
|
-
- **Type Safety**: Optional
|
355
|
-
- **Ruby Integration**: Leverages Ruby's metaprogramming
|
337
|
+
- **Dependency-driven Evaluation**: All computation follows dependency graph for correct order
|
338
|
+
- **Type Safety**: Optional type checking without breaking existing schemas
|
339
|
+
- **Ruby Integration**: Leverages Ruby's metaprogramming with structured analysis
|
356
340
|
- **Unified Error Reporting**: Consistent, localized error messages throughout the system with clear interface patterns
|
357
341
|
|
358
342
|
## Code Organization Patterns
|
@@ -370,7 +354,7 @@ class MyParser
|
|
370
354
|
include ErrorReporting
|
371
355
|
|
372
356
|
def parse_something
|
373
|
-
#
|
357
|
+
# Error raising
|
374
358
|
raise_syntax_error("Invalid syntax", location: current_location)
|
375
359
|
end
|
376
360
|
end
|
@@ -389,7 +373,7 @@ class MyAnalyzerPass < PassBase
|
|
389
373
|
end
|
390
374
|
```
|
391
375
|
### Testing Error Scenarios
|
392
|
-
- Use `spec/integration/dsl_breakage_spec.rb` patterns for
|
376
|
+
- Use `spec/integration/dsl_breakage_spec.rb` patterns for error testing
|
393
377
|
- Use `spec/integration/potential_breakage_spec.rb` for edge cases break
|
394
378
|
- Use `spec/fixtures/location_tracking_test_schema.rb` fixture for testing different syntax error types
|
395
379
|
|
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,9 +59,9 @@ 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
|
|
@@ -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,155 +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
|
-
|
239
|
+
**Explainability**: Trace exactly how any value is computed, step-by-step. This is invaluable for debugging complex logic and auditing results.
|
240
|
+
|
241
|
+
```ruby
|
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
|
248
|
+
```
|
237
249
|
|
238
|
-
|
250
|
+
#### **Schema Metadata API: Build Tooling**
|
239
251
|
|
240
|
-
|
252
|
+
Programmatically access the analyzed structure of your schema to build tools like form generators, documentation sites, or custom validators.
|
241
253
|
|
242
254
|
```ruby
|
243
|
-
|
244
|
-
input do
|
245
|
-
array :line_items do
|
246
|
-
float :price
|
247
|
-
integer :quantity
|
248
|
-
string :category
|
249
|
-
end
|
250
|
-
float :tax_rate
|
251
|
-
end
|
255
|
+
metadata = FederalTax2024.schema_metadata
|
252
256
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
263
|
-
```
|
257
|
+
# Processed, tool-friendly metadata
|
258
|
+
metadata.inputs # => { name: { type: :string, domain: ... } }
|
259
|
+
metadata.values # => { name: { dependencies: [...], expression: "..." } }
|
260
|
+
metadata.traits # => { name: { condition: "...", dependencies: [...] } }
|
264
261
|
|
265
|
-
|
262
|
+
# Raw analyzer state for deep analysis
|
263
|
+
metadata.dependencies # Dependency graph between all declarations
|
264
|
+
metadata.evaluation_order # Topologically sorted computation order
|
266
265
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
string :name
|
272
|
-
end
|
273
|
-
array :logs do
|
274
|
-
string :user_name
|
275
|
-
end
|
276
|
-
end
|
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
|
+
```
|
277
270
|
|
278
|
-
|
279
|
-
|
280
|
-
|
271
|
+
#### **AST Visualization: See the Structure**
|
272
|
+
|
273
|
+
For deep debugging, you can print the raw Abstract Syntax Tree (AST) of a schema.
|
281
274
|
|
282
|
-
|
283
|
-
|
284
|
-
#
|
285
|
-
#
|
286
|
-
#
|
287
|
-
#
|
275
|
+
```ruby
|
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: [...])
|
288
284
|
```
|
289
285
|
|
290
286
|
</details>
|
291
287
|
|
292
288
|
<details>
|
293
|
-
<summary><strong
|
289
|
+
<summary><strong>Hierarchical Broadcasting</strong> - Vectorization over nested data structures</summary>
|
294
290
|
|
295
|
-
###
|
291
|
+
### Hierarchical Broadcasting
|
296
292
|
|
297
|
-
|
293
|
+
Kumi broadcasts operations over hierarchical data structures with two complementary access modes for maximum flexibility.
|
298
294
|
|
299
|
-
|
300
|
-
runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
|
295
|
+
See [docs/features/hierarchical-broadcasting.md](docs/features/hierarchical-broadcasting.md) for detailed documentation.
|
301
296
|
|
302
|
-
|
303
|
-
runner[:total_tax] # => 53,155.20
|
297
|
+
**Business Scenario**: E-commerce checkout with dynamic pricing rules
|
304
298
|
|
305
|
-
|
306
|
-
|
307
|
-
|
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):
|
308
|
+
```ruby
|
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
|
308
349
|
```
|
309
350
|
|
310
|
-
|
351
|
+
**Mixed Access Modes**: Both object and element access can be used together in the same schema:
|
311
352
|
|
312
|
-
|
313
|
-
|
353
|
+
```ruby
|
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
|
314
370
|
|
315
|
-
|
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
|
316
391
|
|
317
|
-
|
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
|
+
})
|
318
397
|
|
319
|
-
|
320
|
-
|
321
|
-
# =>
|
322
|
-
#
|
323
|
-
# = piecewise_sum(85,400, [11,600, 47,150, ...], [0.10, 0.12, ...])
|
324
|
-
# = [15,099.50, 0.22])
|
325
|
-
# = 15,099.50
|
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)
|
326
402
|
```
|
327
403
|
|
328
404
|
</details>
|
329
405
|
|
330
406
|
<details>
|
331
|
-
<summary><strong
|
407
|
+
<summary><strong>Memoization</strong> - Each value computed exactly once</summary>
|
332
408
|
|
333
|
-
###
|
409
|
+
### Memoization
|
334
410
|
|
335
|
-
|
411
|
+
Each value is computed exactly once:
|
336
412
|
|
337
413
|
```ruby
|
338
|
-
|
339
|
-
|
340
|
-
# Processed metadata (tool-friendly)
|
341
|
-
metadata.inputs # Input field types and domains
|
342
|
-
metadata.values # Value declarations with dependencies
|
343
|
-
metadata.traits # Trait conditions and metadata
|
344
|
-
metadata.functions # Function registry information
|
414
|
+
runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
|
345
415
|
|
346
|
-
#
|
347
|
-
|
348
|
-
metadata.evaluation_order # Topologically sorted computation order
|
349
|
-
metadata.inferred_types # Type inference results
|
350
|
-
metadata.declarations # Raw AST declaration nodes
|
416
|
+
# First access computes full dependency chain
|
417
|
+
runner[:total_tax] # => 53,155.20
|
351
418
|
|
352
|
-
#
|
353
|
-
|
354
|
-
|
419
|
+
# Subsequent access uses cached values
|
420
|
+
runner[:fed_tax] # => 39,077.00 (cached)
|
421
|
+
runner[:after_tax] # => 196,844.80 (cached)
|
355
422
|
```
|
356
|
-
|
357
|
-
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).
|
358
|
-
|
359
423
|
</details>
|
360
424
|
|
361
|
-
## Beyond Rules: What the Metadata Unlocks
|
362
|
-
* **Auto-generated forms** – compile schema → field spec → React form
|
363
|
-
* **Scenario explorer** – derive all trait combinations, Monte Carlo outcomes
|
364
|
-
* **Coverage dashboard** – flag branches never hit in prod
|
365
|
-
* **Schema diff** – highlight behaviour changes across versions
|
366
|
-
|
367
425
|
## Usage
|
368
426
|
|
369
427
|
**Suitable for:**
|
370
428
|
- Complex interdependent business rules
|
371
429
|
- Tax calculation engines
|
372
430
|
- Insurance premium calculators
|
373
|
-
- Loan amortization schedules
|
374
431
|
- Commission structures with complex tiers
|
375
432
|
- Pricing engines with multiple discount rules
|
376
433
|
|
377
434
|
**Not suitable for:**
|
378
|
-
-
|
435
|
+
- Basic conditional statements
|
379
436
|
- Sequential procedural workflows
|
380
|
-
-
|
381
|
-
|
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.
|
382
456
|
|
383
457
|
## Performance
|
384
458
|
|
@@ -387,6 +461,12 @@ Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:
|
|
387
461
|
- 1,000 attributes: **131,000/sec** (analysis <50ms)
|
388
462
|
- 10,000 attributes: **14,200/sec** (analysis ~300ms)
|
389
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
|
+
|
390
470
|
## Learn More
|
391
471
|
|
392
472
|
- [DSL Syntax Reference](docs/SYNTAX.md)
|
data/docs/AST.md
CHANGED
@@ -39,6 +39,13 @@ InputReference = Struct.new(:name)
|
|
39
39
|
# Has operator methods: >=, <=, >, <, ==, != that create CallExpression nodes
|
40
40
|
```
|
41
41
|
|
42
|
+
**InputElementReference**: Access of nested input fields (`input.field_name.element.subelement.subsubelement`)
|
43
|
+
```ruby
|
44
|
+
InputElementReference = Struct.new(:path)
|
45
|
+
# Represents nested input access
|
46
|
+
# DSL: input.address.street → InputElementReference([:address, :street])
|
47
|
+
```
|
48
|
+
|
42
49
|
**DeclarationReference**: References to other declarations
|
43
50
|
```ruby
|
44
51
|
DeclarationReference = Struct.new(:name)
|
@@ -70,7 +77,7 @@ CaseExpression = Struct.new(:condition, :result)
|
|
70
77
|
```
|
71
78
|
|
72
79
|
**Case type mappings**:
|
73
|
-
- `on :a, :b, result` → `condition: fn(:
|
80
|
+
- `on :a, :b, result` → `condition: fn(:cascade_and, ref(:a), ref(:b))`
|
74
81
|
- `on_any :a, :b, result` → `condition: fn(:any?, ref(:a), ref(:b))`
|
75
82
|
- `base result` → `condition: literal(true)`
|
76
83
|
|