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,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
class SchemaBuilder
|
7
|
+
include GuardRails
|
8
|
+
include Syntax
|
9
|
+
include ErrorReporting
|
10
|
+
|
11
|
+
DSL_METHODS = %i[value trait input ref literal fn].freeze
|
12
|
+
|
13
|
+
def initialize(context)
|
14
|
+
@context = context
|
15
|
+
end
|
16
|
+
|
17
|
+
def value(name = nil, expr = nil, &blk)
|
18
|
+
update_location
|
19
|
+
validate_value_args(name, expr, blk)
|
20
|
+
|
21
|
+
expression = blk ? build_cascade(&blk) : ensure_syntax(expr)
|
22
|
+
@context.attributes << Kumi::Syntax::ValueDeclaration.new(name, expression, loc: @context.current_location)
|
23
|
+
end
|
24
|
+
|
25
|
+
def trait(*args, **kwargs)
|
26
|
+
update_location
|
27
|
+
raise_syntax_error("keyword trait syntax not supported", location: @context.current_location) unless kwargs.empty?
|
28
|
+
build_positional_trait(args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def input(&blk)
|
32
|
+
return InputProxy.new(@context) unless block_given?
|
33
|
+
|
34
|
+
raise_syntax_error("input block already defined", location: @context.current_location) if @context.input_block_defined?
|
35
|
+
@context.mark_input_block_defined!
|
36
|
+
|
37
|
+
update_location
|
38
|
+
input_builder = InputBuilder.new(@context)
|
39
|
+
input_builder.instance_eval(&blk)
|
40
|
+
end
|
41
|
+
|
42
|
+
def ref(name)
|
43
|
+
update_location
|
44
|
+
Kumi::Syntax::DeclarationReference.new(name, loc: @context.current_location)
|
45
|
+
end
|
46
|
+
|
47
|
+
def literal(value)
|
48
|
+
update_location
|
49
|
+
Kumi::Syntax::Literal.new(value, loc: @context.current_location)
|
50
|
+
end
|
51
|
+
|
52
|
+
def fn(fn_name, *args)
|
53
|
+
update_location
|
54
|
+
expr_args = args.map { |a| ensure_syntax(a) }
|
55
|
+
Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: @context.current_location)
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_missing(method_name, *args, &block)
|
59
|
+
if args.empty? && !block_given?
|
60
|
+
update_location
|
61
|
+
# Create proxy for declaration references (traits/values)
|
62
|
+
DeclarationReferenceProxy.new(method_name, @context)
|
63
|
+
else
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def update_location
|
75
|
+
# Use caller_locations(2, 1) to skip the DSL method and get the actual user code location
|
76
|
+
# Stack: [0] update_location, [1] DSL method (value/trait/etc), [2] user's DSL code
|
77
|
+
caller_location = caller_locations(2, 1).first
|
78
|
+
|
79
|
+
@context.current_location = Location.new(
|
80
|
+
file: caller_location.path,
|
81
|
+
line: caller_location.lineno,
|
82
|
+
column: 0
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate_value_args(name, expr, blk)
|
87
|
+
raise_syntax_error("value requires a name as first argument", location: @context.current_location) if name.nil?
|
88
|
+
unless name.is_a?(Symbol)
|
89
|
+
raise_syntax_error("The name for 'value' must be a Symbol, got #{name.class}",
|
90
|
+
location: @context.current_location)
|
91
|
+
end
|
92
|
+
|
93
|
+
has_expr = !expr.nil?
|
94
|
+
has_block = blk
|
95
|
+
|
96
|
+
if has_expr && has_block
|
97
|
+
raise_syntax_error("value '#{name}' cannot be called with both an expression and a block", location: @context.current_location)
|
98
|
+
elsif !has_expr && !has_block
|
99
|
+
raise_syntax_error("value '#{name}' requires an expression or a block", location: @context.current_location)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_positional_trait(args)
|
104
|
+
case args.size
|
105
|
+
when 0
|
106
|
+
raise_syntax_error("trait requires a name and expression", location: @context.current_location)
|
107
|
+
when 1
|
108
|
+
name = args.first
|
109
|
+
raise_syntax_error("trait '#{name}' requires an expression", location: @context.current_location)
|
110
|
+
when 2
|
111
|
+
name, expression = args
|
112
|
+
validate_trait_name(name)
|
113
|
+
expr = ensure_syntax(expression)
|
114
|
+
@context.traits << Kumi::Syntax::TraitDeclaration.new(name, expr, loc: @context.current_location)
|
115
|
+
else
|
116
|
+
handle_deprecated_trait_syntax(args)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def handle_deprecated_trait_syntax(args)
|
121
|
+
if args.size == 3
|
122
|
+
name, = args
|
123
|
+
raise_syntax_error("trait '#{name}' requires exactly 3 arguments: lhs, operator, and rhs", location: @context.current_location)
|
124
|
+
end
|
125
|
+
|
126
|
+
# warn "DEPRECATION: trait(:name, lhs, operator, rhs) syntax is deprecated. Use: trait :name, (lhs operator rhs)"
|
127
|
+
|
128
|
+
if args.size == 4
|
129
|
+
name, lhs, operator, rhs = args
|
130
|
+
build_deprecated_trait(name, lhs, operator, [rhs])
|
131
|
+
else
|
132
|
+
name, lhs, operator, *rhs = args
|
133
|
+
build_deprecated_trait(name, lhs, operator, rhs)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def build_deprecated_trait(name, lhs, operator, rhs)
|
138
|
+
validate_trait_name(name)
|
139
|
+
validate_operator(operator)
|
140
|
+
|
141
|
+
rhs_exprs = rhs.map { |r| ensure_syntax(r) }
|
142
|
+
expr = Kumi::Syntax::CallExpression.new(operator, [ensure_syntax(lhs)] + rhs_exprs, loc: @context.current_location)
|
143
|
+
@context.traits << Kumi::Syntax::TraitDeclaration.new(name, expr, loc: @context.current_location)
|
144
|
+
end
|
145
|
+
|
146
|
+
def validate_trait_name(name)
|
147
|
+
return if name.is_a?(Symbol)
|
148
|
+
|
149
|
+
raise_syntax_error("The name for 'trait' must be a Symbol, got #{name.class}", location: @context.current_location)
|
150
|
+
end
|
151
|
+
|
152
|
+
def validate_operator(operator)
|
153
|
+
unless operator.is_a?(Symbol)
|
154
|
+
raise_syntax_error("expects a symbol for an operator, got #{operator.class}", location: @context.current_location)
|
155
|
+
end
|
156
|
+
|
157
|
+
return if Kumi::Registry.operator?(operator)
|
158
|
+
|
159
|
+
raise_syntax_error("unsupported operator `#{operator}`", location: @context.current_location)
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_cascade(&blk)
|
163
|
+
expression_converter = ExpressionConverter.new(@context)
|
164
|
+
cascade_builder = DslCascadeBuilder.new(expression_converter, @context.current_location)
|
165
|
+
cascade_builder.instance_eval(&blk)
|
166
|
+
Kumi::Syntax::CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
|
167
|
+
end
|
168
|
+
|
169
|
+
def ensure_syntax(obj)
|
170
|
+
ExpressionConverter.new(@context).ensure_syntax(obj)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
module Sugar
|
7
|
+
include Syntax
|
8
|
+
|
9
|
+
ARITHMETIC_OPS = {
|
10
|
+
:+ => :add, :- => :subtract, :* => :multiply,
|
11
|
+
:/ => :divide, :% => :modulo, :** => :power
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
COMPARISON_OPS = %i[< <= > >= == !=].freeze
|
15
|
+
|
16
|
+
LITERAL_TYPES = [
|
17
|
+
Integer, String, Symbol, TrueClass, FalseClass, Float, Regexp, NilClass
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
# Collection methods that can be applied to arrays/syntax nodes
|
21
|
+
COLLECTION_METHODS = %i[
|
22
|
+
sum size length first last sort reverse unique min max empty? flatten
|
23
|
+
map_with_index indices
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
def self.ensure_literal(obj)
|
27
|
+
return Kumi::Syntax::Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
|
28
|
+
return obj if obj.is_a?(Syntax::Node)
|
29
|
+
return obj.to_ast_node if obj.respond_to?(:to_ast_node)
|
30
|
+
|
31
|
+
Kumi::Syntax::Literal.new(obj)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.syntax_expression?(obj)
|
35
|
+
obj.is_a?(Syntax::Node) || obj.respond_to?(:to_ast_node)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a call expression with consistent error handling
|
39
|
+
def self.create_call_expression(fn_name, args)
|
40
|
+
Kumi::Syntax::CallExpression.new(fn_name, args)
|
41
|
+
end
|
42
|
+
|
43
|
+
module ExpressionRefinement
|
44
|
+
refine Syntax::Node do
|
45
|
+
# Arithmetic operations
|
46
|
+
ARITHMETIC_OPS.each do |op, op_name|
|
47
|
+
define_method(op) do |other|
|
48
|
+
other_node = Sugar.ensure_literal(other)
|
49
|
+
Sugar.create_call_expression(op_name, [self, other_node])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Comparison operations
|
54
|
+
COMPARISON_OPS.each do |op|
|
55
|
+
define_method(op) do |other|
|
56
|
+
other_node = Sugar.ensure_literal(other)
|
57
|
+
Sugar.create_call_expression(op, [self, other_node])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Array access
|
62
|
+
def [](index)
|
63
|
+
Sugar.create_call_expression(:at, [self, Sugar.ensure_literal(index)])
|
64
|
+
end
|
65
|
+
|
66
|
+
# Unary minus
|
67
|
+
def -@
|
68
|
+
Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), self])
|
69
|
+
end
|
70
|
+
|
71
|
+
# Logical operations
|
72
|
+
def &(other)
|
73
|
+
Sugar.create_call_expression(:and, [self, Sugar.ensure_literal(other)])
|
74
|
+
end
|
75
|
+
|
76
|
+
def |(other)
|
77
|
+
Sugar.create_call_expression(:or, [self, Sugar.ensure_literal(other)])
|
78
|
+
end
|
79
|
+
|
80
|
+
# Collection methods - single argument (self)
|
81
|
+
COLLECTION_METHODS.each do |method_name|
|
82
|
+
next if method_name == :include? # Special case with element argument
|
83
|
+
|
84
|
+
define_method(method_name) do
|
85
|
+
Sugar.create_call_expression(method_name, [self])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Special case: include? takes an element argument
|
90
|
+
def include?(element)
|
91
|
+
Sugar.create_call_expression(:include?, [self, Sugar.ensure_literal(element)])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module NumericRefinement
|
97
|
+
[Integer, Float].each do |klass|
|
98
|
+
refine klass do
|
99
|
+
# Arithmetic operations with syntax expressions
|
100
|
+
ARITHMETIC_OPS.each do |op, op_name|
|
101
|
+
define_method(op) do |other|
|
102
|
+
if Sugar.syntax_expression?(other)
|
103
|
+
other_node = Sugar.ensure_literal(other)
|
104
|
+
Sugar.create_call_expression(op_name, [Kumi::Syntax::Literal.new(self), other_node])
|
105
|
+
else
|
106
|
+
super(other)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Comparison operations with syntax expressions
|
112
|
+
COMPARISON_OPS.each do |op|
|
113
|
+
define_method(op) do |other|
|
114
|
+
if Sugar.syntax_expression?(other)
|
115
|
+
other_node = Sugar.ensure_literal(other)
|
116
|
+
Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
|
117
|
+
else
|
118
|
+
super(other)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
module StringRefinement
|
127
|
+
refine String do
|
128
|
+
def +(other)
|
129
|
+
if Sugar.syntax_expression?(other)
|
130
|
+
other_node = Sugar.ensure_literal(other)
|
131
|
+
Sugar.create_call_expression(:concat, [Kumi::Syntax::Literal.new(self), other_node])
|
132
|
+
else
|
133
|
+
super
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
%i[== !=].each do |op|
|
138
|
+
define_method(op) do |other|
|
139
|
+
if Sugar.syntax_expression?(other)
|
140
|
+
other_node = Sugar.ensure_literal(other)
|
141
|
+
Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
|
142
|
+
else
|
143
|
+
super(other)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
module ArrayRefinement
|
151
|
+
refine Array do
|
152
|
+
# Helper method to check if array contains any syntax expressions
|
153
|
+
def any_syntax_expressions?
|
154
|
+
any? { |item| Sugar.syntax_expression?(item) }
|
155
|
+
end
|
156
|
+
|
157
|
+
# Convert array to syntax list expression with all elements as syntax nodes
|
158
|
+
def to_syntax_list
|
159
|
+
syntax_elements = map { |item| Sugar.ensure_literal(item) }
|
160
|
+
Kumi::Syntax::ArrayExpression.new(syntax_elements)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Create array method that works with syntax expressions
|
164
|
+
def self.define_array_syntax_method(method_name, has_argument: false)
|
165
|
+
define_method(method_name) do |*args|
|
166
|
+
if any_syntax_expressions?
|
167
|
+
array_literal = to_syntax_list
|
168
|
+
call_args = [array_literal]
|
169
|
+
call_args.concat(args.map { |arg| Sugar.ensure_literal(arg) }) if has_argument
|
170
|
+
Sugar.create_call_expression(method_name, call_args)
|
171
|
+
else
|
172
|
+
super(*args)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Define collection methods without arguments
|
178
|
+
%i[sum size length first last sort reverse unique min max empty? flatten].each do |method_name|
|
179
|
+
define_array_syntax_method(method_name)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Define methods with arguments
|
183
|
+
define_array_syntax_method(:include?, has_argument: true)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
module ModuleRefinement
|
188
|
+
refine Module do
|
189
|
+
# Allow modules to provide schema utilities and helpers
|
190
|
+
def with_schema_utilities
|
191
|
+
include Kumi::Schema if respond_to?(:include)
|
192
|
+
extend Kumi::Schema if respond_to?(:extend)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Helper for defining schema constants that can be used in multiple schemas
|
196
|
+
def schema_const(name, value)
|
197
|
+
const_set(name, value.freeze)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Enable easy schema composition
|
201
|
+
def compose_schema(*modules)
|
202
|
+
modules.each do |mod|
|
203
|
+
include mod if mod.is_a?(Module)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Shared refinement for proxy objects that need to handle operators
|
210
|
+
# Both DeclarationReferenceProxy and InputFieldProxy can use this
|
211
|
+
module ProxyRefinement
|
212
|
+
def self.extended(proxy_class)
|
213
|
+
# Add operator methods directly to the proxy class
|
214
|
+
proxy_class.class_eval do
|
215
|
+
# Arithmetic operations
|
216
|
+
ARITHMETIC_OPS.each do |op, op_name|
|
217
|
+
define_method(op) do |other|
|
218
|
+
ast_node = to_ast_node
|
219
|
+
other_node = Sugar.ensure_literal(other)
|
220
|
+
Sugar.create_call_expression(op_name, [ast_node, other_node])
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Comparison operations (including == and != that don't work with refinements)
|
225
|
+
COMPARISON_OPS.each do |op|
|
226
|
+
define_method(op) do |other|
|
227
|
+
ast_node = to_ast_node
|
228
|
+
other_node = Sugar.ensure_literal(other)
|
229
|
+
Sugar.create_call_expression(op, [ast_node, other_node])
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Logical operations
|
234
|
+
define_method(:&) do |other|
|
235
|
+
ast_node = to_ast_node
|
236
|
+
other_node = Sugar.ensure_literal(other)
|
237
|
+
Sugar.create_call_expression(:and, [ast_node, other_node])
|
238
|
+
end
|
239
|
+
|
240
|
+
define_method(:|) do |other|
|
241
|
+
ast_node = to_ast_node
|
242
|
+
other_node = Sugar.ensure_literal(other)
|
243
|
+
Sugar.create_call_expression(:or, [ast_node, other_node])
|
244
|
+
end
|
245
|
+
|
246
|
+
# Array access
|
247
|
+
define_method(:[]) do |index|
|
248
|
+
ast_node = to_ast_node
|
249
|
+
Sugar.create_call_expression(:at, [ast_node, Sugar.ensure_literal(index)])
|
250
|
+
end
|
251
|
+
|
252
|
+
# Unary minus
|
253
|
+
define_method(:-@) do
|
254
|
+
ast_node = to_ast_node
|
255
|
+
Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), ast_node])
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
# Ruby DSL parser for Kumi schemas
|
6
|
+
# Converts Ruby block syntax into AST nodes
|
7
|
+
module RubyParser
|
8
|
+
# This module contains all Ruby DSL parsing functionality
|
9
|
+
# The main entry point is through Dsl.build_syntax_tree
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
# A bound pair of <compiled schema + context>. Immutable.
|
6
|
+
#
|
7
|
+
# Public API ----------------------------------------------------------
|
8
|
+
# instance.evaluate # => full Hash of all bindings
|
9
|
+
# instance.evaluate(:tax_due, :rate)
|
10
|
+
# instance.slice(:tax_due) # alias for evaluate(*keys)
|
11
|
+
# instance.explain(:tax_due) # pretty trace string
|
12
|
+
# instance.input # original context (read‑only)
|
13
|
+
|
14
|
+
class SchemaInstance
|
15
|
+
attr_reader :compiled_schema, :metadata, :context
|
16
|
+
|
17
|
+
def initialize(compiled_schema, metadata, context)
|
18
|
+
@compiled_schema = compiled_schema # Kumi::Core::CompiledSchema
|
19
|
+
@metadata = metadata # Frozen state hash
|
20
|
+
@context = context.is_a?(EvaluationWrapper) ? context : EvaluationWrapper.new(context)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Hash‑like read of one or many bindings
|
24
|
+
def evaluate(*key_names)
|
25
|
+
if key_names.empty?
|
26
|
+
@compiled_schema.evaluate(@context)
|
27
|
+
else
|
28
|
+
@compiled_schema.evaluate(@context, *key_names)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def slice(*key_names)
|
33
|
+
return {} if key_names.empty?
|
34
|
+
|
35
|
+
evaluate(*key_names)
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](key_name)
|
39
|
+
evaluate(key_name)[key_name]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Update input values and clear affected cached computations
|
43
|
+
def update(**changes)
|
44
|
+
changes.each do |field, value|
|
45
|
+
# Validate field exists
|
46
|
+
raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
|
47
|
+
|
48
|
+
# Validate domain constraints
|
49
|
+
validate_domain_constraint(field, value)
|
50
|
+
|
51
|
+
# Update the input data
|
52
|
+
@context[field] = value
|
53
|
+
|
54
|
+
# Clear affected cached values using transitive closure by default
|
55
|
+
if ENV["KUMI_SIMPLE_CACHE"] == "true"
|
56
|
+
# Simple fallback: clear all cached values
|
57
|
+
@context.clear_cache
|
58
|
+
else
|
59
|
+
# Default: selective cache clearing using precomputed transitive closure
|
60
|
+
affected_keys = find_dependent_declarations_optimized(field)
|
61
|
+
affected_keys.each { |key| @context.clear_cache(key) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
self # Return self for chaining
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def input_field_exists?(field)
|
71
|
+
# Check if field is declared in input block
|
72
|
+
input_meta = @metadata[:inputs] || {}
|
73
|
+
input_meta.key?(field) || @context.key?(field)
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_domain_constraint(field, value)
|
77
|
+
input_meta = @metadata[:inputs] || {}
|
78
|
+
field_meta = input_meta[field]
|
79
|
+
return unless field_meta&.dig(:domain)
|
80
|
+
|
81
|
+
domain = field_meta[:domain]
|
82
|
+
return unless violates_domain?(value, domain)
|
83
|
+
|
84
|
+
raise ArgumentError, "value #{value} is not in domain #{domain}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def violates_domain?(value, domain)
|
88
|
+
case domain
|
89
|
+
when Range
|
90
|
+
!domain.include?(value)
|
91
|
+
when Array
|
92
|
+
!domain.include?(value)
|
93
|
+
when Proc
|
94
|
+
# For Proc domains, we can't statically analyze
|
95
|
+
false
|
96
|
+
else
|
97
|
+
false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_dependent_declarations_optimized(field)
|
102
|
+
# Use precomputed transitive closure for true O(1) lookup!
|
103
|
+
transitive_dependents = @metadata[:dependents]
|
104
|
+
return [] unless transitive_dependents
|
105
|
+
|
106
|
+
# This is truly O(1) - just array lookup, no traversal needed
|
107
|
+
transitive_dependents[field] || []
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Types
|
6
|
+
# Builds complex type structures
|
7
|
+
class Builder
|
8
|
+
def self.array(elem_type)
|
9
|
+
raise ArgumentError, "Invalid array element type: #{elem_type}" unless Validator.valid_type?(elem_type)
|
10
|
+
|
11
|
+
{ array: elem_type }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.hash(key_type, val_type)
|
15
|
+
raise ArgumentError, "Invalid hash key type: #{key_type}" unless Validator.valid_type?(key_type)
|
16
|
+
raise ArgumentError, "Invalid hash value type: #{val_type}" unless Validator.valid_type?(val_type)
|
17
|
+
|
18
|
+
{ hash: [key_type, val_type] }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Types
|
6
|
+
# Handles type compatibility and unification
|
7
|
+
class Compatibility
|
8
|
+
# Check if two types are compatible
|
9
|
+
def self.compatible?(type1, type2)
|
10
|
+
# Any type is compatible with anything
|
11
|
+
return true if type1 == :any || type2 == :any
|
12
|
+
|
13
|
+
# Exact match
|
14
|
+
return true if type1 == type2
|
15
|
+
|
16
|
+
# Generic array compatibility: :array is compatible with any structured array
|
17
|
+
return true if (type1 == :array && Validator.array_type?(type2)) ||
|
18
|
+
(type2 == :array && Validator.array_type?(type1))
|
19
|
+
|
20
|
+
# Numeric compatibility
|
21
|
+
return true if numeric_compatible?(type1, type2)
|
22
|
+
|
23
|
+
# Array compatibility
|
24
|
+
return array_compatible?(type1, type2) if array_types?(type1, type2)
|
25
|
+
|
26
|
+
# Hash compatibility
|
27
|
+
return hash_compatible?(type1, type2) if hash_types?(type1, type2)
|
28
|
+
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
# Find the most specific common type between two types
|
33
|
+
def self.unify(type1, type2)
|
34
|
+
return type1 if type1 == type2
|
35
|
+
|
36
|
+
# :any unifies to the other type (more specific)
|
37
|
+
return type2 if type1 == :any
|
38
|
+
return type1 if type2 == :any
|
39
|
+
|
40
|
+
# Generic array unification: structured array is more specific than :array
|
41
|
+
return type2 if type1 == :array && Validator.array_type?(type2)
|
42
|
+
return type1 if type2 == :array && Validator.array_type?(type1)
|
43
|
+
|
44
|
+
# Numeric unification
|
45
|
+
if numeric_compatible?(type1, type2)
|
46
|
+
return :integer if type1 == :integer && type2 == :integer
|
47
|
+
|
48
|
+
return :float # One or both are float
|
49
|
+
end
|
50
|
+
|
51
|
+
# Array unification
|
52
|
+
if array_types?(type1, type2)
|
53
|
+
elem1 = type1[:array]
|
54
|
+
elem2 = type2[:array]
|
55
|
+
unified_elem = unify(elem1, elem2)
|
56
|
+
return Kumi::Core::Types.array(unified_elem)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Hash unification
|
60
|
+
if hash_types?(type1, type2)
|
61
|
+
key1, val1 = type1[:hash]
|
62
|
+
key2, val2 = type2[:hash]
|
63
|
+
unified_key = unify(key1, key2)
|
64
|
+
unified_val = unify(val1, val2)
|
65
|
+
return Kumi::Core::Types.hash(unified_key, unified_val)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Fall back to :any for incompatible types
|
69
|
+
:any
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.numeric_compatible?(type1, type2)
|
73
|
+
numeric_types = %i[integer float]
|
74
|
+
numeric_types.include?(type1) && numeric_types.include?(type2)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.array_types?(type1, type2)
|
78
|
+
Validator.array_type?(type1) && Validator.array_type?(type2)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.hash_types?(type1, type2)
|
82
|
+
Validator.hash_type?(type1) && Validator.hash_type?(type2)
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.array_compatible?(type1, type2)
|
86
|
+
compatible?(type1[:array], type2[:array])
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.hash_compatible?(type1, type2)
|
90
|
+
compatible?(type1[:hash][0], type2[:hash][0]) &&
|
91
|
+
compatible?(type1[:hash][1], type2[:hash][1])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|