kumi 0.0.0 → 0.0.3
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/.rubocop.yml +113 -3
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +387 -0
- data/README.md +257 -20
- data/docs/development/README.md +120 -0
- data/docs/development/error-reporting.md +361 -0
- data/documents/AST.md +126 -0
- data/documents/DSL.md +154 -0
- data/documents/FUNCTIONS.md +132 -0
- data/documents/SYNTAX.md +367 -0
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
- data/examples/federal_tax_calculator_2024.rb +112 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
- data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
- data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
- data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
- data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
- data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
- data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
- data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
- data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
- data/lib/kumi/analyzer.rb +54 -0
- data/lib/kumi/atom_unsat_solver.rb +349 -0
- data/lib/kumi/compiled_schema.rb +41 -0
- data/lib/kumi/compiler.rb +127 -0
- data/lib/kumi/domain/enum_analyzer.rb +53 -0
- data/lib/kumi/domain/range_analyzer.rb +83 -0
- data/lib/kumi/domain/validator.rb +84 -0
- data/lib/kumi/domain/violation_formatter.rb +40 -0
- data/lib/kumi/domain.rb +8 -0
- data/lib/kumi/error_reporter.rb +164 -0
- data/lib/kumi/error_reporting.rb +95 -0
- data/lib/kumi/errors.rb +116 -0
- data/lib/kumi/evaluation_wrapper.rb +20 -0
- data/lib/kumi/explain.rb +282 -0
- data/lib/kumi/export/deserializer.rb +39 -0
- data/lib/kumi/export/errors.rb +12 -0
- data/lib/kumi/export/node_builders.rb +140 -0
- data/lib/kumi/export/node_registry.rb +38 -0
- data/lib/kumi/export/node_serializers.rb +156 -0
- data/lib/kumi/export/serializer.rb +23 -0
- data/lib/kumi/export.rb +33 -0
- data/lib/kumi/function_registry/collection_functions.rb +92 -0
- data/lib/kumi/function_registry/comparison_functions.rb +31 -0
- data/lib/kumi/function_registry/conditional_functions.rb +36 -0
- data/lib/kumi/function_registry/function_builder.rb +92 -0
- data/lib/kumi/function_registry/logical_functions.rb +42 -0
- data/lib/kumi/function_registry/math_functions.rb +72 -0
- data/lib/kumi/function_registry/string_functions.rb +54 -0
- data/lib/kumi/function_registry/type_functions.rb +51 -0
- data/lib/kumi/function_registry.rb +138 -0
- data/lib/kumi/input/type_matcher.rb +92 -0
- data/lib/kumi/input/validator.rb +52 -0
- data/lib/kumi/input/violation_creator.rb +50 -0
- data/lib/kumi/input.rb +8 -0
- data/lib/kumi/parser/build_context.rb +25 -0
- data/lib/kumi/parser/dsl.rb +12 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
- data/lib/kumi/parser/expression_converter.rb +58 -0
- data/lib/kumi/parser/guard_rails.rb +43 -0
- data/lib/kumi/parser/input_builder.rb +94 -0
- data/lib/kumi/parser/input_proxy.rb +29 -0
- data/lib/kumi/parser/parser.rb +66 -0
- data/lib/kumi/parser/schema_builder.rb +172 -0
- data/lib/kumi/parser/sugar.rb +108 -0
- data/lib/kumi/schema.rb +49 -0
- data/lib/kumi/schema_instance.rb +43 -0
- data/lib/kumi/syntax/declarations.rb +23 -0
- data/lib/kumi/syntax/expressions.rb +30 -0
- data/lib/kumi/syntax/node.rb +46 -0
- data/lib/kumi/syntax/root.rb +12 -0
- data/lib/kumi/syntax/terminal_expressions.rb +27 -0
- data/lib/kumi/syntax.rb +9 -0
- data/lib/kumi/types/builder.rb +21 -0
- data/lib/kumi/types/compatibility.rb +86 -0
- data/lib/kumi/types/formatter.rb +24 -0
- data/lib/kumi/types/inference.rb +40 -0
- data/lib/kumi/types/normalizer.rb +70 -0
- data/lib/kumi/types/validator.rb +35 -0
- data/lib/kumi/types.rb +64 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +7 -3
- data/scripts/generate_function_docs.rb +59 -0
- data/test_impossible_cascade.rb +51 -0
- metadata +93 -10
- data/sig/kumi.rbs +0 -4
@@ -0,0 +1,361 @@
|
|
1
|
+
# Error Reporting Standards
|
2
|
+
|
3
|
+
This guide provides comprehensive standards for error reporting in Kumi, ensuring consistent, localized error messages throughout the system.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Kumi uses a unified error reporting interface that:
|
8
|
+
- Provides consistent location information (file:line:column)
|
9
|
+
- Categorizes errors by type (syntax, semantic, type, runtime)
|
10
|
+
- Supports both immediate raising and error accumulation patterns
|
11
|
+
- Maintains backward compatibility with existing tests
|
12
|
+
- Enables enhanced error messages with suggestions and context
|
13
|
+
|
14
|
+
## Core Interface Components
|
15
|
+
|
16
|
+
### ErrorReporter Module
|
17
|
+
Central error reporting functionality with standardized error entries:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# Create structured error entry
|
21
|
+
entry = ErrorReporter.create_error(
|
22
|
+
"Error message",
|
23
|
+
location: node.loc,
|
24
|
+
type: :semantic,
|
25
|
+
context: { additional: "info" }
|
26
|
+
)
|
27
|
+
|
28
|
+
# Add error to accumulator
|
29
|
+
ErrorReporter.add_error(errors, "message", location: node.loc)
|
30
|
+
|
31
|
+
# Immediately raise error
|
32
|
+
ErrorReporter.raise_error("message", location: node.loc, error_class: Errors::SyntaxError)
|
33
|
+
```
|
34
|
+
|
35
|
+
### ErrorReporting Mixin
|
36
|
+
Convenient methods for classes that need error reporting:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class MyClass
|
40
|
+
include ErrorReporting
|
41
|
+
|
42
|
+
def process
|
43
|
+
# Accumulated errors (analyzer pattern)
|
44
|
+
report_error(errors, "message", location: node.loc, type: :semantic)
|
45
|
+
|
46
|
+
# Immediate errors (parser pattern)
|
47
|
+
raise_localized_error("message", location: node.loc, error_class: Errors::SyntaxError)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
## Implementation Patterns
|
53
|
+
|
54
|
+
### Parser Classes (Immediate Errors)
|
55
|
+
Parser classes should raise errors immediately when encountered:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class DslBuilderContext
|
59
|
+
include ErrorReporting
|
60
|
+
|
61
|
+
def validate_name(name, type, location)
|
62
|
+
return if name.is_a?(Symbol)
|
63
|
+
|
64
|
+
raise_syntax_error(
|
65
|
+
"The name for '#{type}' must be a Symbol, got #{name.class}",
|
66
|
+
location: location
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def raise_error(message, location)
|
71
|
+
# Legacy method - delegates to new interface
|
72
|
+
raise_syntax_error(message, location: location)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### Analyzer Passes (Accumulated Errors)
|
78
|
+
Analyzer passes should accumulate errors and report them at the end:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class MyAnalyzerPass < PassBase
|
82
|
+
def run(errors)
|
83
|
+
each_decl do |decl|
|
84
|
+
validate_declaration(decl, errors)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def validate_declaration(decl, errors)
|
91
|
+
# New error reporting method
|
92
|
+
report_error(
|
93
|
+
errors,
|
94
|
+
"Validation failed for #{decl.name}",
|
95
|
+
location: decl.loc,
|
96
|
+
type: :semantic
|
97
|
+
)
|
98
|
+
|
99
|
+
# Legacy method (backward compatible)
|
100
|
+
add_error(errors, decl.loc, "Legacy format message")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
## Location Resolution Best Practices
|
106
|
+
|
107
|
+
### Always Provide Location When Available
|
108
|
+
```ruby
|
109
|
+
# Good: Specific node location
|
110
|
+
report_error(errors, "Type mismatch", location: node.loc)
|
111
|
+
|
112
|
+
# Acceptable: Fallback location
|
113
|
+
report_error(errors, "Cycle detected", location: first_node&.loc || :cycle)
|
114
|
+
|
115
|
+
# Avoid: No location information
|
116
|
+
report_error(errors, "Error occurred", location: nil)
|
117
|
+
```
|
118
|
+
|
119
|
+
### Complex Error Location Resolution
|
120
|
+
For errors that span multiple nodes or are contextual:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
def report_cycle(cycle_path, errors)
|
124
|
+
# Find first declaration in cycle for location context
|
125
|
+
first_decl = find_declaration_by_name(cycle_path.first)
|
126
|
+
location = first_decl&.loc || :cycle
|
127
|
+
|
128
|
+
report_error(
|
129
|
+
errors,
|
130
|
+
"cycle detected: #{cycle_path.join(' → ')}",
|
131
|
+
location: location,
|
132
|
+
type: :semantic
|
133
|
+
)
|
134
|
+
end
|
135
|
+
|
136
|
+
def find_declaration_by_name(name)
|
137
|
+
return nil unless schema
|
138
|
+
|
139
|
+
schema.attributes.find { |attr| attr.name == name } ||
|
140
|
+
schema.traits.find { |trait| trait.name == name }
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Location Fallbacks
|
145
|
+
When AST location is not available, use meaningful symbolic locations:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
# Cycle detection
|
149
|
+
location = node.loc || :cycle
|
150
|
+
|
151
|
+
# Type inference failures
|
152
|
+
location = decl.loc || :type_inference
|
153
|
+
|
154
|
+
# Cross-reference resolution
|
155
|
+
location = ref_node.loc || :reference_resolution
|
156
|
+
```
|
157
|
+
|
158
|
+
## Error Categorization
|
159
|
+
|
160
|
+
### Error Types
|
161
|
+
- `:syntax` - Parse-time structural errors
|
162
|
+
- `:semantic` - Analysis-time logical errors
|
163
|
+
- `:type` - Type system violations
|
164
|
+
- `:runtime` - Execution-time failures
|
165
|
+
|
166
|
+
### Type-specific Methods
|
167
|
+
```ruby
|
168
|
+
# Syntax errors (parser)
|
169
|
+
report_syntax_error(errors, "Invalid syntax", location: loc)
|
170
|
+
raise_syntax_error("Invalid syntax", location: loc)
|
171
|
+
|
172
|
+
# Semantic errors (analyzer)
|
173
|
+
report_semantic_error(errors, "Logic error", location: loc)
|
174
|
+
|
175
|
+
# Type errors (type checker)
|
176
|
+
report_type_error(errors, "Type mismatch", location: loc)
|
177
|
+
```
|
178
|
+
|
179
|
+
## Enhanced Error Messages
|
180
|
+
|
181
|
+
### Basic Enhanced Errors
|
182
|
+
```ruby
|
183
|
+
report_enhanced_error(
|
184
|
+
errors,
|
185
|
+
"undefined reference to `missing_field`",
|
186
|
+
location: node.loc,
|
187
|
+
similar_names: ["missing_value", "missing_data"],
|
188
|
+
suggestions: [
|
189
|
+
"Check spelling of field name",
|
190
|
+
"Ensure field is declared in input block"
|
191
|
+
]
|
192
|
+
)
|
193
|
+
```
|
194
|
+
|
195
|
+
### Context-rich Errors
|
196
|
+
```ruby
|
197
|
+
report_error(
|
198
|
+
errors,
|
199
|
+
"Type mismatch in function call",
|
200
|
+
location: call_node.loc,
|
201
|
+
type: :type,
|
202
|
+
context: {
|
203
|
+
function: call_node.fn_name,
|
204
|
+
expected_type: expected,
|
205
|
+
actual_type: actual,
|
206
|
+
argument_position: position
|
207
|
+
}
|
208
|
+
)
|
209
|
+
```
|
210
|
+
|
211
|
+
## Backward Compatibility
|
212
|
+
|
213
|
+
### Legacy Format Support
|
214
|
+
The system supports both legacy `[location, message]` arrays and new `ErrorEntry` objects:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# Analyzer.format_errors handles both formats
|
218
|
+
def format_errors(errors)
|
219
|
+
errors.map do |error|
|
220
|
+
case error
|
221
|
+
when ErrorReporter::ErrorEntry
|
222
|
+
error.to_s # New format: "at file.rb:10:5: message"
|
223
|
+
when Array
|
224
|
+
loc, msg = error
|
225
|
+
"at #{loc || '?'}: #{msg}" # Legacy format
|
226
|
+
end
|
227
|
+
end.join("\n")
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
### Migration Strategy
|
232
|
+
1. **New code**: Use new error reporting methods (`report_error`, `raise_localized_error`)
|
233
|
+
2. **Existing code**: No changes required - `add_error` method maintained for compatibility
|
234
|
+
3. **Enhanced features**: Migrate to new methods to access suggestions, context, and categorization
|
235
|
+
|
236
|
+
## Testing Error Reporting
|
237
|
+
|
238
|
+
### Error Location Testing
|
239
|
+
```ruby
|
240
|
+
RSpec.describe "Error Location Verification" do
|
241
|
+
it "reports errors at correct locations" do
|
242
|
+
schema_code = <<~RUBY
|
243
|
+
Kumi.schema do
|
244
|
+
input { integer :age }
|
245
|
+
trait :adult, (input.age >= 18)
|
246
|
+
trait :adult, (input.age >= 21) # Line 4: Duplicate
|
247
|
+
end
|
248
|
+
RUBY
|
249
|
+
|
250
|
+
begin
|
251
|
+
eval(schema_code, binding, "test.rb", 1)
|
252
|
+
rescue Kumi::Errors::SemanticError => e
|
253
|
+
expect(e.message).to include("test.rb:4")
|
254
|
+
expect(e.message).to include("duplicated definition")
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
260
|
+
### Error Quality Testing
|
261
|
+
```ruby
|
262
|
+
it "provides comprehensive error information" do
|
263
|
+
error = expect_semantic_error do
|
264
|
+
schema do
|
265
|
+
input { string :name }
|
266
|
+
value :result, fn(:add, input.name, 5)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
expect(error.message).to include("add") # Function name
|
271
|
+
expect(error.message).to include("string") # Actual type
|
272
|
+
expect(error.message).to include("expects") # Clear expectation
|
273
|
+
expect(error.message).to match(/:\d+:/) # Line number
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
### Edge Case Testing
|
278
|
+
Use `spec/integration/potential_breakage_spec.rb` patterns:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
it "detects edge case that should break" do
|
282
|
+
expect do
|
283
|
+
schema do
|
284
|
+
input { integer :x }
|
285
|
+
# Edge case that might not be caught
|
286
|
+
value :result, some_edge_case_construct
|
287
|
+
end
|
288
|
+
end.to raise_error(Kumi::Errors::SemanticError)
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
## Performance Considerations
|
293
|
+
|
294
|
+
### Error Object Creation
|
295
|
+
- ErrorEntry objects are lightweight structs
|
296
|
+
- Location formatting is lazy (only when `to_s` is called)
|
297
|
+
- Context information is stored efficiently in hashes
|
298
|
+
|
299
|
+
### Batch Error Processing
|
300
|
+
For analyzer passes processing many nodes:
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
def run(errors)
|
304
|
+
# Batch process nodes to minimize error object creation
|
305
|
+
invalid_nodes = collect_invalid_nodes
|
306
|
+
|
307
|
+
invalid_nodes.each do |node|
|
308
|
+
report_error(errors, "Invalid: #{node.name}", location: node.loc)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
## Common Patterns and Anti-patterns
|
314
|
+
|
315
|
+
### ✅ Good Patterns
|
316
|
+
```ruby
|
317
|
+
# Clear, specific error messages
|
318
|
+
report_error(errors, "argument 1 of `fn(:add)` expects float, got string", location: arg.loc)
|
319
|
+
|
320
|
+
# Proper location resolution
|
321
|
+
location = node.loc || fallback_location_for_context
|
322
|
+
|
323
|
+
# Type-appropriate error categorization
|
324
|
+
report_type_error(errors, "type mismatch", location: node.loc)
|
325
|
+
```
|
326
|
+
|
327
|
+
### ❌ Anti-patterns
|
328
|
+
```ruby
|
329
|
+
# Vague error messages
|
330
|
+
report_error(errors, "error", location: node.loc)
|
331
|
+
|
332
|
+
# Missing location information
|
333
|
+
report_error(errors, "something failed", location: nil)
|
334
|
+
|
335
|
+
# Wrong error categorization
|
336
|
+
report_syntax_error(errors, "type mismatch", location: node.loc) # Should be type error
|
337
|
+
```
|
338
|
+
|
339
|
+
## Error Message Guidelines
|
340
|
+
|
341
|
+
### Message Format
|
342
|
+
- Start with lowercase (automatic capitalization in display)
|
343
|
+
- Be specific about what failed and why
|
344
|
+
- Include relevant context (function names, types, values)
|
345
|
+
- Avoid technical jargon in user-facing messages
|
346
|
+
|
347
|
+
### Examples
|
348
|
+
```ruby
|
349
|
+
# Good messages
|
350
|
+
"argument 1 of `fn(:add)` expects float, got input field `name` of declared type string"
|
351
|
+
"duplicated definition `adult`"
|
352
|
+
"undefined reference to `missing_field`"
|
353
|
+
"cycle detected: a → b → a"
|
354
|
+
|
355
|
+
# Messages to improve
|
356
|
+
"validation failed"
|
357
|
+
"error in processing"
|
358
|
+
"something went wrong"
|
359
|
+
```
|
360
|
+
|
361
|
+
This error reporting system ensures that users get clear, actionable feedback about issues in their Kumi schemas, with precise location information to help them fix problems quickly.
|
data/documents/AST.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# Kumi AST Reference
|
2
|
+
|
3
|
+
## Core Node Types
|
4
|
+
|
5
|
+
**Root**: Schema container
|
6
|
+
```ruby
|
7
|
+
Root = Struct.new(:inputs, :attributes, :traits)
|
8
|
+
```
|
9
|
+
|
10
|
+
**FieldDecl**: Input field metadata
|
11
|
+
```ruby
|
12
|
+
FieldDecl = Struct.new(:name, :domain, :type)
|
13
|
+
# DSL: integer :age, domain: 18..65 → FieldDecl(:age, 18..65, :integer)
|
14
|
+
```
|
15
|
+
|
16
|
+
**Trait**: Boolean predicate
|
17
|
+
```ruby
|
18
|
+
Trait = Struct.new(:name, :expression)
|
19
|
+
# DSL: trait :adult, (input.age >= 18) → Trait(:adult, CallExpression(...))
|
20
|
+
```
|
21
|
+
|
22
|
+
**Attribute**: Computed value
|
23
|
+
```ruby
|
24
|
+
Attribute = Struct.new(:name, :expression)
|
25
|
+
# DSL: value :total, fn(:add, a, b) → Attribute(:total, CallExpression(:add, [...]))
|
26
|
+
```
|
27
|
+
|
28
|
+
## Expression Nodes
|
29
|
+
|
30
|
+
**CallExpression**: Function calls and operators
|
31
|
+
```ruby
|
32
|
+
CallExpression = Struct.new(:fn_name, :args)
|
33
|
+
def &(other) = CallExpression.new(:and, [self, other]) # Enable chaining
|
34
|
+
```
|
35
|
+
|
36
|
+
**FieldRef**: Field access (`input.field_name`)
|
37
|
+
```ruby
|
38
|
+
FieldRef = Struct.new(:name)
|
39
|
+
# Has operator methods: >=, <=, >, <, ==, != that create CallExpression nodes
|
40
|
+
```
|
41
|
+
|
42
|
+
**Binding**: References to other declarations
|
43
|
+
```ruby
|
44
|
+
Binding = Struct.new(:name)
|
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)])
|
48
|
+
```
|
49
|
+
|
50
|
+
**Literal**: Constants (`18`, `"text"`, `true`)
|
51
|
+
```ruby
|
52
|
+
Literal = Struct.new(:value)
|
53
|
+
```
|
54
|
+
|
55
|
+
**ListExpression**: Arrays (`[1, 2, 3]`)
|
56
|
+
```ruby
|
57
|
+
ListExpression = Struct.new(:elements)
|
58
|
+
```
|
59
|
+
|
60
|
+
## Cascade Expressions (Conditional Values)
|
61
|
+
|
62
|
+
**CascadeExpression**: Container for conditional logic
|
63
|
+
```ruby
|
64
|
+
CascadeExpression = Struct.new(:cases)
|
65
|
+
```
|
66
|
+
|
67
|
+
**WhenCaseExpression**: Individual conditions
|
68
|
+
```ruby
|
69
|
+
WhenCaseExpression = Struct.new(:condition, :result)
|
70
|
+
```
|
71
|
+
|
72
|
+
**Case type mappings**:
|
73
|
+
- `on :a, :b, result` → `condition: fn(:all?, ref(:a), ref(:b))`
|
74
|
+
- `on_any :a, :b, result` → `condition: fn(:any?, ref(:a), ref(:b))`
|
75
|
+
- `base result` → `condition: literal(true)`
|
76
|
+
|
77
|
+
## Key Nuances
|
78
|
+
|
79
|
+
**Operator methods on FieldRef**: Enable `input.age >= 18` syntax by defining operators that create `CallExpression` nodes
|
80
|
+
|
81
|
+
**CallExpression `&` method**: Enables expression chaining like `(expr1) & (expr2)`
|
82
|
+
|
83
|
+
**Node immutability**: AST nodes are frozen after construction; analysis results stored separately
|
84
|
+
|
85
|
+
**Location tracking**: All nodes include file/line/column for error reporting
|
86
|
+
|
87
|
+
**Tree traversal**: Each node defines `children` method for recursive processing
|
88
|
+
|
89
|
+
**Expression wrapping**: During parsing, raw values auto-convert to `Literal` nodes via `ensure_syntax()`
|
90
|
+
|
91
|
+
## Common Expression Trees
|
92
|
+
|
93
|
+
**Simple**: `(input.age >= 18)`
|
94
|
+
```
|
95
|
+
CallExpression(:>=, [FieldRef(:age), Literal(18)])
|
96
|
+
```
|
97
|
+
|
98
|
+
**Chained AND**: `(input.age >= 21) & (input.verified == true)`
|
99
|
+
```
|
100
|
+
CallExpression(:and, [
|
101
|
+
CallExpression(:>=, [FieldRef(:age), Literal(21)]),
|
102
|
+
CallExpression(:==, [FieldRef(:verified), Literal(true)])
|
103
|
+
])
|
104
|
+
```
|
105
|
+
|
106
|
+
**Composite Trait**: `adult & verified & high_income`
|
107
|
+
```
|
108
|
+
CallExpression(:and, [
|
109
|
+
CallExpression(:and, [
|
110
|
+
Binding(:adult),
|
111
|
+
Binding(:verified)
|
112
|
+
]),
|
113
|
+
Binding(:high_income)
|
114
|
+
])
|
115
|
+
```
|
116
|
+
|
117
|
+
**Mixed Composition**: `adult & (input.score > 80) & verified`
|
118
|
+
```
|
119
|
+
CallExpression(:and, [
|
120
|
+
CallExpression(:and, [
|
121
|
+
Binding(:adult),
|
122
|
+
CallExpression(:>, [FieldRef(:score), Literal(80)])
|
123
|
+
]),
|
124
|
+
Binding(:verified)
|
125
|
+
])
|
126
|
+
```
|
data/documents/DSL.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
# Kumi DSL Reference
|
2
|
+
|
3
|
+
Kumi is a declarative language for defining, analyzing, and executing complex business logic. It compiles rules into a verifiable dependency graph, ensuring that logic is **sound, maintainable, and free of contradictions** before execution (as much as possible given the current library implementation).
|
4
|
+
|
5
|
+
-----
|
6
|
+
|
7
|
+
## Guiding Principles
|
8
|
+
|
9
|
+
Kumi's design is opinionated and guides you toward creating robust and analyzable business logic.
|
10
|
+
|
11
|
+
* **Logic as Code, Not Just Configuration**: Rules are expressed in a clean, readable DSL that can be version-controlled and tested.
|
12
|
+
* **Provable Correctness**: A multi-pass analyzer statically verifies your schema, detecting duplicates, circular dependencies, type errors, and even **logically impossible conditions** (e.g., `age < 25 AND age > 65`) at compile time.
|
13
|
+
* **Explicit Data Contracts**: The mandatory `input` block serves as a formal, self-documenting contract for the data your schema expects, enabling runtime validation of types and domain constraints.
|
14
|
+
* **Composition Over Complexity**: Complex rules are built by composing simpler, named concepts (`trait`s), rather than creating large, monolithic blocks of logic.
|
15
|
+
|
16
|
+
-----
|
17
|
+
|
18
|
+
## Core Syntax
|
19
|
+
|
20
|
+
A Kumi schema contains an `input` block to declare its data contract, followed by `trait` and `value` definitions.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
schema do
|
24
|
+
# 1. Define the data contract for this schema.
|
25
|
+
input do
|
26
|
+
# ... field declarations
|
27
|
+
end
|
28
|
+
|
29
|
+
# 2. Define reusable boolean predicates (traits).
|
30
|
+
# ... trait definitions
|
31
|
+
|
32
|
+
# 3. Define computed fields (values).
|
33
|
+
# ... value definitions
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
-----
|
38
|
+
|
39
|
+
## Input Fields: The Data Contract
|
40
|
+
|
41
|
+
The `input` block declares the schema's data dependencies. All external data must be accessed via the `input` object (e.g., `input.age`).
|
42
|
+
|
43
|
+
### **Declaration Methods**
|
44
|
+
|
45
|
+
The preferred way to declare fields is with **type-specific methods**, which provide compile-time type checking and runtime validation.
|
46
|
+
|
47
|
+
* **Primitives**:
|
48
|
+
```ruby
|
49
|
+
string :name
|
50
|
+
integer :age, domain: 18..65
|
51
|
+
float :score, domain: 0.0..100.0
|
52
|
+
boolean :is_active
|
53
|
+
```
|
54
|
+
* **Collections**:
|
55
|
+
```ruby
|
56
|
+
array :tags, elem: { type: :string }
|
57
|
+
hash :metadata, key: { type: :string }, val: { type: :any }
|
58
|
+
```
|
59
|
+
|
60
|
+
### **Domain Constraints**
|
61
|
+
|
62
|
+
Attach validation rules directly to input fields using `domain:`. These are checked when data is loaded.
|
63
|
+
|
64
|
+
* **Range**: `domain: 1..100` or `0.0...1.0` (exclusive end)
|
65
|
+
* **Enumeration**: `domain: %w[pending active archived]`
|
66
|
+
* **Custom Logic**: `domain: ->(value) { value.even? }`
|
67
|
+
|
68
|
+
-----
|
69
|
+
|
70
|
+
## Traits: Named Logical Predicates
|
71
|
+
|
72
|
+
A **`trait`** is a named expression that **must evaluate to a boolean**. Traits are the fundamental building blocks of logic, defining reusable, verifiable conditions.
|
73
|
+
|
74
|
+
### **Defining & Composing Traits**
|
75
|
+
|
76
|
+
Traits are defined with a parenthesized expression and composed using the `&` operator. This composition is strictly **conjunctive (logical AND)**, a key constraint that enables Kumi's powerful static analysis.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
# Base Traits
|
80
|
+
trait :is_adult, (input.age >= 18)
|
81
|
+
trait :is_verified, (input.status == "verified")
|
82
|
+
|
83
|
+
# Composite Trait (is_adult AND is_verified)
|
84
|
+
trait :can_proceed, is_adult & is_verified
|
85
|
+
|
86
|
+
# Mix bare trait names with inline expressions
|
87
|
+
trait :is_eligible, is_adult & is_verified & (input.score > 50)
|
88
|
+
```
|
89
|
+
|
90
|
+
-----
|
91
|
+
|
92
|
+
## Values: Computed Fields
|
93
|
+
|
94
|
+
A **`value`** is a named expression that computes a field of any type.
|
95
|
+
|
96
|
+
### **Simple Values**
|
97
|
+
|
98
|
+
Values can be defined with expressions using `input` fields, functions (`fn`), and references to other values.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
value :full_name, fn(:concat, input.first_name, " ", input.last_name)
|
102
|
+
value :discounted_price, fn(:multiply, input.base_price, 0.8)
|
103
|
+
```
|
104
|
+
|
105
|
+
### **Conditional Values (Cascades)**
|
106
|
+
|
107
|
+
For conditional logic, a `value` takes a block to create a **cascade**. Cascades select a result based on a series of conditions, which **must reference named `trait`s**. This enforces clarity by separating the *what* (the condition's name) from the *how* (its implementation).
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
value :access_level do
|
111
|
+
# `on` implies AND: user must be :premium AND :verified.
|
112
|
+
on :premium, :verified, "Full Access"
|
113
|
+
|
114
|
+
# `on_any` implies OR: user can be :staff OR :admin.
|
115
|
+
on_any :staff, :admin, "Elevated Access"
|
116
|
+
|
117
|
+
# `on_none` implies NOT (A OR B): user is neither :blocked NOR :suspended.
|
118
|
+
on_none :blocked, :suspended, "Limited Access"
|
119
|
+
|
120
|
+
# `base` is the default if no other conditions match.
|
121
|
+
base "No Access"
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
-----
|
126
|
+
|
127
|
+
## The Kumi Pattern: Separating AND vs. OR Logic
|
128
|
+
|
129
|
+
Kumi intentionally enforces a pattern for handling different types of logic to maximize clarity and analyzability.
|
130
|
+
|
131
|
+
* **`trait`s and `&` are for AND logic**: Use `trait` composition to build up a set of conditions that must *all* be true. This is your primary tool for defining constraints.
|
132
|
+
|
133
|
+
* **`value` cascades are for OR logic**: Use `on_any` within a `value` cascade to handle conditions where *any* one of several predicates is sufficient. This is the idiomatic way to express disjunctive logic.
|
134
|
+
|
135
|
+
This separation forces complex `OR` conditions to be handled within the clear, readable structure of a cascade, rather than being hidden inside a complex `trait` definition.
|
136
|
+
|
137
|
+
-----
|
138
|
+
|
139
|
+
## Best Practices
|
140
|
+
|
141
|
+
* **Prefer Small, Composable Traits**: Avoid creating large, monolithic traits with many `&` conditions. Instead, define smaller, named traits and compose them.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
# AVOID: Hard to read and reuse
|
145
|
+
trait :eligible, (input.age >= 18) & (input.status == "active") & (input.score > 50)
|
146
|
+
|
147
|
+
# PREFER: Clear, reusable, and self-documenting
|
148
|
+
trait :is_adult, (input.age >= 18)
|
149
|
+
trait :is_active, (input.status == "active")
|
150
|
+
trait :has_good_score, (input.score > 50)
|
151
|
+
trait :is_eligible, is_adult & is_active & has_good_score
|
152
|
+
```
|
153
|
+
|
154
|
+
* **Name All Conditions**: If you need to use a condition in a `value` cascade, define it as a `trait` first. This gives the condition a clear business name and makes the cascade easier to read.
|