kumi 0.0.9 → 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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +18 -258
  4. data/README.md +188 -121
  5. data/docs/AST.md +1 -1
  6. data/docs/FUNCTIONS.md +52 -8
  7. data/docs/VECTOR_SEMANTICS.md +286 -0
  8. data/docs/compiler_design_principles.md +86 -0
  9. data/docs/features/README.md +15 -2
  10. data/docs/features/hierarchical-broadcasting.md +349 -0
  11. data/docs/features/javascript-transpiler.md +148 -0
  12. data/docs/features/performance.md +1 -3
  13. data/docs/features/s-expression-printer.md +2 -2
  14. data/docs/schema_metadata.md +7 -7
  15. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  16. data/examples/game_of_life.rb +2 -4
  17. data/lib/kumi/analyzer.rb +34 -14
  18. data/lib/kumi/compiler.rb +4 -283
  19. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
  20. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  21. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  22. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
  23. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  24. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  25. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  26. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  27. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  29. data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
  30. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  31. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  32. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
  33. data/lib/kumi/core/analyzer/plans.rb +52 -0
  34. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  35. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  36. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  37. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  38. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  39. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  40. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  41. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  42. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  43. data/lib/kumi/core/compiler_base.rb +137 -0
  44. data/lib/kumi/core/error_reporter.rb +6 -5
  45. data/lib/kumi/core/errors.rb +4 -0
  46. data/lib/kumi/core/explain.rb +157 -205
  47. data/lib/kumi/core/export/node_builders.rb +2 -2
  48. data/lib/kumi/core/export/node_serializers.rb +1 -1
  49. data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
  50. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  51. data/lib/kumi/core/function_registry/function_builder.rb +142 -53
  52. data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
  53. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  54. data/lib/kumi/core/function_registry.rb +138 -98
  55. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  56. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  57. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  58. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  59. data/lib/kumi/core/ir.rb +58 -0
  60. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  61. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  62. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  64. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  65. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  66. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  67. data/lib/kumi/errors.rb +2 -0
  68. data/lib/kumi/js.rb +23 -0
  69. data/lib/kumi/registry.rb +17 -22
  70. data/lib/kumi/runtime/executable.rb +213 -0
  71. data/lib/kumi/schema.rb +15 -4
  72. data/lib/kumi/schema_metadata.rb +2 -2
  73. data/lib/kumi/support/ir_dump.rb +491 -0
  74. data/lib/kumi/support/s_expression_printer.rb +17 -16
  75. data/lib/kumi/syntax/array_expression.rb +6 -6
  76. data/lib/kumi/syntax/call_expression.rb +4 -4
  77. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  78. data/lib/kumi/syntax/case_expression.rb +4 -4
  79. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  80. data/lib/kumi/syntax/hash_expression.rb +4 -4
  81. data/lib/kumi/syntax/input_declaration.rb +6 -5
  82. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  83. data/lib/kumi/syntax/input_reference.rb +5 -5
  84. data/lib/kumi/syntax/literal.rb +4 -4
  85. data/lib/kumi/syntax/location.rb +5 -0
  86. data/lib/kumi/syntax/node.rb +33 -34
  87. data/lib/kumi/syntax/root.rb +6 -6
  88. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  89. data/lib/kumi/syntax/value_declaration.rb +4 -4
  90. data/lib/kumi/version.rb +1 -1
  91. data/lib/kumi.rb +6 -15
  92. data/scripts/analyze_broadcast_methods.rb +68 -0
  93. data/scripts/analyze_cascade_methods.rb +74 -0
  94. data/scripts/check_broadcasting_coverage.rb +51 -0
  95. data/scripts/find_dead_code.rb +114 -0
  96. metadata +36 -9
  97. data/docs/features/array-broadcasting.md +0 -170
  98. data/lib/kumi/cli.rb +0 -449
  99. data/lib/kumi/core/compiled_schema.rb +0 -43
  100. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  101. data/lib/kumi/core/schema_instance.rb +0 -111
  102. data/lib/kumi/core/vectorization_metadata.rb +0 -110
  103. data/migrate_to_core_iterative.rb +0 -938
@@ -3,90 +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, 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
9
12
 
10
- 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)
11
118
  Entry.new(
12
- fn: ->(a, b) { a.public_send(operation, b) },
13
- arity: 2,
14
- param_types: %i[float float],
15
- return_type: :boolean,
16
- 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
17
122
  )
