kumi 0.0.9 → 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 +187 -120
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -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/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 +16 -15
- 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 +20 -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
|
|
@@ -9,7 +9,7 @@ module Kumi
|
|
9
9
|
|
10
10
|
def visit(node)
|
11
11
|
return node.inspect unless node.respond_to?(:class)
|
12
|
-
|
12
|
+
|
13
13
|
case node
|
14
14
|
when nil then "nil"
|
15
15
|
when Array then visit_array(node)
|
@@ -38,7 +38,7 @@ module Kumi
|
|
38
38
|
|
39
39
|
def visit_array(node)
|
40
40
|
return "[]" if node.empty?
|
41
|
-
|
41
|
+
|
42
42
|
elements = node.map { |child| child_printer.visit(child) }
|
43
43
|
"[\n#{indent_str(2)}#{elements.join("\n#{indent_str(2)}")}\n#{indent_str}]"
|
44
44
|
end
|
@@ -48,7 +48,7 @@ module Kumi
|
|
48
48
|
value = node.public_send(field)
|
49
49
|
"#{field}: #{child_printer.visit(value)}"
|
50
50
|
end.join("\n#{indent_str(2)}")
|
51
|
-
|
51
|
+
|
52
52
|
"(Root\n#{indent_str(2)}#{fields}\n#{indent_str})"
|
53
53
|
end
|
54
54
|
|
@@ -64,7 +64,8 @@ module Kumi
|
|
64
64
|
fields = [":#{node.name}"]
|
65
65
|
fields << ":#{node.type}" if node.respond_to?(:type) && node.type
|
66
66
|
fields << "domain: #{node.domain.inspect}" if node.respond_to?(:domain) && node.domain
|
67
|
-
|
67
|
+
fields << "access_mode: #{node.access_mode.inspect}" if node.respond_to?(:access_mode) && node.access_mode
|
68
|
+
|
68
69
|
if node.respond_to?(:children) && !node.children.empty?
|
69
70
|
children_str = child_printer.visit(node.children)
|
70
71
|
"(InputDeclaration #{fields.join(' ')}\n#{child_indent}#{children_str}\n#{indent_str})"
|
@@ -75,14 +76,14 @@ module Kumi
|
|
75
76
|
|
76
77
|
def visit_call_expression(node)
|
77
78
|
return "(CallExpression :#{node.fn_name})" if node.args.empty?
|
78
|
-
|
79
|
+
|
79
80
|
args = node.args.map { |arg| child_printer.visit(arg) }
|
80
81
|
"(CallExpression :#{node.fn_name}\n#{indent_str(2)}#{args.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
81
82
|
end
|
82
83
|
|
83
84
|
def visit_array_expression(node)
|
84
85
|
return "(ArrayExpression)" if node.elements.empty?
|
85
|
-
|
86
|
+
|
86
87
|
elements = node.elements.map { |elem| child_printer.visit(elem) }
|
87
88
|
"(ArrayExpression\n#{indent_str(2)}#{elements.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
88
89
|
end
|
@@ -91,7 +92,7 @@ module Kumi
|
|
91
92
|
cases = node.cases.map do |case_expr|
|
92
93
|
"(#{visit(case_expr.condition)} #{visit(case_expr.result)})"
|
93
94
|
end.join("\n#{indent_str(2)}")
|
94
|
-
|
95
|
+
|
95
96
|
"(CascadeExpression\n#{indent_str(2)}#{cases}\n#{indent_str})"
|
96
97
|
end
|
97
98
|
|
@@ -117,17 +118,17 @@ module Kumi
|
|
117
118
|
|
118
119
|
def visit_hash_expression(node)
|
119
120
|
return "(HashExpression)" if node.pairs.empty?
|
120
|
-
|
121
|
+
|
121
122
|
pairs = node.pairs.map do |pair|
|
122
123
|
"(#{visit(pair.key)} #{visit(pair.value)})"
|
123
124
|
end.join("\n#{indent_str(2)}")
|
124
|
-
|
125
|
+
|
125
126
|
"(HashExpression\n#{indent_str(2)}#{pairs}\n#{indent_str})"
|
126
127
|
end
|
127
128
|
|
128
129
|
def visit_generic(node)
|
129
|
-
class_name = node.class.name&.split(
|
130
|
-
|
130
|
+
class_name = node.class.name&.split("::")&.last || node.class.to_s
|
131
|
+
|
131
132
|
if node.respond_to?(:children) && !node.children.empty?
|
132
133
|
children = node.children.map { |child| child_printer.visit(child) }
|
133
134
|
"(#{class_name}\n#{indent_str(2)}#{children.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
@@ -136,9 +137,9 @@ module Kumi
|
|
136
137
|
value = node[member]
|
137
138
|
"#{member}: #{child_printer.visit(value)}"
|
138
139
|
end
|
139
|
-
|
140
|
+
|
140
141
|
return "(#{class_name})" if fields.empty?
|
141
|
-
|
142
|
+
|
142
143
|
"(#{class_name}\n#{indent_str(2)}#{fields.join("\n#{indent_str(2)}")}\n#{indent_str})"
|
143
144
|
else
|
144
145
|
"(#{class_name} #{node.inspect})"
|
@@ -150,7 +151,7 @@ module Kumi
|
|
150
151
|
end
|
151
152
|
|
152
153
|
def indent_str(extra = 0)
|
153
|
-
|
154
|
+
" " * (@indent + extra)
|
154
155
|
end
|
155
156
|
|
156
157
|
def child_indent
|
@@ -158,4 +159,4 @@ module Kumi
|
|
158
159
|
end
|
159
160
|
end
|
160
161
|
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
|
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
# For field metadata declarations inside input blocks
|
6
|
+
InputDeclaration = Struct.new(:name, :domain, :type, :children, :access_mode) do
|
7
|
+
include Node
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
def children = self[:children] || []
|
10
|
+
def access_mode = self[:access_mode]
|
11
|
+
end
|
11
12
|
end
|
12
13
|
end
|