kumi 0.0.7 → 0.0.8

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -1
  3. data/README.md +8 -5
  4. data/examples/game_of_life.rb +1 -1
  5. data/examples/static_analysis_errors.rb +7 -7
  6. data/lib/kumi/analyzer.rb +15 -15
  7. data/lib/kumi/compiler.rb +6 -6
  8. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  9. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  10. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  11. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  13. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  14. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  15. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  16. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  17. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  18. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  19. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  20. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  21. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  22. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  23. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  24. data/lib/kumi/core/compiled_schema.rb +43 -0
  25. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  26. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  27. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  28. data/lib/kumi/core/domain/validator.rb +82 -0
  29. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  30. data/lib/kumi/core/error_reporter.rb +166 -0
  31. data/lib/kumi/core/error_reporting.rb +97 -0
  32. data/lib/kumi/core/errors.rb +120 -0
  33. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  34. data/lib/kumi/core/explain.rb +295 -0
  35. data/lib/kumi/core/export/deserializer.rb +41 -0
  36. data/lib/kumi/core/export/errors.rb +14 -0
  37. data/lib/kumi/core/export/node_builders.rb +142 -0
  38. data/lib/kumi/core/export/node_registry.rb +54 -0
  39. data/lib/kumi/core/export/node_serializers.rb +158 -0
  40. data/lib/kumi/core/export/serializer.rb +25 -0
  41. data/lib/kumi/core/export.rb +35 -0
  42. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  43. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  44. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  45. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  46. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  47. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  48. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  49. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  50. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  51. data/lib/kumi/core/input/type_matcher.rb +97 -0
  52. data/lib/kumi/core/input/validator.rb +51 -0
  53. data/lib/kumi/core/input/violation_creator.rb +52 -0
  54. data/lib/kumi/core/json_schema/generator.rb +65 -0
  55. data/lib/kumi/core/json_schema/validator.rb +27 -0
  56. data/lib/kumi/core/json_schema.rb +16 -0
  57. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  58. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  59. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  60. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  61. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  62. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  64. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  65. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  66. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  67. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  68. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  69. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  70. data/lib/kumi/core/ruby_parser.rb +12 -0
  71. data/lib/kumi/core/schema_instance.rb +111 -0
  72. data/lib/kumi/core/types/builder.rb +23 -0
  73. data/lib/kumi/core/types/compatibility.rb +96 -0
  74. data/lib/kumi/core/types/formatter.rb +26 -0
  75. data/lib/kumi/core/types/inference.rb +42 -0
  76. data/lib/kumi/core/types/normalizer.rb +72 -0
  77. data/lib/kumi/core/types/validator.rb +37 -0
  78. data/lib/kumi/core/types.rb +66 -0
  79. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  80. data/lib/kumi/errors.rb +1 -112
  81. data/lib/kumi/registry.rb +37 -0
  82. data/lib/kumi/schema.rb +5 -5
  83. data/lib/kumi/schema_metadata.rb +3 -3
  84. data/lib/kumi/syntax/array_expression.rb +6 -6
  85. data/lib/kumi/syntax/call_expression.rb +4 -4
  86. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  87. data/lib/kumi/syntax/case_expression.rb +4 -4
  88. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  89. data/lib/kumi/syntax/hash_expression.rb +4 -4
  90. data/lib/kumi/syntax/input_declaration.rb +5 -5
  91. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  92. data/lib/kumi/syntax/input_reference.rb +5 -5
  93. data/lib/kumi/syntax/literal.rb +4 -4
  94. data/lib/kumi/syntax/node.rb +34 -34
  95. data/lib/kumi/syntax/root.rb +6 -6
  96. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  97. data/lib/kumi/syntax/value_declaration.rb +4 -4
  98. data/lib/kumi/version.rb +1 -1
  99. data/migrate_to_core_iterative.rb +938 -0
  100. data/scripts/generate_function_docs.rb +9 -9
  101. metadata +75 -72
  102. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  103. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  104. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
  105. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  106. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  107. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  108. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  109. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  110. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
  111. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  112. data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
  113. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  114. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  115. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
  116. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  117. data/lib/kumi/atom_unsat_solver.rb +0 -394
  118. data/lib/kumi/compiled_schema.rb +0 -41
  119. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  120. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  121. data/lib/kumi/domain/range_analyzer.rb +0 -83
  122. data/lib/kumi/domain/validator.rb +0 -80
  123. data/lib/kumi/domain/violation_formatter.rb +0 -40
  124. data/lib/kumi/error_reporter.rb +0 -164
  125. data/lib/kumi/error_reporting.rb +0 -95
  126. data/lib/kumi/evaluation_wrapper.rb +0 -38
  127. data/lib/kumi/explain.rb +0 -293
  128. data/lib/kumi/export/deserializer.rb +0 -39
  129. data/lib/kumi/export/errors.rb +0 -12
  130. data/lib/kumi/export/node_builders.rb +0 -140
  131. data/lib/kumi/export/node_registry.rb +0 -52
  132. data/lib/kumi/export/node_serializers.rb +0 -156
  133. data/lib/kumi/export/serializer.rb +0 -23
  134. data/lib/kumi/export.rb +0 -33
  135. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  136. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  137. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  138. data/lib/kumi/function_registry/function_builder.rb +0 -93
  139. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  140. data/lib/kumi/function_registry/math_functions.rb +0 -72
  141. data/lib/kumi/function_registry/string_functions.rb +0 -54
  142. data/lib/kumi/function_registry/type_functions.rb +0 -51
  143. data/lib/kumi/input/type_matcher.rb +0 -95
  144. data/lib/kumi/input/validator.rb +0 -49
  145. data/lib/kumi/input/violation_creator.rb +0 -50
  146. data/lib/kumi/json_schema/generator.rb +0 -63
  147. data/lib/kumi/json_schema/validator.rb +0 -25
  148. data/lib/kumi/json_schema.rb +0 -14
  149. data/lib/kumi/ruby_parser/build_context.rb +0 -25
  150. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
  151. data/lib/kumi/ruby_parser/dsl.rb +0 -12
  152. data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
  153. data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
  154. data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
  155. data/lib/kumi/ruby_parser/input_builder.rb +0 -125
  156. data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
  157. data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
  158. data/lib/kumi/ruby_parser/nested_input.rb +0 -15
  159. data/lib/kumi/ruby_parser/parser.rb +0 -69
  160. data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
  161. data/lib/kumi/ruby_parser/sugar.rb +0 -261
  162. data/lib/kumi/ruby_parser.rb +0 -10
  163. data/lib/kumi/schema_instance.rb +0 -109
  164. data/lib/kumi/types/builder.rb +0 -21
  165. data/lib/kumi/types/compatibility.rb +0 -94
  166. data/lib/kumi/types/formatter.rb +0 -24
  167. data/lib/kumi/types/inference.rb +0 -40
  168. data/lib/kumi/types/normalizer.rb +0 -70
  169. data/lib/kumi/types/validator.rb +0 -35
  170. data/lib/kumi/types.rb +0 -64
  171. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Explain