18
123
  end
19
124
 
20
- def self.equality(_name, description, operation)
125
+ def self.equality(_name, description, op)
21
126
  Entry.new(
22
- fn: ->(a, b) { a.public_send(operation, b) },
23
- arity: 2,
24
- param_types: %i[any any],
25
- return_type: :boolean,
26
- 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
27
130
  )
28
131
  end
29
132
 
30
- def self.math_binary(_name, description, operation, return_type: :float)
133
+ def self.math_binary(_name, description, op, return_type: :float)
31
134
  Entry.new(
32
- fn: lambda { |a, b|
33
- a.public_send(operation, b)
34
- },
35
- arity: 2,
36
- param_types: %i[float float],
37
- return_type: return_type,
38
- 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
39
138
  )
40
139
  end
41
140
 
42
- def self.math_unary(_name, description, operation, return_type: :float)
141
+ def self.math_unary(_name, description, op, return_type: :float)
43
142
  Entry.new(
44
- fn: proc(&operation),
45
- arity: 1,
46
- param_types: [:float],
47
- return_type: return_type,
48
- description: description
143
+ fn: proc(&op),
144
+ arity: 1, param_types: [:float],
145
+ return_type: return_type, description: description
49
146
  )
50
147
  end
51
148
 
52
- def self.string_unary(_name, description, operation)
149
+ def self.string_unary(_name, description, op)
53
150
  Entry.new(
54
- fn: ->(str) { str.to_s.public_send(operation) },
55
- arity: 1,
56
- param_types: [:string],
57
- return_type: :string,
58
- description: description
151
+ fn: ->(s) { s.to_s.public_send(op) },
152
+ arity: 1, param_types: [:string],
153
+ return_type: :string, description: description
59
154
  )
60
155
  end
61
156
 
62
- def self.string_binary(_name, description, operation, return_type: :string)
157
+ def self.string_binary(_name, description, op, return_type: :string)
63
158
  Entry.new(
64
- fn: ->(str, arg) { str.to_s.public_send(operation, arg.to_s) },
65
- arity: 2,
66
- param_types: %i[string string],
67
- return_type: return_type,
68
- 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
69
162
  )
70
163
  end
71
164
 
72
- def self.logical_variadic(_name, description, operation)
165
+ def self.logical_variadic(_name, description, op)
73
166
  Entry.new(
74
- fn: ->(conditions) { conditions.public_send(operation) },
75
- arity: -1,
76
- param_types: [:boolean],
77
- return_type: :boolean,
78
- description: description
167
+ fn: ->(*conds) { conds.flatten.public_send(op) },
168
+ arity: -1, param_types: [:boolean],
169
+ return_type: :boolean, description: description
79
170
  )
80
171
  end
81
172
 
82
- def self.collection_unary(_name, description, operation, return_type: :boolean, reducer: false)
173
+ def self.collection_unary(_name, description, op, return_type: :boolean, reducer: false, structure_function: false)
83
174
  Entry.new(
84
- fn: proc(&operation),
85
- arity: 1,
86
- param_types: [Kumi::Core::Types.array(:any)],
87
- return_type: return_type,
88
- description: description,
89
- reducer: reducer
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
90
179
  )
91
180
  end
92
181
  end
@@ -5,6 +5,146 @@ module Kumi
5
5
  module FunctionRegistry
6
6
  # Logical operations and boolean functions
7
7
  module LogicalFunctions
