kumi 0.0.0 → 0.0.3

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 +257 -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 +20 -0
  41. data/lib/kumi/explain.rb +282 -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,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Explain
5
+ class ExplanationGenerator
6
+ def initialize(syntax_tree, analyzer_result, inputs)
7
+ @analyzer_result = analyzer_result
8
+ @inputs = EvaluationWrapper.new(inputs)
9
+ @definitions = analyzer_result.definitions
10
+ @compiled_schema = Compiler.compile(syntax_tree, analyzer: analyzer_result)
11
+
12
+ # TODO: REFACTOR QUICK!
13
+ # Set up compiler once for expression evaluation
14
+ @compiler = Compiler.new(syntax_tree, analyzer_result)
15
+ @compiler.send(:build_index)
16
+
17
+ # Populate bindings from the compiled schema
18
+ @compiled_schema.bindings.each do |name, (type, fn)|
19
+ @compiler.instance_variable_get(:@bindings)[name] = [type, fn]
20
+ end
21
+ end
22
+
23
+ def explain(target_name)
24
+ declaration = @definitions[target_name]
25
+ raise ArgumentError, "Unknown declaration: #{target_name}" unless declaration
26
+
27
+ expression = declaration.expression
28
+ result_value = @compiled_schema.evaluate_binding(target_name, @inputs)
29
+
30
+ prefix = "#{target_name} = "
31
+ expression_str = format_expression(expression, indent_context: prefix.length)
32
+
33
+ "#{prefix}#{expression_str} => #{format_value(result_value)}"
34
+ end
35
+
36
+ private
37
+
38
+ def format_expression(expr, indent_context: 0, nested: false)
39
+ case expr
40
+ when Syntax::TerminalExpressions::FieldRef
41
+ "input.#{expr.name}"
42
+ when Syntax::TerminalExpressions::Binding
43
+ expr.name.to_s
44
+ when Syntax::TerminalExpressions::Literal
45
+ format_value(expr.value)
46
+ when Syntax::Expressions::CallExpression
47
+ format_call_expression(expr, indent_context: indent_context, nested: nested)
48
+ when Syntax::Expressions::ListExpression
49
+ "[#{expr.elements.map { |e| format_expression(e, indent_context: indent_context, nested: nested) }.join(', ')}]"
50
+ when Syntax::Expressions::CascadeExpression
51
+ format_cascade_expression(expr, indent_context: indent_context)
52
+ else
53
+ expr.class.name.split("::").last
54
+ end
55
+ end
56
+
57
+ def format_call_expression(expr, indent_context: 0, nested: false)
58
+ if pretty_printable?(expr.fn_name)
59
+ format_pretty_function(expr, expr.fn_name, indent_context, nested)
60
+ else
61
+ format_generic_function(expr, indent_context)
62
+ end
63
+ end
64
+
65
+ def format_pretty_function(expr, fn_name, _indent_context, nested = false)
66
+ if needs_evaluation?(expr.args) && !nested
67
+ # For top-level expressions, show the flattened symbolic form and evaluation
68
+ if is_chain_of_same_operator?(expr, fn_name)
69
+ # For chains like a + b + c, flatten to show all operands
70
+ all_operands = flatten_operator_chain(expr, fn_name)
71
+ symbolic_operands = all_operands.map { |op| format_expression(op, indent_context: 0, nested: true) }
72
+ symbolic_format = symbolic_operands.join(" #{get_operator_symbol(fn_name)} ")
73
+
74
+ evaluated_operands = all_operands.map do |op|
75
+ if op.is_a?(Syntax::TerminalExpressions::Literal)
76
+ format_expression(op, indent_context: 0, nested: true)
77
+ else
78
+ arg_value = format_value(evaluate_expression(op))
79
+ if op.is_a?(Syntax::TerminalExpressions::Binding) && all_operands.length > 1
80
+ "(#{format_expression(op, indent_context: 0, nested: true)} = #{arg_value})"
81
+ else
82
+ arg_value
83
+ end
84
+ end
85
+ end
86
+ evaluated_format = evaluated_operands.join(" #{get_operator_symbol(fn_name)} ")
87
+
88
+ "#{symbolic_format} = #{evaluated_format}"
89
+ else
90
+ # Regular pretty formatting for non-chain expressions
91
+ symbolic_args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
92
+ symbolic_format = get_display_format(fn_name, symbolic_args)
93
+
94
+ evaluated_args = expr.args.map do |arg|
95
+ if arg.is_a?(Syntax::TerminalExpressions::Literal)
96
+ format_expression(arg, indent_context: 0, nested: true)
97
+ else
98
+ arg_value = format_value(evaluate_expression(arg))
99
+ if arg.is_a?(Syntax::TerminalExpressions::Binding) &&
100
+ expr.args.count { |a| !a.is_a?(Syntax::TerminalExpressions::Literal) } > 1
101
+ "(#{format_expression(arg, indent_context: 0, nested: true)} = #{arg_value})"
102
+ else
103
+ arg_value
104
+ end
105
+ end
106
+ end
107
+ evaluated_format = get_display_format(fn_name, evaluated_args)
108
+
109
+ "#{symbolic_format} = #{evaluated_format}"
110
+ end
111
+ else
112
+ # For nested expressions, just show the symbolic form without evaluation details
113
+ args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
114
+ get_display_format(fn_name, args)
115
+ end
116
+ end
117
+
118
+ def is_chain_of_same_operator?(expr, fn_name)
119
+ return false unless %i[add subtract multiply divide].include?(fn_name)
120
+
121
+ # Check if any argument is the same operator
122
+ expr.args.any? do |arg|
123
+ arg.is_a?(Syntax::Expressions::CallExpression) && arg.fn_name == fn_name
124
+ end
125
+ end
126
+
127
+ def flatten_operator_chain(expr, operator)
128
+ operands = []
129
+
130
+ expr.args.each do |arg|
131
+ if arg.is_a?(Syntax::Expressions::CallExpression) && arg.fn_name == operator
132
+ # Recursively flatten nested operations of the same type
133
+ operands.concat(flatten_operator_chain(arg, operator))
134
+ else
135
+ operands << arg
136
+ end
137
+ end
138
+
139
+ operands
140
+ end
141
+
142
+ def get_operator_symbol(fn_name)
143
+ case fn_name
144
+ when :add then "+"
145
+ when :subtract then "-"
146
+ when :multiply then "×"
147
+ when :divide then "÷"
148
+ else fn_name.to_s
149
+ end
150
+ end
151
+
152
+ def pretty_printable?(fn_name)
153
+ %i[add subtract multiply divide == != > < >= <= and or not].include?(fn_name)
154
+ end
155
+
156
+ def get_display_format(fn_name, args)
157
+ case fn_name
158
+ when :add then args.join(" + ")
159
+ when :subtract then args.join(" - ")
160
+ when :multiply then args.join(" × ")
161
+ when :divide then args.join(" ÷ ")
162
+ when :== then "#{args[0]} == #{args[1]}"
163
+ when :!= then "#{args[0]} != #{args[1]}"
164
+ when :> then "#{args[0]} > #{args[1]}"
165
+ when :< then "#{args[0]} < #{args[1]}"
166
+ when :>= then "#{args[0]} >= #{args[1]}"
167
+ when :<= then "#{args[0]} <= #{args[1]}"
168
+ when :and then args.join(" && ")
169
+ when :or then args.join(" || ")
170
+ when :not then "!#{args[0]}"
171
+ else "#{fn_name}(#{args.join(', ')})"
172
+ end
173
+ end
174
+
175
+ def format_generic_function(expr, indent_context)
176
+ args = expr.args.map do |arg|
177
+ arg_desc = format_expression(arg, indent_context: indent_context)
178
+
179
+ # For literals and literal lists, just show the value, no need for "100 = 100"
180
+ if arg.is_a?(Syntax::TerminalExpressions::Literal) ||
181
+ (arg.is_a?(Syntax::Expressions::ListExpression) && arg.elements.all?(Syntax::TerminalExpressions::Literal))
182
+ arg_desc
183
+ else
184
+ arg_value = evaluate_expression(arg)
185
+ "#{arg_desc} = #{format_value(arg_value)}"
186
+ end
187
+ end
188
+
189
+ if args.length > 1
190
+ # Align with opening parenthesis, accounting for the full context
191
+ function_indent = indent_context + expr.fn_name.to_s.length + 1
192
+ indent = " " * function_indent
193
+ "#{expr.fn_name}(#{args.join(",\n#{indent}")})"
194
+ else
195
+ "#{expr.fn_name}(#{args.join(', ')})"
196
+ end
197
+ end
198
+
199
+ def needs_evaluation?(args)
200
+ args.any? do |arg|
201
+ !arg.is_a?(Syntax::TerminalExpressions::Literal) &&
202
+ !(arg.is_a?(Syntax::Expressions::ListExpression) && arg.elements.all?(Syntax::TerminalExpressions::Literal))
203
+ end
204
+ end
205
+
206
+ def format_cascade_expression(expr, indent_context: 0)
207
+ lines = []
208
+ expr.cases.each do |case_expr|
209
+ condition_result = evaluate_expression(case_expr.condition)
210
+ condition_desc = format_expression(case_expr.condition, indent_context: indent_context)
211
+ result_desc = format_expression(case_expr.result, indent_context: indent_context)
212
+
213
+ status = condition_result ? "✓" : "✗"
214
+ lines << " #{status} on #{condition_desc}, #{result_desc}"
215
+
216
+ break if condition_result
217
+ end
218
+
219
+ "\n#{lines.join("\n")}"
220
+ end
221
+
222
+ def format_value(value)
223
+ case value
224
+ when Float, Integer
225
+ format_number(value)
226
+ when String
227
+ "\"#{value}\""
228
+ when Array
229
+ if value.length <= 4
230
+ "[#{value.map { |v| format_value(v) }.join(', ')}]"
231
+ else
232
+ "[#{value.take(4).map { |v| format_value(v) }.join(', ')}, …]"
233
+ end
234
+ else
235
+ value.to_s
236
+ end
237
+ end
238
+
239
+ def format_number(num)
240
+ return num.to_s unless num.is_a?(Numeric)
241
+
242
+ if num.is_a?(Integer) || (num.is_a?(Float) && num == num.to_i)
243
+ int_val = num.to_i
244
+ if int_val.abs >= 1000
245
+ int_val.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1 ').reverse
246
+ else
247
+ int_val.to_s
248
+ end
249
+ else
250
+ num.to_s
251
+ end
252
+ end
253
+
254
+ def evaluate_expression(expr)
255
+ case expr
256
+ when Syntax::TerminalExpressions::Binding
257
+ @compiled_schema.evaluate_binding(expr.name, @inputs)
258
+ when Syntax::TerminalExpressions::FieldRef
259
+ @inputs[expr.name]
260
+ when Syntax::TerminalExpressions::Literal
261
+ expr.value
262
+ else
263
+ # For complex expressions, compile and evaluate using existing compiler
264
+ compiled_fn = @compiler.send(:compile_expr, expr)
265
+ compiled_fn.call(@inputs)
266
+ end
267
+ end
268
+ end
269
+
270
+ module_function
271
+
272
+ def call(schema_class, target_name, inputs:)
273
+ syntax_tree = schema_class.instance_variable_get(:@__syntax_tree__)
274
+ analyzer_result = schema_class.instance_variable_get(:@__analyzer_result__)
275
+
276
+ raise ArgumentError, "Schema not found or not compiled" unless syntax_tree && analyzer_result
277
+
278
+ generator = ExplanationGenerator.new(syntax_tree, analyzer_result, inputs)
279
+ generator.explain(target_name)
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Export
5
+ class Deserializer
6
+ include NodeBuilders
7
+
8
+ def initialize(validate: true)
9
+ @validate = validate
10
+ end
11
+
12
+ def deserialize(json_string)
13
+ data = parse_json(json_string)
14
+ validate_format(data) if @validate
15
+
16
+ build_node(data[:ast])
17
+ end
18
+
19
+ private
20
+
21
+ def parse_json(json_string)
22
+ JSON.parse(json_string, symbolize_names: true)
23
+ rescue JSON::ParserError => e
24
+ raise Kumi::Export::Errors::DeserializationError, "Invalid JSON: #{e.message}"
25
+ end
26
+
27
+ def validate_format(data)
28
+ unless data[:kumi_version] && data[:ast]
29
+ raise Kumi::Export::Errors::DeserializationError,
30
+ "Missing required fields: kumi_version, ast"
31
+ end
32
+
33
+ return if data[:ast][:type] == "root"
34
+
35
+ raise Kumi::Export::Errors::DeserializationError, "Root node must have type 'root'"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Export
5
+ module Errors
6
+ class ExportError < StandardError; end
7
+ class SerializationError < ExportError; end
8
+ class DeserializationError < ExportError; end
9
+ class VersionMismatchError < ExportError; end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Export
5
+ module NodeBuilders
6
+ def build_root(data, node_class)
7
+ inputs = data[:inputs].map { |input_data| build_node(input_data) }
8
+ attributes = data[:attributes].map { |attr_data| build_node(attr_data) }
9
+ traits = data[:traits].map { |trait_data| build_node(trait_data) }
10
+
11
+ node_class.new(inputs, attributes, traits)
12
+ end
13
+
14
+ def build_field_declaration(data, node_class)
15
+ name = restore_name_type(data[:name], data[:name_type])
16
+ type = deserialize_type(data[:field_type])
17
+ domain = deserialize_domain(data[:domain])
18
+
19
+ # Match the Struct signature of FieldDecl: (name, domain, type)
20
+ node_class.new(name, domain, type)
21
+ end
22
+
23
+ def build_attribute_declaration(data, node_class)
24
+ name = restore_name_type(data[:name], data[:name_type])
25
+ expression = build_node(data[:expression])
26
+
27
+ node_class.new(name, expression)
28
+ end
29
+
30
+ def build_trait_declaration(data, node_class)
31
+ name = restore_name_type(data[:name], data[:name_type])
32
+ expression = build_node(data[:expression])
33
+
34
+ node_class.new(name, expression)
35
+ end
36
+
37
+ def build_call_expression(data, node_class)
38
+ function_name = restore_name_type(data[:function_name], data[:function_name_type])
39
+ arguments = data[:arguments].map { |arg_data| build_node(arg_data) }
40
+
41
+ node_class.new(function_name, arguments)
42
+ end
43
+
44
+ def build_literal(data, node_class)
45
+ value = data[:value]
46
+
47
+ # Restore proper Ruby type if needed
48
+ value = coerce_to_type(value, data[:ruby_type]) if data[:ruby_type] && value.is_a?(String)
49
+
50
+ node_class.new(value)
51
+ end
52
+
53
+ def build_field_reference(data, node_class)
54
+ field_name = restore_name_type(data[:field_name], data[:name_type])
55
+ node_class.new(field_name)
56
+ end
57
+
58
+ def build_binding_reference(data, node_class)
59
+ binding_name = restore_name_type(data[:binding_name], data[:name_type])
60
+ node_class.new(binding_name)
61
+ end
62
+
63
+ def build_list_expression(data, node_class)
64
+ elements = data[:elements].map { |element_data| build_node(element_data) }
65
+ node_class.new(elements)
66
+ end
67
+
68
+ def build_cascade_expression(data, node_class)
69
+ cases = data[:cases].map { |case_data| build_node(case_data) }
70
+ node_class.new(cases)
71
+ end
72
+
73
+ def build_when_case_expression(data, node_class)
74
+ condition = build_node(data[:condition])
75
+ result = build_node(data[:result])
76
+ node_class.new(condition, result)
77
+ end
78
+
79
+ private
80
+
81
+ def deserialize_type(type_data)
82
+ # Handle simple types that weren't serialized with the new format
83
+ return type_data unless type_data.is_a?(Hash) && type_data.key?(:type)
84
+
85
+ case type_data[:type]
86
+ when "symbol"
87
+ type_data[:value].to_sym
88
+ when "array"
89
+ { array: deserialize_type(type_data[:element_type]) }
90
+ when "hash"
91
+ { hash: [deserialize_type(type_data[:key_type]), deserialize_type(type_data[:value_type])] }
92
+ when "literal", nil
93
+ type_data[:value]
94
+ end
95
+ end
96
+
97
+ def build_node(node_data)
98
+ type_name = node_data[:type]
99
+ node_class = NodeRegistry.class_for_type(type_name)
100
+
101
+ build_method = "build_#{type_name}"
102
+ raise Kumi::Export::Errors::DeserializationError, "No builder for type: #{type_name}" unless respond_to?(build_method, true)
103
+
104
+ send(build_method, node_data, node_class)
105
+ end
106
+
107
+ def deserialize_domain(domain_data)
108
+ return nil unless domain_data
109
+
110
+ case domain_data[:type]
111
+ when "range"
112
+ min, max = domain_data.values_at(:min, :max)
113
+ domain_data[:exclude_end] ? (min...max) : (min..max)
114
+ when "array"
115
+ domain_data[:values]
116
+ when "custom"
117
+ # For custom domains, we might need to eval or have a registry
118
+ domain_data[:value]
119
+ end
120
+ end
121
+
122
+ def coerce_to_type(value, type_name)
123
+ case type_name
124
+ when "Integer" then value.to_i
125
+ when "Float" then value.to_f
126
+ when "Symbol" then value.to_sym
127
+ else value
128
+ end
129
+ end
130
+
131
+ def restore_name_type(name_string, name_type)
132
+ case name_type
133
+ when "Symbol" then name_string.to_sym
134
+ when "String" then name_string.to_s
135
+ else name_string
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Export
5
+ class NodeRegistry
6
+ # Maps AST classes to JSON type names
7
+ SERIALIZATION_MAP = {
8
+ "Kumi::Syntax::Root" => "root",
9
+ "Kumi::Syntax::Declarations::FieldDecl" => "field_declaration",
10
+ "Kumi::Syntax::Declarations::Attribute" => "attribute_declaration",
11
+ "Kumi::Syntax::Declarations::Trait" => "trait_declaration",
12
+ "Kumi::Syntax::Expressions::CallExpression" => "call_expression",
13
+ "Kumi::Syntax::TerminalExpressions::Literal" => "literal",
14
+ "Kumi::Syntax::TerminalExpressions::FieldRef" => "field_reference",
15
+ "Kumi::Syntax::TerminalExpressions::Binding" => "binding_reference",
16
+ "Kumi::Syntax::Expressions::ListExpression" => "list_expression",
17
+ "Kumi::Syntax::Expressions::CascadeExpression" => "cascade_expression",
18
+ "Kumi::Syntax::Expressions::WhenCaseExpression" => "when_case_expression"
19
+ }.freeze
20
+
21
+ # Maps JSON type names back to AST classes
22
+ DESERIALIZATION_MAP = SERIALIZATION_MAP.invert.freeze
23
+
24
+ def self.type_name_for(node)
25
+ SERIALIZATION_MAP[node.class.name] or
26
+ raise Kumi::Export::Errors::SerializationError, "Unknown node type: #{node.class.name}"
27
+ end
28
+
29
+ def self.class_for_type(type_name)
30
+ class_name = DESERIALIZATION_MAP[type_name] or
31
+ raise Kumi::Export::Errors::DeserializationError, "Unknown type name: #{type_name}"
32
+
33
+ # Resolve the class from string name
34
+ class_name.split("::").reduce(Object) { |const, name| const.const_get(name) }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Export
5
+ module NodeSerializers
6
+ # Root node: top-level container
7
+ def serialize_root(node)
8
+ {
9
+ type: "root",
10
+ inputs: node.inputs.map { |input| serialize_node(input) },
11
+ attributes: node.attributes.map { |attr| serialize_node(attr) },
12
+ traits: node.traits.map { |trait| serialize_node(trait) }
13
+ }
14
+ end
15
+
16
+ # Field Declaration: preserves type info for analyzer
17
+ def serialize_field_declaration(node)
18
+ {
19
+ name: node.name.to_s,
20
+ name_type: node.name.class.name,
21
+ field_type: serialize_type(node.type),
22
+ domain: serialize_domain(node.domain)
23
+ }
24
+ end
25
+
26
+ # Attribute Declaration: preserves name and expression tree
27
+ def serialize_attribute_declaration(node)
28
+ {
29
+ name: node.name.to_s,
30
+ name_type: node.name.class.name,
31
+ expression: serialize_node(node.expression)
32
+ }
33
+ end
34
+
35
+ # Trait Declaration: preserves name and expression tree
36
+ def serialize_trait_declaration(node)
37
+ {
38
+ name: node.name.to_s,
39
+ name_type: node.name.class.name,
40
+ expression: serialize_node(node.expression)
41
+ }
42
+ end
43
+
44
+ # Call Expression: critical for dependency analysis
45
+ def serialize_call_expression(node)
46
+ {
47
+ function_name: node.fn_name.to_s,
48
+ function_name_type: node.fn_name.class.name,
49
+ arguments: node.args.map { |arg| serialize_node(arg) }
50
+ }
51
+ end
52
+
53
+ # Literal: preserve exact value and Ruby type
54
+ def serialize_literal(node)
55
+ {
56
+ value: node.value,
57
+ ruby_type: node.value.class.name
58
+ }
59
+ end
60
+
61
+ # Field Reference: critical for dependency resolution
62
+ def serialize_field_reference(node)
63
+ {
64
+ field_name: node.name.to_s,
65
+ name_type: node.name.class.name
66
+ }
67
+ end
68
+
69
+ # Binding Reference: critical for dependency resolution
70
+ def serialize_binding_reference(node)
71
+ {
72
+ binding_name: node.name.to_s,
73
+ name_type: node.name.class.name
74
+ }
75
+ end
76
+
77
+ # List Expression: preserve order and elements
78
+ def serialize_list_expression(node)
79
+ {
80
+ elements: node.elements.map { |element| serialize_node(element) }
81
+ }
82
+ end
83
+
84
+ # Cascade Expression: preserve condition/result pairs
85
+ def serialize_cascade_expression(node)
86
+ {
87
+ cases: node.cases.map { |case_node| serialize_node(case_node) }
88
+ }
89
+ end
90
+
91
+ # When Case Expression: individual case in cascade
92
+ def serialize_when_case_expression(node)
93
+ {
94
+ condition: serialize_node(node.condition),
95
+ result: serialize_node(node.result)
96
+ }
97
+ end
98
+
99
+ private
100
+
101
+ def serialize_type(type)
102
+ case type
103
+ when Symbol
104
+ { type: "symbol", value: type.to_s }
105
+ when Hash
106
+ if type.key?(:array)
107
+ { type: "array", element_type: serialize_type(type[:array]) }
108
+ elsif type.key?(:hash)
109
+ { type: "hash", key_type: serialize_type(type[:hash][0]), value_type: serialize_type(type[:hash][1]) }
110
+ else
111
+ { type: "hash", value: type }
112
+ end
113
+ when String, Integer, Float, TrueClass, FalseClass, NilClass
114
+ { type: "literal", value: type }
115
+ else
116
+ { type: "unknown", value: type.to_s }
117
+ end
118
+ end
119
+
120
+ def serialize_domain(domain)
121
+ return nil unless domain
122
+
123
+ case domain
124
+ when Range
125
+ { type: "range", min: domain.min, max: domain.max, exclude_end: domain.exclude_end? }
126
+ when Array
127
+ { type: "array", values: domain }
128
+ else
129
+ { type: "custom", value: domain.to_s }
130
+ end
131
+ end
132
+
133
+ def serialize_node(node)
134
+ type_name = NodeRegistry.type_name_for(node)
135
+
136
+ base_data = {
137
+ type: type_name,
138
+ **send("serialize_#{type_name}", node)
139
+ }
140
+
141
+ add_location_if_present(base_data, node) if @include_locations
142
+ base_data
143
+ end
144
+
145
+ def add_location_if_present(data, node)
146
+ return unless node.respond_to?(:loc) && node.loc
147
+
148
+ data[:location] = {
149
+ line: node.loc.line,
150
+ column: node.loc.column,
151
+ file: node.loc.file
152
+ }
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Export
5
+ class Serializer
6
+ include NodeSerializers
7
+
8
+ def initialize(pretty: false, include_locations: false)
9
+ @pretty = pretty
10
+ @include_locations = include_locations
11
+ end
12
+
13
+ def serialize(syntax_root)
14
+ json_data = {
15
+ kumi_version: VERSION,
16
+ ast: serialize_root(syntax_root)
17
+ }
18
+
19
+ @pretty ? JSON.pretty_generate(json_data) : JSON.generate(json_data)
20
+ end
21
+ end
22
+ end
23
+ end