6
+ class ExplanationGenerator
7
+ def initialize(syntax_tree, analyzer_result, inputs)
8
+ @analyzer_result = analyzer_result
9
+ @inputs = EvaluationWrapper.new(inputs)
10
+ @definitions = analyzer_result.definitions
11
+ @compiled_schema = Compiler.compile(syntax_tree, analyzer: analyzer_result)
12
+
13
+ # TODO: REFACTOR QUICK!
14
+ # Set up compiler once for expression evaluation
15
+ @compiler = Compiler.new(syntax_tree, analyzer_result)
16
+ @compiler.send(:build_index)
17
+
18
+ # Populate bindings from the compiled schema
19
+ @compiled_schema.bindings.each do |name, (type, fn)|
20
+ @compiler.instance_variable_get(:@bindings)[name] = [type, fn]
21
+ end
22
+ end
23
+
24
+ def explain(target_name)
25
+ declaration = @definitions[target_name]
26
+ raise ArgumentError, "Unknown declaration: #{target_name}" unless declaration
27
+
28
+ expression = declaration.expression
29
+ result_value = @compiled_schema.evaluate_binding(target_name, @inputs)
30
+
31
+ prefix = "#{target_name} = "
32
+ expression_str = format_expression(expression, indent_context: prefix.length)
33
+
34
+ "#{prefix}#{expression_str} => #{format_value(result_value)}"
35
+ end
36
+
37
+ private
38
+
39
+ def format_expression(expr, indent_context: 0, nested: false)
40
+ case expr
41
+ when Kumi::Syntax::InputReference
42
+ "input.#{expr.name}"
43
+ when Kumi::Syntax::DeclarationReference
44
+ expr.name.to_s
45
+ when Kumi::Syntax::Literal
46
+ format_value(expr.value)
47
+ when Kumi::Syntax::CallExpression
48
+ format_call_expression(expr, indent_context: indent_context, nested: nested)
49
+ when Kumi::Syntax::ArrayExpression
50
+ "[#{expr.elements.map { |e| format_expression(e, indent_context: indent_context, nested: nested) }.join(', ')}]"
51
+ when Kumi::Syntax::CascadeExpression
52
+ format_cascade_expression(expr, indent_context: indent_context)
53
+ else
54
+ expr.class.name.split("::").last
55
+ end
56
+ end
57
+
58
+ def format_call_expression(expr, indent_context: 0, nested: false)
59
+ if pretty_printable?(expr.fn_name)
60
+ format_pretty_function(expr, expr.fn_name, indent_context, nested: nested)
61
+ else
62
+ format_generic_function(expr, indent_context)
63
+ end
64
+ end
65
+
66
+ def format_pretty_function(expr, fn_name, _indent_context, nested: false)
67
+ if needs_evaluation?(expr.args) && !nested
68
+ # For top-level expressions, show the flattened symbolic form and evaluation
69
+ if chain_of_same_operator?(expr, fn_name)
70
+ # For chains like a + b + c, flatten to show all operands
71
+ all_operands = flatten_operator_chain(expr, fn_name)
72
+ symbolic_operands = all_operands.map { |op| format_expression(op, indent_context: 0, nested: true) }
73
+ symbolic_format = symbolic_operands.join(" #{get_operator_symbol(fn_name)} ")
74
+
75
+ evaluated_operands = all_operands.map do |op|
76
+ if op.is_a?(Kumi::Syntax::Literal)
77
+ format_expression(op, indent_context: 0, nested: true)
78
+ else
79
+ arg_value = format_value(evaluate_expression(op))
80
+ if op.is_a?(Kumi::Syntax::DeclarationReference) && all_operands.length > 1
81
+ "(#{format_expression(op, indent_context: 0, nested: true)} = #{arg_value})"
82
+ else
83
+ arg_value
84
+ end
85
+ end
86
+ end
87
+ evaluated_format = evaluated_operands.join(" #{get_operator_symbol(fn_name)} ")
88
+
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 = display_format(fn_name, symbolic_args)
93
+
94
+ evaluated_args = expr.args.map do |arg|
95
+ if arg.is_a?(Kumi::Syntax::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?(Kumi::Syntax::DeclarationReference) &&
100
+ expr.args.count { |a| !a.is_a?(Kumi::Syntax::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 = display_format(fn_name, evaluated_args)
108
+
109
+ end
110
+ "#{symbolic_format} = #{evaluated_format}"
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
+ display_format(fn_name, args)
115
+ end
116
+ end
117
+
118
+ def 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?(Kumi::Syntax::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?(Kumi::Syntax::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 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?(Kumi::Syntax::Literal) ||
181
+ (arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::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?(Kumi::Syntax::Literal) &&
202
+ !(arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::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 Kumi::Syntax::DeclarationReference
257
+ @compiled_schema.evaluate_binding(expr.name, @inputs)
258
+ when Kumi::Syntax::InputReference
259
+ @inputs[expr.name]
260
+ when Kumi::Syntax::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
+ metadata = analyzer_result.state
279
+
280
+ # Create a minimal analyzer result structure for compatibility
281
+ analyzer_result = OpenStruct.new(
282
+ definitions: metadata[:declarations] || {},
283
+ dependency_graph: metadata[:dependencies] || {},
284
+ leaf_map: metadata[:leaves] || {},
285
+ topo_order: metadata[:evaluation_order] || [],
286
+ decl_types: metadata[:inferred_types] || {},
287
+ state: metadata
288
+ )
289
+
290
+ generator = ExplanationGenerator.new(syntax_tree, analyzer_result, inputs)
291
+ generator.explain(target_name)
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Export
6
+ class Deserializer
7
+ include NodeBuilders
8
+
9
+ def initialize(validate: true)
10
+ @validate = validate
11
+ end
12
+
13
+ def deserialize(json_string)
14
+ data = parse_json(json_string)
15
+ validate_format(data) if @validate
16
+
17
+ build_node(data[:ast])
18
+ end
19
+
20
+ private
21
+
22
+ def parse_json(json_string)
23
+ JSON.parse(json_string, symbolize_names: true)
24
+ rescue JSON::ParserError => e
25
+ raise Kumi::Core::Export::Errors::DeserializationError, "Invalid JSON: #{e.message}"
26
+ end
27
+
28
+ def validate_format(data)
29
+ unless data[:kumi_version] && data[:ast]
30
+ raise Kumi::Core::Export::Errors::DeserializationError,
31
+ "Missing required fields: kumi_version, ast"
32
+ end
33
+
34
+ return if data[:ast][:type] == "root"
35
+
36
+ raise Kumi::Core::Export::Errors::DeserializationError, "Root node must have type 'root'"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Export
6
+ module Errors
7
+ class ExportError < StandardError; end
8
+ class SerializationError < ExportError; end
9
+ class DeserializationError < ExportError; end
10
+ class VersionMismatchError < ExportError; end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Export
6
+ module NodeBuilders
7
+ def build_root(data, node_class)
8
+ inputs = data[:inputs].map { |input_data| build_node(input_data) }
9
+ attributes = data[:attributes].map { |attr_data| build_node(attr_data) }
10
+ traits = data[:traits].map { |trait_data| build_node(trait_data) }
11
+
12
+ node_class.new(inputs, attributes, traits)
13
+ end
14
+
15
+ def build_field_declaration(data, node_class)
16
+ name = restore_name_type(data[:name], data[:name_type])
17
+ type = deserialize_type(data[:field_type])
18
+ domain = deserialize_domain(data[:domain])
19
+
20
+ # Match the Struct signature of FieldDecl: (name, domain, type)
21
+ node_class.new(name, domain, type)
22
+ end
23
+
24
+ def build_attribute_declaration(data, node_class)
25
+ name = restore_name_type(data[:name], data[:name_type])
26
+ expression = build_node(data[:expression])
27
+
28
+ node_class.new(name, expression)
29
+ end
30
+
31
+ def build_trait_declaration(data, node_class)
32
+ name = restore_name_type(data[:name], data[:name_type])
33
+ expression = build_node(data[:expression])
34
+
35
+ node_class.new(name, expression)
36
+ end
37
+
38
+ def build_call_expression(data, node_class)
39
+ function_name = restore_name_type(data[:function_name], data[:function_name_type])
40
+ arguments = data[:arguments].map { |arg_data| build_node(arg_data) }
41
+
42
+ node_class.new(function_name, arguments)
43
+ end
44
+
45
+ def build_literal(data, node_class)
46
+ value = data[:value]
47
+
48
+ # Restore proper Ruby type if needed
49
+ value = coerce_to_type(value, data[:ruby_type]) if data[:ruby_type] && value.is_a?(String)
50
+
51
+ node_class.new(value)
52
+ end
53
+
54
+ def build_field_reference(data, node_class)
55
+ field_name = restore_name_type(data[:field_name], data[:name_type])
56
+ node_class.new(field_name)
57
+ end
58
+
59
+ def build_binding_reference(data, node_class)
60
+ binding_name = restore_name_type(data[:binding_name], data[:name_type])
61
+ node_class.new(binding_name)
62
+ end
63
+
64
+ def build_list_expression(data, node_class)
65
+ elements = data[:elements].map { |element_data| build_node(element_data) }
66
+ node_class.new(elements)
67
+ end
68
+
69
+ def build_cascade_expression(data, node_class)
70
+ cases = data[:cases].map { |case_data| build_node(case_data) }
71
+ node_class.new(cases)
72
+ end
73
+
74
+ def build_when_case_expression(data, node_class)
75
+ condition = build_node(data[:condition])
76
+ result = build_node(data[:result])
77
+ node_class.new(condition, result)
78
+ end
79
+
80
+ private
81
+
82
+ def deserialize_type(type_data)
83
+ # Handle simple types that weren't serialized with the new format
84
+ return type_data unless type_data.is_a?(Hash) && type_data.key?(:type)
85
+
86
+ case type_data[:type]
87
+ when "symbol"
88
+ type_data[:value].to_sym
89
+ when "array"
90
+ { array: deserialize_type(type_data[:element_type]) }
91
+ when "hash"
92
+ { hash: [deserialize_type(type_data[:key_type]), deserialize_type(type_data[:value_type])] }
93
+ when "literal", nil
94
+ type_data[:value]
95
+ end
96
+ end
97
+
98
+ def build_node(node_data)
99
+ type_name = node_data[:type]
100
+ node_class = NodeRegistry.class_for_type(type_name)
101
+
102
+ build_method = "build_#{type_name}"
103
+ raise Kumi::Core::Export::Errors::DeserializationError, "No builder for type: #{type_name}" unless respond_to?(build_method, true)
104
+
105
+ send(build_method, node_data, node_class)
106
+ end
107
+
108
+ def deserialize_domain(domain_data)
109
+ return nil unless domain_data
110
+
111
+ case domain_data[:type]
112
+ when "range"
113
+ min, max = domain_data.values_at(:min, :max)
114
+ domain_data[:exclude_end] ? (min...max) : (min..max)
115
+ when "array"
116
+ domain_data[:values]
117
+ when "custom"
118
+ # For custom domains, we might need to eval or have a registry
119
+ domain_data[:value]
120
+ end
121
+ end
122
+
123
+ def coerce_to_type(value, type_name)
124
+ case type_name
125
+ when "Integer" then value.to_i
126
+ when "Float" then value.to_f
127
+ when "Symbol" then value.to_sym
128
+ else value
129
+ end
130
+ end
131
+
132
+ def restore_name_type(name_string, name_type)
133
+ case name_type
134
+ when "Symbol" then name_string.to_sym
135
+ when "String" then name_string.to_s
136
+ else name_string
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Export
6
+ class NodeRegistry
7
+ # Maps AST classes to JSON type names
8
+ SERIALIZATION_MAP = {
9
+ "Kumi::Syntax::Root" => "root",
10
+ "Kumi::Syntax::InputDeclaration" => "field_declaration",
11
+ "Kumi::Syntax::ValueDeclaration" => "attribute_declaration",
12
+ "Kumi::Syntax::TraitDeclaration" => "trait_declaration",
13
+ "Kumi::Syntax::CallExpression" => "call_expression",
14
+ "Kumi::Syntax::ArrayExpression" => "list_expression",
15
+ "Kumi::Syntax::HashExpression" => "hash_expression",
16
+ "Kumi::Syntax::CascadeExpression" => "cascade_expression",
17
+ "Kumi::Syntax::CaseExpression" => "when_case_expression",
18
+ "Kumi::Syntax::Literal" => "literal",
19
+ "Kumi::Syntax::InputReference" => "field_reference",
20
+ "Kumi::Syntax::DeclarationReference" => "binding_reference"
21
+ }.freeze
22
+
23
+ # Maps JSON type names back to AST classes (using new canonical class names)
24
+ DESERIALIZATION_MAP = {
25
+ "root" => "Kumi::Syntax::Root",
26
+ "field_declaration" => "Kumi::Syntax::InputDeclaration",
27
+ "attribute_declaration" => "Kumi::Syntax::ValueDeclaration",
28
+ "trait_declaration" => "Kumi::Syntax::TraitDeclaration",
29
+ "call_expression" => "Kumi::Syntax::CallExpression",
30
+ "list_expression" => "Kumi::Syntax::ArrayExpression",
31
+ "hash_expression" => "Kumi::Syntax::HashExpression",
32
+ "cascade_expression" => "Kumi::Syntax::CascadeExpression",
33
+ "when_case_expression" => "Kumi::Syntax::CaseExpression",
34
+ "literal" => "Kumi::Syntax::Literal",
35
+ "field_reference" => "Kumi::Syntax::InputReference",
36
+ "binding_reference" => "Kumi::Syntax::DeclarationReference"
37
+ }.freeze
38
+
39
+ def self.type_name_for(node)
40
+ SERIALIZATION_MAP[node.class.name] or
41
+ raise Kumi::Core::Export::Errors::SerializationError, "Unknown node type: #{node.class.name}"
42
+ end
43
+
44
+ def self.class_for_type(type_name)
45
+ class_name = DESERIALIZATION_MAP[type_name] or
46
+ raise Kumi::Core::Export::Errors::DeserializationError, "Unknown type name: #{type_name}"
47
+
48
+ # Resolve the class from string name
49
+ class_name.split("::").reduce(Object) { |const, name| const.const_get(name) }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end