kumi 0.0.4 → 0.0.6
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 +160 -8
- data/README.md +278 -200
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/DSL.md +3 -3
- data/{documents → docs}/SYNTAX.md +107 -24
- data/docs/features/README.md +45 -0
- data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
- data/docs/features/analysis-type-inference.md +42 -0
- data/docs/features/analysis-unsat-detection.md +71 -0
- data/docs/features/array-broadcasting.md +170 -0
- data/docs/features/input-declaration-system.md +42 -0
- data/docs/features/performance.md +16 -0
- data/examples/federal_tax_calculator_2024.rb +43 -40
- data/examples/game_of_life.rb +97 -0
- data/examples/simple_rpg_game.rb +1000 -0
- data/examples/static_analysis_errors.rb +178 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
- data/lib/kumi/analyzer/analysis_state.rb +37 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
- data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
- data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
- data/lib/kumi/analyzer.rb +41 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/compiler.rb +194 -16
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +20 -4
- data/lib/kumi/explain.rb +28 -28
- data/lib/kumi/export/node_registry.rb +26 -12
- data/lib/kumi/export/node_serializers.rb +1 -1
- data/lib/kumi/function_registry/collection_functions.rb +117 -9
- data/lib/kumi/function_registry/function_builder.rb +4 -3
- data/lib/kumi/function_registry.rb +8 -2
- data/lib/kumi/input/type_matcher.rb +3 -0
- data/lib/kumi/input/validator.rb +0 -3
- data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/input_builder.rb +40 -9
- data/lib/kumi/parser/input_field_proxy.rb +46 -0
- data/lib/kumi/parser/input_proxy.rb +3 -3
- data/lib/kumi/parser/nested_input.rb +15 -0
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +10 -9
- data/lib/kumi/parser/sugar.rb +171 -18
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/array_expression.rb +15 -0
- data/lib/kumi/syntax/call_expression.rb +11 -0
- data/lib/kumi/syntax/cascade_expression.rb +11 -0
- data/lib/kumi/syntax/case_expression.rb +11 -0
- data/lib/kumi/syntax/declaration_reference.rb +11 -0
- data/lib/kumi/syntax/hash_expression.rb +11 -0
- data/lib/kumi/syntax/input_declaration.rb +12 -0
- data/lib/kumi/syntax/input_element_reference.rb +12 -0
- data/lib/kumi/syntax/input_reference.rb +12 -0
- data/lib/kumi/syntax/literal.rb +11 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +38 -17
- data/CHANGELOG.md +0 -25
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -23
- data/lib/kumi/syntax/expressions.rb +0 -30
- data/lib/kumi/syntax/terminal_expressions.rb +0 -27
- data/lib/kumi/syntax.rb +0 -9
- data/test_impossible_cascade.rb +0 -51
- /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -2,57 +2,125 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Parser
|
5
|
+
# Converts Ruby objects and DSL expressions into AST nodes
|
6
|
+
# This is the bridge between Ruby's native types and Kumi's syntax tree
|
5
7
|
class ExpressionConverter
|
6
8
|
include Syntax
|
7
9
|
include ErrorReporting
|
8
10
|
|
11
|
+
# Use the same literal types as Sugar module to avoid duplication
|
12
|
+
LITERAL_TYPES = Sugar::LITERAL_TYPES
|
13
|
+
|
9
14
|
def initialize(context)
|
10
15
|
@context = context
|
11
16
|
end
|
12
17
|
|
18
|
+
# Convert any Ruby object into a syntax node
|
19
|
+
# @param obj [Object] The object to convert
|
20
|
+
# @return [Syntax::Node] The corresponding AST node
|
13
21
|
def ensure_syntax(obj)
|
14
22
|
case obj
|
15
|
-
when
|
16
|
-
|
23
|
+
when *LITERAL_TYPES
|
24
|
+
create_literal(obj)
|
17
25
|
when Array
|
18
|
-
|
26
|
+
create_list(obj)
|
19
27
|
when Syntax::Node
|
20
28
|
obj
|
21
29
|
else
|
22
|
-
|
30
|
+
handle_custom_object(obj)
|
23
31
|
end
|
24
32
|
end
|
25
33
|
|
34
|
+
# Create a reference to another declaration
|
35
|
+
# @param name [Symbol] The name to reference
|
36
|
+
# @return [Syntax::DeclarationReference] Reference node
|
26
37
|
def ref(name)
|
27
|
-
|
38
|
+
validate_reference_name(name)
|
39
|
+
Kumi::Syntax::DeclarationReference.new(name, loc: current_location)
|
28
40
|
end
|
29
41
|
|
42
|
+
# Create a literal value node
|
43
|
+
# @param value [Object] The literal value
|
44
|
+
# @return [Syntax::Literal] Literal node
|
30
45
|
def literal(value)
|
31
|
-
Literal.new(value, loc:
|
46
|
+
Kumi::Syntax::Literal.new(value, loc: current_location)
|
32
47
|
end
|
33
48
|
|
49
|
+
# Create a function call expression
|
50
|
+
# @param fn_name [Symbol] The function name
|
51
|
+
# @param args [Array] The function arguments
|
52
|
+
# @return [Syntax::CallExpression] Function call node
|
34
53
|
def fn(fn_name, *args)
|
35
|
-
|
36
|
-
|
54
|
+
validate_function_name(fn_name)
|
55
|
+
expr_args = convert_arguments(args)
|
56
|
+
Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: current_location)
|
37
57
|
end
|
38
58
|
|
59
|
+
# Access the input proxy for field references
|
60
|
+
# @return [InputProxy] Proxy for input field access
|
39
61
|
def input
|
40
62
|
InputProxy.new(@context)
|
41
63
|
end
|
42
64
|
|
65
|
+
# Raise a syntax error with location information
|
66
|
+
# @param message [String] Error message
|
67
|
+
# @param location [Location] Error location
|
43
68
|
def raise_error(message, location)
|
44
69
|
raise_syntax_error(message, location: location)
|
45
70
|
end
|
46
71
|
|
47
72
|
private
|
48
73
|
|
49
|
-
def
|
50
|
-
|
74
|
+
def create_literal(value)
|
75
|
+
Kumi::Syntax::Literal.new(value, loc: current_location)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_list(array)
|
79
|
+
elements = array.map { |element| ensure_syntax(element) }
|
80
|
+
Kumi::Syntax::ArrayExpression.new(elements, loc: current_location)
|
81
|
+
end
|
82
|
+
|
83
|
+
def handle_custom_object(obj)
|
84
|
+
if obj.respond_to?(:to_ast_node)
|
51
85
|
obj.to_ast_node
|
52
86
|
else
|
53
|
-
|
87
|
+
raise_invalid_expression_error(obj)
|
54
88
|
end
|
55
89
|
end
|
90
|
+
|
91
|
+
def validate_reference_name(name)
|
92
|
+
unless name.is_a?(Symbol)
|
93
|
+
raise_syntax_error(
|
94
|
+
"Reference name must be a symbol, got #{name.class}",
|
95
|
+
location: current_location
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_function_name(fn_name)
|
101
|
+
unless fn_name.is_a?(Symbol)
|
102
|
+
raise_syntax_error(
|
103
|
+
"Function name must be a symbol, got #{fn_name.class}",
|
104
|
+
location: current_location
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def convert_arguments(args)
|
110
|
+
args.map { |arg| ensure_syntax(arg) }
|
111
|
+
end
|
112
|
+
|
113
|
+
def raise_invalid_expression_error(obj)
|
114
|
+
raise_syntax_error(
|
115
|
+
"Cannot convert #{obj.class} to AST node. " \
|
116
|
+
"Value: #{obj.inspect}",
|
117
|
+
location: current_location
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def current_location
|
122
|
+
@context.current_location
|
123
|
+
end
|
56
124
|
end
|
57
125
|
end
|
58
|
-
end
|
126
|
+
end
|
@@ -12,17 +12,20 @@ module Kumi
|
|
12
12
|
|
13
13
|
def key(name, type: :any, domain: nil)
|
14
14
|
normalized_type = normalize_type(type, name)
|
15
|
-
@context.inputs <<
|
15
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], loc: @context.current_location)
|
16
16
|
end
|
17
17
|
|
18
|
-
%i[integer float string boolean any].each do |type_name|
|
19
|
-
define_method(type_name) do |name, domain: nil|
|
20
|
-
|
18
|
+
%i[integer float string boolean any scalar].each do |type_name|
|
19
|
+
define_method(type_name) do |name, type: nil, domain: nil|
|
20
|
+
actual_type = type || (type_name == :scalar ? :any : type_name)
|
21
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], loc: @context.current_location)
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
|
-
def array(name_or_elem_type, **kwargs)
|
25
|
-
if
|
25
|
+
def array(name_or_elem_type, **kwargs, &block)
|
26
|
+
if block_given?
|
27
|
+
create_array_field_with_block(name_or_elem_type, kwargs, &block)
|
28
|
+
elsif kwargs.any?
|
26
29
|
create_array_field(name_or_elem_type, kwargs)
|
27
30
|
else
|
28
31
|
Kumi::Types.array(name_or_elem_type)
|
@@ -36,7 +39,7 @@ module Kumi
|
|
36
39
|
end
|
37
40
|
|
38
41
|
def method_missing(method_name, *_args)
|
39
|
-
allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'array', and 'hash'"
|
42
|
+
allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', and 'hash'"
|
40
43
|
raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
|
41
44
|
location: @context.current_location)
|
42
45
|
end
|
@@ -59,7 +62,7 @@ module Kumi
|
|
59
62
|
elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
|
60
63
|
|
61
64
|
array_type = create_array_type(field_name, elem_type)
|
62
|
-
@context.inputs <<
|
65
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], loc: @context.current_location)
|
63
66
|
end
|
64
67
|
|
65
68
|
def create_array_type(field_name, elem_type)
|
@@ -77,7 +80,7 @@ module Kumi
|
|
77
80
|
val_type = extract_type(val_spec)
|
78
81
|
|
79
82
|
hash_type = create_hash_type(field_name, key_type, val_type)
|
80
|
-
@context.inputs <<
|
83
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], loc: @context.current_location)
|
81
84
|
end
|
82
85
|
|
83
86
|
def extract_type(spec)
|
@@ -89,6 +92,34 @@ module Kumi
|
|
89
92
|
rescue ArgumentError => e
|
90
93
|
raise_syntax_error("Invalid types for hash `#{field_name}`: #{e.message}", location: @context.current_location)
|
91
94
|
end
|
95
|
+
|
96
|
+
def create_array_field_with_block(field_name, options, &block)
|
97
|
+
domain = options[:domain]
|
98
|
+
|
99
|
+
# Collect children by creating a nested context
|
100
|
+
children = collect_array_children(&block)
|
101
|
+
|
102
|
+
# Create the InputDeclaration with children
|
103
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(
|
104
|
+
field_name,
|
105
|
+
domain,
|
106
|
+
:array,
|
107
|
+
children,
|
108
|
+
loc: @context.current_location
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def collect_array_children(&block)
|
113
|
+
# Create a temporary nested context to collect children
|
114
|
+
nested_inputs = []
|
115
|
+
nested_context = NestedInput.new(nested_inputs, @context.current_location)
|
116
|
+
nested_builder = InputBuilder.new(nested_context)
|
117
|
+
|
118
|
+
# Execute the block in the nested context
|
119
|
+
nested_builder.instance_eval(&block)
|
120
|
+
|
121
|
+
nested_inputs
|
122
|
+
end
|
92
123
|
end
|
93
124
|
end
|
94
125
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Parser
|
5
|
+
# Proxy for input field access that can handle arbitrary depth nesting
|
6
|
+
# Handles input.field.subfield.subsubfield... syntax by building up path arrays
|
7
|
+
class InputFieldProxy
|
8
|
+
include Syntax
|
9
|
+
|
10
|
+
# Use shared operator methods instead of refinements
|
11
|
+
extend Sugar::ProxyRefinement
|
12
|
+
|
13
|
+
def initialize(path, context)
|
14
|
+
@path = Array(path) # Ensure it's always an array
|
15
|
+
@context = context
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert to appropriate AST node based on path length
|
19
|
+
def to_ast_node
|
20
|
+
if @path.length == 1
|
21
|
+
# Single field: input.field -> InputReference
|
22
|
+
Kumi::Syntax::InputReference.new(@path.first, loc: @context.current_location)
|
23
|
+
else
|
24
|
+
# Nested fields: input.field.subfield... -> InputElementReference
|
25
|
+
Kumi::Syntax::InputElementReference.new(@path, loc: @context.current_location)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def method_missing(method_name, *args, &block)
|
32
|
+
if args.empty? && block.nil?
|
33
|
+
# Extend the path: input.user.details -> InputFieldProxy([user, details])
|
34
|
+
InputFieldProxy.new(@path + [method_name], @context)
|
35
|
+
else
|
36
|
+
# Operators are now handled by ProxyRefinement methods
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -13,13 +13,13 @@ module Kumi
|
|
13
13
|
private
|
14
14
|
|
15
15
|
def method_missing(method_name, *_args)
|
16
|
-
# Create
|
17
|
-
|
16
|
+
# Create InputFieldProxy that can handle further field access
|
17
|
+
InputFieldProxy.new(method_name, @context)
|
18
18
|
end
|
19
19
|
|
20
20
|
# This method is called when the user tries to access a field
|
21
21
|
# on the input object, e.g. `input.field_name`.
|
22
|
-
# It is used to create
|
22
|
+
# It is used to create an InputReference node in the AST.
|
23
23
|
|
24
24
|
def respond_to_missing?(_method_name, _include_private = false)
|
25
25
|
true # Allow any field name
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Parser
|
5
|
+
# Simple context struct for nested input collection
|
6
|
+
class NestedInput
|
7
|
+
attr_reader :inputs, :current_location
|
8
|
+
|
9
|
+
def initialize(inputs_array, location)
|
10
|
+
@inputs = inputs_array
|
11
|
+
@current_location = location
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/kumi/parser/parser.rb
CHANGED
@@ -36,6 +36,8 @@ module Kumi
|
|
36
36
|
rule_block.binding.eval("using Kumi::Parser::Sugar::ExpressionRefinement")
|
37
37
|
rule_block.binding.eval("using Kumi::Parser::Sugar::NumericRefinement")
|
38
38
|
rule_block.binding.eval("using Kumi::Parser::Sugar::StringRefinement")
|
39
|
+
rule_block.binding.eval("using Kumi::Parser::Sugar::ArrayRefinement")
|
40
|
+
rule_block.binding.eval("using Kumi::Parser::Sugar::ModuleRefinement")
|
39
41
|
rescue RuntimeError, NoMethodError
|
40
42
|
# Refinements disabled in method scope - continue without them
|
41
43
|
end
|
@@ -18,7 +18,7 @@ module Kumi
|
|
18
18
|
validate_value_args(name, expr, blk)
|
19
19
|
|
20
20
|
expression = blk ? build_cascade(&blk) : ensure_syntax(expr)
|
21
|
-
@context.attributes <<
|
21
|
+
@context.attributes << Kumi::Syntax::ValueDeclaration.new(name, expression, loc: @context.current_location)
|
22
22
|
end
|
23
23
|
|
24
24
|
def trait(*args, **kwargs)
|
@@ -40,24 +40,25 @@ module Kumi
|
|
40
40
|
|
41
41
|
def ref(name)
|
42
42
|
update_location
|
43
|
-
|
43
|
+
Kumi::Syntax::DeclarationReference.new(name, loc: @context.current_location)
|
44
44
|
end
|
45
45
|
|
46
46
|
def literal(value)
|
47
47
|
update_location
|
48
|
-
Literal.new(value, loc: @context.current_location)
|
48
|
+
Kumi::Syntax::Literal.new(value, loc: @context.current_location)
|
49
49
|
end
|
50
50
|
|
51
51
|
def fn(fn_name, *args)
|
52
52
|
update_location
|
53
53
|
expr_args = args.map { |a| ensure_syntax(a) }
|
54
|
-
CallExpression.new(fn_name, expr_args, loc: @context.current_location)
|
54
|
+
Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: @context.current_location)
|
55
55
|
end
|
56
56
|
|
57
57
|
def method_missing(method_name, *args, &block)
|
58
58
|
if args.empty? && !block_given?
|
59
59
|
update_location
|
60
|
-
|
60
|
+
# Create proxy for declaration references (traits/values)
|
61
|
+
DeclarationReferenceProxy.new(method_name, @context)
|
61
62
|
else
|
62
63
|
super
|
63
64
|
end
|
@@ -109,7 +110,7 @@ module Kumi
|
|
109
110
|
name, expression = args
|
110
111
|
validate_trait_name(name)
|
111
112
|
expr = ensure_syntax(expression)
|
112
|
-
@context.traits <<
|
113
|
+
@context.traits << Kumi::Syntax::TraitDeclaration.new(name, expr, loc: @context.current_location)
|
113
114
|
else
|
114
115
|
handle_deprecated_trait_syntax(args)
|
115
116
|
end
|
@@ -137,8 +138,8 @@ module Kumi
|
|
137
138
|
validate_operator(operator)
|
138
139
|
|
139
140
|
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 <<
|
141
|
+
expr = Kumi::Syntax::CallExpression.new(operator, [ensure_syntax(lhs)] + rhs_exprs, loc: @context.current_location)
|
142
|
+
@context.traits << Kumi::Syntax::TraitDeclaration.new(name, expr, loc: @context.current_location)
|
142
143
|
end
|
143
144
|
|
144
145
|
def validate_trait_name(name)
|
@@ -161,7 +162,7 @@ module Kumi
|
|
161
162
|
expression_converter = ExpressionConverter.new(@context)
|
162
163
|
cascade_builder = DslCascadeBuilder.new(expression_converter, @context.current_location)
|
163
164
|
cascade_builder.instance_eval(&blk)
|
164
|
-
CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
|
165
|
+
Kumi::Syntax::CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
|
165
166
|
end
|
166
167
|
|
167
168
|
def ensure_syntax(obj)
|
data/lib/kumi/parser/sugar.rb
CHANGED
@@ -5,49 +5,89 @@ module Kumi
|
|
5
5
|
module Sugar
|
6
6
|
include Syntax
|
7
7
|
|
8
|
-
ARITHMETIC_OPS = {
|
9
|
-
|
8
|
+
ARITHMETIC_OPS = {
|
9
|
+
:+ => :add, :- => :subtract, :* => :multiply,
|
10
|
+
:/ => :divide, :% => :modulo, :** => :power
|
11
|
+
}.freeze
|
12
|
+
|
10
13
|
COMPARISON_OPS = %i[< <= > >= == !=].freeze
|
11
|
-
|
14
|
+
|
15
|
+
LITERAL_TYPES = [
|
16
|
+
Integer, String, Symbol, TrueClass, FalseClass, Float, Regexp, NilClass
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
# Collection methods that can be applied to arrays/syntax nodes
|
20
|
+
COLLECTION_METHODS = %i[
|
21
|
+
sum size length first last sort reverse unique min max empty? flatten
|
22
|
+
map_with_index indices
|
23
|
+
].freeze
|
12
24
|
|
13
25
|
def self.ensure_literal(obj)
|
14
|
-
return Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
|
26
|
+
return Kumi::Syntax::Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
|
15
27
|
return obj if obj.is_a?(Syntax::Node)
|
16
28
|
return obj.to_ast_node if obj.respond_to?(:to_ast_node)
|
17
29
|
|
18
|
-
Literal.new(obj)
|
30
|
+
Kumi::Syntax::Literal.new(obj)
|
19
31
|
end
|
20
32
|
|
21
33
|
def self.syntax_expression?(obj)
|
22
34
|
obj.is_a?(Syntax::Node) || obj.respond_to?(:to_ast_node)
|
23
35
|
end
|
24
36
|
|
37
|
+
# Create a call expression with consistent error handling
|
38
|
+
def self.create_call_expression(fn_name, args)
|
39
|
+
Kumi::Syntax::CallExpression.new(fn_name, args)
|
40
|
+
end
|
41
|
+
|
25
42
|
module ExpressionRefinement
|
26
43
|
refine Syntax::Node do
|
44
|
+
# Arithmetic operations
|
27
45
|
ARITHMETIC_OPS.each do |op, op_name|
|
28
46
|
define_method(op) do |other|
|
29
47
|
other_node = Sugar.ensure_literal(other)
|
30
|
-
|
48
|
+
Sugar.create_call_expression(op_name, [self, other_node])
|
31
49
|
end
|
32
50
|
end
|
33
51
|
|
52
|
+
# Comparison operations
|
34
53
|
COMPARISON_OPS.each do |op|
|
35
54
|
define_method(op) do |other|
|
36
55
|
other_node = Sugar.ensure_literal(other)
|
37
|
-
|
56
|
+
Sugar.create_call_expression(op, [self, other_node])
|
38
57
|
end
|
39
58
|
end
|
40
59
|
|
60
|
+
# Array access
|
41
61
|
def [](index)
|
42
|
-
|
62
|
+
Sugar.create_call_expression(:at, [self, Sugar.ensure_literal(index)])
|
43
63
|
end
|
44
64
|
|
65
|
+
# Unary minus
|
45
66
|
def -@
|
46
|
-
|
67
|
+
Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), self])
|
47
68
|
end
|
48
69
|
|
70
|
+
# Logical operations
|
49
71
|
def &(other)
|
50
|
-
|
72
|
+
Sugar.create_call_expression(:and, [self, Sugar.ensure_literal(other)])
|
73
|
+
end
|
74
|
+
|
75
|
+
def |(other)
|
76
|
+
Sugar.create_call_expression(:or, [self, Sugar.ensure_literal(other)])
|
77
|
+
end
|
78
|
+
|
79
|
+
# Collection methods - single argument (self)
|
80
|
+
COLLECTION_METHODS.each do |method_name|
|
81
|
+
next if method_name == :include? # Special case with element argument
|
82
|
+
|
83
|
+
define_method(method_name) do
|
84
|
+
Sugar.create_call_expression(method_name, [self])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Special case: include? takes an element argument
|
89
|
+
def include?(element)
|
90
|
+
Sugar.create_call_expression(:include?, [self, Sugar.ensure_literal(element)])
|
51
91
|
end
|
52
92
|
end
|
53
93
|
end
|
@@ -55,22 +95,24 @@ module Kumi
|
|
55
95
|
module NumericRefinement
|
56
96
|
[Integer, Float].each do |klass|
|
57
97
|
refine klass do
|
98
|
+
# Arithmetic operations with syntax expressions
|
58
99
|
ARITHMETIC_OPS.each do |op, op_name|
|
59
100
|
define_method(op) do |other|
|
60
101
|
if Sugar.syntax_expression?(other)
|
61
|
-
other_node =
|
62
|
-
|
102
|
+
other_node = Sugar.ensure_literal(other)
|
103
|
+
Sugar.create_call_expression(op_name, [Kumi::Syntax::Literal.new(self), other_node])
|
63
104
|
else
|
64
105
|
super(other)
|
65
106
|
end
|
66
107
|
end
|
67
108
|
end
|
68
109
|
|
110
|
+
# Comparison operations with syntax expressions
|
69
111
|
COMPARISON_OPS.each do |op|
|
70
112
|
define_method(op) do |other|
|
71
113
|
if Sugar.syntax_expression?(other)
|
72
|
-
other_node =
|
73
|
-
|
114
|
+
other_node = Sugar.ensure_literal(other)
|
115
|
+
Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
|
74
116
|
else
|
75
117
|
super(other)
|
76
118
|
end
|
@@ -84,8 +126,8 @@ module Kumi
|
|
84
126
|
refine String do
|
85
127
|
def +(other)
|
86
128
|
if Sugar.syntax_expression?(other)
|
87
|
-
other_node =
|
88
|
-
|
129
|
+
other_node = Sugar.ensure_literal(other)
|
130
|
+
Sugar.create_call_expression(:concat, [Kumi::Syntax::Literal.new(self), other_node])
|
89
131
|
else
|
90
132
|
super
|
91
133
|
end
|
@@ -94,8 +136,8 @@ module Kumi
|
|
94
136
|
%i[== !=].each do |op|
|
95
137
|
define_method(op) do |other|
|
96
138
|
if Sugar.syntax_expression?(other)
|
97
|
-
other_node =
|
98
|
-
|
139
|
+
other_node = Sugar.ensure_literal(other)
|
140
|
+
Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
|
99
141
|
else
|
100
142
|
super(other)
|
101
143
|
end
|
@@ -103,6 +145,117 @@ module Kumi
|
|
103
145
|
end
|
104
146
|
end
|
105
147
|
end
|
148
|
+
|
149
|
+
module ArrayRefinement
|
150
|
+
refine Array do
|
151
|
+
# Helper method to check if array contains any syntax expressions
|
152
|
+
def any_syntax_expressions?
|
153
|
+
any? { |item| Sugar.syntax_expression?(item) }
|
154
|
+
end
|
155
|
+
|
156
|
+
# Convert array to syntax list expression with all elements as syntax nodes
|
157
|
+
def to_syntax_list
|
158
|
+
syntax_elements = map { |item| Sugar.ensure_literal(item) }
|
159
|
+
Kumi::Syntax::ArrayExpression.new(syntax_elements)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Create array method that works with syntax expressions
|
163
|
+
def self.define_array_syntax_method(method_name, has_argument: false)
|
164
|
+
define_method(method_name) do |*args|
|
165
|
+
if any_syntax_expressions?
|
166
|
+
array_literal = to_syntax_list
|
167
|
+
call_args = [array_literal]
|
168
|
+
call_args.concat(args.map { |arg| Sugar.ensure_literal(arg) }) if has_argument
|
169
|
+
Sugar.create_call_expression(method_name, call_args)
|
170
|
+
else
|
171
|
+
super(*args)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Define collection methods without arguments
|
177
|
+
%i[sum size length first last sort reverse unique min max empty? flatten].each do |method_name|
|
178
|
+
define_array_syntax_method(method_name)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Define methods with arguments
|
182
|
+
define_array_syntax_method(:include?, has_argument: true)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
module ModuleRefinement
|
187
|
+
refine Module do
|
188
|
+
# Allow modules to provide schema utilities and helpers
|
189
|
+
def with_schema_utilities
|
190
|
+
include Kumi::Schema if respond_to?(:include)
|
191
|
+
extend Kumi::Schema if respond_to?(:extend)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Helper for defining schema constants that can be used in multiple schemas
|
195
|
+
def schema_const(name, value)
|
196
|
+
const_set(name, value.freeze)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Enable easy schema composition
|
200
|
+
def compose_schema(*modules)
|
201
|
+
modules.each do |mod|
|
202
|
+
include mod if mod.is_a?(Module)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Shared refinement for proxy objects that need to handle operators
|
209
|
+
# Both DeclarationReferenceProxy and InputFieldProxy can use this
|
210
|
+
module ProxyRefinement
|
211
|
+
def self.extended(proxy_class)
|
212
|
+
# Add operator methods directly to the proxy class
|
213
|
+
proxy_class.class_eval do
|
214
|
+
# Arithmetic operations
|
215
|
+
ARITHMETIC_OPS.each do |op, op_name|
|
216
|
+
define_method(op) do |other|
|
217
|
+
ast_node = to_ast_node
|
218
|
+
other_node = Sugar.ensure_literal(other)
|
219
|
+
Sugar.create_call_expression(op_name, [ast_node, other_node])
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Comparison operations (including == and != that don't work with refinements)
|
224
|
+
COMPARISON_OPS.each do |op|
|
225
|
+
define_method(op) do |other|
|
226
|
+
ast_node = to_ast_node
|
227
|
+
other_node = Sugar.ensure_literal(other)
|
228
|
+
Sugar.create_call_expression(op, [ast_node, other_node])
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Logical operations
|
233
|
+
define_method(:&) do |other|
|
234
|
+
ast_node = to_ast_node
|
235
|
+
other_node = Sugar.ensure_literal(other)
|
236
|
+
Sugar.create_call_expression(:and, [ast_node, other_node])
|
237
|
+
end
|
238
|
+
|
239
|
+
define_method(:|) do |other|
|
240
|
+
ast_node = to_ast_node
|
241
|
+
other_node = Sugar.ensure_literal(other)
|
242
|
+
Sugar.create_call_expression(:or, [ast_node, other_node])
|
243
|
+
end
|
244
|
+
|
245
|
+
# Array access
|
246
|
+
define_method(:[]) do |index|
|
247
|
+
ast_node = to_ast_node
|
248
|
+
Sugar.create_call_expression(:at, [ast_node, Sugar.ensure_literal(index)])
|
249
|
+
end
|
250
|
+
|
251
|
+
# Unary minus
|
252
|
+
define_method(:-@) do
|
253
|
+
ast_node = to_ast_node
|
254
|
+
Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), ast_node])
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
106
259
|
end
|
107
260
|
end
|
108
261
|
end
|
data/lib/kumi/schema.rb
CHANGED
@@ -4,6 +4,8 @@ require "ostruct"
|
|
4
4
|
|
5
5
|
module Kumi
|
6
6
|
module Schema
|
7
|
+
attr_reader :__syntax_tree__, :__analyzer_result__, :__compiled_schema__
|
8
|
+
|
7
9
|
Inspector = Struct.new(:syntax_tree, :analyzer_result, :compiled_schema) do
|
8
10
|
def inspect
|
9
11
|
"#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, schema: #{schema.inspect}>"
|
@@ -19,7 +21,7 @@ module Kumi
|
|
19
21
|
|
20
22
|
raise Errors::InputValidationError, violations unless violations.empty?
|
21
23
|
|
22
|
-
SchemaInstance.new(@__compiled_schema__, @__analyzer_result__
|
24
|
+
SchemaInstance.new(@__compiled_schema__, @__analyzer_result__, context)
|
23
25
|
end
|
24
26
|
|
25
27
|
def explain(context, *keys)
|