8
+ def self.element_wise_and(a, b)
9
+ if ENV["DEBUG_CASCADE"]
10
+ puts "DEBUG element_wise_and called with:"
11
+ puts " a: #{a.inspect} (depth: #{array_depth(a)})"
12
+ puts " b: #{b.inspect} (depth: #{array_depth(b)})"
13
+ end
14
+
15
+ case [a.class, b.class]
16
+ when [Array, Array]
17
+ # Both are arrays - handle hierarchical broadcasting
18
+ if hierarchical_broadcasting_needed?(a, b)
19
+ puts " -> Using hierarchical broadcasting" if ENV["DEBUG_CASCADE"]
20
+ result = perform_hierarchical_and(a, b)
21
+ puts " -> Hierarchical result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
22
+ else
23
+ # Same structure - use zip for element-wise operations
24
+ puts " -> Using same-structure zip" if ENV["DEBUG_CASCADE"]
25
+ result = a.zip(b).map { |elem_a, elem_b| element_wise_and(elem_a, elem_b) }
26
+ puts " -> Zip result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
27
+ end
28
+ result
29
+ when [Array, Object], [Object, Array]
30
+ # One is array, one is scalar - broadcast scalar
31
+ puts " -> Broadcasting scalar to array" if ENV["DEBUG_CASCADE"]
32
+ result = if a.is_a?(Array)
33
+ a.map { |elem| element_wise_and(elem, b) }
34
+ else
35
+ b.map { |elem| element_wise_and(a, elem) }
36
+ end
37
+ puts " -> Broadcast result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
38
+ result
39
+ else
40
+ # Both are scalars - simple AND
41
+ puts " -> Simple scalar AND: #{a} && #{b} = #{a && b}" if ENV["DEBUG_CASCADE"]
42
+ a && b
43
+ end
44
+ end
45
+
46
+ def self.hierarchical_broadcasting_needed?(a, b)
47
+ # Check if arrays have different nesting depths (hierarchical broadcasting)
48
+ depth_a = array_depth(a)
49
+ depth_b = array_depth(b)
50
+ depth_a != depth_b
51
+ end
52
+
53
+ def self.array_depth(arr)
54
+ return 0 unless arr.is_a?(Array)
55
+ return 1 if arr.empty? || !arr.first.is_a?(Array)
56
+
57
+ 1 + array_depth(arr.first)
58
+ end
59
+
60
+ def self.perform_hierarchical_and(a, b)
61
+ # Determine which is the higher dimension and which is lower
62
+ depth_a = array_depth(a)
63
+ depth_b = array_depth(b)
64
+
65
+ puts " perform_hierarchical_and: depth_a=#{depth_a}, depth_b=#{depth_b}" if ENV["DEBUG_CASCADE"]
66
+
67
+ if depth_a > depth_b
68
+ # a is deeper (child level), b is shallower (parent level)
69
+ # Broadcast b values to match a's structure - PRESERVE a's structure
70
+ puts " -> Broadcasting b (parent) to match a (child) structure" if ENV["DEBUG_CASCADE"]
71
+ broadcast_parent_to_child_structure(a, b)
72
+ else
73
+ # b is deeper (child level), a is shallower (parent level)
74
+ # Broadcast a values to match b's structure - PRESERVE b's structure
75
+ puts " -> Broadcasting a (parent) to match b (child) structure" if ENV["DEBUG_CASCADE"]
76
+ broadcast_parent_to_child_structure(b, a)
77
+ end
78
+ end
79
+
80
+ def self.broadcast_parent_to_child_structure(child_array, parent_array)
81
+ # Broadcast parent array values to match child array structure, preserving child structure
82
+ if ENV["DEBUG_CASCADE"]
83
+ puts " broadcast_parent_to_child_structure:"
84
+ puts " child_array: #{child_array.inspect}"
85
+ puts " parent_array: #{parent_array.inspect}"
86
+ puts " child depth: #{array_depth(child_array)}, parent depth: #{array_depth(parent_array)}"
87
+ end
88
+
89
+ # Use child array structure as template and broadcast parent values
90
+ map_with_parent_broadcasting(child_array, parent_array, [])
91
+ end
92
+
93
+ def self.map_with_parent_broadcasting(child_structure, parent_structure, indices)
94
+ if child_structure.is_a?(Array)
95
+ child_structure.map.with_index do |child_elem, index|
96
+ new_indices = indices + [index]
97
+
98
+ # Navigate parent structure with fewer indices (broadcasting)
99
+ parent_depth = array_depth(parent_structure)
100
+ parent_indices = new_indices[0, parent_depth]
101
+ parent_value = navigate_indices(parent_structure, parent_indices)
102
+
103
+ if child_elem.is_a?(Array)
104
+ # Recurse deeper into child structure
105
+ map_with_parent_broadcasting(child_elem, parent_structure, new_indices)
106
+ else
107
+ # Leaf level - apply AND operation
108
+ result = child_elem && parent_value
109
+ if ENV["DEBUG_CASCADE"]
110
+ puts " Leaf: child=#{child_elem}, parent=#{parent_value} (indices #{new_indices.inspect}) -> #{result}"
111
+ end
112
+ result
113
+ end
114
+ end
115
+ else
116
+ # Non-array child - just AND with parent
117
+ child_structure && parent_structure
118
+ end
119
+ end
120
+
121
+ def self.navigate_indices(structure, indices)
122
+ return structure if indices.empty?
123
+ return structure unless structure.is_a?(Array)
124
+ return nil if indices.first >= structure.length
125
+
126
+ navigate_indices(structure[indices.first], indices[1..])
127
+ end
128
+
129
+ def self.broadcast_to_match_structure(child_array, parent_array)
130
+ # Legacy method - keeping for backward compatibility
131
+ if ENV["DEBUG_CASCADE"]
132
+ puts " broadcast_to_match_structure (LEGACY):"
133
+ puts " child_array: #{child_array.inspect}"
134
+ puts " parent_array: #{parent_array.inspect}"
135
+ puts " child_array.length: #{child_array.length}"
136
+ puts " parent_array.length: #{parent_array.length}"
137
+ end
138
+
139
+ result = child_array.zip(parent_array).map do |child_elem, parent_elem|
140
+ puts " Combining child_elem: #{child_elem.inspect} with parent_elem: #{parent_elem.inspect}" if ENV["DEBUG_CASCADE"]
141
+ element_wise_and(child_elem, parent_elem)
142
+ end
143
+
144
+ puts " broadcast result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
145
+ result
146
+ end
147
+
8
148
  def self.definitions
