kumi 0.0.5 → 0.0.7

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +76 -174
  3. data/README.md +205 -52
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/SYNTAX.md +95 -8
  6. data/docs/features/README.md +45 -0
  7. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  8. data/docs/features/analysis-type-inference.md +42 -0
  9. data/docs/features/analysis-unsat-detection.md +71 -0
  10. data/docs/features/array-broadcasting.md +170 -0
  11. data/docs/features/input-declaration-system.md +42 -0
  12. data/docs/features/performance.md +16 -0
  13. data/docs/schema_metadata/broadcasts.md +53 -0
  14. data/docs/schema_metadata/cascades.md +45 -0
  15. data/docs/schema_metadata/declarations.md +54 -0
  16. data/docs/schema_metadata/dependencies.md +57 -0
  17. data/docs/schema_metadata/evaluation_order.md +29 -0
  18. data/docs/schema_metadata/examples.md +95 -0
  19. data/docs/schema_metadata/inferred_types.md +46 -0
  20. data/docs/schema_metadata/inputs.md +86 -0
  21. data/docs/schema_metadata.md +108 -0
  22. data/examples/federal_tax_calculator_2024.rb +11 -6
  23. data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
  24. data/lib/kumi/analyzer/passes/broadcast_detector.rb +246 -0
  25. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
  26. data/lib/kumi/analyzer/passes/dependency_resolver.rb +78 -38
  27. data/lib/kumi/analyzer/passes/input_collector.rb +91 -30
  28. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  29. data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
  30. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +24 -25
  31. data/lib/kumi/analyzer/passes/toposorter.rb +44 -8
  32. data/lib/kumi/analyzer/passes/type_checker.rb +34 -14
  33. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
  34. data/lib/kumi/analyzer/passes/type_inferencer.rb +130 -21
  35. data/lib/kumi/analyzer/passes/unsat_detector.rb +134 -56
  36. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
  37. data/lib/kumi/analyzer.rb +16 -17
  38. data/lib/kumi/compiler.rb +188 -16
  39. data/lib/kumi/constraint_relationship_solver.rb +6 -6
  40. data/lib/kumi/domain/validator.rb +0 -4
  41. data/lib/kumi/error_reporting.rb +1 -1
  42. data/lib/kumi/explain.rb +32 -20
  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 +14 -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/json_schema/generator.rb +63 -0
  51. data/lib/kumi/json_schema/validator.rb +25 -0
  52. data/lib/kumi/json_schema.rb +14 -0
  53. data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
  54. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +36 -0
  55. data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
  56. data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +5 -5
  57. data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +20 -20
  58. data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
  59. data/lib/kumi/{parser → ruby_parser}/input_builder.rb +41 -10
  60. data/lib/kumi/ruby_parser/input_field_proxy.rb +46 -0
  61. data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +4 -4
  62. data/lib/kumi/ruby_parser/nested_input.rb +15 -0
  63. data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
  64. data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +11 -10
  65. data/lib/kumi/{parser → ruby_parser}/sugar.rb +62 -10
  66. data/lib/kumi/ruby_parser.rb +10 -0
  67. data/lib/kumi/schema.rb +10 -4
  68. data/lib/kumi/schema_instance.rb +6 -6
  69. data/lib/kumi/schema_metadata.rb +524 -0
  70. data/lib/kumi/syntax/array_expression.rb +15 -0
  71. data/lib/kumi/syntax/call_expression.rb +11 -0
  72. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  73. data/lib/kumi/syntax/case_expression.rb +11 -0
  74. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  75. data/lib/kumi/syntax/hash_expression.rb +11 -0
  76. data/lib/kumi/syntax/input_declaration.rb +12 -0
  77. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  78. data/lib/kumi/syntax/input_reference.rb +12 -0
  79. data/lib/kumi/syntax/literal.rb +11 -0
  80. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  81. data/lib/kumi/syntax/value_declaration.rb +11 -0
  82. data/lib/kumi/vectorization_metadata.rb +108 -0
  83. data/lib/kumi/version.rb +1 -1
  84. data/lib/kumi.rb +14 -0
  85. metadata +55 -25
  86. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
  87. data/lib/kumi/domain.rb +0 -8
  88. data/lib/kumi/input.rb +0 -8
  89. data/lib/kumi/syntax/declarations.rb +0 -26
  90. data/lib/kumi/syntax/expressions.rb +0 -34
  91. data/lib/kumi/syntax/terminal_expressions.rb +0 -30
  92. data/lib/kumi/syntax.rb +0 -9
  93. /data/{documents → docs}/DSL.md +0 -0
  94. /data/{documents → docs}/FUNCTIONS.md +0 -0
