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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +7 -231
  4. data/README.md +1 -1
  5. data/docs/VECTOR_SEMANTICS.md +286 -0
  6. data/docs/features/hierarchical-broadcasting.md +1 -1
  7. data/docs/features/s-expression-printer.md +2 -2
  8. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  9. data/lib/kumi/analyzer.rb +34 -12
  10. data/lib/kumi/compiler.rb +2 -12
  11. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  13. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  14. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -101
  15. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  16. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  17. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  18. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  19. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
  20. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  21. data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
  22. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  23. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  24. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
  25. data/lib/kumi/core/analyzer/plans.rb +52 -0
  26. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  27. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  28. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  29. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  30. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  31. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  32. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  33. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  34. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  35. data/lib/kumi/core/compiler_base.rb +2 -2
  36. data/lib/kumi/core/error_reporter.rb +6 -5
  37. data/lib/kumi/core/errors.rb +4 -0
  38. data/lib/kumi/core/explain.rb +157 -205
  39. data/lib/kumi/core/export/node_builders.rb +2 -2
  40. data/lib/kumi/core/export/node_serializers.rb +1 -1
  41. data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
  42. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  43. data/lib/kumi/core/function_registry/function_builder.rb +142 -55
  44. data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
  45. data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
  46. data/lib/kumi/core/function_registry.rb +126 -108
  47. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  48. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  49. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  50. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  51. data/lib/kumi/core/ir.rb +58 -0
  52. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  53. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  54. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
  55. data/lib/kumi/core/ruby_parser/input_builder.rb +5 -5
  56. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  57. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  58. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  59. data/lib/kumi/registry.rb +14 -79
  60. data/lib/kumi/runtime/executable.rb +213 -0
  61. data/lib/kumi/schema.rb +14 -3
  62. data/lib/kumi/schema_metadata.rb +2 -2
  63. data/lib/kumi/support/ir_dump.rb +491 -0
  64. data/lib/kumi/support/s_expression_printer.rb +1 -1
  65. data/lib/kumi/syntax/location.rb +5 -0
  66. data/lib/kumi/syntax/node.rb +0 -1
  67. data/lib/kumi/syntax/root.rb +2 -2
  68. data/lib/kumi/version.rb +1 -1
  69. data/lib/kumi.rb +6 -15
  70. metadata +26 -15
  71. data/lib/kumi/core/cascade_executor_builder.rb +0 -132
  72. data/lib/kumi/core/compiled_schema.rb +0 -43
  73. data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
  74. data/lib/kumi/core/compiler/function_invoker.rb +0 -55
  75. data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
  76. data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
  77. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  78. data/lib/kumi/core/nested_structure_utils.rb +0 -78
  79. data/lib/kumi/core/schema_instance.rb +0 -115
  80. data/lib/kumi/core/vectorized_function_builder.rb +0 -88
  81. data/lib/kumi/js/compiler.rb +0 -878
  82. data/lib/kumi/js/function_registry.rb +0 -333
  83. 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
- Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, :reducer, :structure_function,
9
- keyword_init: true)
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
- def self.comparison(_name, description, operation)
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(operation, b) },
14
- arity: 2,
15
- param_types: %i[float float],
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, operation)
125
+ def self.equality(_name, description, op)
22
126
  Entry.new(
23
- fn: ->(a, b) { a.public_send(operation, b) },
24
- arity: 2,
25
- param_types: %i[any any],
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, operation, return_type: :float)
133
+ def self.math_binary(_name, description, op, return_type: :float)
32
134
  Entry.new(
33
- fn: lambda { |a, b|
34
- a.public_send(operation, b)
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, operation, return_type: :float)
141
+ def self.math_unary(_name, description, op, return_type: :float)
44
142
  Entry.new(
45
- fn: proc(&operation),
46
- arity: 1,
47
- param_types: [:float],
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, operation)
149
+ def self.string_unary(_name, description, op)
54
150
  Entry.new(
55
- fn: ->(str) { str.to_s.public_send(operation) },
56
- arity: 1,
57
- param_types: [:string],
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, operation, return_type: :string)
157
+ def self.string_binary(_name, description, op, return_type: :string)
64
158
  Entry.new(
65
- fn: ->(str, arg) { str.to_s.public_send(operation, arg.to_s) },
66
- arity: 2,
67
- param_types: %i[string string],
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, operation)
165
+ def self.logical_variadic(_name, description, op)
74
166
  Entry.new(
75
- fn: ->(conditions) { conditions.public_send(operation) },
76
- arity: -1,
77
- param_types: [:boolean],
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, operation, return_type: :boolean, reducer: false, structure_function: false)
173
+ def self.collection_unary(_name, description, op, return_type: :boolean, reducer: false, structure_function: false)
84
174
  Entry.new(
85
- fn: proc(&operation),
86
- arity: 1,
87
- param_types: [Kumi::Core::Types.array(:any)],
88
- return_type: return_type,
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
- # Element-wise AND for arrays with same nested structure
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 implementation
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 the complete function registry by combining all categories
11
+ # Build core functions once
15
12
  CORE_FUNCTIONS = {}.tap do |registry|
16
- function_modules = [
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
- function_modules.each do |module_definitions|
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
- @functions = CORE_FUNCTIONS.dup
39
- @frozen = false
31
+ @lock = Mutex.new
32
+ @functions = CORE_FUNCTIONS.transform_values(&:dup)
33
+ @frozen = false
40
34
 
41
- # class << self
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
- fn_lambda = block.is_a?(Proc) ? block : ->(*args) { yield(*args) }
47
- register_with_metadata(name, fn_lambda, arity: fn_lambda.arity, param_types: [:any], return_type: :any)
48
- end
49
-
50
- # Register with custom metadata
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
- # Auto-register functions from modules
67
- def auto_register(*modules)
68
- modules.each do |mod|
69
- mod.public_instance_methods(false).each do |method_name|
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
- register(method_name) do |*args|
73
- mod.new.public_send(method_name, *args)
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
- # Query interface
80
- def supported?(name)
81
- @functions.key?(name)
82
- end
83
-
84
- def operator?(name)
85
- return false unless name.is_a?(Symbol)
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
- @functions.key?(name) && CORE_OPERATORS.include?(name)
88
- end
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
- def fetch(name)
91
- @functions.fetch(name) { raise Kumi::Errors::UnknownFunction, "Unknown function: #{name}" }.fn
92
- end
72
+ def frozen?
73
+ @frozen
74
+ end
93
75
 
94
- def signature(name)
95
- entry = @functions.fetch(name) { raise Kumi::Errors::UnknownFunction, "Unknown function: #{name}" }
96
- {
97
- arity: entry.arity,
98
- param_types: entry.param_types,
99
- return_type: entry.return_type,
100
- description: entry.description
101
- }
102
- end
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
- def all_functions
105
- @functions.keys
106
- end
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
- def reducer?(name)
109
- entry = @functions.fetch(name) { return false }
110
- entry.reducer || false
111
- end
117
+ #
118
+ # Queries
119
+ #
120
+ def function?(name)
121
+ @functions.key?(name)
122
+ end
123
+ alias supported? function?
112
124
 
113
- def structure_function?(name)
114
- entry = @functions.fetch(name) { return false }
115
- entry.structure_function || false
116
- end
125
+ def operator?(name)
126
+ name.is_a?(Symbol) && function?(name) && CORE_OPERATORS.include?(name)
127
+ end
117
128
 
118
- # Alias for compatibility
119
- def all
120
- @functions.keys
121
- end
129
+ def entry(name)
130
+ @functions[name]
131
+ end
122
132
 
123
- # Category accessors for introspection
124
- def comparison_operators
125
- ComparisonFunctions.definitions.keys
126
- end
133
+ def fetch(name)
134
+ ent = entry(name)
135
+ raise Kumi::Errors::UnknownFunction, "Unknown function: #{name}" unless ent
127
136
 
128
- def math_operations
129
- MathFunctions.definitions.keys
130
- end
137
+ ent.fn
138
+ end
131
139
 
132
- def string_operations
133
- StringFunctions.definitions.keys
134
- end
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
- def logical_operations
137
- LogicalFunctions.definitions.keys
138
- end
145
+ def reducer?(name)
146
+ ent = entry(name)
147
+ ent ? !!ent.reducer : false
148
+ end
139
149
 
140
- def collection_operations
141
- CollectionFunctions.definitions.keys
142
- end
150
+ def structure_function?(name)
151
+ ent = entry(name)
152
+ ent ? !!ent.structure_function : false
153
+ end
143
154
 
144
- def conditional_operations
145
- ConditionalFunctions.definitions.keys
146
- end
155
+ def all_functions
156
+ @functions.keys
157
+ end
158
+ alias all all_functions
147
159
 
148
- def type_operations
149
- TypeFunctions.definitions.keys
150
- end
160
+ def functions
161
+ @functions.dup
162
+ end
151
163
 
152
- def stat_operations
153
- StatFunctions.definitions.keys
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