9
149
  {
10
150
  # Basic logical operations
@@ -33,9 +173,39 @@ module Kumi
33
173
  ),
34
174
 
35
175
  # Collection logical operations
36
- all?: FunctionBuilder.collection_unary(:all?, "Check if all elements in collection are truthy", :all?),
37
- any?: FunctionBuilder.collection_unary(:any?, "Check if any element in collection is truthy", :any?),
38
- 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
+
180
+ # Element-wise AND for cascades - works on arrays with same structure
181
+ cascade_and: FunctionBuilder::Entry.new(
182
+ fn: lambda do |*conditions|
183
+ if ENV["DEBUG_CASCADE"]
184
+ puts "DEBUG cascade_and called with #{conditions.length} conditions:"
185
+ conditions.each_with_index do |cond, i|
186
+ puts " condition[#{i}]: #{cond.inspect}"
187
+ end
188
+ end
189
+
190
+ return false if conditions.empty?
191
+
192
+ # Always process uniformly, even for single conditions
193
+ # This ensures DeclarationReferences are evaluated properly
194
+ result = conditions.first
195
+ conditions[1..].each_with_index do |condition, i|
196
+ puts " Combining result with condition[#{i + 1}]" if ENV["DEBUG_CASCADE"]
197
+ result = LogicalFunctions.element_wise_and(result, condition)
198
+ puts " Result after combining: #{result.inspect}" if ENV["DEBUG_CASCADE"]
199
+ end
200
+
201
+ puts " Final cascade_and result: #{result.inspect}" if ENV["DEBUG_CASCADE"]
202
+ result
203
+ end,
204
+ arity: -1,
205
+ param_types: [:boolean],
206
+ return_type: :boolean,
207
+ description: "Element-wise AND for arrays with same nested structure"
208
+ )
39
209
  }
40
210
  end
