kumi 0.0.0 → 0.0.4
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/.rubocop.yml +113 -3
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +387 -0
- data/README.md +270 -20
- data/docs/development/README.md +120 -0
- data/docs/development/error-reporting.md +361 -0
- data/documents/AST.md +126 -0
- data/documents/DSL.md +154 -0
- data/documents/FUNCTIONS.md +132 -0
- data/documents/SYNTAX.md +367 -0
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
- data/examples/federal_tax_calculator_2024.rb +112 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
- data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
- data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
- data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
- data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
- data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
- data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
- data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
- data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
- data/lib/kumi/analyzer.rb +54 -0
- data/lib/kumi/atom_unsat_solver.rb +349 -0
- data/lib/kumi/compiled_schema.rb +41 -0
- data/lib/kumi/compiler.rb +127 -0
- data/lib/kumi/domain/enum_analyzer.rb +53 -0
- data/lib/kumi/domain/range_analyzer.rb +83 -0
- data/lib/kumi/domain/validator.rb +84 -0
- data/lib/kumi/domain/violation_formatter.rb +40 -0
- data/lib/kumi/domain.rb +8 -0
- data/lib/kumi/error_reporter.rb +164 -0
- data/lib/kumi/error_reporting.rb +95 -0
- data/lib/kumi/errors.rb +116 -0
- data/lib/kumi/evaluation_wrapper.rb +22 -0
- data/lib/kumi/explain.rb +281 -0
- data/lib/kumi/export/deserializer.rb +39 -0
- data/lib/kumi/export/errors.rb +12 -0
- data/lib/kumi/export/node_builders.rb +140 -0
- data/lib/kumi/export/node_registry.rb +38 -0
- data/lib/kumi/export/node_serializers.rb +156 -0
- data/lib/kumi/export/serializer.rb +23 -0
- data/lib/kumi/export.rb +33 -0
- data/lib/kumi/function_registry/collection_functions.rb +92 -0
- data/lib/kumi/function_registry/comparison_functions.rb +31 -0
- data/lib/kumi/function_registry/conditional_functions.rb +36 -0
- data/lib/kumi/function_registry/function_builder.rb +92 -0
- data/lib/kumi/function_registry/logical_functions.rb +42 -0
- data/lib/kumi/function_registry/math_functions.rb +72 -0
- data/lib/kumi/function_registry/string_functions.rb +54 -0
- data/lib/kumi/function_registry/type_functions.rb +51 -0
- data/lib/kumi/function_registry.rb +138 -0
- data/lib/kumi/input/type_matcher.rb +92 -0
- data/lib/kumi/input/validator.rb +52 -0
- data/lib/kumi/input/violation_creator.rb +50 -0
- data/lib/kumi/input.rb +8 -0
- data/lib/kumi/parser/build_context.rb +25 -0
- data/lib/kumi/parser/dsl.rb +12 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
- data/lib/kumi/parser/expression_converter.rb +58 -0
- data/lib/kumi/parser/guard_rails.rb +43 -0
- data/lib/kumi/parser/input_builder.rb +94 -0
- data/lib/kumi/parser/input_proxy.rb +29 -0
- data/lib/kumi/parser/parser.rb +66 -0
- data/lib/kumi/parser/schema_builder.rb +172 -0
- data/lib/kumi/parser/sugar.rb +108 -0
- data/lib/kumi/schema.rb +49 -0
- data/lib/kumi/schema_instance.rb +43 -0
- data/lib/kumi/syntax/declarations.rb +23 -0
- data/lib/kumi/syntax/expressions.rb +30 -0
- data/lib/kumi/syntax/node.rb +46 -0
- data/lib/kumi/syntax/root.rb +12 -0
- data/lib/kumi/syntax/terminal_expressions.rb +27 -0
- data/lib/kumi/syntax.rb +9 -0
- data/lib/kumi/types/builder.rb +21 -0
- data/lib/kumi/types/compatibility.rb +86 -0
- data/lib/kumi/types/formatter.rb +24 -0
- data/lib/kumi/types/inference.rb +40 -0
- data/lib/kumi/types/normalizer.rb +70 -0
- data/lib/kumi/types/validator.rb +35 -0
- data/lib/kumi/types.rb +64 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +7 -3
- data/scripts/generate_function_docs.rb +59 -0
- data/test_impossible_cascade.rb +51 -0
- metadata +93 -10
- data/sig/kumi.rbs +0 -4
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Parser
|
5
|
+
class SchemaBuilder
|
6
|
+
include GuardRails
|
7
|
+
include Syntax
|
8
|
+
include ErrorReporting
|
9
|
+
|
10
|
+
DSL_METHODS = %i[value trait input ref literal fn].freeze
|
11
|
+
|
12
|
+
def initialize(context)
|
13
|
+
@context = context
|
14
|
+
end
|
15
|
+
|
16
|
+
def value(name = nil, expr = nil, &blk)
|
17
|
+
update_location
|
18
|
+
validate_value_args(name, expr, blk)
|
19
|
+
|
20
|
+
expression = blk ? build_cascade(&blk) : ensure_syntax(expr)
|
21
|
+
@context.attributes << Attribute.new(name, expression, loc: @context.current_location)
|
22
|
+
end
|
23
|
+
|
24
|
+
def trait(*args, **kwargs)
|
25
|
+
update_location
|
26
|
+
raise_syntax_error("keyword trait syntax not supported", location: @context.current_location) unless kwargs.empty?
|
27
|
+
build_positional_trait(args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def input(&blk)
|
31
|
+
return InputProxy.new(@context) unless block_given?
|
32
|
+
|
33
|
+
raise_syntax_error("input block already defined", location: @context.current_location) if @context.input_block_defined?
|
34
|
+
@context.mark_input_block_defined!
|
35
|
+
|
36
|
+
update_location
|
37
|
+
input_builder = InputBuilder.new(@context)
|
38
|
+
input_builder.instance_eval(&blk)
|
39
|
+
end
|
40
|
+
|
41
|
+
def ref(name)
|
42
|
+
update_location
|
43
|
+
Binding.new(name, loc: @context.current_location)
|
44
|
+
end
|
45
|
+
|
46
|
+
def literal(value)
|
47
|
+
update_location
|
48
|
+
Literal.new(value, loc: @context.current_location)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fn(fn_name, *args)
|
52
|
+
update_location
|
53
|
+
expr_args = args.map { |a| ensure_syntax(a) }
|
54
|
+
CallExpression.new(fn_name, expr_args, loc: @context.current_location)
|
55
|
+
end
|
56
|
+
|
57
|
+
def method_missing(method_name, *args, &block)
|
58
|
+
if args.empty? && !block_given?
|
59
|
+
update_location
|
60
|
+
Binding.new(method_name, loc: @context.current_location)
|
61
|
+
else
|
62
|
+
super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def update_location
|
73
|
+
# Use caller_locations(2, 1) to skip the DSL method and get the actual user code location
|
74
|
+
# Stack: [0] update_location, [1] DSL method (value/trait/etc), [2] user's DSL code
|
75
|
+
caller_location = caller_locations(2, 1).first
|
76
|
+
|
77
|
+
@context.current_location = Location.new(
|
78
|
+
file: caller_location.path,
|
79
|
+
line: caller_location.lineno,
|
80
|
+
column: 0
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_value_args(name, expr, blk)
|
85
|
+
raise_syntax_error("value requires a name as first argument", location: @context.current_location) if name.nil?
|
86
|
+
unless name.is_a?(Symbol)
|
87
|
+
raise_syntax_error("The name for 'value' must be a Symbol, got #{name.class}",
|
88
|
+
location: @context.current_location)
|
89
|
+
end
|
90
|
+
|
91
|
+
has_expr = !expr.nil?
|
92
|
+
has_block = blk
|
93
|
+
|
94
|
+
if has_expr && has_block
|
95
|
+
raise_syntax_error("value '#{name}' cannot be called with both an expression and a block", location: @context.current_location)
|
96
|
+
elsif !has_expr && !has_block
|
97
|
+
raise_syntax_error("value '#{name}' requires an expression or a block", location: @context.current_location)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_positional_trait(args)
|
102
|
+
case args.size
|
103
|
+
when 0
|
104
|
+
raise_syntax_error("trait requires a name and expression", location: @context.current_location)
|
105
|
+
when 1
|
106
|
+
name = args.first
|
107
|
+
raise_syntax_error("trait '#{name}' requires an expression", location: @context.current_location)
|
108
|
+
when 2
|
109
|
+
name, expression = args
|
110
|
+
validate_trait_name(name)
|
111
|
+
expr = ensure_syntax(expression)
|
112
|
+
@context.traits << Trait.new(name, expr, loc: @context.current_location)
|
113
|
+
else
|
114
|
+
handle_deprecated_trait_syntax(args)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def handle_deprecated_trait_syntax(args)
|
119
|
+
if args.size == 3
|
120
|
+
name, = args
|
121
|
+
raise_syntax_error("trait '#{name}' requires exactly 3 arguments: lhs, operator, and rhs", location: @context.current_location)
|
122
|
+
end
|
123
|
+
|
124
|
+
# warn "DEPRECATION: trait(:name, lhs, operator, rhs) syntax is deprecated. Use: trait :name, (lhs operator rhs)"
|
125
|
+
|
126
|
+
if args.size == 4
|
127
|
+
name, lhs, operator, rhs = args
|
128
|
+
build_deprecated_trait(name, lhs, operator, [rhs])
|
129
|
+
else
|
130
|
+
name, lhs, operator, *rhs = args
|
131
|
+
build_deprecated_trait(name, lhs, operator, rhs)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def build_deprecated_trait(name, lhs, operator, rhs)
|
136
|
+
validate_trait_name(name)
|
137
|
+
validate_operator(operator)
|
138
|
+
|
139
|
+
rhs_exprs = rhs.map { |r| ensure_syntax(r) }
|
140
|
+
expr = CallExpression.new(operator, [ensure_syntax(lhs)] + rhs_exprs, loc: @context.current_location)
|
141
|
+
@context.traits << Trait.new(name, expr, loc: @context.current_location)
|
142
|
+
end
|
143
|
+
|
144
|
+
def validate_trait_name(name)
|
145
|
+
return if name.is_a?(Symbol)
|
146
|
+
|
147
|
+
raise_syntax_error("The name for 'trait' must be a Symbol, got #{name.class}", location: @context.current_location)
|
148
|
+
end
|
149
|
+
|
150
|
+
def validate_operator(operator)
|
151
|
+
unless operator.is_a?(Symbol)
|
152
|
+
raise_syntax_error("expects a symbol for an operator, got #{operator.class}", location: @context.current_location)
|
153
|
+
end
|
154
|
+
|
155
|
+
return if FunctionRegistry.operator?(operator)
|
156
|
+
|
157
|
+
raise_syntax_error("unsupported operator `#{operator}`", location: @context.current_location)
|
158
|
+
end
|
159
|
+
|
160
|
+
def build_cascade(&blk)
|
161
|
+
expression_converter = ExpressionConverter.new(@context)
|
162
|
+
cascade_builder = DslCascadeBuilder.new(expression_converter, @context.current_location)
|
163
|
+
cascade_builder.instance_eval(&blk)
|
164
|
+
CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
|
165
|
+
end
|
166
|
+
|
167
|
+
def ensure_syntax(obj)
|
168
|
+
ExpressionConverter.new(@context).ensure_syntax(obj)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Parser
|
5
|
+
module Sugar
|
6
|
+
include Syntax
|
7
|
+
|
8
|
+
ARITHMETIC_OPS = { :+ => :add, :- => :subtract, :* => :multiply,
|
9
|
+
:/ => :divide, :% => :modulo, :** => :power }.freeze
|
10
|
+
COMPARISON_OPS = %i[< <= > >= == !=].freeze
|
11
|
+
LITERAL_TYPES = [Integer, String, Symbol, TrueClass, FalseClass, Float, Regexp].freeze
|
12
|
+
|
13
|
+
def self.ensure_literal(obj)
|
14
|
+
return Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
|
15
|
+
return obj if obj.is_a?(Syntax::Node)
|
16
|
+
return obj.to_ast_node if obj.respond_to?(:to_ast_node)
|
17
|
+
|
18
|
+
Literal.new(obj)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.syntax_expression?(obj)
|
22
|
+
obj.is_a?(Syntax::Node) || obj.respond_to?(:to_ast_node)
|
23
|
+
end
|
24
|
+
|
25
|
+
module ExpressionRefinement
|
26
|
+
refine Syntax::Node do
|
27
|
+
ARITHMETIC_OPS.each do |op, op_name|
|
28
|
+
define_method(op) do |other|
|
29
|
+
other_node = Sugar.ensure_literal(other)
|
30
|
+
Syntax::CallExpression.new(op_name, [self, other_node])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
COMPARISON_OPS.each do |op|
|
35
|
+
define_method(op) do |other|
|
36
|
+
other_node = Sugar.ensure_literal(other)
|
37
|
+
Syntax::CallExpression.new(op, [self, other_node])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](index)
|
42
|
+
Syntax::CallExpression.new(:at, [self, Sugar.ensure_literal(index)])
|
43
|
+
end
|
44
|
+
|
45
|
+
def -@
|
46
|
+
Syntax::CallExpression.new(:subtract, [Sugar.ensure_literal(0), self])
|
47
|
+
end
|
48
|
+
|
49
|
+
def &(other)
|
50
|
+
Syntax::CallExpression.new(:and, [self, Sugar.ensure_literal(other)])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module NumericRefinement
|
56
|
+
[Integer, Float].each do |klass|
|
57
|
+
refine klass do
|
58
|
+
ARITHMETIC_OPS.each do |op, op_name|
|
59
|
+
define_method(op) do |other|
|
60
|
+
if Sugar.syntax_expression?(other)
|
61
|
+
other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
|
62
|
+
Syntax::CallExpression.new(op_name, [Syntax::Literal.new(self), other_node])
|
63
|
+
else
|
64
|
+
super(other)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
COMPARISON_OPS.each do |op|
|
70
|
+
define_method(op) do |other|
|
71
|
+
if Sugar.syntax_expression?(other)
|
72
|
+
other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
|
73
|
+
Syntax::CallExpression.new(op, [Syntax::Literal.new(self), other_node])
|
74
|
+
else
|
75
|
+
super(other)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module StringRefinement
|
84
|
+
refine String do
|
85
|
+
def +(other)
|
86
|
+
if Sugar.syntax_expression?(other)
|
87
|
+
other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
|
88
|
+
Syntax::CallExpression.new(:concat, [Syntax::Literal.new(self), other_node])
|
89
|
+
else
|
90
|
+
super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
%i[== !=].each do |op|
|
95
|
+
define_method(op) do |other|
|
96
|
+
if Sugar.syntax_expression?(other)
|
97
|
+
other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
|
98
|
+
Syntax::CallExpression.new(op, [Syntax::Literal.new(self), other_node])
|
99
|
+
else
|
100
|
+
super(other)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/kumi/schema.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
module Kumi
|
6
|
+
module Schema
|
7
|
+
Inspector = Struct.new(:syntax_tree, :analyzer_result, :compiled_schema) do
|
8
|
+
def inspect
|
9
|
+
"#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, schema: #{schema.inspect}>"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def from(context)
|
14
|
+
raise("No schema defined") unless @__compiled_schema__
|
15
|
+
|
16
|
+
# Validate input types and domain constraints
|
17
|
+
input_meta = @__analyzer_result__.state[:input_meta] || {}
|
18
|
+
violations = Input::Validator.validate_context(context, input_meta)
|
19
|
+
|
20
|
+
raise Errors::InputValidationError, violations unless violations.empty?
|
21
|
+
|
22
|
+
SchemaInstance.new(@__compiled_schema__, @__analyzer_result__.definitions, context)
|
23
|
+
end
|
24
|
+
|
25
|
+
def explain(context, *keys)
|
26
|
+
raise("No schema defined") unless @__compiled_schema__
|
27
|
+
|
28
|
+
# Validate input types and domain constraints
|
29
|
+
input_meta = @__analyzer_result__.state[:input_meta] || {}
|
30
|
+
violations = Input::Validator.validate_context(context, input_meta)
|
31
|
+
|
32
|
+
raise Errors::InputValidationError, violations unless violations.empty?
|
33
|
+
|
34
|
+
keys.each do |key|
|
35
|
+
puts Kumi::Explain.call(self, key, inputs: context)
|
36
|
+
end
|
37
|
+
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def schema(&block)
|
42
|
+
@__syntax_tree__ = Kumi::Parser::Dsl.build_syntax_tree(&block).freeze
|
43
|
+
@__analyzer_result__ = Analyzer.analyze!(@__syntax_tree__).freeze
|
44
|
+
@__compiled_schema__ = Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__).freeze
|
45
|
+
|
46
|
+
Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
# A bound pair of <compiled schema + context>. Immutable.
|
5
|
+
#
|
6
|
+
# Public API ----------------------------------------------------------
|
7
|
+
# instance.evaluate # => full Hash of all bindings
|
8
|
+
# instance.evaluate(:tax_due, :rate)
|
9
|
+
# instance.slice(:tax_due) # alias for evaluate(*keys)
|
10
|
+
# instance.explain(:tax_due) # pretty trace string
|
11
|
+
# instance.input # original context (read‑only)
|
12
|
+
#
|
13
|
+
class SchemaInstance
|
14
|
+
def initialize(compiled_schema, analysis, context)
|
15
|
+
@compiled_schema = compiled_schema # Kumi::CompiledSchema
|
16
|
+
@analysis = analysis # Analyzer result (for deps)
|
17
|
+
@context = context.is_a?(EvaluationWrapper) ? context : EvaluationWrapper.new(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Hash‑like read of one or many bindings
|
21
|
+
def evaluate(*key_names)
|
22
|
+
if key_names.empty?
|
23
|
+
@compiled_schema.evaluate(@context)
|
24
|
+
else
|
25
|
+
@compiled_schema.evaluate(@context, *key_names)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def slice(*key_names)
|
30
|
+
return {} if key_names.empty?
|
31
|
+
|
32
|
+
evaluate(*key_names)
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](key_name)
|
36
|
+
evaluate(key_name)[key_name]
|
37
|
+
end
|
38
|
+
|
39
|
+
def input
|
40
|
+
@context
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Syntax
|
5
|
+
module Declarations
|
6
|
+
Attribute = Struct.new(:name, :expression) do
|
7
|
+
include Node
|
8
|
+
def children = [expression]
|
9
|
+
end
|
10
|
+
|
11
|
+
Trait = Struct.new(:name, :expression) do
|
12
|
+
include Node
|
13
|
+
def children = [expression]
|
14
|
+
end
|
15
|
+
|
16
|
+
# For field metadata declarations inside input blocks
|
17
|
+
FieldDecl = Struct.new(:name, :domain, :type) do
|
18
|
+
include Node
|
19
|
+
def children = []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Syntax
|
5
|
+
module Expressions
|
6
|
+
CallExpression = Struct.new(:fn_name, :args) do
|
7
|
+
include Node
|
8
|
+
def children = args
|
9
|
+
end
|
10
|
+
CascadeExpression = Struct.new(:cases) do
|
11
|
+
include Node
|
12
|
+
def children = cases
|
13
|
+
end
|
14
|
+
|
15
|
+
WhenCaseExpression = Struct.new(:condition, :result) do
|
16
|
+
include Node
|
17
|
+
def children = [condition, result]
|
18
|
+
end
|
19
|
+
|
20
|
+
ListExpression = Struct.new(:elements) do
|
21
|
+
include Node
|
22
|
+
def children = elements
|
23
|
+
|
24
|
+
def size
|
25
|
+
elements.size
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Syntax
|
5
|
+
# A struct to hold standardized source location information.
|
6
|
+
Location = Struct.new(:file, :line, :column, keyword_init: true)
|
7
|
+
|
8
|
+
# Base module included by all AST nodes to provide a standard
|
9
|
+
# interface for accessing source location information..
|
10
|
+
module Node
|
11
|
+
attr_accessor :loc
|
12
|
+
|
13
|
+
def initialize(*args, loc: nil, **kwargs)
|
14
|
+
@loc = loc
|
15
|
+
super(*args, **kwargs)
|
16
|
+
freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
other.is_a?(self.class) &&
|
21
|
+
# for Struct-based nodes
|
22
|
+
(if respond_to?(:members)
|
23
|
+
members.all? { |m| self[m] == other[m] }
|
24
|
+
else
|
25
|
+
instance_variables.reject { |iv| iv == :@loc }
|
26
|
+
.all? do |iv|
|
27
|
+
instance_variable_get(iv) ==
|
28
|
+
other.instance_variable_get(iv)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
)
|
32
|
+
end
|
33
|
+
alias eql? ==
|
34
|
+
|
35
|
+
def hash
|
36
|
+
values = if respond_to?(:members)
|
37
|
+
members.map { |m| self[m] }
|
38
|
+
else
|
39
|
+
instance_variables.reject { |iv| iv == :@loc }
|
40
|
+
.map { |iv| instance_variable_get(iv) }
|
41
|
+
end
|
42
|
+
[self.class, *values].hash
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Syntax
|
5
|
+
# Represents the root of the Abstract Syntax Tree.
|
6
|
+
# It holds all the top-level declarations parsed from the source.
|
7
|
+
Root = Struct.new(:inputs, :attributes, :traits) do
|
8
|
+
include Node
|
9
|
+
def children = [inputs, attributes, traits]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "node"
|
4
|
+
|
5
|
+
module Kumi
|
6
|
+
module Syntax
|
7
|
+
module TerminalExpressions
|
8
|
+
# Leaf expressions that represent a value or reference and terminate a branch.
|
9
|
+
|
10
|
+
Literal = Struct.new(:value) do
|
11
|
+
include Node
|
12
|
+
def children = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# For field usage/reference in expressions (input.field_name)
|
16
|
+
FieldRef = Struct.new(:name) do
|
17
|
+
include Node
|
18
|
+
def children = []
|
19
|
+
end
|
20
|
+
|
21
|
+
Binding = Struct.new(:name) do
|
22
|
+
include Node
|
23
|
+
def children = []
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/kumi/syntax.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Types
|
5
|
+
# Builds complex type structures
|
6
|
+
class Builder
|
7
|
+
def self.array(elem_type)
|
8
|
+
raise ArgumentError, "Invalid array element type: #{elem_type}" unless Validator.valid_type?(elem_type)
|
9
|
+
|
10
|
+
{ array: elem_type }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.hash(key_type, val_type)
|
14
|
+
raise ArgumentError, "Invalid hash key type: #{key_type}" unless Validator.valid_type?(key_type)
|
15
|
+
raise ArgumentError, "Invalid hash value type: #{val_type}" unless Validator.valid_type?(val_type)
|
16
|
+
|
17
|
+
{ hash: [key_type, val_type] }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Types
|
5
|
+
# Handles type compatibility and unification
|
6
|
+
class Compatibility
|
7
|
+
# Check if two types are compatible
|
8
|
+
def self.compatible?(type1, type2)
|
9
|
+
# Any type is compatible with anything
|
10
|
+
return true if type1 == :any || type2 == :any
|
11
|
+
|
12
|
+
# Exact match
|
13
|
+
return true if type1 == type2
|
14
|
+
|
15
|
+
# Numeric compatibility
|
16
|
+
return true if numeric_compatible?(type1, type2)
|
17
|
+
|
18
|
+
# Array compatibility
|
19
|
+
return array_compatible?(type1, type2) if array_types?(type1, type2)
|
20
|
+
|
21
|
+
# Hash compatibility
|
22
|
+
return hash_compatible?(type1, type2) if hash_types?(type1, type2)
|
23
|
+
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Find the most specific common type between two types
|
28
|
+
def self.unify(type1, type2)
|
29
|
+
return type1 if type1 == type2
|
30
|
+
|
31
|
+
# :any unifies to the other type (more specific)
|
32
|
+
return type2 if type1 == :any
|
33
|
+
return type1 if type2 == :any
|
34
|
+
|
35
|
+
# Numeric unification
|
36
|
+
if numeric_compatible?(type1, type2)
|
37
|
+
return :integer if type1 == :integer && type2 == :integer
|
38
|
+
|
39
|
+
return :float # One or both are float
|
40
|
+
end
|
41
|
+
|
42
|
+
# Array unification
|
43
|
+
if array_types?(type1, type2)
|
44
|
+
elem1 = type1[:array]
|
45
|
+
elem2 = type2[:array]
|
46
|
+
unified_elem = unify(elem1, elem2)
|
47
|
+
return Kumi::Types.array(unified_elem)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Hash unification
|
51
|
+
if hash_types?(type1, type2)
|
52
|
+
key1, val1 = type1[:hash]
|
53
|
+
key2, val2 = type2[:hash]
|
54
|
+
unified_key = unify(key1, key2)
|
55
|
+
unified_val = unify(val1, val2)
|
56
|
+
return Kumi::Types.hash(unified_key, unified_val)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Fall back to :any for incompatible types
|
60
|
+
:any
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.numeric_compatible?(type1, type2)
|
64
|
+
numeric_types = %i[integer float]
|
65
|
+
numeric_types.include?(type1) && numeric_types.include?(type2)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.array_types?(type1, type2)
|
69
|
+
Validator.array_type?(type1) && Validator.array_type?(type2)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.hash_types?(type1, type2)
|
73
|
+
Validator.hash_type?(type1) && Validator.hash_type?(type2)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.array_compatible?(type1, type2)
|
77
|
+
compatible?(type1[:array], type2[:array])
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.hash_compatible?(type1, type2)
|
81
|
+
compatible?(type1[:hash][0], type2[:hash][0]) &&
|
82
|
+
compatible?(type1[:hash][1], type2[:hash][1])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Types
|
5
|
+
# Formats types for display and debugging
|
6
|
+
class Formatter
|
7
|
+
# Convert types to string representation
|
8
|
+
def self.type_to_s(type)
|
9
|
+
case type
|
10
|
+
when Hash
|
11
|
+
if type[:array]
|
12
|
+
"array(#{type_to_s(type[:array])})"
|
13
|
+
elsif type[:hash]
|
14
|
+
"hash(#{type_to_s(type[:hash][0])}, #{type_to_s(type[:hash][1])})"
|
15
|
+
else
|
16
|
+
type.to_s
|
17
|
+
end
|
18
|
+
else
|
19
|
+
type.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|