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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +28 -44
  3. data/README.md +188 -108
  4. data/docs/AST.md +8 -1
  5. data/docs/FUNCTIONS.md +52 -8
  6. data/docs/compiler_design_principles.md +86 -0
  7. data/docs/features/README.md +22 -2
  8. data/docs/features/hierarchical-broadcasting.md +349 -0
  9. data/docs/features/javascript-transpiler.md +148 -0
  10. data/docs/features/performance.md +1 -3
  11. data/docs/features/s-expression-printer.md +77 -0
  12. data/docs/schema_metadata.md +7 -7
  13. data/examples/game_of_life.rb +2 -4
  14. data/lib/kumi/analyzer.rb +0 -2
  15. data/lib/kumi/compiler.rb +6 -275
  16. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
  17. data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
  18. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
  19. data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
  20. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
  21. data/lib/kumi/core/cascade_executor_builder.rb +132 -0
  22. data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
  23. data/lib/kumi/core/compiler/function_invoker.rb +55 -0
  24. data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
  25. data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
  26. data/lib/kumi/core/compiler_base.rb +137 -0
  27. data/lib/kumi/core/explain.rb +2 -2
  28. data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
  29. data/lib/kumi/core/function_registry/function_builder.rb +5 -3
  30. data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
  31. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  32. data/lib/kumi/core/function_registry.rb +32 -10
  33. data/lib/kumi/core/nested_structure_utils.rb +78 -0
  34. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
  35. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  36. data/lib/kumi/core/schema_instance.rb +4 -0
  37. data/lib/kumi/core/vectorized_function_builder.rb +88 -0
  38. data/lib/kumi/errors.rb +2 -0
  39. data/lib/kumi/js/compiler.rb +878 -0
  40. data/lib/kumi/js/function_registry.rb +333 -0
  41. data/lib/kumi/js.rb +23 -0
  42. data/lib/kumi/registry.rb +61 -1
  43. data/lib/kumi/schema.rb +1 -1
  44. data/lib/kumi/support/s_expression_printer.rb +162 -0
  45. data/lib/kumi/syntax/array_expression.rb +6 -6
  46. data/lib/kumi/syntax/call_expression.rb +4 -4
  47. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  48. data/lib/kumi/syntax/case_expression.rb +4 -4
  49. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  50. data/lib/kumi/syntax/hash_expression.rb +4 -4
  51. data/lib/kumi/syntax/input_declaration.rb +6 -5
  52. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  53. data/lib/kumi/syntax/input_reference.rb +5 -5
  54. data/lib/kumi/syntax/literal.rb +4 -4
  55. data/lib/kumi/syntax/node.rb +34 -34
  56. data/lib/kumi/syntax/root.rb +6 -6
  57. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  58. data/lib/kumi/syntax/value_declaration.rb +4 -4
  59. data/lib/kumi/version.rb +1 -1
  60. data/lib/kumi.rb +1 -1
  61. data/scripts/analyze_broadcast_methods.rb +68 -0
  62. data/scripts/analyze_cascade_methods.rb +74 -0
  63. data/scripts/check_broadcasting_coverage.rb +51 -0
  64. data/scripts/find_dead_code.rb +114 -0
  65. metadata +22 -4
  66. data/docs/features/array-broadcasting.md +0 -170
  67. data/lib/kumi/cli.rb +0 -449
  68. 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
- super
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}, schema: #{schema.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
- ArrayExpression = Struct.new(:elements) do
6
- include Node
5
+ ArrayExpression = Struct.new(:elements) do
6
+ include Node
7
7
 
8
- def children = elements
8
+ def children = elements
9
9
 
10
- def size
11
- elements.size
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
- CallExpression = Struct.new(:fn_name, :args) do
6
- include Node
5
+ CallExpression = Struct.new(:fn_name, :args) do
6
+ include Node
7
7
 
8
- def children = args
9
- end
8
+ def children = args
9
+ end
10
10
  end
11
11
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- CascadeExpression = Struct.new(:cases) do
6
- include Node
5
+ CascadeExpression = Struct.new(:cases) do
6
+ include Node
7
7
 
8
- def children = cases
9
- end
8
+ def children = cases
9
+ end
10
10
  end
11
11
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- CaseExpression = Struct.new(:condition, :result) do
6
- include Node
5
+ CaseExpression = Struct.new(:condition, :result) do
6
+ include Node
7
7
 
8
- def children = [condition, result]
9
- end
8
+ def children = [condition, result]
9
+ end
10
10
  end
11
11
  end