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.
- checksums.yaml +4 -4
- data/CLAUDE.md +1 -1
- data/README.md +8 -5
- data/examples/game_of_life.rb +1 -1
- data/examples/static_analysis_errors.rb +7 -7
- data/lib/kumi/analyzer.rb +15 -15
- data/lib/kumi/compiler.rb +6 -6
- data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
- data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
- data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
- data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
- data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
- data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
- data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
- data/lib/kumi/core/atom_unsat_solver.rb +396 -0
- data/lib/kumi/core/compiled_schema.rb +43 -0
- data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
- data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
- data/lib/kumi/core/domain/range_analyzer.rb +85 -0
- data/lib/kumi/core/domain/validator.rb +82 -0
- data/lib/kumi/core/domain/violation_formatter.rb +42 -0
- data/lib/kumi/core/error_reporter.rb +166 -0
- data/lib/kumi/core/error_reporting.rb +97 -0
- data/lib/kumi/core/errors.rb +120 -0
- data/lib/kumi/core/evaluation_wrapper.rb +40 -0
- data/lib/kumi/core/explain.rb +295 -0
- data/lib/kumi/core/export/deserializer.rb +41 -0
- data/lib/kumi/core/export/errors.rb +14 -0
- data/lib/kumi/core/export/node_builders.rb +142 -0
- data/lib/kumi/core/export/node_registry.rb +54 -0
- data/lib/kumi/core/export/node_serializers.rb +158 -0
- data/lib/kumi/core/export/serializer.rb +25 -0
- data/lib/kumi/core/export.rb +35 -0
- data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
- data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
- data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
- data/lib/kumi/core/function_registry/function_builder.rb +95 -0
- data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
- data/lib/kumi/core/function_registry/math_functions.rb +74 -0
- data/lib/kumi/core/function_registry/string_functions.rb +57 -0
- data/lib/kumi/core/function_registry/type_functions.rb +53 -0
- data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
- data/lib/kumi/core/input/type_matcher.rb +97 -0
- data/lib/kumi/core/input/validator.rb +51 -0
- data/lib/kumi/core/input/violation_creator.rb +52 -0
- data/lib/kumi/core/json_schema/generator.rb +65 -0
- data/lib/kumi/core/json_schema/validator.rb +27 -0
- data/lib/kumi/core/json_schema.rb +16 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
- data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
- data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
- data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
- data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
- data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
- data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
- data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
- data/lib/kumi/core/ruby_parser/parser.rb +71 -0
- data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
- data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
- data/lib/kumi/core/ruby_parser.rb +12 -0
- data/lib/kumi/core/schema_instance.rb +111 -0
- data/lib/kumi/core/types/builder.rb +23 -0
- data/lib/kumi/core/types/compatibility.rb +96 -0
- data/lib/kumi/core/types/formatter.rb +26 -0
- data/lib/kumi/core/types/inference.rb +42 -0
- data/lib/kumi/core/types/normalizer.rb +72 -0
- data/lib/kumi/core/types/validator.rb +37 -0
- data/lib/kumi/core/types.rb +66 -0
- data/lib/kumi/core/vectorization_metadata.rb +110 -0
- data/lib/kumi/errors.rb +1 -112
- data/lib/kumi/registry.rb +37 -0
- data/lib/kumi/schema.rb +5 -5
- data/lib/kumi/schema_metadata.rb +3 -3
- 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 +5 -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/migrate_to_core_iterative.rb +938 -0
- data/scripts/generate_function_docs.rb +9 -9
- metadata +75 -72
- data/lib/kumi/analyzer/analysis_state.rb +0 -37
- data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
- data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
- data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
- data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
- data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
- data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
- data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
- data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
- data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
- data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
- data/lib/kumi/atom_unsat_solver.rb +0 -394
- data/lib/kumi/compiled_schema.rb +0 -41
- data/lib/kumi/constraint_relationship_solver.rb +0 -638
- data/lib/kumi/domain/enum_analyzer.rb +0 -53
- data/lib/kumi/domain/range_analyzer.rb +0 -83
- data/lib/kumi/domain/validator.rb +0 -80
- data/lib/kumi/domain/violation_formatter.rb +0 -40
- data/lib/kumi/error_reporter.rb +0 -164
- data/lib/kumi/error_reporting.rb +0 -95
- data/lib/kumi/evaluation_wrapper.rb +0 -38
- data/lib/kumi/explain.rb +0 -293
- data/lib/kumi/export/deserializer.rb +0 -39
- data/lib/kumi/export/errors.rb +0 -12
- data/lib/kumi/export/node_builders.rb +0 -140
- data/lib/kumi/export/node_registry.rb +0 -52
- data/lib/kumi/export/node_serializers.rb +0 -156
- data/lib/kumi/export/serializer.rb +0 -23
- data/lib/kumi/export.rb +0 -33
- data/lib/kumi/function_registry/collection_functions.rb +0 -200
- data/lib/kumi/function_registry/comparison_functions.rb +0 -31
- data/lib/kumi/function_registry/conditional_functions.rb +0 -36
- data/lib/kumi/function_registry/function_builder.rb +0 -93
- data/lib/kumi/function_registry/logical_functions.rb +0 -42
- data/lib/kumi/function_registry/math_functions.rb +0 -72
- data/lib/kumi/function_registry/string_functions.rb +0 -54
- data/lib/kumi/function_registry/type_functions.rb +0 -51
- data/lib/kumi/input/type_matcher.rb +0 -95
- data/lib/kumi/input/validator.rb +0 -49
- data/lib/kumi/input/violation_creator.rb +0 -50
- data/lib/kumi/json_schema/generator.rb +0 -63
- data/lib/kumi/json_schema/validator.rb +0 -25
- data/lib/kumi/json_schema.rb +0 -14
- data/lib/kumi/ruby_parser/build_context.rb +0 -25
- data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
- data/lib/kumi/ruby_parser/dsl.rb +0 -12
- data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
- data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
- data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
- data/lib/kumi/ruby_parser/input_builder.rb +0 -125
- data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
- data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
- data/lib/kumi/ruby_parser/nested_input.rb +0 -15
- data/lib/kumi/ruby_parser/parser.rb +0 -69
- data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
- data/lib/kumi/ruby_parser/sugar.rb +0 -261
- data/lib/kumi/ruby_parser.rb +0 -10
- data/lib/kumi/schema_instance.rb +0 -109
- data/lib/kumi/types/builder.rb +0 -21
- data/lib/kumi/types/compatibility.rb +0 -94
- data/lib/kumi/types/formatter.rb +0 -24
- data/lib/kumi/types/inference.rb +0 -40
- data/lib/kumi/types/normalizer.rb +0 -70
- data/lib/kumi/types/validator.rb +0 -35
- data/lib/kumi/types.rb +0 -64
- 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
|