41
211
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module FunctionRegistry
6
+ module StatFunctions
7
+ def self.definitions
8
+ {
9
+ # Statistical Functions
10
+ avg: FunctionBuilder::Entry.new(
11
+ fn: ->(array) { array.empty? ? nil : array.sum.to_f / array.size },
12
+ arity: 1,
13
+ param_types: [Kumi::Core::Types.array(:float)],
14
+ return_type: :float,
15
+ description: "Calculate arithmetic mean (average) of numeric collection",
16
+ reducer: true
17
+ ),
18
+
19
+ mean: FunctionBuilder::Entry.new(
20
+ fn: ->(array) { array.empty? ? nil : array.sum.to_f / array.size },
21
+ arity: 1,
22
+ param_types: [Kumi::Core::Types.array(:float)],
23
+ return_type: :float,
24
+ description: "Calculate arithmetic mean (average) of numeric collection (alias for avg)",
25
+ reducer: true
26
+ ),
27
+
28
+ median: FunctionBuilder::Entry.new(
29
+ fn: lambda do |array|
30
+ sorted = array.sort
31
+ len = sorted.length
32
+ if len.odd?
33
+ sorted[len / 2]
34
+ else
35
+ (sorted[(len / 2) - 1] + sorted[len / 2]) / 2.0
36
+ end
37
+ end,
38
+ arity: 1,
39
+ param_types: [Kumi::Core::Types.array(:float)],
40
+ return_type: :float,
41
+ description: "Calculate median (middle value) of numeric collection",
42
+ reducer: true
43
+ ),
44
+
45
+ variance: FunctionBuilder::Entry.new(
46
+ fn: lambda do |array|
47
+ mean = array.sum.to_f / array.size
48
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
49
+ sum_of_squares / array.size
50
+ end,
51
+ arity: 1,
52
+ param_types: [Kumi::Core::Types.array(:float)],
53
+ return_type: :float,
54
+ description: "Calculate population variance of numeric collection",
55
+ reducer: true
56
+ ),
57
+
58
+ stdev: FunctionBuilder::Entry.new(
59
+ fn: lambda do |array|
60
+ mean = array.sum.to_f / array.size
61
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
62
+ variance = sum_of_squares / array.size
63
+ Math.sqrt(variance)
64
+ end,
65
+ arity: 1,
66
+ param_types: [Kumi::Core::Types.array(:float)],
67
+ return_type: :float,
68
+ description: "Calculate population standard deviation of numeric collection",
69
+ reducer: true
70
+ ),
71
+
72
+ sample_variance: FunctionBuilder::Entry.new(
73
+ fn: lambda do |array|
74
+ return 0.0 if array.size <= 1
75
+
76
+ mean = array.sum.to_f / array.size
77
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
78
+ sum_of_squares / (array.size - 1)
79
+ end,
80
+ arity: 1,
81
+ param_types: [Kumi::Core::Types.array(:float)],
82
+ return_type: :float,
83
+ description: "Calculate sample variance of numeric collection",
84
+ reducer: true
85
+ ),
86
+
87
+ sample_stdev: FunctionBuilder::Entry.new(
88
+ fn: lambda do |array|
89
+ return 0.0 if array.size <= 1
90
+
91
+ mean = array.sum.to_f / array.size
92
+ sum_of_squares = array.sum { |x| (x - mean)**2 }
93
+ variance = sum_of_squares / (array.size - 1)
94
+ Math.sqrt(variance)
95
+ end,
96
+ arity: 1,
97
+ param_types: [Kumi::Core::Types.array(:float)],
98
+ return_type: :float,
99
+ description: "Calculate sample standard deviation of numeric collection",
100
+ reducer: true
101
+ ),
102
+
103
+ # Convenience functions for flattened statistics
104
+ flat_size: FunctionBuilder::Entry.new(
105
+ fn: ->(nested_array) { nested_array.flatten.size },
106
+ arity: 1,
107
+ param_types: [Kumi::Core::Types.array(:any)],
108
+ return_type: :integer,
109
+ description: "Count total elements across all nested levels",
110
+ reducer: true
111
+ ),
112
+
113
+ flat_sum: FunctionBuilder::Entry.new(
114
+ fn: ->(nested_array) { nested_array.flatten.sum },
115
+ arity: 1,
116
+ param_types: [Kumi::Core::Types.array(:float)],
117
+ return_type: :float,
118
+ description: "Sum all numeric elements across all nested levels",
119
+ reducer: true
120
+ ),
121
+
122
+ flat_avg: FunctionBuilder::Entry.new(
123
+ fn: lambda do |nested_array|
124
+ flattened = nested_array.flatten
125
+ flattened.sum.to_f / flattened.size
126
+ end,
127
+ arity: 1,
128
+ param_types: [Kumi::Core::Types.array(:float)],
129
+ return_type: :float,
130
+ description: "Calculate average across all nested levels",
131
+ reducer: true
132
+ ),
133
+
134
+ flat_max: FunctionBuilder::Entry.new(
135
+ fn: ->(nested_array) { nested_array.flatten.max },
136
+ arity: 1,
137
+ param_types: [Kumi::Core::Types.array(:float)],
138
+ return_type: :float,
139
+ description: "Find maximum value across all nested levels",
140
+ reducer: true
141
+ ),
142
+
143
+ flat_min: FunctionBuilder::Entry.new(
144
+ fn: ->(nested_array) { nested_array.flatten.min },
145
+ arity: 1,
146
+ param_types: [Kumi::Core::Types.array(:float)],
147
+ return_type: :float,
148
+ description: "Find minimum value across all nested levels",
149
+ reducer: true
150
+ )
151
+ }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end