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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +113 -3
  3. data/CHANGELOG.md +21 -1
  4. data/CLAUDE.md +387 -0
  5. data/README.md +270 -20
  6. data/docs/development/README.md +120 -0
  7. data/docs/development/error-reporting.md +361 -0
  8. data/documents/AST.md +126 -0
  9. data/documents/DSL.md +154 -0
  10. data/documents/FUNCTIONS.md +132 -0
  11. data/documents/SYNTAX.md +367 -0
  12. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
  13. data/examples/federal_tax_calculator_2024.rb +112 -0
  14. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
  15. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
  16. data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
  17. data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
  18. data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
  19. data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
  20. data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
  21. data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
  22. data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
  23. data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
  24. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
  25. data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
  26. data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
  27. data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
  28. data/lib/kumi/analyzer.rb +54 -0
  29. data/lib/kumi/atom_unsat_solver.rb +349 -0
  30. data/lib/kumi/compiled_schema.rb +41 -0
  31. data/lib/kumi/compiler.rb +127 -0
  32. data/lib/kumi/domain/enum_analyzer.rb +53 -0
  33. data/lib/kumi/domain/range_analyzer.rb +83 -0
  34. data/lib/kumi/domain/validator.rb +84 -0
  35. data/lib/kumi/domain/violation_formatter.rb +40 -0
  36. data/lib/kumi/domain.rb +8 -0
  37. data/lib/kumi/error_reporter.rb +164 -0
  38. data/lib/kumi/error_reporting.rb +95 -0
  39. data/lib/kumi/errors.rb +116 -0
  40. data/lib/kumi/evaluation_wrapper.rb +22 -0
  41. data/lib/kumi/explain.rb +281 -0
  42. data/lib/kumi/export/deserializer.rb +39 -0
  43. data/lib/kumi/export/errors.rb +12 -0
  44. data/lib/kumi/export/node_builders.rb +140 -0
  45. data/lib/kumi/export/node_registry.rb +38 -0
  46. data/lib/kumi/export/node_serializers.rb +156 -0
  47. data/lib/kumi/export/serializer.rb +23 -0
  48. data/lib/kumi/export.rb +33 -0
  49. data/lib/kumi/function_registry/collection_functions.rb +92 -0
  50. data/lib/kumi/function_registry/comparison_functions.rb +31 -0
  51. data/lib/kumi/function_registry/conditional_functions.rb +36 -0
  52. data/lib/kumi/function_registry/function_builder.rb +92 -0
  53. data/lib/kumi/function_registry/logical_functions.rb +42 -0
  54. data/lib/kumi/function_registry/math_functions.rb +72 -0
  55. data/lib/kumi/function_registry/string_functions.rb +54 -0
  56. data/lib/kumi/function_registry/type_functions.rb +51 -0
  57. data/lib/kumi/function_registry.rb +138 -0
  58. data/lib/kumi/input/type_matcher.rb +92 -0
  59. data/lib/kumi/input/validator.rb +52 -0
  60. data/lib/kumi/input/violation_creator.rb +50 -0
  61. data/lib/kumi/input.rb +8 -0
  62. data/lib/kumi/parser/build_context.rb +25 -0
  63. data/lib/kumi/parser/dsl.rb +12 -0
  64. data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
  65. data/lib/kumi/parser/expression_converter.rb +58 -0
  66. data/lib/kumi/parser/guard_rails.rb +43 -0
  67. data/lib/kumi/parser/input_builder.rb +94 -0
  68. data/lib/kumi/parser/input_proxy.rb +29 -0
  69. data/lib/kumi/parser/parser.rb +66 -0
  70. data/lib/kumi/parser/schema_builder.rb +172 -0
  71. data/lib/kumi/parser/sugar.rb +108 -0
  72. data/lib/kumi/schema.rb +49 -0
  73. data/lib/kumi/schema_instance.rb +43 -0
  74. data/lib/kumi/syntax/declarations.rb +23 -0
  75. data/lib/kumi/syntax/expressions.rb +30 -0
  76. data/lib/kumi/syntax/node.rb +46 -0
  77. data/lib/kumi/syntax/root.rb +12 -0
  78. data/lib/kumi/syntax/terminal_expressions.rb +27 -0
  79. data/lib/kumi/syntax.rb +9 -0
  80. data/lib/kumi/types/builder.rb +21 -0
  81. data/lib/kumi/types/compatibility.rb +86 -0
  82. data/lib/kumi/types/formatter.rb +24 -0
  83. data/lib/kumi/types/inference.rb +40 -0
  84. data/lib/kumi/types/normalizer.rb +70 -0
  85. data/lib/kumi/types/validator.rb +35 -0
  86. data/lib/kumi/types.rb +64 -0
  87. data/lib/kumi/version.rb +1 -1
  88. data/lib/kumi.rb +7 -3
  89. data/scripts/generate_function_docs.rb +59 -0
  90. data/test_impossible_cascade.rb +51 -0
  91. metadata +93 -10
  92. 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)`
@@ -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