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
@@ -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
@@ -37,17 +37,17 @@ module Kumi
|
|
37
37
|
|
38
38
|
def format_expression(expr, indent_context: 0, nested: false)
|
39
39
|
case expr
|
40
|
-
when Syntax::
|
40
|
+
when Kumi::Syntax::InputReference
|
41
41
|
"input.#{expr.name}"
|
42
|
-
when Syntax::
|
42
|
+
when Kumi::Syntax::DeclarationReference
|
43
43
|
expr.name.to_s
|
44
|
-
when Syntax::
|
44
|
+
when Kumi::Syntax::Literal
|
45
45
|
format_value(expr.value)
|
46
|
-
when Syntax::
|
46
|
+
when Kumi::Syntax::CallExpression
|
47
47
|
format_call_expression(expr, indent_context: indent_context, nested: nested)
|
48
|
-
when Syntax::
|
48
|
+
when Kumi::Syntax::ArrayExpression
|
49
49
|
"[#{expr.elements.map { |e| format_expression(e, indent_context: indent_context, nested: nested) }.join(', ')}]"
|
50
|
-
when Syntax::
|
50
|
+
when Kumi::Syntax::CascadeExpression
|
51
51
|
format_cascade_expression(expr, indent_context: indent_context)
|
52
52
|
else
|
53
53
|
expr.class.name.split("::").last
|
@@ -56,27 +56,27 @@ 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) }
|
72
72
|
symbolic_format = symbolic_operands.join(" #{get_operator_symbol(fn_name)} ")
|
73
73
|
|
74
74
|
evaluated_operands = all_operands.map do |op|
|
75
|
-
if op.is_a?(Syntax::
|
75
|
+
if op.is_a?(Kumi::Syntax::Literal)
|
76
76
|
format_expression(op, indent_context: 0, nested: true)
|
77
77
|
else
|
78
78
|
arg_value = format_value(evaluate_expression(op))
|
79
|
-
if op.is_a?(Syntax::
|
79
|
+
if op.is_a?(Kumi::Syntax::DeclarationReference) && all_operands.length > 1
|
80
80
|
"(#{format_expression(op, indent_context: 0, nested: true)} = #{arg_value})"
|
81
81
|
else
|
82
82
|
arg_value
|
@@ -88,38 +88,38 @@ 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
|
-
if arg.is_a?(Syntax::
|
94
|
+
if arg.is_a?(Kumi::Syntax::Literal)
|
95
95
|
format_expression(arg, indent_context: 0, nested: true)
|
96
96
|
else
|
97
97
|
arg_value = format_value(evaluate_expression(arg))
|
98
|
-
if arg.is_a?(Syntax::
|
99
|
-
expr.args.count { |a| !a.is_a?(Syntax::
|
98
|
+
if arg.is_a?(Kumi::Syntax::DeclarationReference) &&
|
99
|
+
expr.args.count { |a| !a.is_a?(Kumi::Syntax::Literal) } > 1
|
100
100
|
"(#{format_expression(arg, indent_context: 0, nested: true)} = #{arg_value})"
|
101
101
|
else
|
102
102
|
arg_value
|
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
|
121
121
|
expr.args.any? do |arg|
|
122
|
-
arg.is_a?(Syntax::
|
122
|
+
arg.is_a?(Kumi::Syntax::CallExpression) && arg.fn_name == fn_name
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
@@ -127,7 +127,7 @@ module Kumi
|
|
127
127
|
operands = []
|
128
128
|
|
129
129
|
expr.args.each do |arg|
|
130
|
-
if arg.is_a?(Syntax::
|
130
|
+
if arg.is_a?(Kumi::Syntax::CallExpression) && arg.fn_name == operator
|
131
131
|
# Recursively flatten nested operations of the same type
|
132
132
|
operands.concat(flatten_operator_chain(arg, operator))
|
133
133
|
else
|
@@ -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(" - ")
|
@@ -176,8 +176,8 @@ module Kumi
|
|
176
176
|
arg_desc = format_expression(arg, indent_context: indent_context)
|
177
177
|
|
178
178
|
# For literals and literal lists, just show the value, no need for "100 = 100"
|
179
|
-
if arg.is_a?(Syntax::
|
180
|
-
(arg.is_a?(Syntax::
|
179
|
+
if arg.is_a?(Kumi::Syntax::Literal) ||
|
180
|
+
(arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::Literal))
|
181
181
|
arg_desc
|
182
182
|
else
|
183
183
|
arg_value = evaluate_expression(arg)
|
@@ -197,8 +197,8 @@ module Kumi
|
|
197
197
|
|
198
198
|
def needs_evaluation?(args)
|
199
199
|
args.any? do |arg|
|
200
|
-
!arg.is_a?(Syntax::
|
201
|
-
!(arg.is_a?(Syntax::
|
200
|
+
!arg.is_a?(Kumi::Syntax::Literal) &&
|
201
|
+
!(arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.all?(Kumi::Syntax::Literal))
|
202
202
|
end
|
203
203
|
end
|
204
204
|
|
@@ -252,11 +252,11 @@ module Kumi
|
|
252
252
|
|
253
253
|
def evaluate_expression(expr)
|
254
254
|
case expr
|
255
|
-
when Syntax::
|
255
|
+
when Kumi::Syntax::DeclarationReference
|
256
256
|
@compiled_schema.evaluate_binding(expr.name, @inputs)
|
257
|
-
when Syntax::
|
257
|
+
when Kumi::Syntax::InputReference
|
258
258
|
@inputs[expr.name]
|
259
|
-
when Syntax::
|
259
|
+
when Kumi::Syntax::Literal
|
260
260
|
expr.value
|
261
261
|
else
|
262
262
|
# For complex expressions, compile and evaluate using existing compiler
|
@@ -6,20 +6,34 @@ module Kumi
|
|
6
6
|
# Maps AST classes to JSON type names
|
7
7
|
SERIALIZATION_MAP = {
|
8
8
|
"Kumi::Syntax::Root" => "root",
|
9
|
-
"Kumi::Syntax::
|
10
|
-
"Kumi::Syntax::
|
11
|
-
"Kumi::Syntax::
|
12
|
-
"Kumi::Syntax::
|
13
|
-
"Kumi::Syntax::
|
14
|
-
"Kumi::Syntax::
|
15
|
-
"Kumi::Syntax::
|
16
|
-
"Kumi::Syntax::
|
17
|
-
"Kumi::Syntax::
|
18
|
-
"Kumi::Syntax::
|
9
|
+
"Kumi::Syntax::InputDeclaration" => "field_declaration",
|
10
|
+
"Kumi::Syntax::ValueDeclaration" => "attribute_declaration",
|
11
|
+
"Kumi::Syntax::TraitDeclaration" => "trait_declaration",
|
12
|
+
"Kumi::Syntax::CallExpression" => "call_expression",
|
13
|
+
"Kumi::Syntax::ArrayExpression" => "list_expression",
|
14
|
+
"Kumi::Syntax::HashExpression" => "hash_expression",
|
15
|
+
"Kumi::Syntax::CascadeExpression" => "cascade_expression",
|
16
|
+
"Kumi::Syntax::CaseExpression" => "when_case_expression",
|
17
|
+
"Kumi::Syntax::Literal" => "literal",
|
18
|
+
"Kumi::Syntax::InputReference" => "field_reference",
|
19
|
+
"Kumi::Syntax::DeclarationReference" => "binding_reference"
|
19
20
|
}.freeze
|
20
21
|
|
21
|
-
# Maps JSON type names back to AST classes
|
22
|
-
DESERIALIZATION_MAP =
|
22
|
+
# Maps JSON type names back to AST classes (using new canonical class names)
|
23
|
+
DESERIALIZATION_MAP = {
|
24
|
+
"root" => "Kumi::Syntax::Root",
|
25
|
+
"field_declaration" => "Kumi::Syntax::InputDeclaration",
|
26
|
+
"attribute_declaration" => "Kumi::Syntax::ValueDeclaration",
|
27
|
+
"trait_declaration" => "Kumi::Syntax::TraitDeclaration",
|
28
|
+
"call_expression" => "Kumi::Syntax::CallExpression",
|
29
|
+
"list_expression" => "Kumi::Syntax::ArrayExpression",
|
30
|
+
"hash_expression" => "Kumi::Syntax::HashExpression",
|
31
|
+
"cascade_expression" => "Kumi::Syntax::CascadeExpression",
|
32
|
+
"when_case_expression" => "Kumi::Syntax::CaseExpression",
|
33
|
+
"literal" => "Kumi::Syntax::Literal",
|
34
|
+
"field_reference" => "Kumi::Syntax::InputReference",
|
35
|
+
"binding_reference" => "Kumi::Syntax::DeclarationReference"
|
36
|
+
}.freeze
|
23
37
|
|
24
38
|
def self.type_name_for(node)
|
25
39
|
SERIALIZATION_MAP[node.class.name] or
|
@@ -6,10 +6,10 @@ module Kumi
|
|
6
6
|
module CollectionFunctions
|
7
7
|
def self.definitions
|
8
8
|
{
|
9
|
-
# Collection queries
|
10
|
-
empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty
|
11
|
-
size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer),
|
12
|
-
length: FunctionBuilder.collection_unary(:length, "Get collection length", :length, return_type: :integer),
|
9
|
+
# Collection queries (these are reducers - they reduce arrays to scalars)
|
10
|
+
empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty?, reducer: true),
|
11
|
+
size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer, reducer: true),
|
12
|
+
length: FunctionBuilder.collection_unary(:length, "Get collection length", :length, return_type: :integer, reducer: true),
|
13
13
|
|
14
14
|
# Element access
|
15
15
|
first: FunctionBuilder::Entry.new(
|
@@ -17,7 +17,8 @@ module Kumi
|
|
17
17
|
arity: 1,
|
18
18
|
param_types: [Kumi::Types.array(:any)],
|
19
19
|
return_type: :any,
|
20
|
-
description: "Get first element of collection"
|
20
|
+
description: "Get first element of collection",
|
21
|
+
reducer: true
|
21
22
|
),
|
22
23
|
|
23
24
|
last: FunctionBuilder::Entry.new(
|
@@ -25,7 +26,8 @@ module Kumi
|
|
25
26
|
arity: 1,
|
26
27
|
param_types: [Kumi::Types.array(:any)],
|
27
28
|
return_type: :any,
|
28
|
-
description: "Get last element of collection"
|
29
|
+
description: "Get last element of collection",
|
30
|
+
reducer: true
|
29
31
|
),
|
30
32
|
|
31
33
|
# Mathematical operations on collections
|
@@ -34,7 +36,8 @@ module Kumi
|
|
34
36
|
arity: 1,
|
35
37
|
param_types: [Kumi::Types.array(:float)],
|
36
38
|
return_type: :float,
|
37
|
-
description: "Sum all numeric elements in collection"
|
39
|
+
description: "Sum all numeric elements in collection",
|
40
|
+
reducer: true
|
38
41
|
),
|
39
42
|
|
40
43
|
min: FunctionBuilder::Entry.new(
|
@@ -42,7 +45,8 @@ module Kumi
|
|
42
45
|
arity: 1,
|
43
46
|
param_types: [Kumi::Types.array(:float)],
|
44
47
|
return_type: :float,
|
45
|
-
description: "Find minimum value in numeric collection"
|
48
|
+
description: "Find minimum value in numeric collection",
|
49
|
+
reducer: true
|
46
50
|
),
|
47
51
|
|
48
52
|
max: FunctionBuilder::Entry.new(
|
@@ -50,7 +54,8 @@ module Kumi
|
|
50
54
|
arity: 1,
|
51
55
|
param_types: [Kumi::Types.array(:float)],
|
52
56
|
return_type: :float,
|
53
|
-
description: "Find maximum value in numeric collection"
|
57
|
+
description: "Find maximum value in numeric collection",
|
58
|
+
reducer: true
|
54
59
|
),
|
55
60
|
|
56
61
|
# Collection operations
|
@@ -84,6 +89,109 @@ module Kumi
|
|
84
89
|
param_types: [Kumi::Types.array(:any)],
|
85
90
|
return_type: Kumi::Types.array(:any),
|
86
91
|
description: "Remove duplicate elements from collection"
|
92
|
+
),
|
93
|
+
|
94
|
+
# Array transformation functions
|
95
|
+
flatten: FunctionBuilder::Entry.new(
|
96
|
+
fn: lambda(&:flatten),
|
97
|
+
arity: 1,
|
98
|
+
param_types: [Kumi::Types.array(:any)],
|
99
|
+
return_type: Kumi::Types.array(:any),
|
100
|
+
description: "Flatten nested arrays into a single array"
|
101
|
+
),
|
102
|
+
|
103
|
+
# Mathematical transformation functions
|
104
|
+
map_multiply: FunctionBuilder::Entry.new(
|
105
|
+
fn: ->(collection, factor) { collection.map { |x| x * factor } },
|
106
|
+
arity: 2,
|
107
|
+
param_types: [Kumi::Types.array(:float), :float],
|
108
|
+
return_type: Kumi::Types.array(:float),
|
109
|
+
description: "Multiply each element by factor"
|
110
|
+
),
|
111
|
+
|
112
|
+
map_add: FunctionBuilder::Entry.new(
|
113
|
+
fn: ->(collection, value) { collection.map { |x| x + value } },
|
114
|
+
arity: 2,
|
115
|
+
param_types: [Kumi::Types.array(:float), :float],
|
116
|
+
return_type: Kumi::Types.array(:float),
|
117
|
+
description: "Add value to each element"
|
118
|
+
),
|
119
|
+
|
120
|
+
# Conditional transformation functions
|
121
|
+
map_conditional: FunctionBuilder::Entry.new(
|
122
|
+
fn: lambda { |collection, condition_value, true_value, false_value|
|
123
|
+
collection.map { |x| x == condition_value ? true_value : false_value }
|
124
|
+
},
|
125
|
+
arity: 4,
|
126
|
+
param_types: %i[array any any any],
|
127
|
+
return_type: :array,
|
128
|
+
description: "Transform elements based on condition: if element == condition_value then true_value else false_value"
|
129
|
+
),
|
130
|
+
|
131
|
+
# Range/index functions for grid operations
|
132
|
+
build_array: FunctionBuilder::Entry.new(
|
133
|
+
fn: lambda { |size, &generator|
|
134
|
+
(0...size).map { |i| generator ? generator.call(i) : i }
|
135
|
+
},
|
136
|
+
arity: 1,
|
137
|
+
param_types: [:integer],
|
138
|
+
return_type: Kumi::Types.array(:any),
|
139
|
+
description: "Build array of given size with index values"
|
140
|
+
),
|
141
|
+
|
142
|
+
range: FunctionBuilder::Entry.new(
|
143
|
+
fn: ->(start, finish) { (start...finish).to_a },
|
144
|
+
arity: 2,
|
145
|
+
param_types: %i[integer integer],
|
146
|
+
return_type: Kumi::Types.array(:integer),
|
147
|
+
description: "Generate range of integers from start to finish (exclusive)"
|
148
|
+
),
|
149
|
+
|
150
|
+
# Array slicing and grouping for rendering
|
151
|
+
each_slice: FunctionBuilder::Entry.new(
|
152
|
+
fn: ->(array, size) { array.each_slice(size).to_a },
|
153
|
+
arity: 2,
|
154
|
+
param_types: %i[array integer],
|
155
|
+
return_type: Kumi::Types.array(:array),
|
156
|
+
description: "Group array elements into subarrays of given size"
|
157
|
+
),
|
158
|
+
|
159
|
+
join: FunctionBuilder::Entry.new(
|
160
|
+
fn: lambda { |array, separator = ""|
|
161
|
+
array.map(&:to_s).join(separator.to_s)
|
162
|
+
},
|
163
|
+
arity: 2,
|
164
|
+
param_types: %i[array string],
|
165
|
+
return_type: :string,
|
166
|
+
description: "Join array elements into string with separator"
|
167
|
+
),
|
168
|
+
|
169
|
+
# Transform each subarray to string and join the results
|
170
|
+
map_join_rows: FunctionBuilder::Entry.new(
|
171
|
+
fn: lambda { |array_of_arrays, row_separator = "", column_separator = "\n"|
|
172
|
+
array_of_arrays.map { |row| row.join(row_separator.to_s) }.join(column_separator.to_s)
|
173
|
+
},
|
174
|
+
arity: 3,
|
175
|
+
param_types: [Kumi::Types.array(:array), :string, :string],
|
176
|
+
return_type: :string,
|
177
|
+
description: "Join 2D array into string with row and column separators"
|
178
|
+
),
|
179
|
+
|
180
|
+
# Higher-order collection functions (limited to common patterns)
|
181
|
+
map_with_index: FunctionBuilder::Entry.new(
|
182
|
+
fn: ->(collection) { collection.map.with_index.to_a },
|
183
|
+
arity: 1,
|
184
|
+
param_types: [Kumi::Types.array(:any)],
|
185
|
+
return_type: Kumi::Types.array(:any),
|
186
|
+
description: "Map collection elements to [element, index] pairs"
|
187
|
+
),
|
188
|
+
|
189
|
+
indices: FunctionBuilder::Entry.new(
|
190
|
+
fn: ->(collection) { (0...collection.size).to_a },
|
191
|
+
arity: 1,
|
192
|
+
param_types: [Kumi::Types.array(:any)],
|
193
|
+
return_type: Kumi::Types.array(:integer),
|
194
|
+
description: "Generate array of indices for the collection"
|
87
195
|
)
|
88
196
|
}
|
89
197
|
end
|
@@ -4,7 +4,7 @@ module Kumi
|
|
4
4
|
module FunctionRegistry
|
5
5
|
# Utility class to reduce repetition in function definitions
|
6
6
|
class FunctionBuilder
|
7
|
-
Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, keyword_init: true)
|
7
|
+
Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, :reducer, keyword_init: true)
|
8
8
|
|
9
9
|
def self.comparison(_name, description, operation)
|
10
10
|
Entry.new(
|
@@ -78,13 +78,14 @@ module Kumi
|
|
78
78
|
)
|
79
79
|
end
|
80
80
|
|
81
|
-
def self.collection_unary(_name, description, operation, return_type: :boolean)
|
81
|
+
def self.collection_unary(_name, description, operation, return_type: :boolean, reducer: false)
|
82
82
|
Entry.new(
|
83
83
|
fn: proc(&operation),
|
84
84
|
arity: 1,
|
85
85
|
param_types: [Kumi::Types.array(:any)],
|
86
86
|
return_type: return_type,
|
87
|
-
description: description
|
87
|
+
description: description,
|
88
|
+
reducer: reducer
|
88
89
|
)
|
89
90
|
end
|
90
91
|
end
|
@@ -36,7 +36,7 @@ module Kumi
|
|
36
36
|
|
37
37
|
# Register with custom metadata
|
38
38
|
def register_with_metadata(name, fn_lambda, arity:, param_types: [:any], return_type: :any, description: nil,
|
39
|
-
inverse: nil)
|
39
|
+
inverse: nil, reducer: false)
|
40
40
|
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
41
41
|
|
42
42
|
@functions[name] = Entry.new(
|
@@ -45,7 +45,8 @@ module Kumi
|
|
45
45
|
param_types: param_types,
|
46
46
|
return_type: return_type,
|
47
47
|
description: description,
|
48
|
-
inverse: inverse
|
48
|
+
inverse: inverse,
|
49
|
+
reducer: reducer
|
49
50
|
)
|
50
51
|
end
|
51
52
|
|
@@ -91,6 +92,11 @@ module Kumi
|
|
91
92
|
@functions.keys
|
92
93
|
end
|
93
94
|
|
95
|
+
def reducer?(name)
|
96
|
+
entry = @functions.fetch(name) { return false }
|
97
|
+
entry.reducer || false
|
98
|
+
end
|
99
|
+
|
94
100
|
# Alias for compatibility
|
95
101
|
def all
|
96
102
|
@functions.keys
|
data/lib/kumi/input/validator.rb
CHANGED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Parser
|
5
|
+
# DSL proxy for declaration references (traits and values)
|
6
|
+
# Handles references to declared items and field access on them
|
7
|
+
class DeclarationReferenceProxy
|
8
|
+
include Syntax
|
9
|
+
|
10
|
+
# Use shared operator methods
|
11
|
+
extend Sugar::ProxyRefinement
|
12
|
+
|
13
|
+
def initialize(name, context)
|
14
|
+
@name = name
|
15
|
+
@context = context
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert to DeclarationReference AST node
|
19
|
+
def to_ast_node
|
20
|
+
Kumi::Syntax::DeclarationReference.new(@name, loc: @context.current_location)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def method_missing(method_name, *args, &block)
|
26
|
+
# All operators are handled by ProxyRefinement methods
|
27
|
+
# Field access should use input.field.subfield syntax, not bare identifiers
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
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,13 +83,21 @@ 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 DeclarationReference
|
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)
|
89
|
-
@cases <<
|
100
|
+
@cases << Kumi::Syntax::CaseExpression.new(condition, result)
|
90
101
|
end
|
91
102
|
|
92
103
|
def ref(name)
|
@@ -118,7 +129,7 @@ module Kumi
|
|
118
129
|
end
|
119
130
|
|
120
131
|
def create_binding(name, location)
|
121
|
-
|
132
|
+
Kumi::Syntax::DeclarationReference.new(name, loc: location)
|
122
133
|
end
|
123
134
|
end
|
124
135
|
end
|