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,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
class DslCascadeBuilder
|
7
|
+
include Syntax
|
8
|
+
|
9
|
+
attr_reader :cases
|
10
|
+
|
11
|
+
def initialize(context, loc)
|
12
|
+
@context = context
|
13
|
+
@cases = []
|
14
|
+
@loc = loc
|
15
|
+
end
|
16
|
+
|
17
|
+
def on(*args)
|
18
|
+
on_loc = current_location
|
19
|
+
validate_on_args(args, "on", on_loc)
|
20
|
+
|
21
|
+
trait_names = args[0..-2]
|
22
|
+
expr = args.last
|
23
|
+
|
24
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
|
25
|
+
condition = create_fn(:all?, trait_bindings)
|
26
|
+
result = ensure_syntax(expr)
|
27
|
+
add_case(condition, result)
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_any(*args)
|
31
|
+
on_loc = current_location
|
32
|
+
validate_on_args(args, "on_any", on_loc)
|
33
|
+
|
34
|
+
trait_names = args[0..-2]
|
35
|
+
expr = args.last
|
36
|
+
|
37
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
|
38
|
+
condition = create_fn(:any?, trait_bindings)
|
39
|
+
result = ensure_syntax(expr)
|
40
|
+
add_case(condition, result)
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_none(*args)
|
44
|
+
on_loc = current_location
|
45
|
+
validate_on_args(args, "on_none", on_loc)
|
46
|
+
|
47
|
+
trait_names = args[0..-2]
|
48
|
+
expr = args.last
|
49
|
+
|
50
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
|
51
|
+
condition = create_fn(:none?, trait_bindings)
|
52
|
+
result = ensure_syntax(expr)
|
53
|
+
add_case(condition, result)
|
54
|
+
end
|
55
|
+
|
56
|
+
def base(expr)
|
57
|
+
result = ensure_syntax(expr)
|
58
|
+
add_case(create_literal(true), result)
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(method_name, *args, &block)
|
62
|
+
return super if !args.empty? || block_given?
|
63
|
+
|
64
|
+
# Allow direct trait references in cascade conditions
|
65
|
+
create_binding(method_name, @loc)
|
66
|
+
end
|
67
|
+
|
68
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def current_location
|
75
|
+
caller_info = caller_locations(1, 1).first
|
76
|
+
Location.new(file: caller_info.path, line: caller_info.lineno, column: 0)
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_on_args(args, method_name, location)
|
80
|
+
raise_error("cascade '#{method_name}' requires at least one trait name", location) if args.empty?
|
81
|
+
|
82
|
+
return unless args.size == 1
|
83
|
+
|
84
|
+
raise_error("cascade '#{method_name}' requires an expression as the last argument", location)
|
85
|
+
end
|
86
|
+
|
87
|
+
def convert_trait_names_to_bindings(trait_names, location)
|
88
|
+
trait_names.map do |name|
|
89
|
+
case name
|
90
|
+
when Symbol
|
91
|
+
create_binding(name, location)
|
92
|
+
when DeclarationReference
|
93
|
+
name # Already a binding from method_missing
|
94
|
+
else
|
95
|
+
raise_error("trait reference must be a symbol or bare identifier, got #{name.class}", location)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_case(condition, result)
|
101
|
+
@cases << Kumi::Syntax::CaseExpression.new(condition, result)
|
102
|
+
end
|
103
|
+
|
104
|
+
def ref(name)
|
105
|
+
@context.ref(name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def fn(name, *args)
|
109
|
+
@context.fn(name, *args)
|
110
|
+
end
|
111
|
+
|
112
|
+
def create_literal(value)
|
113
|
+
@context.literal(value)
|
114
|
+
end
|
115
|
+
|
116
|
+
def create_fn(name, args)
|
117
|
+
@context.fn(name, args)
|
118
|
+
end
|
119
|
+
|
120
|
+
def input
|
121
|
+
@context.input
|
122
|
+
end
|
123
|
+
|
124
|
+
def ensure_syntax(expr)
|
125
|
+
@context.ensure_syntax(expr)
|
126
|
+
end
|
127
|
+
|
128
|
+
def raise_error(message, location)
|
129
|
+
@context.raise_error(message, location)
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_binding(name, location)
|
133
|
+
Kumi::Syntax::DeclarationReference.new(name, loc: location)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
# Converts Ruby objects and DSL expressions into AST nodes
|
7
|
+
# This is the bridge between Ruby's native types and Kumi's syntax tree
|
8
|
+
class ExpressionConverter
|
9
|
+
include Syntax
|
10
|
+
include ErrorReporting
|
11
|
+
|
12
|
+
# Use the same literal types as Sugar module to avoid duplication
|
13
|
+
LITERAL_TYPES = Sugar::LITERAL_TYPES
|
14
|
+
|
15
|
+
def initialize(context)
|
16
|
+
@context = context
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convert any Ruby object into a syntax node
|
20
|
+
# @param obj [Object] The object to convert
|
21
|
+
# @return [Syntax::Node] The corresponding AST node
|
22
|
+
def ensure_syntax(obj)
|
23
|
+
case obj
|
24
|
+
when *LITERAL_TYPES
|
25
|
+
create_literal(obj)
|
26
|
+
when Array
|
27
|
+
create_list(obj)
|
28
|
+
when Syntax::Node
|
29
|
+
obj
|
30
|
+
else
|
31
|
+
handle_custom_object(obj)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create a reference to another declaration
|
36
|
+
# @param name [Symbol] The name to reference
|
37
|
+
# @return [Syntax::DeclarationReference] Reference node
|
38
|
+
def ref(name)
|
39
|
+
validate_reference_name(name)
|
40
|
+
Kumi::Syntax::DeclarationReference.new(name, loc: current_location)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create a literal value node
|
44
|
+
# @param value [Object] The literal value
|
45
|
+
# @return [Syntax::Literal] Literal node
|
46
|
+
def literal(value)
|
47
|
+
Kumi::Syntax::Literal.new(value, loc: current_location)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create a function call expression
|
51
|
+
# @param fn_name [Symbol] The function name
|
52
|
+
# @param args [Array] The function arguments
|
53
|
+
# @return [Syntax::CallExpression] Function call node
|
54
|
+
def fn(fn_name, *args)
|
55
|
+
validate_function_name(fn_name)
|
56
|
+
expr_args = convert_arguments(args)
|
57
|
+
Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: current_location)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Access the input proxy for field references
|
61
|
+
# @return [InputProxy] Proxy for input field access
|
62
|
+
def input
|
63
|
+
InputProxy.new(@context)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Raise a syntax error with location information
|
67
|
+
# @param message [String] Error message
|
68
|
+
# @param location [Location] Error location
|
69
|
+
def raise_error(message, location)
|
70
|
+
raise_syntax_error(message, location: location)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def create_literal(value)
|
76
|
+
Kumi::Syntax::Literal.new(value, loc: current_location)
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_list(array)
|
80
|
+
elements = array.map { |element| ensure_syntax(element) }
|
81
|
+
Kumi::Syntax::ArrayExpression.new(elements, loc: current_location)
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_custom_object(obj)
|
85
|
+
if obj.respond_to?(:to_ast_node)
|
86
|
+
obj.to_ast_node
|
87
|
+
else
|
88
|
+
raise_invalid_expression_error(obj)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_reference_name(name)
|
93
|
+
return if name.is_a?(Symbol)
|
94
|
+
|
95
|
+
raise_syntax_error(
|
96
|
+
"Reference name must be a symbol, got #{name.class}",
|
97
|
+
location: current_location
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_function_name(fn_name)
|
102
|
+
return if fn_name.is_a?(Symbol)
|
103
|
+
|
104
|
+
raise_syntax_error(
|
105
|
+
"Function name must be a symbol, got #{fn_name.class}",
|
106
|
+
location: current_location
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
def convert_arguments(args)
|
111
|
+
args.map { |arg| ensure_syntax(arg) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def raise_invalid_expression_error(obj)
|
115
|
+
raise_syntax_error(
|
116
|
+
"Cannot convert #{obj.class} to AST node. " \
|
117
|
+
"Value: #{obj.inspect}",
|
118
|
+
location: current_location
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
def current_location
|
123
|
+
@context.current_location
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
module GuardRails
|
7
|
+
RESERVED = %i[input trait value fn lit ref].freeze
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.singleton_class.prepend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
# prevent accidental addition of new DSL keywords
|
15
|
+
def method_added(name)
|
16
|
+
if GuardRails::RESERVED.include?(name)
|
17
|
+
# Check if this is a redefinition by looking at the call stack
|
18
|
+
# We want to allow the original definition but prevent redefinition
|
19
|
+
calling_location = caller_locations(1, 1).first
|
20
|
+
|
21
|
+
# Allow the original definition from schema_builder.rb
|
22
|
+
if calling_location&.path&.include?("schema_builder.rb")
|
23
|
+
super
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
# This is a redefinition attempt, prevent it
|
28
|
+
raise Kumi::Core::Errors::SemanticError,
|
29
|
+
"DSL keyword `#{name}` is reserved; " \
|
30
|
+
"do not redefine it inside SchemaBuilder"
|
31
|
+
end
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# catch any stray method call inside DSL block
|
37
|
+
def method_missing(name, *_args)
|
38
|
+
raise NoMethodError, "unknown DSL keyword `#{name}`"
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond_to_missing?(*) = false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
class InputBuilder
|
7
|
+
include Syntax
|
8
|
+
include ErrorReporting
|
9
|
+
|
10
|
+
def initialize(context)
|
11
|
+
@context = context
|
12
|
+
end
|
13
|
+
|
14
|
+
def key(name, type: :any, domain: nil)
|
15
|
+
normalized_type = normalize_type(type, name)
|
16
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], loc: @context.current_location)
|
17
|
+
end
|
18
|
+
|
19
|
+
%i[integer float string boolean any scalar].each do |type_name|
|
20
|
+
define_method(type_name) do |name, type: nil, domain: nil|
|
21
|
+
actual_type = type || (type_name == :scalar ? :any : type_name)
|
22
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], loc: @context.current_location)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def array(name_or_elem_type, **kwargs, &block)
|
27
|
+
if block_given?
|
28
|
+
create_array_field_with_block(name_or_elem_type, kwargs, &block)
|
29
|
+
elsif kwargs.any?
|
30
|
+
create_array_field(name_or_elem_type, kwargs)
|
31
|
+
else
|
32
|
+
Kumi::Core::Types.array(name_or_elem_type)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def hash(name_or_key_type, val_type = nil, **kwargs)
|
37
|
+
return Kumi::Core::Types.hash(name_or_key_type, val_type) unless val_type.nil?
|
38
|
+
|
39
|
+
create_hash_field(name_or_key_type, kwargs)
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(method_name, *_args)
|
43
|
+
allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', and 'hash'"
|
44
|
+
raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
|
45
|
+
location: @context.current_location)
|
46
|
+
end
|
47
|
+
|
48
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def normalize_type(type, name)
|
55
|
+
Kumi::Core::Types.normalize(type)
|
56
|
+
rescue ArgumentError => e
|
57
|
+
raise_syntax_error("Invalid type for input `#{name}`: #{e.message}", location: @context.current_location)
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_array_field(field_name, options)
|
61
|
+
elem_spec = options[:elem]
|
62
|
+
domain = options[:domain]
|
63
|
+
elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
|
64
|
+
|
65
|
+
array_type = create_array_type(field_name, elem_type)
|
66
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], loc: @context.current_location)
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_array_type(field_name, elem_type)
|
70
|
+
Kumi::Core::Types.array(elem_type)
|
71
|
+
rescue ArgumentError => e
|
72
|
+
raise_syntax_error("Invalid element type for array `#{field_name}`: #{e.message}", location: @context.current_location)
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_hash_field(field_name, options)
|
76
|
+
key_spec = options[:key]
|
77
|
+
val_spec = options[:val] || options[:value]
|
78
|
+
domain = options[:domain]
|
79
|
+
|
80
|
+
key_type = extract_type(key_spec)
|
81
|
+
val_type = extract_type(val_spec)
|
82
|
+
|
83
|
+
hash_type = create_hash_type(field_name, key_type, val_type)
|
84
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], loc: @context.current_location)
|
85
|
+
end
|
86
|
+
|
87
|
+
def extract_type(spec)
|
88
|
+
spec.is_a?(Hash) && spec[:type] ? spec[:type] : :any
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_hash_type(field_name, key_type, val_type)
|
92
|
+
Kumi::Core::Types.hash(key_type, val_type)
|
93
|
+
rescue ArgumentError => e
|
94
|
+
raise_syntax_error("Invalid types for hash `#{field_name}`: #{e.message}", location: @context.current_location)
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_array_field_with_block(field_name, options, &block)
|
98
|
+
domain = options[:domain]
|
99
|
+
|
100
|
+
# Collect children by creating a nested context
|
101
|
+
children = collect_array_children(&block)
|
102
|
+
|
103
|
+
# Create the InputDeclaration with children
|
104
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(
|
105
|
+
field_name,
|
106
|
+
domain,
|
107
|
+
:array,
|
108
|
+
children,
|
109
|
+
loc: @context.current_location
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def collect_array_children(&block)
|
114
|
+
# Create a temporary nested context to collect children
|
115
|
+
nested_inputs = []
|
116
|
+
nested_context = NestedInput.new(nested_inputs, @context.current_location)
|
117
|
+
nested_builder = InputBuilder.new(nested_context)
|
118
|
+
|
119
|
+
# Execute the block in the nested context
|
120
|
+
nested_builder.instance_eval(&block)
|
121
|
+
|
122
|
+
nested_inputs
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
# Proxy for input field access that can handle arbitrary depth nesting
|
7
|
+
# Handles input.field.subfield.subsubfield... syntax by building up path arrays
|
8
|
+
class InputFieldProxy
|
9
|
+
include Syntax
|
10
|
+
|
11
|
+
# Use shared operator methods instead of refinements
|
12
|
+
extend Sugar::ProxyRefinement
|
13
|
+
|
14
|
+
def initialize(path, context)
|
15
|
+
@path = Array(path) # Ensure it's always an array
|
16
|
+
@context = context
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convert to appropriate AST node based on path length
|
20
|
+
def to_ast_node
|
21
|
+
if @path.length == 1
|
22
|
+
# Single field: input.field -> InputReference
|
23
|
+
Kumi::Syntax::InputReference.new(@path.first, loc: @context.current_location)
|
24
|
+
else
|
25
|
+
# Nested fields: input.field.subfield... -> InputElementReference
|
26
|
+
Kumi::Syntax::InputElementReference.new(@path, loc: @context.current_location)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def method_missing(method_name, *args, &block)
|
33
|
+
if args.empty? && block.nil?
|
34
|
+
# Extend the path: input.user.details -> InputFieldProxy([user, details])
|
35
|
+
InputFieldProxy.new(@path + [method_name], @context)
|
36
|
+
else
|
37
|
+
# Operators are now handled by ProxyRefinement methods
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
# Proxy object for input field references (input.field_name)
|
7
|
+
class InputProxy
|
8
|
+
include Syntax
|
9
|
+
|
10
|
+
def initialize(context)
|
11
|
+
@context = context
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def method_missing(method_name, *_args)
|
17
|
+
# Create InputFieldProxy that can handle further field access
|
18
|
+
InputFieldProxy.new(method_name, @context)
|
19
|
+
end
|
20
|
+
|
21
|
+
# This method is called when the user tries to access a field
|
22
|
+
# on the input object, e.g. `input.field_name`.
|
23
|
+
# It is used to create an InputReference node in the AST.
|
24
|
+
|
25
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
26
|
+
true # Allow any field name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
# Simple context struct for nested input collection
|
7
|
+
class NestedInput
|
8
|
+
attr_reader :inputs, :current_location
|
9
|
+
|
10
|
+
def initialize(inputs_array, location)
|
11
|
+
@inputs = inputs_array
|
12
|
+
@current_location = location
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module RubyParser
|
6
|
+
# Main parser class for Ruby DSL
|
7
|
+
class Parser
|
8
|
+
include Syntax
|
9
|
+
include ErrorReporting
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@context = BuildContext.new
|
13
|
+
@interface = SchemaBuilder.new(@context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(&rule_block)
|
17
|
+
enable_refinements(rule_block)
|
18
|
+
|
19
|
+
before_consts = ::Object.constants
|
20
|
+
@interface.freeze # stop singleton hacks
|
21
|
+
@interface.instance_eval(&rule_block)
|
22
|
+
added = ::Object.constants - before_consts
|
23
|
+
|
24
|
+
unless added.empty?
|
25
|
+
raise Kumi::Core::Errors::SemanticError,
|
26
|
+
"DSL cannot define global constants: #{added.join(', ')}"
|
27
|
+
end
|
28
|
+
|
29
|
+
build_syntax_tree
|
30
|
+
rescue ArgumentError => e
|
31
|
+
handle_parse_error(e)
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def enable_refinements(rule_block)
|
38
|
+
rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::ExpressionRefinement")
|
39
|
+
rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::NumericRefinement")
|
40
|
+
rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::StringRefinement")
|
41
|
+
rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::ArrayRefinement")
|
42
|
+
rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::ModuleRefinement")
|
43
|
+
rescue RuntimeError, NoMethodError
|
44
|
+
# Refinements disabled in method scope - continue without them
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_syntax_tree
|
48
|
+
Root.new(@context.inputs, @context.attributes, @context.traits)
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_parse_error(error)
|
52
|
+
return unless literal_comparison_error?(error)
|
53
|
+
|
54
|
+
warn <<~HINT
|
55
|
+
#{error.backtrace.first.split(':', 2).join(':')}: \
|
56
|
+
Literal‑left comparison failed because the schema block is \
|
57
|
+
defined inside a method (Ruby disallows refinements there).
|
58
|
+
|
59
|
+
• Move the `schema do … end` block to the top level of a class or module, OR
|
60
|
+
• Write the comparison as `input.age >= 80` (preferred), OR
|
61
|
+
• Wrap the literal: `lit(80) <= input.age`.
|
62
|
+
HINT
|
63
|
+
end
|
64
|
+
|
65
|
+
def literal_comparison_error?(error)
|
66
|
+
error.message =~ /comparison of Integer with Kumi::Syntax::/i
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|