data/README.md CHANGED
@@ -3,7 +3,7 @@
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 computational rules engine for Ruby (plus static validation, dependency tracking, and more)
6
+ Kumi is a Declarative logic and rules engine 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 in 30 lines of validated, readable code:
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
- Is is well-suited for scenarios with complex, interdependent calculations, enforcing ...
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
- Here's a concise "Key Concepts" section for your README:
77
+ <details>
78
+ <summary><strong>📊 Schema Primitives</strong> - Four building blocks for business logic</summary>
78
79
 
79
- ## Key Concepts
80
+ ### Schema Primitives
80
81
 
81
- Kumi schemas are built from four simple primitives that compose into powerful business logic:
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
- string :category, domain: %w[standard premium] # Validates inclusion
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 & premium_customer, 0.25 # 25% for bulk premium orders
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](documents/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
- These primitives are statically analyzed during schema definition, catching logical errors before runtime and ensuring your business rules are internally consistent.
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 real business logic errors during schema definition:
130
+ Kumi catches business logic errors that cause runtime failures or silent bugs:
126
131
 
127
132
  ```ruby
128
- module CommissionCalculator
133
+ module InsurancePolicyPricer
129
134
  extend Kumi::Schema
130
135
 
131
136
  schema do
132
137
  input do
133
- float :sales_amount, domain: 0..Float::INFINITY
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
- string :region, domain: %w[east west north south]
142
+ boolean :has_claims
136
143
  end
137
144
 
138
- # Commission tiers based on experience
139
- trait :junior, input.years_experience < 2
140
- trait :senior, input.years_experience >= 5
141
- trait :veteran, input.years_experience >= 10
142
-
143
- # Base commission rates
144
- value :base_rate do
145
- on veteran, 0.08
146
- on senior, 0.06
147
- on junior, 0.04
148
- base 0.05
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
- # Regional multipliers
152
- value :regional_bonus do
153
- on input.region == "west", 1.2 # West coast bonus
154
- on input.region == "east", 1.1 # East coast bonus
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
- # Problem: Veteran bonus conflicts with senior cap
159
- value :experience_bonus do
160
- on veteran, 2.0 # Veterans get 2x bonus
161
- on senior, 1.5 # Seniors get 1.5x bonus
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
- value :total_rate, base_rate * regional_bonus * experience_bonus
181
+ # Premium calculation chain
182
+ value :adjusted_premium, base_premium * experience_factor * risk_multiplier
166
183
 
167
- # Business rule error: Veterans (10+ years) are also seniors (5+ years)
168
- # This creates impossible logic in commission caps
169
- trait :capped_senior, :senior & (total_rate <= 0.10) # Senior cap
170
- trait :uncapped_veteran, :veteran & (total_rate > 0.10) # Veteran override
184
+ # Age-based impossible logic
185
+ trait :mature_professional, senior_driver & experienced & young_driver # Can't be senior AND young
171
186
 
172
- value :final_commission do
173
- on capped_senior & uncapped_veteran, "Impossible!" # Can't be both
174
- on uncapped_veteran, input.sales_amount * total_rate
175
- on capped_senior, input.sales_amount * 0.10
176
- base input.sales_amount * total_rate
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
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
+ ```
180
267
 
181
- # => conjunction `capped_senior AND uncapped_veteran` is impossible
268
+ **Dimension Mismatch Detection**: Operations across different arrays generate error messages:
269
+
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] # => 52,937.50
306
+ runner[:total_tax] # => 53,155.20
193
307
 
194
308
  # Subsequent access uses cached values
195
- runner[:fed_tax] # => 37,437.50 (cached)
196
- runner[:after_tax] # => 197,062.50 (cached)
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
- See exactly how any value was calculated:
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,44 @@ Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_st
209
328
  # = 15,099.50
