kumi 0.0.5 → 0.0.6
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 +51 -6
- data/README.md +173 -51
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/SYNTAX.md +93 -1
- data/docs/features/README.md +45 -0
- data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
- data/docs/features/analysis-type-inference.md +42 -0
- data/docs/features/analysis-unsat-detection.md +71 -0
- data/docs/features/array-broadcasting.md +170 -0
- data/docs/features/input-declaration-system.md +42 -0
- data/docs/features/performance.md +16 -0
- data/examples/federal_tax_calculator_2024.rb +11 -6
- data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +72 -32
- data/lib/kumi/analyzer/passes/input_collector.rb +90 -29
- data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +9 -9
- data/lib/kumi/analyzer/passes/toposorter.rb +42 -6
- data/lib/kumi/analyzer/passes/type_checker.rb +32 -10
- data/lib/kumi/analyzer/passes/type_inferencer.rb +126 -17
- data/lib/kumi/analyzer/passes/unsat_detector.rb +133 -53
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
- data/lib/kumi/analyzer.rb +11 -12
- data/lib/kumi/compiler.rb +194 -16
- data/lib/kumi/constraint_relationship_solver.rb +6 -6
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/explain.rb +20 -20
- data/lib/kumi/export/node_registry.rb +26 -12
- data/lib/kumi/export/node_serializers.rb +1 -1
- data/lib/kumi/function_registry/collection_functions.rb +14 -9
- data/lib/kumi/function_registry/function_builder.rb +4 -3
- data/lib/kumi/function_registry.rb +8 -2
- data/lib/kumi/input/type_matcher.rb +3 -0
- data/lib/kumi/input/validator.rb +0 -3
- data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +3 -3
- data/lib/kumi/parser/expression_converter.rb +6 -6
- data/lib/kumi/parser/input_builder.rb +40 -9
- data/lib/kumi/parser/input_field_proxy.rb +46 -0
- data/lib/kumi/parser/input_proxy.rb +3 -3
- data/lib/kumi/parser/nested_input.rb +15 -0
- data/lib/kumi/parser/schema_builder.rb +10 -9
- data/lib/kumi/parser/sugar.rb +61 -9
- data/lib/kumi/syntax/array_expression.rb +15 -0
- data/lib/kumi/syntax/call_expression.rb +11 -0
- data/lib/kumi/syntax/cascade_expression.rb +11 -0
- data/lib/kumi/syntax/case_expression.rb +11 -0
- data/lib/kumi/syntax/declaration_reference.rb +11 -0
- data/lib/kumi/syntax/hash_expression.rb +11 -0
- data/lib/kumi/syntax/input_declaration.rb +12 -0
- data/lib/kumi/syntax/input_element_reference.rb +12 -0
- data/lib/kumi/syntax/input_reference.rb +12 -0
- data/lib/kumi/syntax/literal.rb +11 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- metadata +31 -14
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -26
- data/lib/kumi/syntax/expressions.rb +0 -34
- data/lib/kumi/syntax/terminal_expressions.rb +0 -30
- data/lib/kumi/syntax.rb +0 -9
- /data/{documents → docs}/DSL.md +0 -0
- /data/{documents → docs}/FUNCTIONS.md +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91e6e3b6ce1e4911d37588ba8aaf9b89f8129d23fe93b05268756c417249da90
|
4
|
+
data.tar.gz: f019de05a9e05957184fc649d187306fa7cd3736cb9d04c56d6080d4a3cde9de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d6439258bcb876374c5f7ca3bafa4e59faabf112e3cfe2dac532f752b2b7ef8533bf888c4e6feb1509995069a103a258b37c6b18b0164487e5215e03dd85b10
|
7
|
+
data.tar.gz: 6e64f9725569836d7a17b83aa82f316f01d4a9169a1323cee3d1d5fcf7d3782902b9c0695bcdb3735a4c206906463a4b7ca3e14f39f253ba23bf23d572ce95aa
|
data/CLAUDE.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
!! 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
4
|
!! We are using zeitwerk, i.e.: no requires
|
5
5
|
!! Do not care about linter or coverage unless asked to do so.
|
6
|
+
!! IMPORTANT: 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.
|
6
7
|
|
7
8
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
8
9
|
|
@@ -60,9 +61,17 @@ Kumi is a declarative decision-modeling compiler for Ruby that transforms comple
|
|
60
61
|
**Syntax Tree** (`lib/kumi/syntax/`):
|
61
62
|
- `node.rb` - Base node class with location tracking
|
62
63
|
- `root.rb` - Root schema node containing inputs, attributes, and traits
|
63
|
-
- `
|
64
|
-
- `
|
65
|
-
- `
|
64
|
+
- `value_declaration.rb` - Value declaration nodes (formerly Attribute)
|
65
|
+
- `trait_declaration.rb` - Trait declaration nodes (formerly Trait)
|
66
|
+
- `input_declaration.rb` - Input field declaration nodes (formerly FieldDecl)
|
67
|
+
- `call_expression.rb` - Function call expression nodes
|
68
|
+
- `array_expression.rb` - Array expression nodes (formerly ListExpression)
|
69
|
+
- `hash_expression.rb` - Hash expression nodes (for future hash literals)
|
70
|
+
- `cascade_expression.rb` - Cascade expression nodes (conditional values)
|
71
|
+
- `case_expression.rb` - Case expression nodes (formerly WhenCaseExpression)
|
72
|
+
- `literal.rb` - Literal value nodes
|
73
|
+
- `input_reference.rb` - Input field reference nodes (formerly FieldRef)
|
74
|
+
- `declaration_reference.rb` - Declaration reference nodes (formerly Binding)
|
66
75
|
|
67
76
|
**Analyzer** (`lib/kumi/analyzer.rb`):
|
68
77
|
- Multi-pass analysis system that validates schemas and builds dependency graphs
|
@@ -235,6 +244,7 @@ end
|
|
235
244
|
The `examples/` directory contains comprehensive examples showing Kumi usage patterns:
|
236
245
|
- `cascade_demonstration.rb` - Demonstrates cascade logic with UnsatDetector fixes (working)
|
237
246
|
- `working_comprehensive_schema.rb` - Complete feature showcase (current best practices, working)
|
247
|
+
- Mathematical predicate examples - Safe mutual recursion patterns using cascade mutual exclusion
|
238
248
|
- `federal_tax_calculator_2024.rb` - Real-world tax calculation example (working)
|
239
249
|
- `tax_2024.rb` - Simple tax example with explain functionality (working)
|
240
250
|
- `wide_schema_compilation_and_evaluation_benchmark.rb` - Performance benchmark for wide schemas (compilation and evaluation scaling)
|
@@ -261,11 +271,13 @@ The `examples/` directory contains comprehensive examples showing Kumi usage pat
|
|
261
271
|
7. `lib/kumi/analyzer/passes/type_checker.rb` - Type validation with enhanced error messages
|
262
272
|
8. `spec/kumi/input_block_spec.rb` - Input block syntax and behavior
|
263
273
|
9. `spec/integration/compiler_integration_spec.rb` - End-to-end test examples
|
264
|
-
10. `
|
265
|
-
11. `
|
266
|
-
12. `
|
274
|
+
10. `docs/DSL.md` - Concise DSL syntax reference
|
275
|
+
11. `docs/AST.md` - AST node types and structure reference
|
276
|
+
12. `docs/SYNTAX.md` - Comprehensive sugar vs sugar-free syntax comparison with examples
|
267
277
|
13. `lib/kumi/cli.rb` - CLI implementation with REPL and file execution
|
268
278
|
14. `examples/simple_tax_schema.rb` - CLI-compatible schema example
|
279
|
+
15. `docs/features/analysis-cascade-mutual-exclusion.md` - Cascade mutual exclusion detection feature documentation
|
280
|
+
16. `docs/features/array-broadcasting.md` - Array broadcasting and vectorization system documentation
|
269
281
|
|
270
282
|
## CLI Usage and Best Practices
|
271
283
|
|
@@ -346,6 +358,39 @@ input do
|
|
346
358
|
end
|
347
359
|
```
|
348
360
|
|
361
|
+
### Array Broadcasting System
|
362
|
+
|
363
|
+
**Automatic Vectorization**: Field access on array inputs (`input.items.price`) applies operations element-wise with intelligent map/reduce detection.
|
364
|
+
|
365
|
+
**Basic Broadcasting**:
|
366
|
+
```ruby
|
367
|
+
input do
|
368
|
+
array :line_items do
|
369
|
+
float :price
|
370
|
+
integer :quantity
|
371
|
+
string :category
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Element-wise computation - broadcasts over each item
|
376
|
+
value :subtotals, input.line_items.price * input.line_items.quantity
|
377
|
+
trait :is_taxable, (input.line_items.category != "digital")
|
378
|
+
```
|
379
|
+
|
380
|
+
**Aggregation Operations**: Functions consuming arrays automatically detected:
|
381
|
+
```ruby
|
382
|
+
value :total_subtotal, fn(:sum, subtotals)
|
383
|
+
value :avg_price, fn(:avg, input.line_items.price)
|
384
|
+
value :max_quantity, fn(:max, input.line_items.quantity)
|
385
|
+
```
|
386
|
+
|
387
|
+
**Implementation Components**:
|
388
|
+
- **InputElementReference** AST nodes for nested field access paths
|
389
|
+
- **BroadcastDetector** analyzer pass identifies vectorized vs scalar operations
|
390
|
+
- **Compiler** generates appropriate map/reduce functions based on usage context
|
391
|
+
- **Type Inference** automatically infers types for array element operations
|
392
|
+
- Supports arbitrary depth field access with nested arrays and hashes
|
393
|
+
|
349
394
|
### Trait Syntax Evolution
|
350
395
|
|
351
396
|
**Current Syntax** (recommended):
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[](https://github.com/amuta/kumi/actions)
|
4
4
|
[](https://badge.fury.io/rb/kumi)
|
5
5
|
|
6
|
-
Kumi is a
|
6
|
+
Kumi is a Declarative logic framework with static analysis for Ruby.
|
7
7
|
|
8
8
|
It is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
|
9
9
|
|
@@ -11,7 +11,7 @@ It is well-suited for scenarios with complex, interdependent calculations, enfor
|
|
11
11
|
|
12
12
|
## What can you build?
|
13
13
|
|
14
|
-
Calculate U.S. federal taxes
|
14
|
+
Calculate U.S. federal taxes:
|
15
15
|
|
16
16
|
```ruby
|
17
17
|
module FederalTax2024
|
@@ -62,7 +62,7 @@ result[:after_tax] # => 78,509.00
|
|
62
62
|
|
63
63
|
Real tax calculation with brackets, deductions, and FICA caps. Validation happens during schema definition.
|
64
64
|
|
65
|
-
|
65
|
+
Kumi is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
|
66
66
|
|
67
67
|
## Installation
|
68
68
|
|
@@ -74,17 +74,19 @@ gem install kumi
|
|
74
74
|
|
75
75
|
## Core Features
|
76
76
|
|
77
|
-
|
77
|
+
<details>
|
78
|
+
<summary><strong>📊 Schema Primitives</strong> - Four building blocks for business logic</summary>
|
78
79
|
|
79
|
-
|
80
|
+
### Schema Primitives
|
80
81
|
|
81
|
-
Kumi schemas are built from four
|
82
|
+
Kumi schemas are built from four primitives:
|
82
83
|
|
83
84
|
**Inputs** define the data flowing into your schema with built-in validation:
|
84
85
|
```ruby
|
85
86
|
input do
|
86
87
|
float :price, domain: 0..1000.0 # Validates range
|
87
|
-
|
88
|
+
integer :quantity, domain: 1..10000 # Validates range
|
89
|
+
string :tier, domain: %w[standard premium] # Validates inclusion
|
88
90
|
end
|
89
91
|
```
|
90
92
|
|
@@ -101,7 +103,7 @@ trait :bulk_order, input.quantity >= 100
|
|
101
103
|
trait :premium_customer, input.tier == "premium"
|
102
104
|
|
103
105
|
value :discount do
|
104
|
-
on bulk_order
|
106
|
+
on bulk_order, premium_customer, 0.25 # 25% for bulk premium orders
|
105
107
|
on bulk_order, 0.15 # 15% for bulk orders
|
106
108
|
on premium_customer, 0.10 # 10% for premium customers
|
107
109
|
base 0.0 # No discount otherwise
|
@@ -114,73 +116,185 @@ end
|
|
114
116
|
value :final_price, [subtotal - discount_amount, 0].max
|
115
117
|
value :monthly_payment, fn(:pmt, rate: 0.05/12, nper: 36, pv: -loan_amount)
|
116
118
|
```
|
117
|
-
Note: You can find a list all core functions [FUNCTIONS.md](
|
119
|
+
Note: You can find a list all core functions in [docs/FUNCTIONS.md](docs/FUNCTIONS.md)
|
118
120
|
|
121
|
+
These primitives are statically analyzed during schema definition to catch logical errors before runtime.
|
119
122
|
|
120
|
-
|
123
|
+
</details>
|
121
124
|
|
125
|
+
<details>
|
126
|
+
<summary><strong>🔍 Static Analysis</strong> - Catch business logic errors at definition time</summary>
|
122
127
|
|
123
128
|
### Static Analysis
|
124
129
|
|
125
|
-
Kumi catches
|
130
|
+
Kumi catches business logic errors that cause runtime failures or silent bugs:
|
126
131
|
|
127
132
|
```ruby
|
128
|
-
module
|
133
|
+
module InsurancePolicyPricer
|
129
134
|
extend Kumi::Schema
|
130
135
|
|
131
136
|
schema do
|
132
137
|
input do
|
133
|
-
|
138
|
+
integer :age, domain: 18..80
|
139
|
+
string :risk_category, domain: %w[low medium high]
|
140
|
+
float :coverage_amount, domain: 50_000..2_000_000
|
134
141
|
integer :years_experience, domain: 0..50
|
135
|
-
|
142
|
+
boolean :has_claims
|
136
143
|
end
|
137
144
|
|
138
|
-
#
|
139
|
-
trait :
|
140
|
-
trait :
|
141
|
-
trait :
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
145
|
+
# Risk assessment with subtle interdependencies
|
146
|
+
trait :young_driver, input.age < 25
|
147
|
+
trait :experienced, input.years_experience >= 5
|
148
|
+
trait :high_risk, input.risk_category == "high"
|
149
|
+
trait :senior_driver, input.age >= 65
|
150
|
+
|
151
|
+
# Base premium calculation
|
152
|
+
value :base_premium, input.coverage_amount * 0.02
|
153
|
+
|
154
|
+
# Experience adjustment with subtle circular reference
|
155
|
+
value :experience_factor do
|
156
|
+
on experienced & young_driver, experience_discount * 0.8 # ❌ Uses experience_discount before it's defined
|
157
|
+
on experienced, 0.85
|
158
|
+
on young_driver, 1.25
|
159
|
+
base 1.0
|
149
160
|
end
|
150
161
|
|
151
|
-
#
|
152
|
-
value :
|
153
|
-
on
|
154
|
-
on
|
162
|
+
# Risk multipliers that create impossible combinations
|
163
|
+
value :risk_multiplier do
|
164
|
+
on high_risk & experienced, 1.5 # High risk but experienced
|
165
|
+
on high_risk, 2.0 # Just high risk
|
166
|
+
on low_risk & young_driver, 0.9 # ❌ low_risk is undefined (typo for input.risk_category)
|
155
167
|
base 1.0
|
156
168
|
end
|
157
169
|
|
158
|
-
#
|
159
|
-
|
160
|
-
|
161
|
-
|
170
|
+
# Claims history impact
|
171
|
+
trait :claims_free, fn(:not, input.has_claims)
|
172
|
+
trait :perfect_record, claims_free & experienced & fn(:not, young_driver)
|
173
|
+
|
174
|
+
# Discount calculation with type error
|
175
|
+
value :experience_discount do
|
176
|
+
on perfect_record, input.years_experience + "%" # ❌ String concatenation with integer
|
177
|
+
on claims_free, 0.95
|
162
178
|
base 1.0
|
163
179
|
end
|
164
180
|
|
165
|
-
|
181
|
+
# Premium calculation chain
|
182
|
+
value :adjusted_premium, base_premium * experience_factor * risk_multiplier
|
183
|
+
|
184
|
+
# Age-based impossible logic
|
185
|
+
trait :mature_professional, senior_driver & experienced & young_driver # ❌ Can't be senior AND young
|
166
186
|
|
167
|
-
#
|
168
|
-
|
169
|
-
|
170
|
-
|
187
|
+
# Final premium with self-referencing cascade
|
188
|
+
value :final_premium do
|
189
|
+
on mature_professional, adjusted_premium * 0.8
|
190
|
+
on senior_driver, adjusted_premium * senior_adjustment # ❌ senior_adjustment undefined
|
191
|
+
base final_premium * 1.1 # ❌ Self-reference in base case
|
192
|
+
end
|
171
193
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
194
|
+
# Monthly payment calculation with function arity error
|
195
|
+
value :monthly_payment, fn(:divide, final_premium) # ❌ divide needs 2 arguments, got 1
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Static analysis catches these errors:
|
200
|
+
# ❌ Circular reference: experience_factor → experience_discount → experience_factor
|
201
|
+
# ❌ Undefined reference: low_risk (should be input.risk_category == "low")
|
202
|
+
# ❌ Type mismatch: integer + string in experience_discount
|
203
|
+
# ❌ Impossible conjunction: senior_driver & young_driver
|
204
|
+
# ❌ Undefined reference: senior_adjustment
|
205
|
+
# ❌ Self-reference cycle: final_premium references itself in base case
|
206
|
+
# ❌ Function arity error: divide expects 2 arguments, got 1
|
207
|
+
```
|
208
|
+
|
209
|
+
**Bounded Recursion**: Kumi allows mutual recursion when cascade conditions are mutually exclusive:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
trait :is_forward, input.operation == "forward"
|
213
|
+
trait :is_reverse, input.operation == "reverse"
|
214
|
+
|
215
|
+
# Safe mutual recursion - conditions are mutually exclusive
|
216
|
+
value :forward_processor do
|
217
|
+
on is_forward, input.value * 2 # Direct calculation
|
218
|
+
on is_reverse, reverse_processor + 10 # Delegates to reverse (safe)
|
219
|
+
base "invalid operation"
|
220
|
+
end
|
221
|
+
|
222
|
+
value :reverse_processor do
|
223
|
+
on is_forward, forward_processor - 5 # Delegates to forward (safe)
|
224
|
+
on is_reverse, input.value / 2 # Direct calculation
|
225
|
+
base "invalid operation"
|
226
|
+
end
|
227
|
+
|
228
|
+
# Usage examples:
|
229
|
+
# operation="forward", value=10 => forward: 20, reverse: 15
|
230
|
+
# operation="reverse", value=10 => forward: 15, reverse: 5
|
231
|
+
# operation="unknown", value=10 => both: "invalid operation"
|
232
|
+
```
|
233
|
+
|
234
|
+
This compiles because `operation` can only be "forward" or "reverse", never both. Each recursion executes one step before hitting a direct calculation.
|
235
|
+
|
236
|
+
</details>
|
237
|
+
|
238
|
+
<details>
|
239
|
+
<summary><strong>🔍 Array Broadcasting</strong> - Automatic vectorization over array fields</summary>
|
240
|
+
|
241
|
+
### Array Broadcasting
|
242
|
+
|
243
|
+
Kumi broadcasts operations over array fields for element-wise computation:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
schema do
|
247
|
+
input do
|
248
|
+
array :line_items do
|
249
|
+
float :price
|
250
|
+
integer :quantity
|
251
|
+
string :category
|
177
252
|
end
|
253
|
+
float :tax_rate
|
178
254
|
end
|
255
|
+
|
256
|
+
# Element-wise computation - broadcasts over each item
|
257
|
+
value :subtotals, input.line_items.price * input.line_items.quantity
|
258
|
+
|
259
|
+
# Element-wise traits - applied to each item
|
260
|
+
trait :is_taxable, (input.line_items.category != "digital")
|
261
|
+
|
262
|
+
# Aggregation operations - consume arrays to produce scalars
|
263
|
+
value :total_subtotal, fn(:sum, subtotals)
|
264
|
+
value :item_count, fn(:size, input.line_items)
|
179
265
|
end
|
266
|
+
```
|
267
|
+
|
268
|
+
**Dimension Mismatch Detection**: Operations across different arrays generate error messages:
|
180
269
|
|
181
|
-
|
270
|
+
```ruby
|
271
|
+
schema do
|
272
|
+
input do
|
273
|
+
array :items do
|
274
|
+
string :name
|
275
|
+
end
|
276
|
+
array :logs do
|
277
|
+
string :user_name
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# This generates an error
|
282
|
+
trait :same_name, input.items.name == input.logs.user_name
|
283
|
+
end
|
284
|
+
|
285
|
+
# Error:
|
286
|
+
# Cannot broadcast operation across arrays from different sources: items, logs.
|
287
|
+
# Problem: Multiple operands are arrays from different sources:
|
288
|
+
# - Operand 1 resolves to array(string) from array 'items'
|
289
|
+
# - Operand 2 resolves to array(string) from array 'logs'
|
290
|
+
# Direct operations on arrays from different sources is ambiguous and not supported.
|
182
291
|
```
|
183
292
|
|
293
|
+
</details>
|
294
|
+
|
295
|
+
<details>
|
296
|
+
<summary><strong>💾 Automatic Memoization</strong> - Each value computed exactly once</summary>
|
297
|
+
|
184
298
|
### Automatic Memoization
|
185
299
|
|
186
300
|
Each value is computed exactly once:
|
@@ -189,16 +303,21 @@ Each value is computed exactly once:
|
|
189
303
|
runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
|
190
304
|
|
191
305
|
# First access computes full dependency chain
|
192
|
-
runner[:total_tax] # =>
|
306
|
+
runner[:total_tax] # => 53,155.20
|
193
307
|
|
194
308
|
# Subsequent access uses cached values
|
195
|
-
runner[:fed_tax] # =>
|
196
|
-
runner[:after_tax] # =>
|
309
|
+
runner[:fed_tax] # => 39,077.00 (cached)
|
310
|
+
runner[:after_tax] # => 196,844.80 (cached)
|
197
311
|
```
|
198
312
|
|
313
|
+
</details>
|
314
|
+
|
315
|
+
<details>
|
316
|
+
<summary><strong>🔍 Introspection</strong> - See exactly how values are calculated</summary>
|
317
|
+
|
199
318
|
### Introspection
|
200
319
|
|
201
|
-
|
320
|
+
Show how values are calculated:
|
202
321
|
|
203
322
|
```ruby
|
204
323
|
Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_status: "single"})
|
@@ -209,10 +328,13 @@ Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_st
|
|
209
328
|
# = 15,099.50
|
210
329
|
```
|
211
330
|
|
212
|
-
|
331
|
+
</details>
|
332
|
+
|
333
|
+
## Usage
|
213
334
|
|
335
|
+
**Suitable for:**
|
214
336
|
- Complex interdependent business rules
|
215
|
-
- Tax calculation engines
|
337
|
+
- Tax calculation engines
|
216
338
|
- Insurance premium calculators
|
217
339
|
- Loan amortization schedules
|
218
340
|
- Commission structures with complex tiers
|
@@ -233,7 +355,7 @@ Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:
|
|
233
355
|
|
234
356
|
## Learn More
|
235
357
|
|
236
|
-
- [DSL Syntax Reference](
|
358
|
+
- [DSL Syntax Reference](docs/SYNTAX.md)
|
237
359
|
- [Examples](examples/)/
|
238
360
|
|
239
361
|
## Contributing
|
data/{documents → docs}/AST.md
RENAMED
@@ -7,22 +7,22 @@
|
|
7
7
|
Root = Struct.new(:inputs, :attributes, :traits)
|
8
8
|
```
|
9
9
|
|
10
|
-
**
|
10
|
+
**InputDeclaration**: Input field metadata
|
11
11
|
```ruby
|
12
|
-
|
13
|
-
# DSL: integer :age, domain: 18..65 →
|
12
|
+
InputDeclaration = Struct.new(:name, :domain, :type)
|
13
|
+
# DSL: integer :age, domain: 18..65 → InputDeclaration(:age, 18..65, :integer)
|
14
14
|
```
|
15
15
|
|
16
|
-
**
|
16
|
+
**TraitDeclaration**: Boolean predicate
|
17
17
|
```ruby
|
18
|
-
|
19
|
-
# DSL: trait :adult, (input.age >= 18) →
|
18
|
+
TraitDeclaration = Struct.new(:name, :expression)
|
19
|
+
# DSL: trait :adult, (input.age >= 18) → TraitDeclaration(:adult, CallExpression(...))
|
20
20
|
```
|
21
21
|
|
22
|
-
**
|
22
|
+
**ValueDeclaration**: Computed value
|
23
23
|
```ruby
|
24
|
-
|
25
|
-
# DSL: value :total, fn(:add, a, b) →
|
24
|
+
ValueDeclaration = Struct.new(:name, :expression)
|
25
|
+
# DSL: value :total, fn(:add, a, b) → ValueDeclaration(:total, CallExpression(:add, [...]))
|
26
26
|
```
|
27
27
|
|
28
28
|
## Expression Nodes
|
@@ -33,18 +33,18 @@ CallExpression = Struct.new(:fn_name, :args)
|
|
33
33
|
def &(other) = CallExpression.new(:and, [self, other]) # Enable chaining
|
34
34
|
```
|
35
35
|
|
36
|
-
**
|
36
|
+
**InputReference**: Field access (`input.field_name`)
|
37
37
|
```ruby
|
38
|
-
|
38
|
+
InputReference = Struct.new(:name)
|
39
39
|
# Has operator methods: >=, <=, >, <, ==, != that create CallExpression nodes
|
40
40
|
```
|
41
41
|
|
42
|
-
**
|
42
|
+
**DeclarationReference**: References to other declarations
|
43
43
|
```ruby
|
44
|
-
|
44
|
+
DeclarationReference = Struct.new(:name)
|
45
45
|
# Created by: ref(:name) OR bare identifier (trait_name) in composite traits
|
46
|
-
# DSL: ref(:adult) →
|
47
|
-
# DSL: adult & verified → CallExpression(:and, [
|
46
|
+
# DSL: ref(:adult) → DeclarationReference(:adult)
|
47
|
+
# DSL: adult & verified → CallExpression(:and, [DeclarationReference(:adult), DeclarationReference(:verified)])
|
48
48
|
```
|
49
49
|
|
50
50
|
**Literal**: Constants (`18`, `"text"`, `true`)
|
@@ -52,9 +52,9 @@ Binding = Struct.new(:name)
|
|
52
52
|
Literal = Struct.new(:value)
|
53
53
|
```
|
54
54
|
|
55
|
-
**
|
55
|
+
**ArrayExpression**: Arrays (`[1, 2, 3]`)
|
56
56
|
```ruby
|
57
|
-
|
57
|
+
ArrayExpression = Struct.new(:elements)
|
58
58
|
```
|
59
59
|
|
60
60
|
## Cascade Expressions (Conditional Values)
|
@@ -64,9 +64,9 @@ ListExpression = Struct.new(:elements)
|
|
64
64
|
CascadeExpression = Struct.new(:cases)
|
65
65
|
```
|
66
66
|
|
67
|
-
**
|
67
|
+
**CaseExpression**: Individual conditions
|
68
68
|
```ruby
|
69
|
-
|
69
|
+
CaseExpression = Struct.new(:condition, :result)
|
70
70
|
```
|
71
71
|
|
72
72
|
**Case type mappings**:
|
@@ -76,7 +76,7 @@ WhenCaseExpression = Struct.new(:condition, :result)
|
|
76
76
|
|
77
77
|
## Key Nuances
|
78
78
|
|
79
|
-
**Operator methods on
|
79
|
+
**Operator methods on InputReference**: Enable `input.age >= 18` syntax by defining operators that create `CallExpression` nodes
|
80
80
|
|
81
81
|
**CallExpression `&` method**: Enables expression chaining like `(expr1) & (expr2)`
|
82
82
|
|
@@ -92,14 +92,14 @@ WhenCaseExpression = Struct.new(:condition, :result)
|
|
92
92
|
|
93
93
|
**Simple**: `(input.age >= 18)`
|
94
94
|
```
|
95
|
-
CallExpression(:>=, [
|
95
|
+
CallExpression(:>=, [InputReference(:age), Literal(18)])
|
96
96
|
```
|
97
97
|
|
98
98
|
**Chained AND**: `(input.age >= 21) & (input.verified == true)`
|
99
99
|
```
|
100
100
|
CallExpression(:and, [
|
101
|
-
CallExpression(:>=, [
|
102
|
-
CallExpression(:==, [
|
101
|
+
CallExpression(:>=, [InputReference(:age), Literal(21)]),
|
102
|
+
CallExpression(:==, [InputReference(:verified), Literal(true)])
|
103
103
|
])
|
104
104
|
```
|
105
105
|
|
@@ -107,10 +107,10 @@ CallExpression(:and, [
|
|
107
107
|
```
|
108
108
|
CallExpression(:and, [
|
109
109
|
CallExpression(:and, [
|
110
|
-
|
111
|
-
|
110
|
+
DeclarationReference(:adult),
|
111
|
+
DeclarationReference(:verified)
|
112
112
|
]),
|
113
|
-
|
113
|
+
DeclarationReference(:high_income)
|
114
114
|
])
|
115
115
|
```
|
116
116
|
|
@@ -118,9 +118,9 @@ CallExpression(:and, [
|
|
118
118
|
```
|
119
119
|
CallExpression(:and, [
|
120
120
|
CallExpression(:and, [
|
121
|
-
|
122
|
-
CallExpression(:>, [
|
121
|
+
DeclarationReference(:adult),
|
122
|
+
CallExpression(:>, [InputReference(:score), Literal(80)])
|
123
123
|
]),
|
124
|
-
|
124
|
+
DeclarationReference(:verified)
|
125
125
|
])
|
126
126
|
```
|