kumi 0.0.10 → 0.0.11
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/CHANGELOG.md +18 -0
- data/CLAUDE.md +7 -231
- data/README.md +1 -1
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/features/hierarchical-broadcasting.md +1 -1
- data/docs/features/s-expression-printer.md +2 -2
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/lib/kumi/analyzer.rb +34 -12
- data/lib/kumi/compiler.rb +2 -12
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -101
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +2 -2
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -55
- data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
- data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
- data/lib/kumi/core/function_registry.rb +126 -108
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
- data/lib/kumi/core/ruby_parser/input_builder.rb +5 -5
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/registry.rb +14 -79
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +14 -3
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +1 -1
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +0 -1
- data/lib/kumi/syntax/root.rb +2 -2
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- metadata +26 -15
- data/lib/kumi/core/cascade_executor_builder.rb +0 -132
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
- data/lib/kumi/core/compiler/function_invoker.rb +0 -55
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
- data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/nested_structure_utils.rb +0 -78
- data/lib/kumi/core/schema_instance.rb +0 -115
- data/lib/kumi/core/vectorized_function_builder.rb +0 -88
- data/lib/kumi/js/compiler.rb +0 -878
- data/lib/kumi/js/function_registry.rb +0 -333
- data/migrate_to_core_iterative.rb +0 -938
@@ -3,92 +3,179 @@
|
|
3
3
|
module Kumi
|
4
4
|
module Core
|
5
5
|
module FunctionRegistry
|
6
|
-
# Utility class to reduce repetition in function definitions
|
7
6
|
class FunctionBuilder
|
8
|
-
|
9
|
-
|
7
|
+
# Rich, defaulted function entry
|
8
|
+
class Entry
|
9
|
+
# NOTE: Keep ctor args minimal; everything else has sensible defaults.
|
10
|
+
attr_reader :fn, :arity, :param_types, :return_type, :description,
|
11
|
+
:reducer, :structure_function, :param_modes, :param_info
|
10
12
|
|
11
|
-
|
13
|
+
# param_modes: nil | ->(argc){[:elem,:scalar,...]} | {fixed: [...], variadic: :elem|:scalar}
|
14
|
+
# param_info: nil | ->(argc){[specs]} | {fixed: [...], variadic: {…}}
|
15
|
+
# where a spec is: { name:, type:, mode:, required:, default:, doc: }
|
16
|
+
def initialize(
|
17
|
+
fn:,
|
18
|
+
arity: nil, # Integer (>=0) or -1 / nil for variadic
|
19
|
+
param_types: nil, # defaults to [:any] * arity (when fixed)
|
20
|
+
return_type: :any,
|
21
|
+
description: "",
|
22
|
+
reducer: false,
|
23
|
+
structure_function: false,
|
24
|
+
param_modes: nil,
|
25
|
+
param_info: nil
|
26
|
+
)
|
27
|
+
@fn = fn
|
28
|
+
@arity = arity
|
29
|
+
@param_types = param_types || default_param_types(arity)
|
30
|
+
@return_type = return_type
|
31
|
+
@description = description
|
32
|
+
@reducer = !!reducer
|
33
|
+
@structure_function = !!structure_function
|
34
|
+
@param_modes = normalize_param_modes(param_modes, arity)
|
35
|
+
@param_info = normalize_param_info(param_info, arity, @param_types)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Concrete modes for a call site
|
39
|
+
def param_modes_for(argc)
|
40
|
+
pm = @param_modes
|
41
|
+
return pm.call(argc) if pm.respond_to?(:call)
|
42
|
+
|
43
|
+
fixed = Array(pm[:fixed] || [])
|
44
|
+
return fixed.first(argc) if argc <= fixed.size
|
45
|
+
|
46
|
+
fixed + Array.new(argc - fixed.size, pm.fetch(:variadic, :elem))
|
47
|
+
end
|
48
|
+
|
49
|
+
# Concrete param specs for a call site
|
50
|
+
# → [{name:, type:, mode:, required:, default:, doc:}, ...]
|
51
|
+
def param_specs_for(argc)
|
52
|
+
base = if @param_info.respond_to?(:call)
|
53
|
+
@param_info.call(argc)
|
54
|
+
else
|
55
|
+
fixed = Array(@param_info[:fixed] || [])
|
56
|
+
if argc <= fixed.size
|
57
|
+
fixed.first(argc)
|
58
|
+
else
|
59
|
+
fixed + Array.new(argc - fixed.size, @param_info.fetch(:variadic, {}))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
modes = param_modes_for(argc)
|
64
|
+
types = expand_types_for(argc)
|
65
|
+
|
66
|
+
base.each_with_index.map do |spec, i|
|
67
|
+
{
|
68
|
+
name: spec[:name] || auto_name(i),
|
69
|
+
type: spec[:type] || types[i] || :any,
|
70
|
+
mode: spec[:mode] || modes[i] || :elem,
|
71
|
+
required: spec.key?(:required) ? spec[:required] : true,
|
72
|
+
default: spec[:default],
|
73
|
+
doc: spec[:doc] || ""
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def default_param_types(arity)
|
81
|
+
if arity.is_a?(Integer) && arity >= 0
|
82
|
+
Array.new(arity, :any)
|
83
|
+
else
|
84
|
+
[] # variadic → types resolved per call
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def expand_types_for(argc)
|
89
|
+
if @param_types.nil? || @param_types.empty?
|
90
|
+
Array.new(argc, :any)
|
91
|
+
elsif @param_types.length >= argc
|
92
|
+
@param_types.first(argc)
|
93
|
+
else
|
94
|
+
@param_types + Array.new(argc - @param_types.length, @param_types.last || :any)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def normalize_param_modes(pm, arity)
|
99
|
+
return pm if pm
|
100
|
+
|
101
|
+
# Default: everything element-wise/broadcastable
|
102
|
+
->(argc) { Array.new(argc, :elem) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def normalize_param_info(info, arity, types)
|
106
|
+
return info if info
|
107
|
+
|
108
|
+
# Default: synthesize from types/modes at call time
|
109
|
+
->(argc) { Array.new(argc) { {} } }
|
110
|
+
end
|
111
|
+
|
112
|
+
def auto_name(i) = :"arg#{i + 1}"
|
113
|
+
end
|
114
|
+
|
115
|
+
# ===== Helper constructors (unchanged usage; now benefit from defaults) =====
|
116
|
+
|
117
|
+
def self.comparison(_name, description, op)
|
12
118
|
Entry.new(
|
13
|
-
fn: ->(a, b) { a.public_send(
|
14
|
-
arity: 2,
|
15
|
-
|
16
|
-
return_type: :boolean,
|
17
|
-
description: description
|
119
|
+
fn: ->(a, b) { a.public_send(op, b) },
|
120
|
+
arity: 2, param_types: %i[float float],
|
121
|
+
return_type: :boolean, description: description
|
18
122
|
)
|
19
123
|
end
|
20
124
|
|
21
|
-
def self.equality(_name, description,
|
125
|
+
def self.equality(_name, description, op)
|
22
126
|
Entry.new(
|
23
|
-
fn: ->(a, b) { a.public_send(
|
24
|
-
arity: 2,
|
25
|
-
|
26
|
-
return_type: :boolean,
|
27
|
-
description: description
|
127
|
+
fn: ->(a, b) { a.public_send(op, b) },
|
128
|
+
arity: 2, param_types: %i[any any],
|
129
|
+
return_type: :boolean, description: description
|
28
130
|
)
|
29
131
|
end
|
30
132
|
|
31
|
-
def self.math_binary(_name, description,
|
133
|
+
def self.math_binary(_name, description, op, return_type: :float)
|
32
134
|
Entry.new(
|
33
|
-
fn:
|
34
|
-
|
35
|
-
|
36
|
-
arity: 2,
|
37
|
-
param_types: %i[float float],
|
38
|
-
return_type: return_type,
|
39
|
-
description: description
|
135
|
+
fn: ->(a, b) { a.public_send(op, b) },
|
136
|
+
arity: 2, param_types: %i[float float],
|
137
|
+
return_type: return_type, description: description
|
40
138
|
)
|
41
139
|
end
|
42
140
|
|
43
|
-
def self.math_unary(_name, description,
|
141
|
+
def self.math_unary(_name, description, op, return_type: :float)
|
44
142
|
Entry.new(
|
45
|
-
fn: proc(&
|
46
|
-
arity: 1,
|
47
|
-
|
48
|
-
return_type: return_type,
|
49
|
-
description: description
|
143
|
+
fn: proc(&op),
|
144
|
+
arity: 1, param_types: [:float],
|
145
|
+
return_type: return_type, description: description
|
50
146
|
)
|
51
147
|
end
|
52
148
|
|
53
|
-
def self.string_unary(_name, description,
|
149
|
+
def self.string_unary(_name, description, op)
|
54
150
|
Entry.new(
|
55
|
-
fn: ->(
|
56
|
-
arity: 1,
|
57
|
-
|
58
|
-
return_type: :string,
|
59
|
-
description: description
|
151
|
+
fn: ->(s) { s.to_s.public_send(op) },
|
152
|
+
arity: 1, param_types: [:string],
|
153
|
+
return_type: :string, description: description
|
60
154
|
)
|
61
155
|
end
|
62
156
|
|
63
|
-
def self.string_binary(_name, description,
|
157
|
+
def self.string_binary(_name, description, op, return_type: :string)
|
64
158
|
Entry.new(
|
65
|
-
fn: ->(
|
66
|
-
arity: 2,
|
67
|
-
|
68
|
-
return_type: return_type,
|
69
|
-
description: description
|
159
|
+
fn: ->(s, x) { s.to_s.public_send(op, x.to_s) },
|
160
|
+
arity: 2, param_types: %i[string string],
|
161
|
+
return_type: return_type, description: description
|
70
162
|
)
|
71
163
|
end
|
72
164
|
|
73
|
-
def self.logical_variadic(_name, description,
|
165
|
+
def self.logical_variadic(_name, description, op)
|
74
166
|
Entry.new(
|
75
|
-
fn: ->(
|
76
|
-
arity: -1,
|
77
|
-
|
78
|
-
return_type: :boolean,
|
79
|
-
description: description
|
167
|
+
fn: ->(*conds) { conds.flatten.public_send(op) },
|
168
|
+
arity: -1, param_types: [:boolean],
|
169
|
+
return_type: :boolean, description: description
|
80
170
|
)
|
81
171
|
end
|
82
172
|
|
83
|
-
def self.collection_unary(_name, description,
|
173
|
+
def self.collection_unary(_name, description, op, return_type: :boolean, reducer: false, structure_function: false)
|
84
174
|
Entry.new(
|
85
|
-
fn: proc(&
|
86
|
-
arity: 1,
|
87
|
-
|
88
|
-
|
89
|
-
description: description,
|
90
|
-
reducer: reducer,
|
91
|
-
structure_function: structure_function
|
175
|
+
fn: proc(&op),
|
176
|
+
arity: 1, param_types: [Kumi::Core::Types.array(:any)],
|
177
|
+
return_type: return_type, description: description,
|
178
|
+
reducer: reducer, structure_function: structure_function
|
92
179
|
)
|
93
180
|
end
|
94
181
|
end
|
@@ -173,9 +173,9 @@ module Kumi
|
|
173
173
|
),
|
174
174
|
|
175
175
|
# Collection logical operations
|
176
|
-
all?: FunctionBuilder.collection_unary(:all?, "Check if all elements in collection are truthy", :all
|
177
|
-
any?: FunctionBuilder.collection_unary(:any?, "Check if any element in collection is truthy", :any
|
178
|
-
none?: FunctionBuilder.collection_unary(:none?, "Check if no elements in collection are truthy", :none
|
176
|
+
all?: FunctionBuilder.collection_unary(:all?, "Check if all elements in collection are truthy", :all?, reducer: true),
|
177
|
+
any?: FunctionBuilder.collection_unary(:any?, "Check if any element in collection is truthy", :any?, reducer: true),
|
178
|
+
none?: FunctionBuilder.collection_unary(:none?, "Check if no elements in collection are truthy", :none?, reducer: true),
|
179
179
|
|
180
180
|
# Element-wise AND for cascades - works on arrays with same structure
|
181
181
|
cascade_and: FunctionBuilder::Entry.new(
|
@@ -188,9 +188,9 @@ module Kumi
|
|
188
188
|
end
|
189
189
|
|
190
190
|
return false if conditions.empty?
|
191
|
-
return conditions.first if conditions.length == 1
|
192
191
|
|
193
|
-
#
|
192
|
+
# Always process uniformly, even for single conditions
|
193
|
+
# This ensures DeclarationReferences are evaluated properly
|
194
194
|
result = conditions.first
|
195
195
|
conditions[1..].each_with_index do |condition, i|
|
196
196
|
puts " Combining result with condition[#{i + 1}]" if ENV["DEBUG_CASCADE"]
|
@@ -8,7 +8,7 @@ module Kumi
|
|
8
8
|
{
|
9
9
|
# Statistical Functions
|
10
10
|
avg: FunctionBuilder::Entry.new(
|
11
|
-
fn: ->(array) { array.sum.to_f / array.size },
|
11
|
+
fn: ->(array) { array.empty? ? nil : array.sum.to_f / array.size },
|
12
12
|
arity: 1,
|
13
13
|
param_types: [Kumi::Core::Types.array(:float)],
|
14
14
|
return_type: :float,
|
@@ -17,7 +17,7 @@ module Kumi
|
|
17
17
|
),
|
18
18
|
|
19
19
|
mean: FunctionBuilder::Entry.new(
|
20
|
-
fn: ->(array) { array.sum.to_f / array.size },
|
20
|
+
fn: ->(array) { array.empty? ? nil : array.sum.to_f / array.size },
|
21
21
|
arity: 1,
|
22
22
|
param_types: [Kumi::Core::Types.array(:float)],
|
23
23
|
return_type: :float,
|
@@ -2,18 +2,15 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Core
|
5
|
-
# Internal function registry
|
6
|
-
# Use Kumi::Registry for the public interface to register custom functions
|
5
|
+
# Internal function registry (single source of truth).
|
7
6
|
module FunctionRegistry
|
8
|
-
# Re-export the Entry struct from FunctionBuilder for compatibility
|
9
7
|
Entry = FunctionBuilder::Entry
|
10
8
|
|
11
|
-
# Core operators that are always available
|
12
9
|
CORE_OPERATORS = %i[== > < >= <= != between?].freeze
|
13
10
|
|
14
|
-
# Build
|
11
|
+
# Build core functions once
|
15
12
|
CORE_FUNCTIONS = {}.tap do |registry|
|
16
|
-
|
13
|
+
[
|
17
14
|
ComparisonFunctions.definitions,
|
18
15
|
MathFunctions.definitions,
|
19
16
|
StringFunctions.definitions,
|
@@ -22,137 +19,158 @@ module Kumi
|
|
22
19
|
ConditionalFunctions.definitions,
|
23
20
|
TypeFunctions.definitions,
|
24
21
|
StatFunctions.definitions
|
25
|
-
]
|
22
|
+
].each do |defs|
|
23
|
+
defs.each do |name, entry|
|
24
|
+
raise ArgumentError, "Duplicate core function: #{name}" if registry.key?(name)
|
26
25
|
|
27
|
-
|
28
|
-
module_definitions.each do |name, definition|
|
29
|
-
if registry.key?(name)
|
30
|
-
raise ArgumentError, "Function #{name.inspect} is already registered. Found duplicate in function registry."
|
31
|
-
end
|
32
|
-
|
33
|
-
registry[name] = definition
|
26
|
+
registry[name] = entry
|
34
27
|
end
|
35
28
|
end
|
36
29
|
end.freeze
|
37
30
|
|
38
|
-
@
|
39
|
-
@
|
31
|
+
@lock = Mutex.new
|
32
|
+
@functions = CORE_FUNCTIONS.transform_values(&:dup)
|
33
|
+
@frozen = false
|
40
34
|
|
41
|
-
|
42
|
-
# Internal interface for registering custom functions
|
43
|
-
def register(name, &block)
|
44
|
-
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
35
|
+
class FrozenError < RuntimeError; end
|
45
36
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
def register_with_metadata(name, fn_lambda, arity:, param_types: [:any], return_type: :any, description: nil,
|
52
|
-
inverse: nil, reducer: false)
|
53
|
-
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
54
|
-
|
55
|
-
@functions[name] = Entry.new(
|
56
|
-
fn: fn_lambda,
|
57
|
-
arity: arity,
|
58
|
-
param_types: param_types,
|
59
|
-
return_type: return_type,
|
60
|
-
description: description,
|
61
|
-
inverse: inverse,
|
62
|
-
reducer: reducer
|
63
|
-
)
|
64
|
-
end
|
37
|
+
class << self
|
38
|
+
def auto_register(*mods)
|
39
|
+
mods.each do |mod|
|
40
|
+
mod.public_instance_methods(false).each do |m|
|
41
|
+
next if function?(m)
|
65
42
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
next if supported?(method_name)
|
43
|
+
register(m) { |*args| mod.new.public_send(m, *args) }
|
44
|
+
end
|
45
|
+
mod.singleton_methods(false).each do |m|
|
46
|
+
next if function?(m)
|
71
47
|
|
72
|
-
|
73
|
-
|
48
|
+
fn = mod.method(m)
|
49
|
+
register(m) { |*args| fn.call(*args) }
|
74
50
|
end
|
75
51
|
end
|
76
52
|
end
|
77
|
-
end
|
78
53
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
54
|
+
#
|
55
|
+
# Lifecycle
|
56
|
+
#
|
57
|
+
def reset!
|
58
|
+
@lock.synchronize do
|
59
|
+
@functions = CORE_FUNCTIONS.transform_values(&:dup)
|
60
|
+
@frozen = false
|
61
|
+
end
|
62
|
+
end
|
86
63
|
|
87
|
-
|
88
|
-
|
64
|
+
def freeze!
|
65
|
+
@lock.synchronize do
|
66
|
+
@functions.each_value(&:freeze)
|
67
|
+
@functions.freeze
|
68
|
+
@frozen = true
|
69
|
+
end
|
70
|
+
end
|
89
71
|
|
90
|
-
|
91
|
-
|
92
|
-
|
72
|
+
def frozen?
|
73
|
+
@frozen
|
74
|
+
end
|
93
75
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
76
|
+
#
|
77
|
+
# Registration
|
78
|
+
#
|
79
|
+
# Unified entry point; used by both public and internal callers.
|
80
|
+
def register(name, fn_or = nil, **meta, &block)
|
81
|
+
fn = fn_or || block
|
82
|
+
raise ArgumentError, "block or Proc required" unless fn.is_a?(Proc)
|
83
|
+
|
84
|
+
defaults = {
|
85
|
+
arity: fn.arity,
|
86
|
+
param_types: [:any],
|
87
|
+
return_type: :any,
|
88
|
+
description: nil,
|
89
|
+
param_modes: nil,
|
90
|
+
reducer: false,
|
91
|
+
structure_function: false
|
92
|
+
}
|
93
|
+
register_with_metadata(name, fn, **defaults, **meta)
|
94
|
+
end
|
103
95
|
|
104
|
-
|
105
|
-
|
106
|
-
|
96
|
+
# Back-compat explicit API
|
97
|
+
def register_with_metadata(name, fn, arity:, param_types: [:any], return_type: :any,
|
98
|
+
description: nil, param_modes: nil, reducer: false,
|
99
|
+
structure_function: false)
|
100
|
+
@lock.synchronize do
|
101
|
+
raise FrozenError, "registry is frozen" if @frozen
|
102
|
+
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
103
|
+
|
104
|
+
@functions[name] = Entry.new(
|
105
|
+
fn: fn,
|
106
|
+
arity: arity,
|
107
|
+
param_types: param_types,
|
108
|
+
return_type: return_type,
|
109
|
+
description: description,
|
110
|
+
param_modes: param_modes,
|
111
|
+
reducer: reducer,
|
112
|
+
structure_function: structure_function
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
107
116
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
117
|
+
#
|
118
|
+
# Queries
|
119
|
+
#
|
120
|
+
def function?(name)
|
121
|
+
@functions.key?(name)
|
122
|
+
end
|
123
|
+
alias supported? function?
|
112
124
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end
|
125
|
+
def operator?(name)
|
126
|
+
name.is_a?(Symbol) && function?(name) && CORE_OPERATORS.include?(name)
|
127
|
+
end
|
117
128
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
129
|
+
def entry(name)
|
130
|
+
@functions[name]
|
131
|
+
end
|
122
132
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
end
|
133
|
+
def fetch(name)
|
134
|
+
ent = entry(name)
|
135
|
+
raise Kumi::Errors::UnknownFunction, "Unknown function: #{name}" unless ent
|
127
136
|
|
128
|
-
|
129
|
-
|
130
|
-
end
|
137
|
+
ent.fn
|
138
|
+
end
|
131
139
|
|
132
|
-
|
133
|
-
|
134
|
-
|
140
|
+
def signature(name)
|
141
|
+
ent = entry(name) or raise Kumi::Errors::UnknownFunction, "Unknown function: #{name}"
|
142
|
+
{ arity: ent.arity, param_types: ent.param_types, return_type: ent.return_type, description: ent.description }
|
143
|
+
end
|
135
144
|
|
136
|
-
|
137
|
-
|
138
|
-
|
145
|
+
def reducer?(name)
|
146
|
+
ent = entry(name)
|
147
|
+
ent ? !!ent.reducer : false
|
148
|
+
end
|
139
149
|
|
140
|
-
|
141
|
-
|
142
|
-
|
150
|
+
def structure_function?(name)
|
151
|
+
ent = entry(name)
|
152
|
+
ent ? !!ent.structure_function : false
|
153
|
+
end
|
143
154
|
|
144
|
-
|
145
|
-
|
146
|
-
|
155
|
+
def all_functions
|
156
|
+
@functions.keys
|
157
|
+
end
|
158
|
+
alias all all_functions
|
147
159
|
|
148
|
-
|
149
|
-
|
150
|
-
|
160
|
+
def functions
|
161
|
+
@functions.dup
|
162
|
+
end
|
151
163
|
|
152
|
-
|
153
|
-
|
164
|
+
# Introspection helpers
|
165
|
+
def comparison_operators = ComparisonFunctions.definitions.keys
|
166
|
+
def math_operations = MathFunctions.definitions.keys
|
167
|
+
def string_operations = StringFunctions.definitions.keys
|
168
|
+
def logical_operations = LogicalFunctions.definitions.keys
|
169
|
+
def collection_operations = CollectionFunctions.definitions.keys
|
170
|
+
def conditional_operations = ConditionalFunctions.definitions.keys
|
171
|
+
def type_operations = TypeFunctions.definitions.keys
|
172
|
+
def stat_operations = StatFunctions.definitions.keys
|
154
173
|
end
|
155
174
|
end
|
156
175
|
end
|
157
176
|
end
|
158
|
-
# end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module IR
|
6
|
+
module ExecutionEngine
|
7
|
+
# Pure combinators for data transformation
|
8
|
+
module Combinators
|
9
|
+
# Broadcast scalar over vec (scalar→vec only)
|
10
|
+
# @param s [Hash] scalar value {:k => :scalar, :v => value}
|
11
|
+
# @param v [Hash] vector value {:k => :vec, :scope => [...], :rows => [...]}
|
12
|
+
# @return [Hash] broadcasted vector
|
13
|
+
def self.broadcast_scalar(s, v)
|
14
|
+
raise "First arg must be scalar" unless s[:k] == :scalar
|
15
|
+
raise "Second arg must be vec" unless v[:k] == :vec
|
16
|
+
|
17
|
+
rows = v[:rows].map do |r|
|
18
|
+
r.key?(:idx) ? { v: s[:v], idx: r[:idx] } : { v: s[:v] }
|
19
|
+
end
|
20
|
+
|
21
|
+
Values.vec(v[:scope], rows, v[:has_idx])
|
22
|
+
end
|
23
|
+
|
24
|
+
# Positional zip for same-scope vecs
|
25
|
+
# @param vecs [Array<Hash>] vectors to zip together
|
26
|
+
# @return [Hash] zipped vector
|
27
|
+
def self.zip_same_scope(*vecs)
|
28
|
+
raise "All arguments must be vecs" unless vecs.all? { |v| v[:k] == :vec }
|
29
|
+
raise "All vecs must have same scope" unless vecs.map { |v| v[:scope] }.uniq.size == 1
|
30
|
+
raise "All vecs must have same row count" unless vecs.map { |v| v[:rows].size }.uniq.size == 1
|
31
|
+
return vecs.first if vecs.length == 1
|
32
|
+
|
33
|
+
first_vec = vecs.first
|
34
|
+
zipped_rows = first_vec[:rows].zip(*vecs[1..].map { |v| v[:rows] }).map do |row_group|
|
35
|
+
combined_values = row_group.map { |r| r[:v] }
|
36
|
+
result_row = { v: combined_values }
|
37
|
+
result_row[:idx] = row_group.first[:idx] if row_group.first.key?(:idx)
|
38
|
+
result_row
|
39
|
+
end
|
40
|
+
|
41
|
+
Values.vec(first_vec[:scope], zipped_rows, first_vec[:has_idx])
|
42
|
+
end
|
43
|
+
|
44
|
+
# Prefix-index alignment for rank expansion/broadcasting
|
45
|
+
# @param tgt [Hash] target vector (defines output structure)
|
46
|
+
# @param src [Hash] source vector (values to align)
|
47
|
+
# @param to_scope [Array] target scope
|
48
|
+
# @param require_unique [Boolean] enforce unique prefixes
|
49
|
+
# @param on_missing [Symbol] :error or :nil policy
|
50
|
+
# @return [Hash] aligned vector
|
51
|
+
def self.align_to(tgt, src, to_scope:, require_unique: false, on_missing: :error)
|
52
|
+
raise "align_to expects vecs with indices" unless [tgt, src].all? { |v| v[:k] == :vec && v[:has_idx] }
|
53
|
+
|
54
|
+
to_rank = to_scope.length
|
55
|
+
src_rank = src[:rows].first[:idx].length
|
56
|
+
raise "scope not prefix-compatible: #{src_rank} > #{to_rank}" unless src_rank <= to_rank
|
57
|
+
|
58
|
+
# Build prefix->value hash
|
59
|
+
h = {}
|
60
|
+
src[:rows].each do |r|
|
61
|
+
k = r[:idx].first(src_rank)
|
62
|
+
raise "non-unique prefix for align_to: #{k.inspect}" if require_unique && h.key?(k)
|
63
|
+
|
64
|
+
h[k] = r[:v]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Map target rows through alignment
|
68
|
+
rows = tgt[:rows].map do |r|
|
69
|
+
k = r[:idx].first(src_rank)
|
70
|
+
if h.key?(k)
|
71
|
+
{ v: h[k], idx: r[:idx] }
|
72
|
+
else
|
73
|
+
case on_missing
|
74
|
+
when :nil then { v: nil, idx: r[:idx] }
|
75
|
+
when :error then raise "missing prefix #{k.inspect} in align_to"
|
76
|
+
else raise "unknown on_missing policy: #{on_missing}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
Values.vec(to_scope, rows, true)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Build hierarchical groups for lift operation
|
85
|
+
# @param rows [Array<Hash>] rows with indices
|
86
|
+
# @param depth [Integer] nesting depth
|
87
|
+
# @return [Array] nested array structure
|
88
|
+
# rows: [{ v: ..., idx: [i0,i1,...] }, ...] with lexicographically sorted :idx
|
89
|
+
def self.group_rows(rows, depth = 0)
|
90
|
+
return [] if rows.empty?
|
91
|
+
raise ArgumentError, "depth < 0" if depth < 0
|
92
|
+
|
93
|
+
if depth == 0
|
94
|
+
return rows.first[:v] if rows.first[:idx].nil? || rows.first[:idx].empty?
|
95
|
+
|
96
|
+
return rows.map { |r| r[:v] }
|
97
|
+
end
|
98
|
+
|
99
|
+
out = []
|
100
|
+
i = 0
|
101
|
+
n = rows.length
|
102
|
+
while i < n
|
103
|
+
head = rows[i][:idx].first
|
104
|
+
j = i + 1
|
105
|
+
j += 1 while j < n && rows[j][:idx].first == head
|
106
|
+
|
107
|
+
tail = rows[i...j].map { |r| { v: r[:v], idx: r[:idx][1..-1] } }
|
108
|
+
out << group_rows(tail, depth - 1)
|
109
|
+
i = j
|
110
|
+
end
|
111
|
+
out
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|