210
329
  ```
211
330
 
212
- ## Suggested Use Cases
331
+ </details>
332
+
333
+ <details>
334
+ <summary><strong>📋 Schema Metadata</strong> - Extract structured information for tooling</summary>
335
+
336
+ ### Schema Metadata
337
+
338
+ Access structured metadata for building tools like form generators and dependency analyzers:
339
+
340
+ ```ruby
341
+ metadata = FederalTax2024.schema_metadata
342
+
343
+ # Processed metadata (tool-friendly)
344
+ metadata.inputs # Input field types and domains
345
+ metadata.values # Value declarations with dependencies
346
+ metadata.traits # Trait conditions and metadata
347
+ metadata.functions # Function registry information
348
+
349
+ # Raw analyzer state (advanced usage)
350
+ metadata.dependencies # Dependency graph between declarations
351
+ metadata.evaluation_order # Topologically sorted computation order
352
+ metadata.inferred_types # Type inference results
353
+ metadata.declarations # Raw AST declaration nodes
354
+
355
+ # Export formats
356
+ metadata.to_h # Serializable hash for JSON/APIs
357
+ metadata.to_json_schema # JSON Schema for input validation
358
+ ```
359
+
360
+ 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).
361
+
362
+ </details>
363
+
364
+ ## Usage
213
365
 
366
+ **Suitable for:**
214
367
  - Complex interdependent business rules
215
- - Tax calculation engines (as demonstrated)
368
+ - Tax calculation engines
216
369
  - Insurance premium calculators
217
370
  - Loan amortization schedules
218
371
  - Commission structures with complex tiers
@@ -233,7 +386,7 @@ Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:
233
386
 
234
387
  ## Learn More
235
388
 
236
- - [DSL Syntax Reference](documents/SYNTAX.md)
389
+ - [DSL Syntax Reference](docs/SYNTAX.md)
237
390
  - [Examples](examples/)/
238
391
 
239
392
  ## Contributing
@@ -242,4 +395,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/amuta/
242
395
 
243
396
  ## License
244
397
 
245
- MIT License. See [LICENSE](LICENSE).
398
+ MIT License. See [LICENSE](LICENSE).
@@ -7,22 +7,22 @@
7
7
  Root = Struct.new(:inputs, :attributes, :traits)
8
8
  ```
9
9
 
10
- **FieldDecl**: Input field metadata
10
+ **InputDeclaration**: Input field metadata
11
11
  ```ruby
12
- FieldDecl = Struct.new(:name, :domain, :type)
13
- # DSL: integer :age, domain: 18..65 → FieldDecl(:age, 18..65, :integer)
12
+ InputDeclaration = Struct.new(:name, :domain, :type)
13
+ # DSL: integer :age, domain: 18..65 → InputDeclaration(:age, 18..65, :integer)
14
14
  ```
15
15
 
16
- **Trait**: Boolean predicate
16
+ **TraitDeclaration**: Boolean predicate
17
17
  ```ruby
18
- Trait = Struct.new(:name, :expression)
19
- # DSL: trait :adult, (input.age >= 18) → Trait(:adult, CallExpression(...))
18
+ TraitDeclaration = Struct.new(:name, :expression)
19
+ # DSL: trait :adult, (input.age >= 18) → TraitDeclaration(:adult, CallExpression(...))
20
20
  ```
21
21
 
22
- **Attribute**: Computed value
22
+ **ValueDeclaration**: Computed value
23
23
  ```ruby
24
- Attribute = Struct.new(:name, :expression)
25
- # DSL: value :total, fn(:add, a, b) → Attribute(:total, CallExpression(:add, [...]))
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
- **FieldRef**: Field access (`input.field_name`)
36
+ **InputReference**: Field access (`input.field_name`)
37
37
  ```ruby
38
- FieldRef = Struct.new(:name)
38
+ InputReference = Struct.new(:name)
39
39
  # Has operator methods: >=, <=, >, <, ==, != that create CallExpression nodes
40
40
  ```
41
41
 
42
- **Binding**: References to other declarations
42
+ **DeclarationReference**: References to other declarations
43
43
  ```ruby
44
- Binding = Struct.new(:name)
44
+ DeclarationReference = Struct.new(:name)
45
45
  # Created by: ref(:name) OR bare identifier (trait_name) in composite traits
46
- # DSL: ref(:adult) → Binding(:adult)
47
- # DSL: adult & verified → CallExpression(:and, [Binding(:adult), Binding(:verified)])
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
- **ListExpression**: Arrays (`[1, 2, 3]`)
55
+ **ArrayExpression**: Arrays (`[1, 2, 3]`)
56
56
  ```ruby
57
- ListExpression = Struct.new(:elements)
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
- **WhenCaseExpression**: Individual conditions
67
+ **CaseExpression**: Individual conditions
68
68
  ```ruby
