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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
data/README.md CHANGED
@@ -3,287 +3,365 @@
3
3
  [![CI](https://github.com/amuta/kumi/workflows/CI/badge.svg)](https://github.com/amuta/kumi/actions)
4
4
  [![Gem Version](https://badge.fury.io/rb/kumi.svg)](https://badge.fury.io/rb/kumi)
5
5
 
6
- Kumi is a declarative rule‑and‑calculation DSL for Ruby that turns scattered business logic into a statically‑checked dependency graph.
6
+ Kumi is a Declarative logic framework with static analysis for Ruby.
7
7
 
8
- Every input, trait, and formula is compiled into a typed AST node, so the entire graph is explicit and introspectable.
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
- ## How to get started
12
+ ## What can you build?
14
13
 
15
- Install Kumi and try running the examples below or explore the `./examples` directory of this repository.
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
- def calculate_loan_approval(credit_score, income, debt_ratio)
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
- integer :credit_score
45
- float :income
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
- value :interest_rate do
55
- on :approved, 3.5
56
- base 0.0
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
- runner = LoanApproval.from(credit_score: 750, income: 60_000, debt_to_income_ratio: 0.25)
62
- puts runner[:approved] # => true
63
- puts runner[:interest_rate] # => 3.5
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
- This gets you:
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
- ## Static Analysis & Safety
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
- Kumi analyzes your rules to catch logical impossibilities:
67
+ ## Installation
76
68
 
77
- ```ruby
78
- module ImpossibleLogic
79
- extend Kumi::Schema
69
+ ```bash
70
+ # Requires Ruby 3.0+
71
+ # No external dependencies
72
+ gem install kumi
73
+ ```
80
74
 
81
- schema do
82
- input {} # No inputs needed
75
+ ## Core Features
83
76
 
84
- value :x, 100
85
- trait :x_less_than_100, x < 100 # false: 100 < 100
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
- value :result do
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
- # Kumi::Errors::SemanticError: conjunction `x_less_than_100 AND y_greater_than_1000` is impossible
99
- ```
82
+ Kumi schemas are built from four primitives:
100
83
 
101
- Cycle detection:
84
+ **Inputs** define the data flowing into your schema with built-in validation:
102
85
  ```ruby
103
- module CircularDependency
104
- extend Kumi::Schema
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
- schema do
107
- input { float :base }
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
- # These create a circular dependency
110
- value :monthly_rate, yearly_rate / 12
111
- value :yearly_rate, monthly_rate * 12
112
- end
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
- # Kumi::Errors::SemanticError: cycle detected involving: monthly_rate → yearly_rate → monthly_rate
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
- ## Performance
121
+ These primitives are statically analyzed during schema definition to catch logical errors before runtime.
119
122
 
120
- Kumi has microsecond evaluation times through automatic memoization:
123
+ </details>
121
124
 
122
- ### Deep Dependency Chains
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
- ### Wide Complex Schemas
132
- ```
133
- === Evaluation Performance (with Memoization) ===
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 ProductPricing
133
+ module InsurancePolicyPricer
142
134
  extend Kumi::Schema
143
135
 
144
136
  schema do
145
137
  input do
146
- float :base_price
147
- float :tax_rate
148
- integer :quantity
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
- value :unit_price_with_tax, input.base_price * (1 + input.tax_rate)
152
- value :total_before_discount, unit_price_with_tax * input.quantity
153
- value :bulk_discount, input.quantity >= 10 ? 0.1 : 0.0
154
- value :final_total, total_before_discount * (1 - bulk_discount)
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
- runner = ProductPricing.from(base_price: 100.0, tax_rate: 0.08, quantity: 15)
159
-
160
- # First access: computes and caches all intermediate values
161
- puts runner[:final_total] # => 1458.0 (computed + cached)
162
-
163
- # Subsequent accesses: pure cache lookups (microsecond performance)
164
- puts runner[:unit_price_with_tax] # => 108.0 (from cache)
165
- puts runner[:bulk_discount] # => 0.1 (from cache)
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
- Architecture notes:
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
- module UserProfile
180
- extend Kumi::Schema
181
-
182
- schema do
183
- input do
184
- integer :age, domain: 0..150
185
- string :status, domain: %w[active inactive suspended]
186
- float :score, domain: 0.0..100.0
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
- # Valid data works fine
195
- UserProfile.from(age: 25, status: "active", score: 85.5)
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
- # Invalid data raises detailed errors
198
- UserProfile.from(age: 200, status: "invalid", score: -10)
199
- # => Kumi::Errors::DomainViolationError: Domain constraint violations...
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
- ### Cascade Logic
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
- schema do
208
- input do
209
- float :order_total
210
- string :membership_level
211
- end
236
+ </details>
212
237
 
213
- trait :premium_member, input.membership_level == "premium"
214
- trait :large_order, input.order_total >= 100
238
+ <details>
239
+ <summary><strong>🔍 Array Broadcasting</strong> - Automatic vectorization over array fields</summary>
215
240
 
216
- value :shipping_cost do
217
- on :premium_member, 0.0
218
- on :large_order, 5.0
219
- base 15.0
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
- runner = ShippingCost.from(order_total: 75, membership_level: "standard")
225
- puts runner[:shipping_cost] # => 15.0
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
- ### Functions
229
- ```ruby
230
- module Statistics
231
- extend Kumi::Schema
268
+ **Dimension Mismatch Detection**: Operations across different arrays generate error messages:
232
269
 
233
- schema do
234
- input do
235
- array :scores, elem: { type: :float }
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
- runner = Statistics.from(scores: [85.5, 92.0, 78.5, 96.0])
246
- puts runner[:average] # => 88.0
247
- puts runner[:max_score] # => 96.0
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
- ## Introspection
293
+ </details>
294
+
295
+ <details>
296
+ <summary><strong>💾 Automatic Memoization</strong> - Each value computed exactly once</summary>
297
+
298
+ ### Automatic Memoization
251
299
 
252
- You can see exactly how any value was computed:
300
+ Each value is computed exactly once:
253
301
 
254
302
  ```ruby
255
- module TaxCalculator
256
- extend Kumi::Schema
303
+ runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
257
304
 
258
- schema do
259
- input do
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
- value :taxable_income, input.income - input.deductions
266
- value :tax_amount, taxable_income * input.tax_rate
267
- end
268
- end
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
- inputs = { income: 100_000, tax_rate: 0.25, deductions: 12_000 }
313
+ </details>
271
314
 
272
- puts Kumi::Explain.call(TaxCalculator, :taxable_income, inputs: inputs)
273
- # => taxable_income = input.income - deductions = (input.income = 100 000) - (deductions = 12 000) => 88 000
315
+ <details>
316
+ <summary><strong>🔍 Introspection</strong> - See exactly how values are calculated</summary>
274
317
 
275
- puts Kumi::Explain.call(TaxCalculator, :tax_amount, inputs: inputs)
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
- ## Try It Yourself
320
+ Show how values are calculated:
280
321
 
281
- Run the performance benchmarks:
282
- ```bash
283
- bundle exec ruby examples/wide_schema_compilation_and_evaluation_benchmark.rb
284
- bundle exec ruby examples/deep_schema_compilation_and_evaluation_benchmark.rb
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
- ## DSL Syntax Reference
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 [`documents/SYNTAX.md`](documents/SYNTAX.md) for complete syntax documentation with sugar vs sugar-free examples.
367
+ MIT License. See [LICENSE](LICENSE).