kumi 0.0.4 → 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 +160 -8
- data/README.md +278 -200
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/DSL.md +3 -3
- data/{documents → docs}/SYNTAX.md +107 -24
- 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 +43 -40
- data/examples/game_of_life.rb +97 -0
- data/examples/simple_rpg_game.rb +1000 -0
- data/examples/static_analysis_errors.rb +178 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
- data/lib/kumi/analyzer/analysis_state.rb +37 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
- data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
- data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
- data/lib/kumi/analyzer.rb +41 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/compiler.rb +194 -16
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +20 -4
- data/lib/kumi/explain.rb +28 -28
- 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 +117 -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 +19 -8
- data/lib/kumi/parser/expression_converter.rb +80 -12
- 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/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +10 -9
- data/lib/kumi/parser/sugar.rb +171 -18
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- 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/root.rb +1 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +38 -17
- data/CHANGELOG.md +0 -25
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -23
- data/lib/kumi/syntax/expressions.rb +0 -30
- data/lib/kumi/syntax/terminal_expressions.rb +0 -27
- data/lib/kumi/syntax.rb +0 -9
- data/test_impossible_cascade.rb +0 -51
- /data/{documents → docs}/FUNCTIONS.md +0 -0
data/README.md
CHANGED
@@ -3,287 +3,365 @@
|
|
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
|
|
10
|
-
Note: The examples here are small for the sake of readability. I would not recommend using this gem unless you need to keep track of 100+ conditions/variables.
|
11
10
|
|
12
11
|
|
13
|
-
##
|
12
|
+
## What can you build?
|
14
13
|
|
15
|
-
|
16
|
-
```
|
17
|
-
gem install kumi
|
18
|
-
```
|
19
|
-
|
20
|
-
## Example
|
14
|
+
Calculate U.S. federal taxes:
|
21
15
|
|
22
|
-
**Instead of scattered logic:**
|
23
16
|
```ruby
|
24
|
-
|
25
|
-
good_credit = credit_score >= 700
|
26
|
-
sufficient_income = income >= 50_000
|
27
|
-
low_debt = debt_ratio <= 0.3
|
28
|
-
|
29
|
-
if good_credit && sufficient_income && low_debt
|
30
|
-
{ approved: true, rate: 3.5 }
|
31
|
-
else
|
32
|
-
{ approved: false, rate: nil }
|
33
|
-
end
|
34
|
-
end
|
35
|
-
```
|
36
|
-
|
37
|
-
**You can write:**
|
38
|
-
```ruby
|
39
|
-
module LoanApproval
|
17
|
+
module FederalTax2024
|
40
18
|
extend Kumi::Schema
|
41
|
-
|
19
|
+
|
42
20
|
schema do
|
43
21
|
input do
|
44
|
-
|
45
|
-
|
46
|
-
float :debt_to_income_ratio
|
22
|
+
float :income
|
23
|
+
string :filing_status, domain: %w[single married_joint]
|
47
24
|
end
|
48
|
-
|
49
|
-
trait :good_credit, input.credit_score >= 700
|
50
|
-
trait :sufficient_income, input.income >= 50_000
|
51
|
-
trait :low_debt, input.debt_to_income_ratio <= 0.3
|
52
|
-
trait :approved, good_credit & sufficient_income & low_debt
|
53
25
|
|
54
|
-
|
55
|
-
|
56
|
-
|
26
|
+
# Standard deduction by filing status
|
27
|
+
trait :single, input.filing_status == "single"
|
28
|
+
trait :married, input.filing_status == "married_joint"
|
29
|
+
|
30
|
+
value :std_deduction do
|
31
|
+
on single, 14_600
|
32
|
+
on married, 29_200
|
33
|
+
base 21_900 # head_of_household
|
34
|
+
end
|
35
|
+
|
36
|
+
value :taxable_income, fn(:max, [input.income - std_deduction, 0])
|
37
|
+
|
38
|
+
# Federal tax brackets
|
39
|
+
value :fed_breaks do
|
40
|
+
on single, [11_600, 47_150, 100_525, 191_950, 243_725, 609_350, Float::INFINITY]
|
41
|
+
on married, [23_200, 94_300, 201_050, 383_900, 487_450, 731_200, Float::INFINITY]
|
57
42
|
end
|
43
|
+
|
44
|
+
value :fed_rates, [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37]
|
45
|
+
value :fed_calc, fn(:piecewise_sum, taxable_income, fed_breaks, fed_rates)
|
46
|
+
value :fed_tax, fed_calc[0]
|
47
|
+
|
48
|
+
# FICA taxes
|
49
|
+
value :ss_tax, fn(:min, [input.income, 168_600]) * 0.062
|
50
|
+
value :medicare_tax, input.income * 0.0145
|
51
|
+
|
52
|
+
value :total_tax, fed_tax + ss_tax + medicare_tax
|
53
|
+
value :after_tax, input.income - total_tax
|
58
54
|
end
|
59
55
|
end
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
# Use it
|
58
|
+
result = FederalTax2024.from(income: 100_000, filing_status: "single")
|
59
|
+
result[:total_tax] # => 21,491.00
|
60
|
+
result[:after_tax] # => 78,509.00
|
64
61
|
```
|
65
62
|
|
66
|
-
|
67
|
-
- Static analysis catches impossible logic combinations at compile time
|
68
|
-
- Automatic dependency tracking prevents circular references
|
69
|
-
- Type safety with domain constraints (age: 0..150, status: %w[active inactive])
|
70
|
-
- Microsecond performance not much different than optimized pure ruby
|
71
|
-
- Introspectable - see exactly how any value was computed
|
63
|
+
Real tax calculation with brackets, deductions, and FICA caps. Validation happens during schema definition.
|
72
64
|
|
73
|
-
|
65
|
+
Kumi is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
|
74
66
|
|
75
|
-
|
67
|
+
## Installation
|
76
68
|
|
77
|
-
```
|
78
|
-
|
79
|
-
|
69
|
+
```bash
|
70
|
+
# Requires Ruby 3.0+
|
71
|
+
# No external dependencies
|
72
|
+
gem install kumi
|
73
|
+
```
|
80
74
|
|
81
|
-
|
82
|
-
input {} # No inputs needed
|
75
|
+
## Core Features
|
83
76
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
value :y, x * 10 # 1000
|
88
|
-
trait :y_greater_than_1000, y > 1000 # false: 1000 > 1000
|
77
|
+
<details>
|
78
|
+
<summary><strong>📊 Schema Primitives</strong> - Four building blocks for business logic</summary>
|
89
79
|
|
90
|
-
|
91
|
-
# This is impossible
|
92
|
-
on :x_less_than_100 & :y_greater_than_1000, "Impossible!"
|
93
|
-
base "Default"
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
80
|
+
### Schema Primitives
|
97
81
|
|
98
|
-
|
99
|
-
```
|
82
|
+
Kumi schemas are built from four primitives:
|
100
83
|
|
101
|
-
|
84
|
+
**Inputs** define the data flowing into your schema with built-in validation:
|
102
85
|
```ruby
|
103
|
-
|
104
|
-
|
86
|
+
input do
|
87
|
+
float :price, domain: 0..1000.0 # Validates range
|
88
|
+
integer :quantity, domain: 1..10000 # Validates range
|
89
|
+
string :tier, domain: %w[standard premium] # Validates inclusion
|
90
|
+
end
|
91
|
+
```
|
105
92
|
|
106
|
-
|
107
|
-
|
93
|
+
**Values** are computed attributes that automatically memoize their results
|
94
|
+
```ruby
|
95
|
+
value :subtotal, input.price * input.quantity
|
96
|
+
value :tax_rate, 0.08
|
97
|
+
value :tax_amount, subtotal * tax_rate
|
98
|
+
```
|
108
99
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
100
|
+
**Traits** are boolean conditions that enable branching logic:
|
101
|
+
```ruby
|
102
|
+
trait :bulk_order, input.quantity >= 100
|
103
|
+
trait :premium_customer, input.tier == "premium"
|
104
|
+
|
105
|
+
value :discount do
|
106
|
+
on bulk_order, premium_customer, 0.25 # 25% for bulk premium orders
|
107
|
+
on bulk_order, 0.15 # 15% for bulk orders
|
108
|
+
on premium_customer, 0.10 # 10% for premium customers
|
109
|
+
base 0.0 # No discount otherwise
|
113
110
|
end
|
111
|
+
```
|
114
112
|
|
115
|
-
|
113
|
+
**Functions** provide computational building blocks:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
value :final_price, [subtotal - discount_amount, 0].max
|
117
|
+
value :monthly_payment, fn(:pmt, rate: 0.05/12, nper: 36, pv: -loan_amount)
|
116
118
|
```
|
119
|
+
Note: You can find a list all core functions in [docs/FUNCTIONS.md](docs/FUNCTIONS.md)
|
117
120
|
|
118
|
-
|
121
|
+
These primitives are statically analyzed during schema definition to catch logical errors before runtime.
|
119
122
|
|
120
|
-
|
123
|
+
</details>
|
121
124
|
|
122
|
-
|
123
|
-
|
124
|
-
=== Evaluation Performance (with Memoization) ===
|
125
|
-
eval 50-deep: 817,497 i/s (1.22 μs/i)
|
126
|
-
eval 100-deep: 509,567 i/s (1.96 μs/i)
|
127
|
-
eval 150-deep: 376,429 i/s (2.66 μs/i)
|
128
|
-
eval 200-deep: 282,243 i/s (3.54 μs/i)
|
129
|
-
```
|
125
|
+
<details>
|
126
|
+
<summary><strong>🔍 Static Analysis</strong> - Catch business logic errors at definition time</summary>
|
130
127
|
|
131
|
-
###
|
132
|
-
|
133
|
-
|
134
|
-
eval 1,000-wide: 127,652 i/s (7.83 μs/i)
|
135
|
-
eval 5,000-wide: 26,604 i/s (37.59 μs/i)
|
136
|
-
eval 10,000-wide: 13,670 i/s (73.15 μs/i)
|
137
|
-
```
|
128
|
+
### Static Analysis
|
129
|
+
|
130
|
+
Kumi catches business logic errors that cause runtime failures or silent bugs:
|
138
131
|
|
139
|
-
Here's how the memoization works:
|
140
132
|
```ruby
|
141
|
-
module
|
133
|
+
module InsurancePolicyPricer
|
142
134
|
extend Kumi::Schema
|
143
135
|
|
144
136
|
schema do
|
145
137
|
input do
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
141
|
+
integer :years_experience, domain: 0..50
|
142
|
+
boolean :has_claims
|
143
|
+
end
|
144
|
+
|
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
|
160
|
+
end
|
161
|
+
|
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)
|
167
|
+
base 1.0
|
149
168
|
end
|
150
169
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
178
|
+
base 1.0
|
179
|
+
end
|
180
|
+
|
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
|
186
|
+
|
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
|
193
|
+
|
194
|
+
# Monthly payment calculation with function arity error
|
195
|
+
value :monthly_payment, fn(:divide, final_premium) # ❌ divide needs 2 arguments, got 1
|
155
196
|
end
|
156
197
|
end
|
157
198
|
|
158
|
-
|
159
|
-
|
160
|
-
#
|
161
|
-
|
162
|
-
|
163
|
-
#
|
164
|
-
|
165
|
-
|
166
|
-
puts runner[:final_total] # => 1458.0 (from cache)
|
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
|
167
207
|
```
|
168
208
|
|
169
|
-
|
170
|
-
- Compile-once, evaluate-many: Schema compilation happens once, evaluations are pure computation
|
171
|
-
- `EvaluationWrapper` caches computed values automatically for subsequent access
|
172
|
-
- Stack-safe algorithms: Iterative cycle detection and dependency resolution prevent stack overflow
|
173
|
-
- Type-safe execution: No runtime type checking overhead after compilation
|
174
|
-
|
175
|
-
## DSL Features
|
209
|
+
**Bounded Recursion**: Kumi allows mutual recursion when cascade conditions are mutually exclusive:
|
176
210
|
|
177
|
-
### Domain Constraints
|
178
211
|
```ruby
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
end
|
188
|
-
|
189
|
-
trait :adult, input.age >= 18
|
190
|
-
trait :active_user, input.status == "active"
|
191
|
-
end
|
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"
|
192
220
|
end
|
193
221
|
|
194
|
-
|
195
|
-
|
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
|
196
227
|
|
197
|
-
#
|
198
|
-
|
199
|
-
# =>
|
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"
|
200
232
|
```
|
201
233
|
|
202
|
-
|
203
|
-
```ruby
|
204
|
-
module ShippingCost
|
205
|
-
extend Kumi::Schema
|
234
|
+
This compiles because `operation` can only be "forward" or "reverse", never both. Each recursion executes one step before hitting a direct calculation.
|
206
235
|
|
207
|
-
|
208
|
-
input do
|
209
|
-
float :order_total
|
210
|
-
string :membership_level
|
211
|
-
end
|
236
|
+
</details>
|
212
237
|
|
213
|
-
|
214
|
-
|
238
|
+
<details>
|
239
|
+
<summary><strong>🔍 Array Broadcasting</strong> - Automatic vectorization over array fields</summary>
|
215
240
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
220
252
|
end
|
253
|
+
float :tax_rate
|
221
254
|
end
|
222
|
-
end
|
223
255
|
|
224
|
-
|
225
|
-
|
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)
|
265
|
+
end
|
226
266
|
```
|
227
267
|
|
228
|
-
|
229
|
-
```ruby
|
230
|
-
module Statistics
|
231
|
-
extend Kumi::Schema
|
268
|
+
**Dimension Mismatch Detection**: Operations across different arrays generate error messages:
|
232
269
|
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
236
278
|
end
|
237
|
-
|
238
|
-
value :total, fn(:sum, input.scores)
|
239
|
-
value :count, fn(:size, input.scores)
|
240
|
-
value :average, total / count
|
241
|
-
value :max_score, fn(:max, input.scores)
|
242
279
|
end
|
280
|
+
|
281
|
+
# This generates an error
|
282
|
+
trait :same_name, input.items.name == input.logs.user_name
|
243
283
|
end
|
244
284
|
|
245
|
-
|
246
|
-
|
247
|
-
|
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.
|
248
291
|
```
|
249
292
|
|
250
|
-
|
293
|
+
</details>
|
294
|
+
|
295
|
+
<details>
|
296
|
+
<summary><strong>💾 Automatic Memoization</strong> - Each value computed exactly once</summary>
|
297
|
+
|
298
|
+
### Automatic Memoization
|
251
299
|
|
252
|
-
|
300
|
+
Each value is computed exactly once:
|
253
301
|
|
254
302
|
```ruby
|
255
|
-
|
256
|
-
extend Kumi::Schema
|
303
|
+
runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
|
257
304
|
|
258
|
-
|
259
|
-
|
260
|
-
float :income
|
261
|
-
float :tax_rate
|
262
|
-
float :deductions
|
263
|
-
end
|
305
|
+
# First access computes full dependency chain
|
306
|
+
runner[:total_tax] # => 53,155.20
|
264
307
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
308
|
+
# Subsequent access uses cached values
|
309
|
+
runner[:fed_tax] # => 39,077.00 (cached)
|
310
|
+
runner[:after_tax] # => 196,844.80 (cached)
|
311
|
+
```
|
269
312
|
|
270
|
-
|
313
|
+
</details>
|
271
314
|
|
272
|
-
|
273
|
-
|
315
|
+
<details>
|
316
|
+
<summary><strong>🔍 Introspection</strong> - See exactly how values are calculated</summary>
|
274
317
|
|
275
|
-
|
276
|
-
# => tax_amount = taxable_income × input.tax_rate = (taxable_income = 88 000) × (input.tax_rate = 0.25) => 22 000
|
277
|
-
```
|
318
|
+
### Introspection
|
278
319
|
|
279
|
-
|
320
|
+
Show how values are calculated:
|
280
321
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
322
|
+
```ruby
|
323
|
+
Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_status: "single"})
|
324
|
+
# => fed_tax = fed_calc[0]
|
325
|
+
# = (fed_calc = piecewise_sum(taxable_income, fed_breaks, fed_rates)
|
326
|
+
# = piecewise_sum(85,400, [11,600, 47,150, ...], [0.10, 0.12, ...])
|
327
|
+
# = [15,099.50, 0.22])
|
328
|
+
# = 15,099.50
|
285
329
|
```
|
286
330
|
|
287
|
-
|
331
|
+
</details>
|
332
|
+
|
333
|
+
## Usage
|
334
|
+
|
335
|
+
**Suitable for:**
|
336
|
+
- Complex interdependent business rules
|
337
|
+
- Tax calculation engines
|
338
|
+
- Insurance premium calculators
|
339
|
+
- Loan amortization schedules
|
340
|
+
- Commission structures with complex tiers
|
341
|
+
- Pricing engines with multiple discount rules
|
342
|
+
|
343
|
+
**Not suitable for:**
|
344
|
+
- Simple conditional statements
|
345
|
+
- Sequential procedural workflows
|
346
|
+
- Rules that change during execution
|
347
|
+
- High-frequency real-time processing
|
348
|
+
|
349
|
+
## Performance
|
350
|
+
|
351
|
+
Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:
|
352
|
+
- 50-deep dependency chain: **740,000/sec** (analysis <50ms)
|
353
|
+
- 1,000 attributes: **131,000/sec** (analysis <50ms)
|
354
|
+
- 10,000 attributes: **14,200/sec** (analysis ~300ms)
|
355
|
+
|
356
|
+
## Learn More
|
357
|
+
|
358
|
+
- [DSL Syntax Reference](docs/SYNTAX.md)
|
359
|
+
- [Examples](examples/)/
|
360
|
+
|
361
|
+
## Contributing
|
362
|
+
|
363
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amuta/kumi.
|
364
|
+
|
365
|
+
## License
|
288
366
|
|
289
|
-
See [
|
367
|
+
MIT License. See [LICENSE](LICENSE).
|