kumi 0.0.8 → 0.0.10
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 +28 -44
- data/README.md +188 -108
- data/docs/AST.md +8 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +22 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/features/s-expression-printer.md +77 -0
- data/docs/schema_metadata.md +7 -7
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +0 -2
- data/lib/kumi/compiler.rb +6 -275
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
- data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
- data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
- data/lib/kumi/core/cascade_executor_builder.rb +132 -0
- data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
- data/lib/kumi/core/compiler/function_invoker.rb +55 -0
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
- data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
- data/lib/kumi/core/compiler_base.rb +137 -0
- data/lib/kumi/core/explain.rb +2 -2
- data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
- data/lib/kumi/core/function_registry/function_builder.rb +5 -3
- data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +32 -10
- data/lib/kumi/core/nested_structure_utils.rb +78 -0
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- data/lib/kumi/core/schema_instance.rb +4 -0
- data/lib/kumi/core/vectorized_function_builder.rb +88 -0
- data/lib/kumi/errors.rb +2 -0
- data/lib/kumi/js/compiler.rb +878 -0
- data/lib/kumi/js/function_registry.rb +333 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +61 -1
- data/lib/kumi/schema.rb +1 -1
- data/lib/kumi/support/s_expression_printer.rb +162 -0
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/node.rb +34 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +1 -1
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +22 -4
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
@@ -0,0 +1,333 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Js
|
5
|
+
# Maps Ruby function registry to JavaScript implementations
|
6
|
+
# Each function maintains the same signature and behavior as the Ruby version
|
7
|
+
module FunctionRegistry
|
8
|
+
# Generate complete JavaScript function registry
|
9
|
+
def self.generate_js_registry
|
10
|
+
{
|
11
|
+
# Mathematical functions
|
12
|
+
**math_functions,
|
13
|
+
|
14
|
+
# Comparison functions
|
15
|
+
**comparison_functions,
|
16
|
+
|
17
|
+
# Logical functions
|
18
|
+
**logical_functions,
|
19
|
+
|
20
|
+
# String functions
|
21
|
+
**string_functions,
|
22
|
+
|
23
|
+
# Collection functions
|
24
|
+
**collection_functions,
|
25
|
+
|
26
|
+
# Conditional functions
|
27
|
+
**conditional_functions,
|
28
|
+
|
29
|
+
# Type functions
|
30
|
+
**type_functions,
|
31
|
+
|
32
|
+
# Statistical functions
|
33
|
+
**stat_functions
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate JavaScript code for the function registry
|
38
|
+
def self.generate_js_code(functions_required: nil)
|
39
|
+
registry = generate_js_registry
|
40
|
+
|
41
|
+
# Filter registry to only include required functions if specified
|
42
|
+
registry = registry.slice(*functions_required) if functions_required && !functions_required.empty?
|
43
|
+
|
44
|
+
functions_js = registry.map do |name, js_code|
|
45
|
+
# Handle symbol names that need quoting in JS
|
46
|
+
js_name = name.to_s.match?(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) ? name : "\"#{name}\""
|
47
|
+
" #{js_name}: #{js_code}"
|
48
|
+
end.join(",\n")
|
49
|
+
|
50
|
+
<<~JAVASCRIPT
|
51
|
+
const kumiRegistry = {
|
52
|
+
#{functions_js}
|
53
|
+
};
|
54
|
+
JAVASCRIPT
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.math_functions
|
58
|
+
{
|
59
|
+
# Basic arithmetic
|
60
|
+
add: "(a, b) => a + b",
|
61
|
+
subtract: "(a, b) => a - b",
|
62
|
+
multiply: "(a, b) => a * b",
|
63
|
+
divide: "(a, b) => a / b",
|
64
|
+
modulo: "(a, b) => a % b",
|
65
|
+
power: "(a, b) => Math.pow(a, b)",
|
66
|
+
|
67
|
+
# Unary operations
|
68
|
+
abs: "(a) => Math.abs(a)",
|
69
|
+
floor: "(a) => Math.floor(a)",
|
70
|
+
ceil: "(a) => Math.ceil(a)",
|
71
|
+
|
72
|
+
# Special operations
|
73
|
+
round: "(a, precision = 0) => Number(a.toFixed(precision))",
|
74
|
+
clamp: "(value, min, max) => Math.min(max, Math.max(min, value))",
|
75
|
+
|
76
|
+
# Complex mathematical operations
|
77
|
+
piecewise_sum: <<~JS.strip
|
78
|
+
(value, breaks, rates) => {
|
79
|
+
if (breaks.length !== rates.length) {
|
80
|
+
throw new Error('breaks & rates size mismatch');
|
81
|
+
}
|
82
|
+
#{' '}
|
83
|
+
let acc = 0.0;
|
84
|
+
let previous = 0.0;
|
85
|
+
let marginal = rates[rates.length - 1];
|
86
|
+
#{' '}
|
87
|
+
for (let i = 0; i < breaks.length; i++) {
|
88
|
+
const upper = breaks[i];
|
89
|
+
const rate = rates[i];
|
90
|
+
#{' '}
|
91
|
+
if (value <= upper) {
|
92
|
+
marginal = rate;
|
93
|
+
acc += (value - previous) * rate;
|
94
|
+
break;
|
95
|
+
} else {
|
96
|
+
acc += (upper - previous) * rate;
|
97
|
+
previous = upper;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
#{' '}
|
101
|
+
return [acc, marginal];
|
102
|
+
}
|
103
|
+
JS
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.comparison_functions
|
108
|
+
{
|
109
|
+
# Equality operators (using strict equality)
|
110
|
+
"==": "(a, b) => a === b",
|
111
|
+
"!=": "(a, b) => a !== b",
|
112
|
+
|
113
|
+
# Comparison operators
|
114
|
+
">": "(a, b) => a > b",
|
115
|
+
"<": "(a, b) => a < b",
|
116
|
+
">=": "(a, b) => a >= b",
|
117
|
+
"<=": "(a, b) => a <= b",
|
118
|
+
|
119
|
+
# Range comparison
|
120
|
+
between?: "(value, min, max) => value >= min && value <= max"
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.logical_functions
|
125
|
+
{
|
126
|
+
# Basic logical operations
|
127
|
+
and: "(...conditions) => conditions.every(x => x)",
|
128
|
+
or: "(...conditions) => conditions.some(x => x)",
|
129
|
+
not: "(a) => !a",
|
130
|
+
|
131
|
+
# Collection logical operations
|
132
|
+
all?: "(collection) => collection.every(x => x)",
|
133
|
+
any?: "(collection) => collection.some(x => x)",
|
134
|
+
none?: "(collection) => !collection.some(x => x)",
|
135
|
+
|
136
|
+
# Element-wise AND for cascades - works on arrays with hierarchical broadcasting
|
137
|
+
cascade_and: <<~JS.strip
|
138
|
+
(...conditions) => {
|
139
|
+
if (conditions.length === 0) return false;
|
140
|
+
if (conditions.length === 1) return conditions[0];
|
141
|
+
#{' '}
|
142
|
+
// Start with first condition
|
143
|
+
let result = conditions[0];
|
144
|
+
#{' '}
|
145
|
+
// Apply element-wise AND with remaining conditions using hierarchical broadcasting
|
146
|
+
for (let i = 1; i < conditions.length; i++) {
|
147
|
+
result = kumiRuntime.elementWiseAnd(result, conditions[i]);
|
148
|
+
}
|
149
|
+
#{' '}
|
150
|
+
return result;
|
151
|
+
}
|
152
|
+
JS
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.string_functions
|
157
|
+
{
|
158
|
+
# String transformations
|
159
|
+
upcase: "(str) => str.toString().toUpperCase()",
|
160
|
+
downcase: "(str) => str.toString().toLowerCase()",
|
161
|
+
capitalize: "(str) => { const s = str.toString(); return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); }",
|
162
|
+
strip: "(str) => str.toString().trim()",
|
163
|
+
|
164
|
+
# String queries
|
165
|
+
string_length: "(str) => str.toString().length",
|
166
|
+
length: "(str) => str.toString().length",
|
167
|
+
|
168
|
+
# String inclusion checks
|
169
|
+
string_include?: "(str, substr) => str.toString().includes(substr.toString())",
|
170
|
+
includes?: "(str, substr) => str.toString().includes(substr.toString())",
|
171
|
+
contains?: "(str, substr) => str.toString().includes(substr.toString())",
|
172
|
+
start_with?: "(str, prefix) => str.toString().startsWith(prefix.toString())",
|
173
|
+
end_with?: "(str, suffix) => str.toString().endsWith(suffix.toString())",
|
174
|
+
|
175
|
+
# String building
|
176
|
+
concat: "(...strings) => strings.map(s => s.toString()).join('')"
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.collection_functions
|
181
|
+
{
|
182
|
+
# Collection queries (reducers)
|
183
|
+
empty?: "(collection) => collection.length === 0",
|
184
|
+
size: "(collection) => collection.length",
|
185
|
+
|
186
|
+
# Element access
|
187
|
+
first: "(collection) => collection[0]",
|
188
|
+
last: "(collection) => collection[collection.length - 1]",
|
189
|
+
|
190
|
+
# Mathematical operations on collections
|
191
|
+
sum: "(collection) => collection.reduce((a, b) => a + b, 0)",
|
192
|
+
min: "(collection) => Math.min(...collection)",
|
193
|
+
max: "(collection) => Math.max(...collection)",
|
194
|
+
|
195
|
+
# Collection operations
|
196
|
+
include?: "(collection, element) => collection.includes(element)",
|
197
|
+
reverse: "(collection) => [...collection].reverse()",
|
198
|
+
sort: "(collection) => [...collection].sort()",
|
199
|
+
unique: "(collection) => [...new Set(collection)]",
|
200
|
+
flatten: "(collection) => collection.flat(Infinity)",
|
201
|
+
flatten_one: "(collection) => collection.flat(1)",
|
202
|
+
flatten_deep: "(collection) => collection.flat(Infinity)",
|
203
|
+
|
204
|
+
# Array transformation functions
|
205
|
+
map_multiply: "(collection, factor) => collection.map(x => x * factor)",
|
206
|
+
map_add: "(collection, value) => collection.map(x => x + value)",
|
207
|
+
map_conditional: "(collection, condition_value, true_value, false_value) => collection.map(x => x === condition_value ? true_value : false_value)",
|
208
|
+
|
209
|
+
# Range/index functions
|
210
|
+
build_array: "(size) => Array.from({length: size}, (_, i) => i)",
|
211
|
+
range: "(start, finish) => Array.from({length: finish - start}, (_, i) => start + i)",
|
212
|
+
|
213
|
+
# Array slicing and grouping
|
214
|
+
each_slice: <<~JS.strip,
|
215
|
+
(array, size) => {
|
216
|
+
const result = [];
|
217
|
+
for (let i = 0; i < array.length; i += size) {
|
218
|
+
result.push(array.slice(i, i + size));
|
219
|
+
}
|
220
|
+
return result;
|
221
|
+
}
|
222
|
+
JS
|
223
|
+
|
224
|
+
join: "(array, separator = '') => array.map(x => x.toString()).join(separator.toString())",
|
225
|
+
|
226
|
+
map_join_rows: "(array_of_arrays, row_separator = '', column_separator = '\\n') => array_of_arrays.map(row => row.join(row_separator.toString())).join(column_separator.toString())",
|
227
|
+
|
228
|
+
# Higher-order collection functions
|
229
|
+
map_with_index: "(collection) => collection.map((item, index) => [item, index])",
|
230
|
+
indices: "(collection) => Array.from({length: collection.length}, (_, i) => i)",
|
231
|
+
|
232
|
+
# Conditional aggregation functions
|
233
|
+
count_if: "(condition_array) => condition_array.filter(x => x === true).length",
|
234
|
+
sum_if: "(value_array, condition_array) => value_array.reduce((sum, value, i) => sum + (condition_array[i] ? value : 0), 0)",
|
235
|
+
avg_if: <<~JS.strip,
|
236
|
+
(value_array, condition_array) => {
|
237
|
+
const pairs = value_array.map((value, i) => [value, condition_array[i]]);
|
238
|
+
const true_values = pairs.filter(([_, condition]) => condition).map(([value, _]) => value);
|
239
|
+
return true_values.length === 0 ? 0.0 : true_values.reduce((a, b) => a + b, 0) / true_values.length;
|
240
|
+
}
|
241
|
+
JS
|
242
|
+
|
243
|
+
# Flattening utilities for hierarchical data
|
244
|
+
any_across: "(nested_array) => nested_array.flat(Infinity).some(x => x)",
|
245
|
+
all_across: "(nested_array) => nested_array.flat(Infinity).every(x => x)",
|
246
|
+
count_across: "(nested_array) => nested_array.flat(Infinity).length"
|
247
|
+
}
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.conditional_functions
|
251
|
+
{
|
252
|
+
conditional: "(condition, true_value, false_value) => condition ? true_value : false_value",
|
253
|
+
if: "(condition, true_value, false_value = null) => condition ? true_value : false_value",
|
254
|
+
coalesce: "(...values) => values.find(v => v != null)"
|
255
|
+
}
|
256
|
+
end
|
257
|
+
|
258
|
+
def self.type_functions
|
259
|
+
{
|
260
|
+
# Hash/object operations - assuming TypeFunctions exists
|
261
|
+
fetch: "(hash, key, default_value = null) => hash.hasOwnProperty(key) ? hash[key] : default_value",
|
262
|
+
has_key?: "(hash, key) => hash.hasOwnProperty(key)",
|
263
|
+
keys: "(hash) => Object.keys(hash)",
|
264
|
+
values: "(hash) => Object.values(hash)",
|
265
|
+
at: "(collection, index) => collection[index]"
|
266
|
+
}
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.stat_functions
|
270
|
+
{
|
271
|
+
# Statistical functions - mirror Ruby StatFunctions behavior
|
272
|
+
avg: "(array) => array.length === 0 ? 0 : array.reduce((sum, val) => sum + val, 0) / array.length",
|
273
|
+
mean: "(array) => array.length === 0 ? 0 : array.reduce((sum, val) => sum + val, 0) / array.length",
|
274
|
+
median: <<~JS.strip,
|
275
|
+
(array) => {
|
276
|
+
if (array.length === 0) return 0;
|
277
|
+
const sorted = [...array].sort((a, b) => a - b);
|
278
|
+
const mid = Math.floor(sorted.length / 2);
|
279
|
+
return sorted.length % 2 === 0#{' '}
|
280
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
281
|
+
: sorted[mid];
|
282
|
+
}
|
283
|
+
JS
|
284
|
+
variance: <<~JS.strip,
|
285
|
+
(array) => {
|
286
|
+
if (array.length === 0) return 0;
|
287
|
+
const mean = array.reduce((sum, val) => sum + val, 0) / array.length;
|
288
|
+
const squaredDiffs = array.map(val => Math.pow(val - mean, 2));
|
289
|
+
return squaredDiffs.reduce((sum, val) => sum + val, 0) / array.length;
|
290
|
+
}
|
291
|
+
JS
|
292
|
+
stdev: <<~JS.strip,
|
293
|
+
(array) => {
|
294
|
+
if (array.length === 0) return 0;
|
295
|
+
const mean = array.reduce((sum, val) => sum + val, 0) / array.length;
|
296
|
+
const squaredDiffs = array.map(val => Math.pow(val - mean, 2));
|
297
|
+
const variance = squaredDiffs.reduce((sum, val) => sum + val, 0) / array.length;
|
298
|
+
return Math.sqrt(variance);
|
299
|
+
}
|
300
|
+
JS
|
301
|
+
sample_variance: <<~JS.strip,
|
302
|
+
(array) => {
|
303
|
+
if (array.length <= 1) return 0;
|
304
|
+
const mean = array.reduce((sum, val) => sum + val, 0) / array.length;
|
305
|
+
const squaredDiffs = array.map(val => Math.pow(val - mean, 2));
|
306
|
+
return squaredDiffs.reduce((sum, val) => sum + val, 0) / (array.length - 1);
|
307
|
+
}
|
308
|
+
JS
|
309
|
+
sample_stdev: <<~JS.strip,
|
310
|
+
(array) => {
|
311
|
+
if (array.length <= 1) return 0;
|
312
|
+
const mean = array.reduce((sum, val) => sum + val, 0) / array.length;
|
313
|
+
const squaredDiffs = array.map(val => Math.pow(val - mean, 2));
|
314
|
+
const variance = squaredDiffs.reduce((sum, val) => sum + val, 0) / (array.length - 1);
|
315
|
+
return Math.sqrt(variance);
|
316
|
+
}
|
317
|
+
JS
|
318
|
+
# Flattened statistics functions
|
319
|
+
flat_size: "(nestedArray) => nestedArray.flat(Infinity).length",
|
320
|
+
flat_sum: "(nestedArray) => nestedArray.flat(Infinity).reduce((sum, val) => sum + val, 0)",
|
321
|
+
flat_avg: <<~JS.strip,
|
322
|
+
(nestedArray) => {
|
323
|
+
const flattened = nestedArray.flat(Infinity);
|
324
|
+
return flattened.length === 0 ? 0 : flattened.reduce((sum, val) => sum + val, 0) / flattened.length;
|
325
|
+
}
|
326
|
+
JS
|
327
|
+
flat_max: "(nestedArray) => Math.max(...nestedArray.flat(Infinity))",
|
328
|
+
flat_min: "(nestedArray) => Math.min(...nestedArray.flat(Infinity))"
|
329
|
+
}
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
data/lib/kumi/js.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Js
|
5
|
+
# JavaScript transpiler for Kumi schemas
|
6
|
+
# Extends the existing compiler architecture to output JavaScript instead of Ruby lambdas
|
7
|
+
|
8
|
+
# Export a compiled schema to JavaScript
|
9
|
+
def self.compile(schema_class, **options)
|
10
|
+
syntax_tree = schema_class.__syntax_tree__
|
11
|
+
analyzer_result = schema_class.__analyzer_result__
|
12
|
+
|
13
|
+
compiler = Compiler.new(syntax_tree, analyzer_result)
|
14
|
+
compiler.compile(**options)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Export to JavaScript file
|
18
|
+
def self.export_to_file(schema_class, filename, **options)
|
19
|
+
js_code = compile(schema_class, **options)
|
20
|
+
File.write(filename, js_code)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/kumi/registry.rb
CHANGED
@@ -1,6 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Kumi
|
4
|
+
# Public interface for registering custom functions in Kumi schemas
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
# Kumi::Registry.register(:my_function) do |x|
|
8
|
+
# x * 2
|
9
|
+
# end
|
2
10
|
module Registry
|
3
11
|
extend Core::FunctionRegistry
|
12
|
+
|
4
13
|
Entry = Core::FunctionRegistry::FunctionBuilder::Entry
|
5
14
|
|
6
15
|
@functions = Core::FunctionRegistry::CORE_FUNCTIONS.transform_values(&:dup)
|
@@ -17,11 +26,62 @@ module Kumi
|
|
17
26
|
end
|
18
27
|
end
|
19
28
|
|
29
|
+
# Register a custom function with the Kumi function registry
|
30
|
+
#
|
31
|
+
# Example:
|
32
|
+
# Kumi::Registry.register(:double) do |x|
|
33
|
+
# x * 2
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # Use in schema:
|
37
|
+
# value :doubled, fn(:double, input.number)
|
20
38
|
def register(name, &block)
|
21
39
|
@lock.synchronize do
|
22
40
|
raise FrozenError, "registry is frozen" if @frozen
|
41
|
+
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
42
|
+
|
43
|
+
fn_lambda = block.is_a?(Proc) ? block : ->(*args) { yield(*args) }
|
44
|
+
@functions[name] = Entry.new(
|
45
|
+
fn: fn_lambda,
|
46
|
+
arity: fn_lambda.arity,
|
47
|
+
param_types: [:any],
|
48
|
+
return_type: :any,
|
49
|
+
description: nil,
|
50
|
+
inverse: nil,
|
51
|
+
reducer: false
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Register a custom function with detailed metadata for type and domain validation
|
57
|
+
#
|
58
|
+
# Example:
|
59
|
+
# Kumi::Registry.register_with_metadata(
|
60
|
+
# :add_tax,
|
61
|
+
# ->(amount, rate) { amount * (1 + rate) },
|
62
|
+
# arity: 2,
|
63
|
+
# param_types: [:float, :float],
|
64
|
+
# return_type: :float,
|
65
|
+
# description: "Adds tax to an amount",
|
66
|
+
# )
|
67
|
+
#
|
68
|
+
# # Use in schema:
|
69
|
+
# value :total, fn(:add_tax, input.price, input.tax_rate)
|
70
|
+
def register_with_metadata(name, fn_lambda, arity:, param_types: [:any], return_type: :any, description: nil, inverse: nil,
|
71
|
+
reducer: false)
|
72
|
+
@lock.synchronize do
|
73
|
+
raise FrozenError, "registry is frozen" if @frozen
|
74
|
+
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
23
75
|
|
24
|
-
|
76
|
+
@functions[name] = Entry.new(
|
77
|
+
fn: fn_lambda,
|
78
|
+
arity: arity,
|
79
|
+
param_types: param_types,
|
80
|
+
return_type: return_type,
|
81
|
+
description: description,
|
82
|
+
inverse: inverse,
|
83
|
+
reducer: reducer
|
84
|
+
)
|
25
85
|
end
|
26
86
|
end
|
27
87
|
|
data/lib/kumi/schema.rb
CHANGED
@@ -8,7 +8,7 @@ module Kumi
|
|
8
8
|
|
9
9
|
Inspector = Struct.new(:syntax_tree, :analyzer_result, :compiled_schema) do
|
10
10
|
def inspect
|
11
|
-
"#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect},
|
11
|
+
"#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, compiled_schema: #{compiled_schema.inspect}>"
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Support
|
5
|
+
class SExpressionPrinter
|
6
|
+
def initialize(indent: 0)
|
7
|
+
@indent = indent
|
8
|
+
end
|
9
|
+
|
10
|
+
def visit(node)
|
11
|
+
return node.inspect unless node.respond_to?(:class)
|
12
|
+
|
13
|
+
case node
|
14
|
+
when nil then "nil"
|
15
|
+
when Array then visit_array(node)
|
16
|
+
when Kumi::Syntax::Root then visit_root(node)
|
17
|
+
when Kumi::Syntax::ValueDeclaration then visit_value_declaration(node)
|
18
|
+
when Kumi::Syntax::TraitDeclaration then visit_trait_declaration(node)
|
19
|
+
when Kumi::Syntax::InputDeclaration then visit_input_declaration(node)
|
20
|
+
when Kumi::Syntax::CallExpression then visit_call_expression(node)
|
21
|
+
when Kumi::Syntax::ArrayExpression then visit_array_expression(node)
|
22
|
+
when Kumi::Syntax::CascadeExpression then visit_cascade_expression(node)
|
23
|
+
when Kumi::Syntax::CaseExpression then visit_case_expression(node)
|
24
|
+
when Kumi::Syntax::InputReference then visit_input_reference(node)
|
25
|
+
when Kumi::Syntax::InputElementReference then visit_input_element_reference(node)
|
26
|
+
when Kumi::Syntax::DeclarationReference then visit_declaration_reference(node)
|
27
|
+
when Kumi::Syntax::Literal then visit_literal(node)
|
28
|
+
when Kumi::Syntax::HashExpression then visit_hash_expression(node)
|
29
|
+
else visit_generic(node)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.print(node, indent: 0)
|
34
|
+
new(indent: indent).visit(node)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def visit_array(node)
|
40
|
+
return "[]" if node.empty?
|
41
|
+
|
42
|
+
elements = node.map { |child| child_printer.visit(child) }
|
43
|
+
"[\n#{indent_str(2)}#{elements.join("\n#{indent_str(2)}")}\n#{indent_str}]"
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit_root(node)
|
47
|
+
fields = %i[inputs attributes traits].map do |field|
|
48
|
+
value = node.public_send(field)
|
49
|
+
"#{field}: #{child_printer.visit(value)}"
|
50
|
+
end.join("\n#{indent_str(2)}")
|
51
|
+
|
52
|
+
"(Root\n#{indent_str(2)}#{fields}\n#{indent_str})"
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_value_declaration(node)
|
56
|
+
"(ValueDeclaration :#{node.name}\n#{child_indent}#{child_printer.visit(node.expression)}\n#{indent_str})"
|
57
|
+
end
|
58
|
+
|
59
|
+
def visit_trait_declaration(node)
|
60
|
+
"(TraitDeclaration :#{node.name}\n#{child_indent}#{child_printer.visit(node.expression)}\n#{indent_str})"
|
61
|
+
end
|
62
|
+
|
63
|
+
def visit_input_declaration(node)
|
64
|
+
fields = [":#{node.name}"]
|
65
|
+
fields << ":#{node.type}" if node.respond_to?(:type) && node.type
|
66
|
+
fields << "domain: #{node.domain.inspect}" if node.respond_to?(:domain) && node.domain
|
67
|
+
fields << "access_mode: #{node.access_mode.inspect}" if node.respond_to?(:access_mode) && node.access_mode
|
68
|
+
|
69
|
+
if node.respond_to?(:children) && !node.children.empty?
|
70
|
+
children_str = child_printer.visit(node.children)
|
71
|
+
"(InputDeclaration #{fields.join(' ')}\n#{child_indent}#{children_str}\n#{indent_str})"
|
72
|
+
else
|
73
|
+
"(InputDeclaration #{fields.join(' ')})"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def visit_call_expression(node)
|
78
|
+
return "(CallExpression :#{node.fn_name})" if node.args.empty?
|
79
|
+
|
80
|
+
args = node.args.map { |arg| child_printer.visit(arg) }
|
81
|
+
"(CallExpression :#{node.fn_name}\n#{indent_str(2)}#{args.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
82
|
+
end
|
83
|
+
|
84
|
+
def visit_array_expression(node)
|
85
|
+
return "(ArrayExpression)" if node.elements.empty?
|
86
|
+
|
87
|
+
elements = node.elements.map { |elem| child_printer.visit(elem) }
|
88
|
+
"(ArrayExpression\n#{indent_str(2)}#{elements.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
89
|
+
end
|
90
|
+
|
91
|
+
def visit_cascade_expression(node)
|
92
|
+
cases = node.cases.map do |case_expr|
|
93
|
+
"(#{visit(case_expr.condition)} #{visit(case_expr.result)})"
|
94
|
+
end.join("\n#{indent_str(2)}")
|
95
|
+
|
96
|
+
"(CascadeExpression\n#{indent_str(2)}#{cases}\n#{indent_str})"
|
97
|
+
end
|
98
|
+
|
99
|
+
def visit_case_expression(node)
|
100
|
+
"(CaseExpression #{visit(node.condition)} #{visit(node.result)})"
|
101
|
+
end
|
102
|
+
|
103
|
+
def visit_input_reference(node)
|
104
|
+
"(InputReference :#{node.name})"
|
105
|
+
end
|
106
|
+
|
107
|
+
def visit_input_element_reference(node)
|
108
|
+
"(InputElementReference #{node.path.map(&:to_s).join('.')})"
|
109
|
+
end
|
110
|
+
|
111
|
+
def visit_declaration_reference(node)
|
112
|
+
"(DeclarationReference :#{node.name})"
|
113
|
+
end
|
114
|
+
|
115
|
+
def visit_literal(node)
|
116
|
+
"(Literal #{node.value.inspect})"
|
117
|
+
end
|
118
|
+
|
119
|
+
def visit_hash_expression(node)
|
120
|
+
return "(HashExpression)" if node.pairs.empty?
|
121
|
+
|
122
|
+
pairs = node.pairs.map do |pair|
|
123
|
+
"(#{visit(pair.key)} #{visit(pair.value)})"
|
124
|
+
end.join("\n#{indent_str(2)}")
|
125
|
+
|
126
|
+
"(HashExpression\n#{indent_str(2)}#{pairs}\n#{indent_str})"
|
127
|
+
end
|
128
|
+
|
129
|
+
def visit_generic(node)
|
130
|
+
class_name = node.class.name&.split("::")&.last || node.class.to_s
|
131
|
+
|
132
|
+
if node.respond_to?(:children) && !node.children.empty?
|
133
|
+
children = node.children.map { |child| child_printer.visit(child) }
|
134
|
+
"(#{class_name}\n#{indent_str(2)}#{children.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
135
|
+
elsif node.respond_to?(:members)
|
136
|
+
fields = node.members.reject { |m| m == :loc }.map do |member|
|
137
|
+
value = node[member]
|
138
|
+
"#{member}: #{child_printer.visit(value)}"
|
139
|
+
end
|
140
|
+
|
141
|
+
return "(#{class_name})" if fields.empty?
|
142
|
+
|
143
|
+
"(#{class_name}\n#{indent_str(2)}#{fields.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
144
|
+
else
|
145
|
+
"(#{class_name} #{node.inspect})"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def child_printer
|
150
|
+
@child_printer ||= self.class.new(indent: @indent + 2)
|
151
|
+
end
|
152
|
+
|
153
|
+
def indent_str(extra = 0)
|
154
|
+
" " * (@indent + extra)
|
155
|
+
end
|
156
|
+
|
157
|
+
def child_indent
|
158
|
+
indent_str(2)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
5
|
+
ArrayExpression = Struct.new(:elements) do
|
6
|
+
include Node
|
7
7
|
|
8
|
-
|
8
|
+
def children = elements
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
end
|
10
|
+
def size
|
11
|
+
elements.size
|
13
12
|
end
|
13
|
+
end
|
14
14
|
end
|
15
15
|
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
5
|
+
CaseExpression = Struct.new(:condition, :result) do
|
6
|
+
include Node
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
def children = [condition, result]
|
9
|
+
end
|
10
10
|
end
|
11
11
|
end
|