kumi 0.0.0 → 0.0.4
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 +270 -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 +22 -0
- data/lib/kumi/explain.rb +281 -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,132 @@
|
|
1
|
+
# Kumi Standard Function Library Reference
|
2
|
+
|
3
|
+
Kumi provides a rich library of built-in functions for use within `value` and `trait` expressions via `fn(...)`.
|
4
|
+
|
5
|
+
## Logical Functions
|
6
|
+
|
7
|
+
* **`all?`**: Check if all elements in collection are truthy
|
8
|
+
* **Usage**: `fn(:all?, array(any) arg1)` → `boolean`
|
9
|
+
* **`and`**: Logical AND of multiple conditions
|
10
|
+
* **Usage**: `fn(:and, boolean1, boolean2, ...)` → `boolean`
|
11
|
+
* **`any?`**: Check if any element in collection is truthy
|
12
|
+
* **Usage**: `fn(:any?, array(any) arg1)` → `boolean`
|
13
|
+
* **`none?`**: Check if no elements in collection are truthy
|
14
|
+
* **Usage**: `fn(:none?, array(any) arg1)` → `boolean`
|
15
|
+
* **`not`**: Logical NOT
|
16
|
+
* **Usage**: `fn(:not, boolean arg1)` → `boolean`
|
17
|
+
* **`or`**: Logical OR of multiple conditions
|
18
|
+
* **Usage**: `fn(:or, boolean1, boolean2, ...)` → `boolean`
|
19
|
+
|
20
|
+
## Comparison Functions
|
21
|
+
|
22
|
+
* **`!=`**: Inequality comparison
|
23
|
+
* **Usage**: `fn(:!=, any arg1, any arg2)` → `boolean`
|
24
|
+
* **`<`**: Less than comparison
|
25
|
+
* **Usage**: `fn(:<, float arg1, float arg2)` → `boolean`
|
26
|
+
* **`<=`**: Less than or equal comparison
|
27
|
+
* **Usage**: `fn(:<=, float arg1, float arg2)` → `boolean`
|
28
|
+
* **`==`**: Equality comparison
|
29
|
+
* **Usage**: `fn(:==, any arg1, any arg2)` → `boolean`
|
30
|
+
* **`>`**: Greater than comparison
|
31
|
+
* **Usage**: `fn(:>, float arg1, float arg2)` → `boolean`
|
32
|
+
* **`>=`**: Greater than or equal comparison
|
33
|
+
* **Usage**: `fn(:>=, float arg1, float arg2)` → `boolean`
|
34
|
+
* **`between?`**: Check if value is between min and max
|
35
|
+
* **Usage**: `fn(:between?, float arg1, float arg2, float arg3)` → `boolean`
|
36
|
+
|
37
|
+
## Math Functions
|
38
|
+
|
39
|
+
* **`abs`**: Absolute value
|
40
|
+
* **Usage**: `fn(:abs, float arg1)` → `float`
|
41
|
+
* **`add`**: Add two numbers
|
42
|
+
* **Usage**: `fn(:add, float arg1, float arg2)` → `float`
|
43
|
+
* **`ceil`**: Ceiling of number
|
44
|
+
* **Usage**: `fn(:ceil, float arg1)` → `integer`
|
45
|
+
* **`clamp`**: Clamp value between min and max
|
46
|
+
* **Usage**: `fn(:clamp, float arg1, float arg2, float arg3)` → `float`
|
47
|
+
* **`divide`**: Divide first number by second
|
48
|
+
* **Usage**: `fn(:divide, float arg1, float arg2)` → `float`
|
49
|
+
* **`floor`**: Floor of number
|
50
|
+
* **Usage**: `fn(:floor, float arg1)` → `integer`
|
51
|
+
* **`modulo`**: Modulo operation
|
52
|
+
* **Usage**: `fn(:modulo, float arg1, float arg2)` → `float`
|
53
|
+
* **`multiply`**: Multiply two numbers
|
54
|
+
* **Usage**: `fn(:multiply, float arg1, float arg2)` → `float`
|
55
|
+
* **`power`**: Raise first number to power of second
|
56
|
+
* **Usage**: `fn(:power, float arg1, float arg2)` → `float`
|
57
|
+
* **`round`**: Round number to specified precision
|
58
|
+
* **Usage**: `fn(:round, float1, float2, ...)` → `float`
|
59
|
+
* **`subtract`**: Subtract second number from first
|
60
|
+
* **Usage**: `fn(:subtract, float arg1, float arg2)` → `float`
|
61
|
+
* **`piecewise_sum`**: Accumulate over tiered ranges; returns [sum, marginal_rate]
|
62
|
+
* **Usage**: `fn(:piecewise_sum, float arg1, array(float) arg2, array(float) arg3)` → `array(float)`
|
63
|
+
|
64
|
+
## String Functions
|
65
|
+
|
66
|
+
* **`capitalize`**: Capitalize first letter of string
|
67
|
+
* **Usage**: `fn(:capitalize, string arg1)` → `string`
|
68
|
+
* **`concat`**: Concatenate multiple strings
|
69
|
+
* **Usage**: `fn(:concat, string1, string2, ...)` → `string`
|
70
|
+
* **`downcase`**: Convert string to lowercase
|
71
|
+
* **Usage**: `fn(:downcase, string arg1)` → `string`
|
72
|
+
* **`end_with?`**: Check if string ends with suffix
|
73
|
+
* **Usage**: `fn(:end_with?, string arg1, string arg2)` → `boolean`
|
74
|
+
* **`include?`**: Check if collection includes element
|
75
|
+
* **Usage**: `fn(:include?, array(any) arg1, any arg2)` → `boolean`
|
76
|
+
* **`length`**: Get collection length
|
77
|
+
* **Usage**: `fn(:length, array(any) arg1)` → `integer`
|
78
|
+
* **`start_with?`**: Check if string starts with prefix
|
79
|
+
* **Usage**: `fn(:start_with?, string arg1, string arg2)` → `boolean`
|
80
|
+
* **`strip`**: Remove leading and trailing whitespace
|
81
|
+
* **Usage**: `fn(:strip, string arg1)` → `string`
|
82
|
+
* **`upcase`**: Convert string to uppercase
|
83
|
+
* **Usage**: `fn(:upcase, string arg1)` → `string`
|
84
|
+
|
85
|
+
## Collection Functions
|
86
|
+
|
87
|
+
* **`empty?`**: Check if collection is empty
|
88
|
+
* **Usage**: `fn(:empty?, array(any) arg1)` → `boolean`
|
89
|
+
* **`first`**: Get first element of collection
|
90
|
+
* **Usage**: `fn(:first, array(any) arg1)` → `any`
|
91
|
+
* **`include?`**: Check if collection includes element
|
92
|
+
* **Usage**: `fn(:include?, array(any) arg1, any arg2)` → `boolean`
|
93
|
+
* **`last`**: Get last element of collection
|
94
|
+
* **Usage**: `fn(:last, array(any) arg1)` → `any`
|
95
|
+
* **`length`**: Get collection length
|
96
|
+
* **Usage**: `fn(:length, array(any) arg1)` → `integer`
|
97
|
+
* **`max`**: Find maximum value in numeric collection
|
98
|
+
* **Usage**: `fn(:max, array(float) arg1)` → `float`
|
99
|
+
* **`min`**: Find minimum value in numeric collection
|
100
|
+
* **Usage**: `fn(:min, array(float) arg1)` → `float`
|
101
|
+
* **`reverse`**: Reverse collection order
|
102
|
+
* **Usage**: `fn(:reverse, array(any) arg1)` → `array(any)`
|
103
|
+
* **`size`**: Get collection size
|
104
|
+
* **Usage**: `fn(:size, array(any) arg1)` → `integer`
|
105
|
+
* **`sort`**: Sort collection
|
106
|
+
* **Usage**: `fn(:sort, array(any) arg1)` → `array(any)`
|
107
|
+
* **`sum`**: Sum all numeric elements in collection
|
108
|
+
* **Usage**: `fn(:sum, array(float) arg1)` → `float`
|
109
|
+
* **`unique`**: Remove duplicate elements from collection
|
110
|
+
* **Usage**: `fn(:unique, array(any) arg1)` → `array(any)`
|
111
|
+
|
112
|
+
## Conditional Functions
|
113
|
+
|
114
|
+
* **`coalesce`**: Return first non-nil value
|
115
|
+
* **Usage**: `fn(:coalesce, any1, any2, ...)` → `any`
|
116
|
+
* **`conditional`**: Ternary conditional operator
|
117
|
+
* **Usage**: `fn(:conditional, boolean arg1, any arg2, any arg3)` → `any`
|
118
|
+
* **`if`**: If-then-else conditional
|
119
|
+
* **Usage**: `fn(:if, boolean1, boolean2, ...)` → `any`
|
120
|
+
|
121
|
+
## Type & Hash Functions
|
122
|
+
|
123
|
+
* **`at`**: Get element at index from array
|
124
|
+
* **Usage**: `fn(:at, array(any) arg1, integer arg2)` → `any`
|
125
|
+
* **`fetch`**: Fetch value from hash with optional default
|
126
|
+
* **Usage**: `fn(:fetch, hash(any, any)1, hash(any, any)2, ...)` → `any`
|
127
|
+
* **`has_key?`**: Check if hash has the given key
|
128
|
+
* **Usage**: `fn(:has_key?, hash(any, any) arg1, any arg2)` → `boolean`
|
129
|
+
* **`keys`**: Get all keys from hash
|
130
|
+
* **Usage**: `fn(:keys, hash(any, any) arg1)` → `array(any)`
|
131
|
+
* **`values`**: Get all values from hash
|
132
|
+
* **Usage**: `fn(:values, hash(any, any) arg1)` → `array(any)`
|
data/documents/SYNTAX.md
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
# Kumi DSL Syntax Reference
|
2
|
+
|
3
|
+
This document provides a comprehensive comparison of Kumi's DSL syntax showing both the sugar syntax (convenient, readable) and the underlying sugar-free syntax (explicit function calls).
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Schema Structure](#schema-structure)
|
8
|
+
- [Input Declarations](#input-declarations)
|
9
|
+
- [Value Declarations](#value-declarations)
|
10
|
+
- [Trait Declarations](#trait-declarations)
|
11
|
+
- [Expressions](#expressions)
|
12
|
+
- [Functions](#functions)
|
13
|
+
- [Cascade Logic](#cascade-logic)
|
14
|
+
- [References](#references)
|
15
|
+
|
16
|
+
## Schema Structure
|
17
|
+
|
18
|
+
### Basic Schema Template
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# With Sugar (Recommended)
|
22
|
+
module MySchema
|
23
|
+
extend Kumi::Schema
|
24
|
+
|
25
|
+
schema do
|
26
|
+
input do
|
27
|
+
# Input field declarations
|
28
|
+
end
|
29
|
+
|
30
|
+
# Traits and values using sugar syntax
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sugar-Free (Explicit)
|
35
|
+
module MySchema
|
36
|
+
extend Kumi::Schema
|
37
|
+
|
38
|
+
schema do
|
39
|
+
input do
|
40
|
+
# Input field declarations (same)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Traits and values using explicit function calls
|
44
|
+
end
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
## Input Declarations
|
49
|
+
|
50
|
+
Input declarations are the same in both syntaxes:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
input do
|
54
|
+
# Type-specific declarations
|
55
|
+
integer :age, domain: 18..65
|
56
|
+
string :status, domain: %w[active inactive suspended]
|
57
|
+
float :score, domain: 0.0..100.0
|
58
|
+
array :tags, elem: { type: :string }
|
59
|
+
hash :metadata, key: { type: :string }, val: { type: :any }
|
60
|
+
|
61
|
+
# Untyped fields
|
62
|
+
any :misc_data
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
## Value Declarations
|
67
|
+
|
68
|
+
### Arithmetic Operations
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# With Sugar
|
72
|
+
value :total_score, input.math_score + input.verbal_score + input.writing_score
|
73
|
+
value :average_score, total_score / 3
|
74
|
+
value :scaled_score, average_score * 1.5
|
75
|
+
value :final_score, scaled_score - input.penalty_points
|
76
|
+
|
77
|
+
# Sugar-Free
|
78
|
+
value :total_score, fn(:add, fn(:add, input.math_score, input.verbal_score), input.writing_score)
|
79
|
+
value :average_score, fn(:divide, total_score, 3)
|
80
|
+
value :scaled_score, fn(:multiply, average_score, 1.5)
|
81
|
+
value :final_score, fn(:subtract, scaled_score, input.penalty_points)
|
82
|
+
```
|
83
|
+
|
84
|
+
### Mathematical Functions
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# With Sugar (Note: Some functions require sugar-free syntax)
|
88
|
+
value :score_variance, fn(:power, fn(:subtract, input.score, average_score), 2)
|
89
|
+
value :max_possible, fn(:max, [input.math_score, input.verbal_score, input.writing_score])
|
90
|
+
value :min_score, fn(:min, [input.math_score, input.verbal_score])
|
91
|
+
|
92
|
+
# Sugar-Free
|
93
|
+
value :score_variance, fn(:power, fn(:subtract, input.score, average_score), 2)
|
94
|
+
value :max_possible, fn(:max, [input.math_score, input.verbal_score, input.writing_score])
|
95
|
+
value :min_score, fn(:min, [input.math_score, input.verbal_score])
|
96
|
+
```
|
97
|
+
|
98
|
+
## Trait Declarations
|
99
|
+
|
100
|
+
### Comparison Operations
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
# With Sugar
|
104
|
+
trait :high_scorer, input.total_score >= 1400
|
105
|
+
trait :perfect_math, input.math_score == 800
|
106
|
+
trait :needs_improvement, input.total_score < 1000
|
107
|
+
trait :above_average, input.average_score > 500
|
108
|
+
|
109
|
+
# Sugar-Free
|
110
|
+
trait :high_scorer, fn(:>=, input.total_score, 1400)
|
111
|
+
trait :perfect_math, fn(:==, input.math_score, 800)
|
112
|
+
trait :needs_improvement, fn(:<, input.total_score, 1000)
|
113
|
+
trait :above_average, fn(:>, input.average_score, 500)
|
114
|
+
```
|
115
|
+
|
116
|
+
### Logical Operations
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
# With Sugar
|
120
|
+
trait :excellent_student, high_scorer & perfect_math
|
121
|
+
trait :qualified, (input.age >= 18) & (input.score >= 1200) & (input.status == "active")
|
122
|
+
trait :needs_review, needs_improvement & (input.attempts > 2)
|
123
|
+
|
124
|
+
# Sugar-Free
|
125
|
+
trait :excellent_student, fn(:and, high_scorer, perfect_math)
|
126
|
+
trait :qualified, fn(:and, fn(:and, fn(:>=, input.age, 18), fn(:>=, input.score, 1200)), fn(:==, input.status, "active"))
|
127
|
+
trait :needs_review, fn(:and, needs_improvement, fn(:>, input.attempts, 2))
|
128
|
+
```
|
129
|
+
|
130
|
+
### String Operations
|
131
|
+
|
132
|
+
```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)
|
140
|
+
trait :starts_with_a, fn(:start_with?, input.name, "A")
|
141
|
+
trait :contains_space, fn(:contains?, input.name, " ")
|
142
|
+
```
|
143
|
+
|
144
|
+
## Expressions
|
145
|
+
|
146
|
+
### Complex Expressions
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# With Sugar
|
150
|
+
value :weighted_score, (input.math_score * 0.4) + (input.verbal_score * 0.3) + (input.writing_score * 0.3)
|
151
|
+
value :percentile_rank, ((scored_better_than / total_students) * 100).round(2)
|
152
|
+
|
153
|
+
# Sugar-Free
|
154
|
+
value :weighted_score, fn(:add,
|
155
|
+
fn(:add,
|
156
|
+
fn(:multiply, input.math_score, 0.4),
|
157
|
+
fn(:multiply, input.verbal_score, 0.3)
|
158
|
+
),
|
159
|
+
fn(:multiply, input.writing_score, 0.3)
|
160
|
+
)
|
161
|
+
value :percentile_rank, fn(:round,
|
162
|
+
fn(:multiply,
|
163
|
+
fn(:divide, scored_better_than, total_students),
|
164
|
+
100
|
165
|
+
),
|
166
|
+
2
|
167
|
+
)
|
168
|
+
```
|
169
|
+
|
170
|
+
### Collection Operations
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
# With Sugar
|
174
|
+
value :total_scores, input.score_array.sum
|
175
|
+
value :score_count, input.score_array.size
|
176
|
+
value :unique_scores, input.score_array.uniq.size
|
177
|
+
value :sorted_scores, input.score_array.sort
|
178
|
+
|
179
|
+
# Sugar-Free
|
180
|
+
value :total_scores, fn(:sum, input.score_array)
|
181
|
+
value :score_count, fn(:size, input.score_array)
|
182
|
+
value :unique_scores, fn(:size, fn(:unique, input.score_array))
|
183
|
+
value :sorted_scores, fn(:sort, input.score_array)
|
184
|
+
```
|
185
|
+
|
186
|
+
## Functions
|
187
|
+
|
188
|
+
### Built-in Functions Available
|
189
|
+
|
190
|
+
| Category | Sugar | Sugar-Free |
|
191
|
+
|----------|-------|------------|
|
192
|
+
| **Arithmetic** | `+`, `-`, `*`, `/`, `**` | `fn(:add, a, b)`, `fn(:subtract, a, b)`, etc. |
|
193
|
+
| **Comparison** | `>`, `<`, `>=`, `<=`, `==`, `!=` | `fn(:>, a, b)`, `fn(:<, a, b)`, etc. |
|
194
|
+
| **Logical** | `&` (AND only) | `fn(:and, a, b)`, `fn(:or, a, b)`, `fn(:not, a)` |
|
195
|
+
| **Math** | `abs`, `round`, `ceil`, `floor` | `fn(:abs, x)`, `fn(:round, x)`, etc. |
|
196
|
+
| **String** | `.length`, `.upcase`, `.downcase` | `fn(:string_length, s)`, `fn(:upcase, s)`, etc. |
|
197
|
+
| **Collection** | `.sum`, `.size`, `.max`, `.min` | `fn(:sum, arr)`, `fn(:size, arr)`, etc. |
|
198
|
+
|
199
|
+
### Custom Function Calls
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
# With Sugar (when available)
|
203
|
+
value :clamped_score, input.raw_score.clamp(0, 1600)
|
204
|
+
value :formatted_name, input.first_name + " " + input.last_name
|
205
|
+
|
206
|
+
# Sugar-Free (always available)
|
207
|
+
value :clamped_score, fn(:clamp, input.raw_score, 0, 1600)
|
208
|
+
value :formatted_name, fn(:add, fn(:add, input.first_name, " "), input.last_name)
|
209
|
+
```
|
210
|
+
|
211
|
+
## Cascade Logic
|
212
|
+
|
213
|
+
Cascade syntax is the same in both approaches, but conditions use different syntax:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
# With Sugar
|
217
|
+
value :grade_letter do
|
218
|
+
on :excellent_student, "A+"
|
219
|
+
on :high_scorer, "A"
|
220
|
+
on :above_average, "B"
|
221
|
+
on :needs_improvement, "C"
|
222
|
+
base "F"
|
223
|
+
end
|
224
|
+
|
225
|
+
# Sugar-Free
|
226
|
+
value :grade_letter do
|
227
|
+
on :excellent_student, "A+"
|
228
|
+
on :high_scorer, "A"
|
229
|
+
on :above_average, "B"
|
230
|
+
on :needs_improvement, "C"
|
231
|
+
base "F"
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
The difference is in how the traits referenced in cascade conditions are defined (see Trait Declarations above).
|
236
|
+
|
237
|
+
## References
|
238
|
+
|
239
|
+
### Referencing Other Declarations
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
# Both syntaxes (same)
|
243
|
+
trait :qualified_senior, qualified & (input.age >= 65)
|
244
|
+
value :bonus_points, qualified_senior ? 100 : 0
|
245
|
+
|
246
|
+
# Explicit reference syntax (both syntaxes)
|
247
|
+
trait :qualified_senior, ref(:qualified) & (input.age >= 65)
|
248
|
+
value :bonus_points, ref(:qualified_senior) ? 100 : 0
|
249
|
+
```
|
250
|
+
|
251
|
+
### Input Field References
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
# Both syntaxes (same)
|
255
|
+
input.field_name # Access input field
|
256
|
+
input.field_name.method # Call method on input field (sugar)
|
257
|
+
fn(:method, input.field_name) # Call method on input field (sugar-free)
|
258
|
+
```
|
259
|
+
|
260
|
+
## Complete Example Comparison
|
261
|
+
|
262
|
+
### With Sugar (Recommended)
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
module StudentEvaluation
|
266
|
+
extend Kumi::Schema
|
267
|
+
|
268
|
+
schema do
|
269
|
+
input do
|
270
|
+
integer :math_score, domain: 0..800
|
271
|
+
integer :verbal_score, domain: 0..800
|
272
|
+
integer :writing_score, domain: 0..800
|
273
|
+
integer :age, domain: 16..25
|
274
|
+
string :status, domain: %w[active inactive]
|
275
|
+
end
|
276
|
+
|
277
|
+
# Calculated values with sugar
|
278
|
+
value :total_score, input.math_score + input.verbal_score + input.writing_score
|
279
|
+
value :average_score, total_score / 3
|
280
|
+
value :scaled_average, fn(:round, average_score * 1.2, 2)
|
281
|
+
|
282
|
+
# Traits with sugar
|
283
|
+
trait :high_performer, total_score >= 2100
|
284
|
+
trait :math_excellence, input.math_score >= 750
|
285
|
+
trait :eligible_student, (input.age >= 18) & (input.status == "active")
|
286
|
+
trait :scholarship_candidate, high_performer & math_excellence & eligible_student
|
287
|
+
|
288
|
+
# Cascade with sugar-defined traits
|
289
|
+
value :scholarship_amount do
|
290
|
+
on :scholarship_candidate, 10000
|
291
|
+
on :high_performer, 5000
|
292
|
+
on :math_excellence, 2500
|
293
|
+
base 0
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
### Sugar-Free (Explicit)
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
module StudentEvaluation
|
303
|
+
extend Kumi::Schema
|
304
|
+
|
305
|
+
schema do
|
306
|
+
input do
|
307
|
+
integer :math_score, domain: 0..800
|
308
|
+
integer :verbal_score, domain: 0..800
|
309
|
+
integer :writing_score, domain: 0..800
|
310
|
+
integer :age, domain: 16..25
|
311
|
+
string :status, domain: %w[active inactive]
|
312
|
+
end
|
313
|
+
|
314
|
+
# Calculated values without sugar
|
315
|
+
value :total_score, fn(:add, fn(:add, input.math_score, input.verbal_score), input.writing_score)
|
316
|
+
value :average_score, fn(:divide, total_score, 3)
|
317
|
+
value :scaled_average, fn(:round, fn(:multiply, average_score, 1.2), 2)
|
318
|
+
|
319
|
+
# Traits without sugar
|
320
|
+
trait :high_performer, fn(:>=, total_score, 2100)
|
321
|
+
trait :math_excellence, fn(:>=, input.math_score, 750)
|
322
|
+
trait :eligible_student, fn(:and, fn(:>=, input.age, 18), fn(:==, input.status, "active"))
|
323
|
+
trait :scholarship_candidate, fn(:and, fn(:and, high_performer, math_excellence), eligible_student)
|
324
|
+
|
325
|
+
# Cascade with sugar-free defined traits
|
326
|
+
value :scholarship_amount do
|
327
|
+
on :scholarship_candidate, 10000
|
328
|
+
on :high_performer, 5000
|
329
|
+
on :math_excellence, 2500
|
330
|
+
base 0
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
```
|
335
|
+
|
336
|
+
## When to Use Each Syntax
|
337
|
+
|
338
|
+
### Use Sugar Syntax When:
|
339
|
+
- ✅ Writing schemas by hand
|
340
|
+
- ✅ Readability is important
|
341
|
+
- ✅ Working with simple to moderate complexity
|
342
|
+
- ✅ You want concise, Ruby-like expressions
|
343
|
+
|
344
|
+
### Use Sugar-Free Syntax When:
|
345
|
+
- ✅ Generating schemas programmatically
|
346
|
+
- ✅ Building dynamic schemas in loops/methods
|
347
|
+
- ✅ You need explicit control over function calls
|
348
|
+
- ✅ Working with complex nested expressions
|
349
|
+
- ✅ Debugging expression evaluation issues
|
350
|
+
|
351
|
+
## Syntax Limitations
|
352
|
+
|
353
|
+
### Sugar Syntax Limitations:
|
354
|
+
- Only supports `&` for logical AND (no `&&` due to Ruby precedence)
|
355
|
+
- No logical OR sugar syntax (must use `fn(:or, a, b)`)
|
356
|
+
- Limited operator precedence control
|
357
|
+
- Some Ruby methods not available as sugar
|
358
|
+
|
359
|
+
### Sugar-Free Advantages:
|
360
|
+
- Full access to all registered functions
|
361
|
+
- Clear operator precedence through explicit nesting
|
362
|
+
- Works in all contexts (including programmatic generation)
|
363
|
+
- More explicit about what operations are being performed
|
364
|
+
|
365
|
+
## Performance Notes
|
366
|
+
|
367
|
+
Both syntaxes compile to identical internal representations, so there is **no performance difference** between sugar and sugar-free syntax. Choose based on readability and maintenance needs.
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# Deep Schema Compilation and Evaluation Benchmark
|
2
|
+
#
|
3
|
+
# This benchmark measures Kumi's performance with increasingly deep dependency chains
|
4
|
+
# to understand how compilation and evaluation times scale with schema depth.
|
5
|
+
#
|
6
|
+
# What it tests:
|
7
|
+
# - Compilation time for schemas with deep dependency chains (50, 100, 150 levels)
|
8
|
+
# - Evaluation performance for computing results through long dependency paths
|
9
|
+
# - Stack-safe evaluation through iterative dependency resolution
|
10
|
+
#
|
11
|
+
# Schema structure:
|
12
|
+
# - input: single integer seed
|
13
|
+
# - values: chain of dependencies v0 = seed, v1 = v0 + 1, v2 = v1 + 2, ..., v_n = v_(n-1) + n
|
14
|
+
# - traits: conditional checks at each level (value > threshold)
|
15
|
+
# - cascade: final_result depends on first trait that evaluates to true
|
16
|
+
#
|
17
|
+
# Depth limits: Ruby stack overflow occurs around 200-300 levels depending on system,
|
18
|
+
# so we test with conservative depths (50, 100, 150) to ensure reliability.
|
19
|
+
#
|
20
|
+
# Usage: bundle exec ruby examples/deep_schema_compilation_and_evaluation_benchmark.rb
|
21
|
+
require "benchmark"
|
22
|
+
require "benchmark/ips"
|
23
|
+
require_relative "../lib/kumi"
|
24
|
+
|
25
|
+
# ------------------------------------------------------------------
|
26
|
+
# 1. Helper that builds a deep dependency chain schema
|
27
|
+
# ------------------------------------------------------------------
|
28
|
+
def build_deep_schema(depth)
|
29
|
+
Class.new do
|
30
|
+
extend Kumi::Schema
|
31
|
+
|
32
|
+
schema do
|
33
|
+
input { integer :seed }
|
34
|
+
|
35
|
+
# Build dependency chain: v0 = seed, v1 = v0 + 1, v2 = v1 + 2, etc.
|
36
|
+
value :v0, input.seed
|
37
|
+
|
38
|
+
(1...depth).each do |i|
|
39
|
+
value :"v#{i}", fn(:add, ref(:"v#{i-1}"), i)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Build traits that check if values exceed thresholds
|
43
|
+
# Use varying thresholds to create realistic evaluation scenarios
|
44
|
+
(0...depth).each do |i|
|
45
|
+
threshold = i * (i + 1) / 2 + 1000 # Quadratic growth to ensure variety
|
46
|
+
trait :"threshold_#{i}", fn(:>, ref(:"v#{i}"), threshold)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Final cascade that finds first trait that's true
|
50
|
+
value :final_result do
|
51
|
+
(0...depth).each do |i|
|
52
|
+
on :"threshold_#{i}", fn(:multiply, ref(:"v#{i}"), 2)
|
53
|
+
end
|
54
|
+
base ref(:"v#{depth-1}") # Default to final value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Conservative depths to avoid Ruby stack overflow
|
61
|
+
# Ruby stack depth limit is around 1000-2000 frames depending on the system
|
62
|
+
# Keep depths well below this limit for reliable operation
|
63
|
+
DEPTHS = [50, 100, 150, 200]
|
64
|
+
|
65
|
+
# ------------------------------------------------------------------
|
66
|
+
# 2. Measure compilation once per depth
|
67
|
+
# ------------------------------------------------------------------
|
68
|
+
compile_times = {}
|
69
|
+
schemas = {}
|
70
|
+
|
71
|
+
DEPTHS.each do |d|
|
72
|
+
puts "\n--- Building #{d}-deep schema ---"
|
73
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
74
|
+
schemas[d] = build_deep_schema(d)
|
75
|
+
compile_times[d] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
76
|
+
end
|
77
|
+
|
78
|
+
puts "=== compilation times ==="
|
79
|
+
compile_times.each do |d, t|
|
80
|
+
puts format("compile %3d-deep: %6.1f ms", d, t * 1_000)
|
81
|
+
end
|
82
|
+
puts
|
83
|
+
|
84
|
+
# ------------------------------------------------------------------
|
85
|
+
# 3. Pure evaluation benchmark – no compilation inside the loop
|
86
|
+
# ------------------------------------------------------------------
|
87
|
+
Benchmark.ips do |x|
|
88
|
+
schemas.each do |d, schema|
|
89
|
+
runner = schema.from(seed: 0) # memoised runner
|
90
|
+
x.report("eval #{d}-deep") { runner[:final_result] }
|
91
|
+
end
|
92
|
+
x.compare!
|
93
|
+
end
|
94
|
+
# Warming up --------------------------------------
|
95
|
+
# eval 50-deep 222.000 i/100ms
|
96
|
+
# eval 100-deep 57.000 i/100ms
|
97
|
+
# eval 150-deep 26.000 i/100ms
|
98
|
+
# Calculating -------------------------------------
|
99
|
+
# eval 50-deep 2.166k (± 1.9%) i/s (461.70 μs/i) - 10.878k in 5.024320s
|
100
|
+
# eval 100-deep 561.698 (± 1.4%) i/s (1.78 ms/i) - 2.850k in 5.075057s
|
101
|
+
# eval 150-deep 253.732 (± 0.8%) i/s (3.94 ms/i) - 1.274k in 5.021499s
|
102
|
+
|
103
|
+
# Comparison:
|
104
|
+
# eval 50-deep: 2165.9 i/s
|
105
|
+
# eval 100-deep: 561.7 i/s - 3.86x slower
|
106
|
+
# eval 150-deep: 253.7 i/s - 8.54x slower
|