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.
- checksums.yaml +4 -4
- data/CLAUDE.md +76 -174
- data/README.md +205 -52
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/SYNTAX.md +95 -8
- 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/docs/schema_metadata/broadcasts.md +53 -0
- data/docs/schema_metadata/cascades.md +45 -0
- data/docs/schema_metadata/declarations.md +54 -0
- data/docs/schema_metadata/dependencies.md +57 -0
- data/docs/schema_metadata/evaluation_order.md +29 -0
- data/docs/schema_metadata/examples.md +95 -0
- data/docs/schema_metadata/inferred_types.md +46 -0
- data/docs/schema_metadata/inputs.md +86 -0
- data/docs/schema_metadata.md +108 -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 +246 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +78 -38
- data/lib/kumi/analyzer/passes/input_collector.rb +91 -30
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +24 -25
- data/lib/kumi/analyzer/passes/toposorter.rb +44 -8
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -14
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/analyzer/passes/type_inferencer.rb +130 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +134 -56
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
- data/lib/kumi/analyzer.rb +16 -17
- data/lib/kumi/compiler.rb +188 -16
- data/lib/kumi/constraint_relationship_solver.rb +6 -6
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporting.rb +1 -1
- data/lib/kumi/explain.rb +32 -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/json_schema/generator.rb +63 -0
- data/lib/kumi/json_schema/validator.rb +25 -0
- data/lib/kumi/json_schema.rb +14 -0
- data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
- data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +5 -5
- data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +20 -20
- data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_builder.rb +41 -10
- data/lib/kumi/ruby_parser/input_field_proxy.rb +46 -0
- data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +4 -4
- data/lib/kumi/ruby_parser/nested_input.rb +15 -0
- data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/sugar.rb +62 -10
- data/lib/kumi/ruby_parser.rb +10 -0
- data/lib/kumi/schema.rb +10 -4
- data/lib/kumi/schema_instance.rb +6 -6
- data/lib/kumi/schema_metadata.rb +524 -0
- 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
- data/lib/kumi.rb +14 -0
- metadata +55 -25
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
- 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
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 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
|
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
|
166
183
|
|
167
|
-
#
|
168
|
-
#
|
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
|
-
|
173
|
-
|
174
|
-
on
|
175
|
-
on
|
176
|
-
base
|
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
|
-
|
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] # =>
|
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,44 @@ 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
|
+
<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
|
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](
|
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).
|
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
|
```
|
@@ -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
|
-
#
|
134
|
-
trait :long_name, input.name
|
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](
|
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
|