69
- WhenCaseExpression = Struct.new(:condition, :result)
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 FieldRef**: Enable `input.age >= 18` syntax by defining operators that create `CallExpression` nodes
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(:>=, [FieldRef(:age), Literal(18)])
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(:>=, [FieldRef(:age), Literal(21)]),
102
- CallExpression(:==, [FieldRef(:verified), Literal(true)])
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
- Binding(:adult),
111
- Binding(:verified)
110
+ DeclarationReference(:adult),
111
+ DeclarationReference(:verified)
112
112
  ]),
113
- Binding(:high_income)
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
- Binding(:adult),
122
- CallExpression(:>, [FieldRef(:score), Literal(80)])
121
+ DeclarationReference(:adult),
122
+ CallExpression(:>, [InputReference(:score), Literal(80)])
123
123
  ]),
124
- Binding(:verified)
124
+ DeclarationReference(:verified)
125
125
  ])
126
126
  ```
@@ -10,6 +10,7 @@ This document provides a comprehensive comparison of Kumi's DSL syntax showing b
10
10
  - [Trait Declarations](#trait-declarations)
11
11
  - [Expressions](#expressions)
12
12
  - [Functions](#functions)
13
+ - [Array Broadcasting](#array-broadcasting)
13
14
  - [Cascade Logic](#cascade-logic)
14
15
  - [References](#references)
15
16
 
@@ -130,13 +131,8 @@ trait :needs_review, fn(:and, needs_improvement, fn(:>, input.attempts, 2))
130
131
  ### String Operations
131
132
 
132
133
  ```ruby
133
- # With Sugar
134
- trait :long_name, input.name.length > 20
135
- trait :starts_with_a, input.name.start_with?("A")
136
- trait :contains_space, input.name.include?(" ")
137
-
138
- # Sugar-Free
139
- trait :long_name, fn(:>, fn(:string_length, input.name), 20)
134
+ # All string operations use function syntax
135
+ trait :long_name, fn(:string_length, input.name) > 20
140
136
  trait :starts_with_a, fn(:start_with?, input.name, "A")
141
137
  trait :contains_space, fn(:contains?, input.name, " ")
142
138
  ```
@@ -187,7 +183,7 @@ value :sorted_scores, fn(:sort, input.score_array)
187
183
 
188
184
  ### Built-in Functions Available
189
185
 
190
- See [FUNCTIONS.md](documents/FUNCTIONS.md)
186
+ See [FUNCTIONS.md](FUNCTIONS.md)
191
187
 
192
188
  | Category | Sugar | Sugar-Free |
193
189
  |----------|-------|------------|
@@ -210,6 +206,97 @@ value :clamped_score, fn(:clamp, input.raw_score, 0, 1600)
210
206
  value :formatted_name, fn(:add, fn(:add, input.first_name, " "), input.last_name)
211
207
  ```
212
208
 
