kumi 0.0.4 → 0.0.5
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 +109 -2
- data/README.md +169 -213
- data/documents/DSL.md +3 -3
- data/documents/SYNTAX.md +17 -26
- data/examples/federal_tax_calculator_2024.rb +36 -38
- 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/definition_validator.rb +4 -3
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
- data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
- data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
- data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
- data/lib/kumi/analyzer.rb +42 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +20 -4
- data/lib/kumi/explain.rb +8 -8
- data/lib/kumi/function_registry/collection_functions.rb +103 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/sugar.rb +117 -16
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/declarations.rb +3 -0
- data/lib/kumi/syntax/expressions.rb +4 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/terminal_expressions.rb +3 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +10 -6
- data/CHANGELOG.md +0 -25
- data/test_impossible_cascade.rb +0 -51
@@ -3,20 +3,36 @@
|
|
3
3
|
module Kumi
|
4
4
|
EvaluationWrapper = Struct.new(:ctx) do
|
5
5
|
def initialize(ctx)
|
6
|
-
|
6
|
+
super
|
7
7
|
@__schema_cache__ = {} # memoization cache for bindings
|
8
8
|
end
|
9
9
|
|
10
10
|
def [](key)
|
11
|
-
|
11
|
+
ctx[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(key, value)
|
15
|
+
ctx[key] = value
|
12
16
|
end
|
13
17
|
|
14
18
|
def keys
|
15
|
-
|
19
|
+
ctx.keys
|
16
20
|
end
|
17
21
|
|
18
22
|
def key?(key)
|
19
|
-
|
23
|
+
ctx.key?(key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear
|
27
|
+
@__schema_cache__.clear
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear_cache(*keys)
|
31
|
+
if keys.empty?
|
32
|
+
@__schema_cache__.clear
|
33
|
+
else
|
34
|
+
keys.each { |key| @__schema_cache__.delete(key) }
|
35
|
+
end
|
20
36
|
end
|
21
37
|
end
|
22
38
|
end
|
data/lib/kumi/explain.rb
CHANGED
@@ -56,16 +56,16 @@ module Kumi
|
|
56
56
|
|
57
57
|
def format_call_expression(expr, indent_context: 0, nested: false)
|
58
58
|
if pretty_printable?(expr.fn_name)
|
59
|
-
format_pretty_function(expr, expr.fn_name, indent_context, nested)
|
59
|
+
format_pretty_function(expr, expr.fn_name, indent_context, nested: nested)
|
60
60
|
else
|
61
61
|
format_generic_function(expr, indent_context)
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
def format_pretty_function(expr, fn_name, _indent_context, nested
|
65
|
+
def format_pretty_function(expr, fn_name, _indent_context, nested: false)
|
66
66
|
if needs_evaluation?(expr.args) && !nested
|
67
67
|
# For top-level expressions, show the flattened symbolic form and evaluation
|
68
|
-
if
|
68
|
+
if chain_of_same_operator?(expr, fn_name)
|
69
69
|
# For chains like a + b + c, flatten to show all operands
|
70
70
|
all_operands = flatten_operator_chain(expr, fn_name)
|
71
71
|
symbolic_operands = all_operands.map { |op| format_expression(op, indent_context: 0, nested: true) }
|
@@ -88,7 +88,7 @@ module Kumi
|
|
88
88
|
else
|
89
89
|
# Regular pretty formatting for non-chain expressions
|
90
90
|
symbolic_args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
|
91
|
-
symbolic_format =
|
91
|
+
symbolic_format = display_format(fn_name, symbolic_args)
|
92
92
|
|
93
93
|
evaluated_args = expr.args.map do |arg|
|
94
94
|
if arg.is_a?(Syntax::TerminalExpressions::Literal)
|
@@ -103,18 +103,18 @@ module Kumi
|
|
103
103
|
end
|
104
104
|
end
|
105
105
|
end
|
106
|
-
evaluated_format =
|
106
|
+
evaluated_format = display_format(fn_name, evaluated_args)
|
107
107
|
|
108
108
|
end
|
109
109
|
"#{symbolic_format} = #{evaluated_format}"
|
110
110
|
else
|
111
111
|
# For nested expressions, just show the symbolic form without evaluation details
|
112
112
|
args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
|
113
|
-
|
113
|
+
display_format(fn_name, args)
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
117
|
-
def
|
117
|
+
def chain_of_same_operator?(expr, fn_name)
|
118
118
|
return false unless %i[add subtract multiply divide].include?(fn_name)
|
119
119
|
|
120
120
|
# Check if any argument is the same operator
|
@@ -152,7 +152,7 @@ module Kumi
|
|
152
152
|
%i[add subtract multiply divide == != > < >= <= and or not].include?(fn_name)
|
153
153
|
end
|
154
154
|
|
155
|
-
def
|
155
|
+
def display_format(fn_name, args)
|
156
156
|
case fn_name
|
157
157
|
when :add then args.join(" + ")
|
158
158
|
when :subtract then args.join(" - ")
|
@@ -84,6 +84,109 @@ module Kumi
|
|
84
84
|
param_types: [Kumi::Types.array(:any)],
|
85
85
|
return_type: Kumi::Types.array(:any),
|
86
86
|
description: "Remove duplicate elements from collection"
|
87
|
+
),
|
88
|
+
|
89
|
+
# Array transformation functions
|
90
|
+
flatten: FunctionBuilder::Entry.new(
|
91
|
+
fn: lambda(&:flatten),
|
92
|
+
arity: 1,
|
93
|
+
param_types: [Kumi::Types.array(:any)],
|
94
|
+
return_type: Kumi::Types.array(:any),
|
95
|
+
description: "Flatten nested arrays into a single array"
|
96
|
+
),
|
97
|
+
|
98
|
+
# Mathematical transformation functions
|
99
|
+
map_multiply: FunctionBuilder::Entry.new(
|
100
|
+
fn: ->(collection, factor) { collection.map { |x| x * factor } },
|
101
|
+
arity: 2,
|
102
|
+
param_types: [Kumi::Types.array(:float), :float],
|
103
|
+
return_type: Kumi::Types.array(:float),
|
104
|
+
description: "Multiply each element by factor"
|
105
|
+
),
|
106
|
+
|
107
|
+
map_add: FunctionBuilder::Entry.new(
|
108
|
+
fn: ->(collection, value) { collection.map { |x| x + value } },
|
109
|
+
arity: 2,
|
110
|
+
param_types: [Kumi::Types.array(:float), :float],
|
111
|
+
return_type: Kumi::Types.array(:float),
|
112
|
+
description: "Add value to each element"
|
113
|
+
),
|
114
|
+
|
115
|
+
# Conditional transformation functions
|
116
|
+
map_conditional: FunctionBuilder::Entry.new(
|
117
|
+
fn: lambda { |collection, condition_value, true_value, false_value|
|
118
|
+
collection.map { |x| x == condition_value ? true_value : false_value }
|
119
|
+
},
|
120
|
+
arity: 4,
|
121
|
+
param_types: %i[array any any any],
|
122
|
+
return_type: :array,
|
123
|
+
description: "Transform elements based on condition: if element == condition_value then true_value else false_value"
|
124
|
+
),
|
125
|
+
|
126
|
+
# Range/index functions for grid operations
|
127
|
+
build_array: FunctionBuilder::Entry.new(
|
128
|
+
fn: lambda { |size, &generator|
|
129
|
+
(0...size).map { |i| generator ? generator.call(i) : i }
|
130
|
+
},
|
131
|
+
arity: 1,
|
132
|
+
param_types: [:integer],
|
133
|
+
return_type: Kumi::Types.array(:any),
|
134
|
+
description: "Build array of given size with index values"
|
135
|
+
),
|
136
|
+
|
137
|
+
range: FunctionBuilder::Entry.new(
|
138
|
+
fn: ->(start, finish) { (start...finish).to_a },
|
139
|
+
arity: 2,
|
140
|
+
param_types: %i[integer integer],
|
141
|
+
return_type: Kumi::Types.array(:integer),
|
142
|
+
description: "Generate range of integers from start to finish (exclusive)"
|
143
|
+
),
|
144
|
+
|
145
|
+
# Array slicing and grouping for rendering
|
146
|
+
each_slice: FunctionBuilder::Entry.new(
|
147
|
+
fn: ->(array, size) { array.each_slice(size).to_a },
|
148
|
+
arity: 2,
|
149
|
+
param_types: %i[array integer],
|
150
|
+
return_type: Kumi::Types.array(:array),
|
151
|
+
description: "Group array elements into subarrays of given size"
|
152
|
+
),
|
153
|
+
|
154
|
+
join: FunctionBuilder::Entry.new(
|
155
|
+
fn: lambda { |array, separator = ""|
|
156
|
+
array.map(&:to_s).join(separator.to_s)
|
157
|
+
},
|
158
|
+
arity: 2,
|
159
|
+
param_types: %i[array string],
|
160
|
+
return_type: :string,
|
161
|
+
description: "Join array elements into string with separator"
|
162
|
+
),
|
163
|
+
|
164
|
+
# Transform each subarray to string and join the results
|
165
|
+
map_join_rows: FunctionBuilder::Entry.new(
|
166
|
+
fn: lambda { |array_of_arrays, row_separator = "", column_separator = "\n"|
|
167
|
+
array_of_arrays.map { |row| row.join(row_separator.to_s) }.join(column_separator.to_s)
|
168
|
+
},
|
169
|
+
arity: 3,
|
170
|
+
param_types: [Kumi::Types.array(:array), :string, :string],
|
171
|
+
return_type: :string,
|
172
|
+
description: "Join 2D array into string with row and column separators"
|
173
|
+
),
|
174
|
+
|
175
|
+
# Higher-order collection functions (limited to common patterns)
|
176
|
+
map_with_index: FunctionBuilder::Entry.new(
|
177
|
+
fn: ->(collection) { collection.map.with_index.to_a },
|
178
|
+
arity: 1,
|
179
|
+
param_types: [Kumi::Types.array(:any)],
|
180
|
+
return_type: Kumi::Types.array(:any),
|
181
|
+
description: "Map collection elements to [element, index] pairs"
|
182
|
+
),
|
183
|
+
|
184
|
+
indices: FunctionBuilder::Entry.new(
|
185
|
+
fn: ->(collection) { (0...collection.size).to_a },
|
186
|
+
arity: 1,
|
187
|
+
param_types: [Kumi::Types.array(:any)],
|
188
|
+
return_type: Kumi::Types.array(:integer),
|
189
|
+
description: "Generate array of indices for the collection"
|
87
190
|
)
|
88
191
|
}
|
89
192
|
end
|
@@ -20,7 +20,8 @@ module Kumi
|
|
20
20
|
trait_names = args[0..-2]
|
21
21
|
expr = args.last
|
22
22
|
|
23
|
-
|
23
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
|
24
|
+
condition = create_fn(:all?, trait_bindings)
|
24
25
|
result = ensure_syntax(expr)
|
25
26
|
add_case(condition, result)
|
26
27
|
end
|
@@ -32,7 +33,8 @@ module Kumi
|
|
32
33
|
trait_names = args[0..-2]
|
33
34
|
expr = args.last
|
34
35
|
|
35
|
-
|
36
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
|
37
|
+
condition = create_fn(:any?, trait_bindings)
|
36
38
|
result = ensure_syntax(expr)
|
37
39
|
add_case(condition, result)
|
38
40
|
end
|
@@ -44,7 +46,8 @@ module Kumi
|
|
44
46
|
trait_names = args[0..-2]
|
45
47
|
expr = args.last
|
46
48
|
|
47
|
-
|
49
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
|
50
|
+
condition = create_fn(:none?, trait_bindings)
|
48
51
|
result = ensure_syntax(expr)
|
49
52
|
add_case(condition, result)
|
50
53
|
end
|
@@ -80,9 +83,17 @@ module Kumi
|
|
80
83
|
raise_error("cascade '#{method_name}' requires an expression as the last argument", location)
|
81
84
|
end
|
82
85
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
+
def convert_trait_names_to_bindings(trait_names, location)
|
87
|
+
trait_names.map do |name|
|
88
|
+
case name
|
89
|
+
when Symbol
|
90
|
+
create_binding(name, location)
|
91
|
+
when Binding
|
92
|
+
name # Already a binding from method_missing
|
93
|
+
else
|
94
|
+
raise_error("trait reference must be a symbol or bare identifier, got #{name.class}", location)
|
95
|
+
end
|
96
|
+
end
|
86
97
|
end
|
87
98
|
|
88
99
|
def add_case(condition, result)
|
@@ -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::Binding] Reference node
|
26
37
|
def ref(name)
|
27
|
-
|
38
|
+
validate_reference_name(name)
|
39
|
+
Binding.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
|
+
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
|
+
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
|
+
Literal.new(value, loc: current_location)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_list(array)
|
79
|
+
elements = array.map { |element| ensure_syntax(element) }
|
80
|
+
ListExpression.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
|
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
|
data/lib/kumi/parser/sugar.rb
CHANGED
@@ -5,10 +5,22 @@ 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
|
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
26
|
return Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
|
@@ -22,32 +34,60 @@ module Kumi
|
|
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
|
+
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, [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, [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, [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, [Syntax::Literal.new(self), other_node])
|
99
141
|
else
|
100
142
|
super(other)
|
101
143
|
end
|
@@ -103,6 +145,65 @@ 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
|
+
Syntax::ListExpression.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
|
106
207
|
end
|
107
208
|
end
|
108
209
|
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)
|