209
+ ## Array Broadcasting
210
+
211
+ Array broadcasting enables element-wise operations on array fields with automatic vectorization.
212
+
213
+ ### Array Input Declarations
214
+
215
+ ```ruby
216
+ input do
217
+ array :line_items do
218
+ float :price
219
+ integer :quantity
220
+ string :category
221
+ end
222
+
223
+ array :orders do
224
+ array :items do
225
+ hash :product do
226
+ string :name
227
+ float :base_price
228
+ end
229
+ integer :quantity
230
+ end
231
+ end
232
+ end
233
+ ```
234
+
235
+ ### Element-wise Operations
236
+
237
+ ```ruby
238
+ # With Sugar - Automatic Broadcasting
239
+ value :subtotals, input.line_items.price * input.line_items.quantity
240
+ trait :is_taxable, (input.line_items.category != "digital")
241
+ value :discounted_prices, input.line_items.price * 0.9
242
+
243
+ # Sugar-Free - Explicit Broadcasting
244
+ value :subtotals, fn(:multiply, input.line_items.price, input.line_items.quantity)
245
+ trait :is_taxable, fn(:!=, input.line_items.category, "digital")
246
+ value :discounted_prices, fn(:multiply, input.line_items.price, 0.9)
247
+ ```
248
+
249
+ ### Aggregation Operations
250
+
251
+ ```ruby
252
+ # With Sugar - Automatic Aggregation Detection
253
+ value :total_subtotal, fn(:sum, subtotals)
254
+ value :avg_price, fn(:avg, input.line_items.price)
255
+ value :max_quantity, fn(:max, input.line_items.quantity)
256
+ value :item_count, fn(:size, input.line_items)
257
+
258
+ # Sugar-Free - Same Syntax
259
+ value :total_subtotal, fn(:sum, subtotals)
260
+ value :avg_price, fn(:avg, input.line_items.price)
261
+ value :max_quantity, fn(:max, input.line_items.quantity)
262
+ value :item_count, fn(:size, input.line_items)
263
+ ```
264
+
265
+ ### Nested Array Access
266
+
267
+ ```ruby
268
+ # With Sugar - Deep Field Access
269
+ value :all_product_names, input.orders.items.product.name
270
+ value :total_values, input.orders.items.product.base_price * input.orders.items.quantity
271
+
272
+ # Sugar-Free - Same Deep Access
273
+ value :all_product_names, input.orders.items.product.name
274
+ value :total_values, fn(:multiply, input.orders.items.product.base_price, input.orders.items.quantity)
275
+ ```
276
+
277
+ ### Mixed Operations
278
+
279
+ ```ruby
280
+ # With Sugar - Element-wise then Aggregation
281
+ value :line_totals, input.items.price * input.items.quantity
282
+ value :order_total, fn(:sum, line_totals)
283
+ value :avg_line_total, fn(:avg, line_totals)
284
+ trait :has_expensive, fn(:any?, expensive_items)
285
+
286
+ # Sugar-Free - Same Pattern
287
+ value :line_totals, fn(:multiply, input.items.price, input.items.quantity)
288
+ value :order_total, fn(:sum, line_totals)
289
+ value :avg_line_total, fn(:avg, line_totals)
290
+ trait :has_expensive, fn(:any?, expensive_items)
291
+ ```
292
+
293
+ ### Broadcasting Type Inference
294
+
295
+ The type system automatically infers appropriate types:
296
+ - `input.items.price` (float array) → inferred as `:float` per element
297
+ - `input.items.price * input.items.quantity` → element-wise `:float` result
298
+ - `fn(:sum, input.items.price)` → scalar `:float` result
299
+
213
300
  ## Cascade Logic
214
301
 
215
302
  Cascades are similar to Ruby when case, where each case is one of more trait reference and finally the value if that branch is true.
@@ -0,0 +1,45 @@
1
+ # Features
2
+
3
+ ## Core Features
4
+
5
+ ### [Unsatisfiability Detection](analysis-unsat-detection.md)
6
+ Analyzes rule combinations to detect logical impossibilities across dependency chains.
7
+
8
+ - Detects impossible combinations at compile-time
9
+ - Validates domain constraints
10
+ - Reports multiple errors
11
+
12
+ ### [Cascade Mutual Exclusion](analysis-cascade-mutual-exclusion.md)
13
+ Enables safe mutual recursion when cascade conditions are mutually exclusive.
14
+
15
+ - Allows mathematically sound recursive patterns
16
+ - Detects mutually exclusive conditions
17
+ - Prevents unsafe cycles while enabling safe ones
18
+
19
+ ### [Type Inference](analysis-type-inference.md)
20
+ Determines types from expressions and propagates them through dependencies.
21
+
22
+ - Infers types from literals and function calls
23
+ - Propagates types through dependency graph
24
+ - Validates function arguments
25
+
26
+ ### [Input Declarations](input-declaration-system.md)
27
+ Defines expected inputs with types and constraints.
28
+
29
+ - Type-specific declaration methods
30
+ - Domain validation at runtime
31
+ - Separates input metadata from business logic
32
+
33
+ ### [Performance](performance.md)
34
+ TODO: Add benchmark data
35
+ Processes large schemas with optimized algorithms.
36
+
37
+ - Result caching
38
+ - Selective evaluation
39
+
40
+ ## Integration
41
+
42
+ - Type inference uses input declarations
43
+ - Unsatisfiability detection uses type information for validation
44
+ - Cascade mutual exclusion integrates with dependency analysis and cycle detection
45
+ - Performance optimizations apply to all analysis passes