kumi 0.0.13 → 0.0.14

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/BACKLOG.md +34 -0
  4. data/CLAUDE.md +4 -6
  5. data/README.md +0 -18
  6. data/config/functions.yaml +352 -0
  7. data/docs/dev/analyzer-debug.md +52 -0
  8. data/docs/dev/parse-command.md +64 -0
  9. data/docs/functions/analyzer_integration.md +199 -0
  10. data/docs/functions/signatures.md +171 -0
  11. data/examples/hash_objects_demo.rb +138 -0
  12. data/golden/array_operations/schema.kumi +17 -0
  13. data/golden/cascade_logic/schema.kumi +16 -0
  14. data/golden/mixed_nesting/schema.kumi +42 -0
  15. data/golden/simple_math/schema.kumi +10 -0
  16. data/lib/kumi/analyzer.rb +72 -21
  17. data/lib/kumi/core/analyzer/checkpoint.rb +72 -0
  18. data/lib/kumi/core/analyzer/debug.rb +167 -0
  19. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +1 -3
  20. data/lib/kumi/core/analyzer/passes/function_signature_pass.rb +199 -0
  21. data/lib/kumi/core/analyzer/passes/load_input_cse.rb +120 -0
  22. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +72 -157
  23. data/lib/kumi/core/analyzer/passes/toposorter.rb +37 -1
  24. data/lib/kumi/core/analyzer/state_serde.rb +64 -0
  25. data/lib/kumi/core/analyzer/structs/access_plan.rb +12 -10
  26. data/lib/kumi/core/compiler/access_planner.rb +3 -2
  27. data/lib/kumi/core/function_registry/collection_functions.rb +3 -1
  28. data/lib/kumi/core/functions/dimension.rb +98 -0
  29. data/lib/kumi/core/functions/dtypes.rb +20 -0
  30. data/lib/kumi/core/functions/errors.rb +11 -0
  31. data/lib/kumi/core/functions/kernel_adapter.rb +45 -0
  32. data/lib/kumi/core/functions/loader.rb +119 -0
  33. data/lib/kumi/core/functions/registry_v2.rb +68 -0
  34. data/lib/kumi/core/functions/shape.rb +70 -0
  35. data/lib/kumi/core/functions/signature.rb +122 -0
  36. data/lib/kumi/core/functions/signature_parser.rb +86 -0
  37. data/lib/kumi/core/functions/signature_resolver.rb +272 -0
  38. data/lib/kumi/core/ir/execution_engine/interpreter.rb +98 -7
  39. data/lib/kumi/core/ir/execution_engine/profiler.rb +202 -0
  40. data/lib/kumi/dev/ir.rb +75 -0
  41. data/lib/kumi/dev/parse.rb +105 -0
  42. data/lib/kumi/dev/runner.rb +83 -0
  43. data/lib/kumi/frontends/ruby.rb +28 -0
  44. data/lib/kumi/frontends/text.rb +46 -0
  45. data/lib/kumi/frontends.rb +29 -0
  46. data/lib/kumi/kernels/ruby/aggregate_core.rb +105 -0
  47. data/lib/kumi/kernels/ruby/datetime_scalar.rb +21 -0
  48. data/lib/kumi/kernels/ruby/mask_scalar.rb +15 -0
  49. data/lib/kumi/kernels/ruby/scalar_core.rb +63 -0
  50. data/lib/kumi/kernels/ruby/string_scalar.rb +19 -0
  51. data/lib/kumi/kernels/ruby/vector_struct.rb +39 -0
  52. data/lib/kumi/runtime/executable.rb +57 -26
  53. data/lib/kumi/schema.rb +4 -4
  54. data/lib/kumi/support/diff.rb +22 -0
  55. data/lib/kumi/support/ir_render.rb +61 -0
  56. data/lib/kumi/version.rb +1 -1
  57. data/lib/kumi.rb +2 -0
  58. data/performance_results.txt +63 -0
  59. data/scripts/test_mixed_nesting_performance.rb +206 -0
  60. metadata +45 -5
  61. data/docs/features/javascript-transpiler.md +0 -148
  62. data/lib/kumi/js.rb +0 -23
  63. data/lib/kumi/support/ir_dump.rb +0 -491
data/lib/kumi/analyzer.rb CHANGED
@@ -15,11 +15,13 @@ module Kumi
15
15
  Core::Analyzer::Passes::BroadcastDetector, # 9. Detects which operations should be broadcast over arrays.
16
16
  Core::Analyzer::Passes::TypeInferencerPass, # 10. Infers types for all declarations (uses vectorization metadata).
17
17
  Core::Analyzer::Passes::TypeConsistencyChecker, # 11. Validates declared vs inferred type consistency.
18
- Core::Analyzer::Passes::TypeChecker, # 12. Validates types using inferred information.
19
- Core::Analyzer::Passes::InputAccessPlannerPass, # 13. Plans access strategies for input fields.
20
- Core::Analyzer::Passes::ScopeResolutionPass, # 14. Plans execution scope and lifting needs for declarations.
21
- Core::Analyzer::Passes::JoinReducePlanningPass, # 15. Plans join/reduce operations (Generates IR Structs)
22
- Core::Analyzer::Passes::LowerToIRPass # 16. Lowers the schema to IR (Generates IR Structs)
18
+ Core::Analyzer::Passes::FunctionSignaturePass, # 12. Resolves NEP-20 signatures for function calls.
19
+ Core::Analyzer::Passes::TypeChecker, # 13. Validates types using inferred information.
20
+ Core::Analyzer::Passes::InputAccessPlannerPass, # 14. Plans access strategies for input fields.
21
+ Core::Analyzer::Passes::ScopeResolutionPass, # 15. Plans execution scope and lifting needs for declarations.
22
+ Core::Analyzer::Passes::JoinReducePlanningPass, # 16. Plans join/reduce operations (Generates IR Structs)
23
+ Core::Analyzer::Passes::LowerToIRPass, # 17. Lowers the schema to IR (Generates IR Structs)
24
+ Core::Analyzer::Passes::LoadInputCSE # 18. Eliminates redundant load_input operations
23
25
  ].freeze
24
26
 
25
27
  def self.analyze!(schema, passes: DEFAULT_PASSES, **opts)
@@ -32,33 +34,91 @@ module Kumi
32
34
  end
33
35
 
34
36
  def self.run_analysis_passes(schema, passes, state, errors)
35
- passes.each do |pass_class|
37
+ # Resume from a saved state if configured
38
+ state = Core::Analyzer::Checkpoint.load_initial_state(state)
39
+
40
+ debug_on = Core::Analyzer::Debug.enabled?
41
+ resume_at = Core::Analyzer::Checkpoint.resume_at
42
+ stop_after = Core::Analyzer::Checkpoint.stop_after
43
+ skipping = !!resume_at
44
+
45
+ passes.each_with_index do |pass_class, idx|
46
+ pass_name = pass_class.name.split("::").last
47
+
48
+ if skipping
49
+ skipping = false if pass_name == resume_at
50
+ next if skipping
51
+ end
52
+
53
+ Core::Analyzer::Checkpoint.entering(pass_name:, idx:, state:)
54
+
55
+ before = state.to_h if debug_on
56
+ Core::Analyzer::Debug.reset_log(pass: pass_name) if debug_on
57
+
58
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
59
  pass_instance = pass_class.new(schema, state)
37
60
  begin
38
61
  state = pass_instance.run(errors)
39
62
  rescue StandardError => e
40
63
  # TODO: - GREATLY improve this, need to capture the context of the error
41
64
  # and the pass that failed and line number if relevant
42
- pass_name = pass_class.name.split("::").last
43
65
  message = "Error in Analysis Pass(#{pass_name}): #{e.message}"
44
66
  errors << Core::ErrorReporter.create_error(message, location: nil, type: :semantic, backtrace: e.backtrace)
45
67
 
68
+ if debug_on
69
+ logs = Core::Analyzer::Debug.drain_log
70
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
71
+
72
+ Core::Analyzer::Debug.emit(
73
+ pass: pass_name,
74
+ diff: {},
75
+ elapsed_ms: elapsed_ms,
76
+ logs: logs + [{ level: :error, id: :exception, message: e.message, error_class: e.class.name }]
77
+ )
78
+ end
79
+
46
80
  raise
47
81
  end
82
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
83
+
84
+ if debug_on
85
+ after = state.to_h
86
+
87
+ # Optional immutability guard
88
+ if ENV["KUMI_DEBUG_REQUIRE_FROZEN"] == "1"
89
+ (after || {}).each do |k, v|
90
+ if v.nil? || v.is_a?(Numeric) || v.is_a?(Symbol) || v.is_a?(TrueClass) || v.is_a?(FalseClass) || (v.is_a?(String) && v.frozen?)
91
+ next
92
+ end
93
+ raise "State[#{k}] not frozen: #{v.class}" unless v.frozen?
94
+ end
95
+ end
96
+
97
+ diff = Core::Analyzer::Debug.diff_state(before, after)
98
+ logs = Core::Analyzer::Debug.drain_log
99
+
100
+ Core::Analyzer::Debug.emit(
101
+ pass: pass_name,
102
+ diff: diff,
103
+ elapsed_ms: elapsed_ms,
104
+ logs: logs
105
+ )
106
+ end
107
+
108
+ Core::Analyzer::Checkpoint.leaving(pass_name:, idx:, state:)
109
+
110
+ break if stop_after && pass_name == stop_after
48
111
  end
49
112
  state
50
113
  end
51
114
 
52
115
  def self.handle_analysis_errors(errors)
53
116
  type_errors = errors.select { |e| e.type == :type }
54
- semantic_errors = errors.select { |e| e.type == :semantic }
55
117
  first_error_location = errors.first.location
56
118
 
57
119
  raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
58
120
 
59
- raise Errors::SemanticError.new(format_errors(errors), first_error_location) if first_error_location || semantic_errors
60
-
61
- raise Errors::AnalysisError.new(format_errors(errors))
121
+ raise Errors::SemanticError.new(format_errors(errors), first_error_location)
62
122
  end
63
123
 
64
124
  def self.create_analysis_result(state)
@@ -76,16 +136,7 @@ module Kumi
76
136
  def self.format_errors(errors)
77
137
  return "" if errors.empty?
78
138
 
79
- backtrace = errors.first.backtrace
80
-
81
- message = errors.map(&:to_s).join("\n")
82
-
83
- message.tap do |msg|
84
- if backtrace && !backtrace.empty?
85
- msg << "\n\nBacktrace:\n"
86
- msg << backtrace[0..10].join("\n") # Limit to first 10 lines for readability
87
- end
88
- end
139
+ errors.map(&:to_s).join("\n")
89
140
  end
90
141
  end
91
142
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Kumi
6
+ module Core
7
+ module Analyzer
8
+ module Checkpoint
9
+ class << self
10
+ # ===== Interface shape matches Debug =====
11
+ def enabled?
12
+ ENV["KUMI_CHECKPOINT"] == "1" ||
13
+ !resume_from.nil? || !resume_at.nil? || !stop_after.nil?
14
+ end
15
+
16
+ # ---- Config (ENV) ----
17
+ def dir = ENV["KUMI_CHECKPOINT_DIR"] || "tmp/analysis_snapshots"
18
+ def phases = (ENV["KUMI_CHECKPOINT_PHASE"] || "before,after").split(",").map! { _1.strip.downcase.to_sym }
19
+ def formats = (ENV["KUMI_CHECKPOINT_FORMAT"] || "marshal").split(",").map! { _1.strip.downcase } # marshal|json|both
20
+ def resume_from = ENV["KUMI_RESUME_FROM"] # file path (.msh or .json)
21
+ def resume_at = ENV["KUMI_RESUME_AT"] # pass short name
22
+ def stop_after = ENV["KUMI_STOP_AFTER"] # pass short name
23
+
24
+ # ===== Lifecycle (called by analyzer) =====
25
+ def load_initial_state(default_state)
26
+ path = resume_from
27
+ return default_state unless path && File.exist?(path)
28
+ data = File.binread(path)
29
+ path.end_with?(".msh") ? StateSerde.load_marshal(data)
30
+ : StateSerde.load_json(data)
31
+ end
32
+
33
+ def entering(pass_name:, idx:, state:)
34
+ return unless enabled?
35
+ snapshot(pass_name:, idx:, phase: :before, state:) if phases.include?(:before)
36
+ end
37
+
38
+ def leaving(pass_name:, idx:, state:)
39
+ return unless enabled?
40
+ snapshot(pass_name:, idx:, phase: :after, state:) if phases.include?(:after)
41
+ end
42
+
43
+ # ===== Implementation =====
44
+ def snapshot(pass_name:, idx:, phase:, state:)
45
+ FileUtils.mkdir_p(dir)
46
+ base = File.join(dir, "%03d_#{pass_name}_#{phase}" % idx)
47
+ files = []
48
+
49
+ if formats.include?("marshal") || formats.include?("both")
50
+ path = "#{base}.msh"
51
+ File.binwrite(path, StateSerde.dump_marshal(state))
52
+ files << path
53
+ end
54
+
55
+ if formats.include?("json") || formats.include?("both")
56
+ path = "#{base}.json"
57
+ File.write(path, StateSerde.dump_json(state, pretty: true))
58
+ files << path
59
+ end
60
+
61
+ # Fold checkpoint info into the same per-pass logs the Debugger uses.
62
+ if Core::Analyzer::Debug.enabled?
63
+ Core::Analyzer::Debug.info(:checkpoint, phase:, idx:, files:)
64
+ end
65
+
66
+ files
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Kumi
7
+ module Core
8
+ module Analyzer
9
+ module Debug
10
+ KEY = :kumi_debug_log
11
+
12
+ class << self
13
+ def enabled?
14
+ ENV["KUMI_DEBUG_STATE"] == "1"
15
+ end
16
+
17
+ def output_path
18
+ ENV["KUMI_DEBUG_OUTPUT_PATH"]
19
+ end
20
+
21
+ def max_depth
22
+ (ENV["KUMI_DEBUG_MAX_DEPTH"] || "5").to_i
23
+ end
24
+
25
+ def max_items
26
+ (ENV["KUMI_DEBUG_MAX_ITEMS"] || "100").to_i
27
+ end
28
+
29
+ # Log buffer management
30
+ def reset_log(pass:)
31
+ Thread.current[KEY] = { pass: pass, events: [] }
32
+ end
33
+
34
+ def drain_log
35
+ stash = Thread.current[KEY]
36
+ Thread.current[KEY] = nil
37
+ (stash && stash[:events]) || []
38
+ end
39
+
40
+ def log(level:, id:, method: nil, **fields)
41
+ buf = Thread.current[KEY]
42
+ return unless buf
43
+
44
+ loc = caller_locations(1, 1)&.first
45
+ meth = method || loc&.base_label
46
+ buf[:events] << {
47
+ ts: Time.now.utc.iso8601,
48
+ pass: buf[:pass],
49
+ level: level,
50
+ id: id,
51
+ method: meth,
52
+ file: loc&.path,
53
+ line: loc&.lineno,
54
+ **fields
55
+ }
56
+ end
57
+
58
+ def info(id, **fields)
59
+ log(level: :info, id: id, **fields)
60
+ end
61
+
62
+ def debug(id, **fields)
63
+ log(level: :debug, id: id, **fields)
64
+ end
65
+
66
+ def trace(id, **start_fields)
67
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
68
+ info("#{id}_start".to_sym, **start_fields)
69
+ yield.tap do |ret|
70
+ dt = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
71
+ info("#{id}_finish".to_sym, ms: dt)
72
+ ret
73
+ end
74
+ rescue => e
75
+ log(level: :error, id: "#{id}_error".to_sym, error: e.class.name, message: e.message)
76
+ raise
77
+ end
78
+
79
+ # State diffing
80
+ def diff_state(before, after)
81
+ changes = {}
82
+
83
+ all_keys = (before.keys + after.keys).uniq
84
+ all_keys.each do |key|
85
+ if !before.key?(key)
86
+ changes[key] = { type: :added, value: truncate(after[key]) }
87
+ elsif !after.key?(key)
88
+ changes[key] = { type: :removed, value: truncate(before[key]) }
89
+ elsif before[key] != after[key]
90
+ changes[key] = {
91
+ type: :changed,
92
+ before: truncate(before[key]),
93
+ after: truncate(after[key])
94
+ }
95
+ end
96
+ end
97
+
98
+ changes
99
+ end
100
+
101
+ # Emit debug event
102
+ def emit(pass:, diff:, elapsed_ms:, logs:)
103
+ payload = {
104
+ ts: Time.now.utc.iso8601,
105
+ pass: pass,
106
+ elapsed_ms: elapsed_ms,
107
+ diff: diff,
108
+ logs: logs
109
+ }
110
+
111
+ if output_path && !output_path.empty?
112
+ File.open(output_path, "a") { |f| f.puts(JSON.dump(payload)) }
113
+ else
114
+ $stdout.puts "\n=== STATE #{pass} (#{elapsed_ms}ms) ==="
115
+ $stdout.puts JSON.pretty_generate(payload)
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def truncate(value, depth = max_depth)
122
+ return value if depth <= 0
123
+
124
+ case value
125
+ when Hash
126
+ if value.size > max_items
127
+ truncated = value.first(max_items).to_h
128
+ truncated[:__truncated__] = "... #{value.size - max_items} more"
129
+ truncated.transform_values { |v| truncate(v, depth - 1) }
130
+ else
131
+ value.transform_values { |v| truncate(v, depth - 1) }
132
+ end
133
+ when Array
134
+ if value.size > max_items
135
+ truncated = value.first(max_items).dup
136
+ truncated << "... #{value.size - max_items} more"
137
+ truncated.map { |v| truncate(v, depth - 1) }
138
+ else
139
+ value.map { |v| truncate(v, depth - 1) }
140
+ end
141
+ when String
142
+ value
143
+ else
144
+ value
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ # Mixin for passes
151
+ module Loggable
152
+ def log_info(id, **fields)
153
+ Debug.info(id, method: __method__, **fields)
154
+ end
155
+
156
+ def log_debug(id, **fields)
157
+ Debug.debug(id, method: __method__, **fields)
158
+ end
159
+
160
+ def trace(id, **fields, &block)
161
+ Debug.trace(id, **fields, &block)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -40,9 +40,7 @@ module Kumi
40
40
  result = analyze_value_vectorization(name, decl.expression, array_fields, nested_paths, vectorized_values, errors,
41
41
  definitions)
42
42
 
43
- if ENV["DEBUG_BROADCAST_CLEAN"]
44
- puts "#{name}: #{result[:type]} #{format_broadcast_info(result)}"
45
- end
43
+ puts "#{name}: #{result[:type]} #{format_broadcast_info(result)}" if ENV["DEBUG_BROADCAST_CLEAN"]
46
44
 
47
45
  case result[:type]
48
46
  when :vectorized
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Apply NEP-20 signature resolution to function calls
8
+ # DEPENDENCIES: :node_index from Toposorter, :broadcast_metadata (optional)
9
+ # PRODUCES: Signature metadata in node_index for CallExpression nodes
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ class FunctionSignaturePass < PassBase
12
+ def run(errors)
13
+ node_index = get_state(:node_index, required: true)
14
+
15
+ # Process all CallExpression nodes in the index
16
+ node_index.each do |object_id, entry|
17
+ next unless entry[:type] == "CallExpression"
18
+
19
+ resolve_function_signature(entry, object_id, errors)
20
+ end
21
+
22
+ state # Node index is modified in-place
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_function_signature(entry, object_id, errors)
28
+ node = entry[:node]
29
+
30
+ # 1) Gather candidate signatures from current registry
31
+ sig_strings = get_function_signatures(node)
32
+ return if sig_strings.empty?
33
+
34
+ begin
35
+ sigs = parse_signatures(sig_strings)
36
+ rescue Kumi::Core::Functions::SignatureError => e
37
+ report_error(errors, "Invalid signature for function `#{node.fn_name}`: #{e.message}",
38
+ location: node.loc, type: :type)
39
+ return
40
+ end
41
+
42
+ # 2) Build arg_shapes from current node context
43
+ arg_shapes = build_argument_shapes(node, object_id)
44
+
45
+ # 3) Resolve signature
46
+ begin
47
+ plan = Kumi::Core::Functions::SignatureResolver.choose(signatures: sigs, arg_shapes: arg_shapes)
48
+ rescue Kumi::Core::Functions::SignatureMatchError => e
49
+ report_error(errors,
50
+ "Signature mismatch for `#{node.fn_name}` with args #{format_shapes(arg_shapes)}. Candidates: #{format_sigs(sig_strings)}. #{e.message}",
51
+ location: node.loc, type: :type)
52
+ return
53
+ end
54
+
55
+ # 4) Attach metadata to node index entry
56
+ attach_signature_metadata(entry, plan)
57
+ end
58
+
59
+ def get_function_signatures(node)
60
+ # Use RegistryV2 if enabled, otherwise fall back to legacy registry
61
+ if registry_v2_enabled?
62
+ registry_v2_signatures(node)
63
+ else
64
+ legacy_registry_signatures(node)
65
+ end
66
+ end
67
+
68
+ def registry_v2_signatures(node)
69
+ registry_v2.get_function_signatures(node.fn_name)
70
+ rescue => e
71
+ # If RegistryV2 fails, fall back to legacy
72
+ legacy_registry_signatures(node)
73
+ end
74
+
75
+ def legacy_registry_signatures(node)
76
+ # Try to get signatures from the current registry
77
+ # For now, we'll create basic signatures from the current registry format
78
+
79
+ meta = Kumi::Registry.signature(node.fn_name)
80
+
81
+ # Check if the function already has NEP-20 signatures
82
+ return meta[:signatures] if meta[:signatures] && meta[:signatures].is_a?(Array)
83
+
84
+ # Otherwise, create a basic signature from arity
85
+ # This is a bridge until we have full NEP-20 signatures in the registry
86
+ create_basic_signature(meta[:arity])
87
+ rescue Kumi::Errors::UnknownFunction
88
+ # For now, return empty array - function existence will be caught by TypeChecker
89
+ []
90
+ end
91
+
92
+ def create_basic_signature(arity)
93
+ return [] if arity.nil? || arity < 0 # Variable arity - skip for now
94
+
95
+ case arity
96
+ when 0
97
+ ["()->()"] # Scalar function
98
+ when 1
99
+ ["()->()", "(i)->(i)"] # Scalar or element-wise
100
+ when 2
101
+ ["(),()->()", "(i),(i)->(i)"] # Scalar or element-wise binary
102
+ else
103
+ # For higher arity, just provide scalar signature
104
+ args = (["()"] * arity).join(",")
105
+ ["#{args}->()"]
106
+ end
107
+ end
108
+
109
+ def build_argument_shapes(node, object_id)
110
+ # Build argument shapes from current analysis context
111
+ node.args.map do |arg|
112
+ axes = get_broadcast_metadata(arg.object_id)
113
+ normalize_shape(axes)
114
+ end
115
+ end
116
+
117
+ def normalize_shape(axes)
118
+ case axes
119
+ when nil
120
+ [] # scalar
121
+ when Array
122
+ axes.map { |d| d.is_a?(Integer) ? d : d.to_sym }
123
+ else
124
+ [] # defensive fallback
125
+ end
126
+ end
127
+
128
+ def get_broadcast_metadata(arg_object_id)
129
+ # Try to get broadcast metadata from existing analysis state
130
+ broadcast_meta = get_state(:broadcast_metadata, required: false)
131
+ return nil unless broadcast_meta
132
+
133
+ # Look up by node object_id
134
+ broadcast_meta[arg_object_id]&.dig(:axes)
135
+ end
136
+
137
+ def parse_signatures(sig_strings)
138
+ @sig_cache ||= {}
139
+ sig_strings.map do |s|
140
+ @sig_cache[s] ||= Kumi::Core::Functions::SignatureParser.parse(s)
141
+ end
142
+ end
143
+
144
+ def format_shapes(shapes)
145
+ shapes.map { |ax| "(#{ax.join(',')})" }.join(', ')
146
+ end
147
+
148
+ def format_sigs(sig_strings)
149
+ sig_strings.join(" | ")
150
+ end
151
+
152
+ def attach_signature_metadata(entry, plan)
153
+ # Attach signature resolution results to the node index entry
154
+ # This way other passes can access the metadata via the node index
155
+ metadata = entry[:metadata]
156
+
157
+ attach_core_signature_data(metadata, plan)
158
+ attach_shape_contract(metadata, plan)
159
+ end
160
+
161
+ def attach_core_signature_data(metadata, plan)
162
+ metadata[:signature] = plan[:signature]
163
+ metadata[:result_axes] = plan[:result_axes] # e.g., [:i, :j]
164
+ metadata[:join_policy] = plan[:join_policy] # nil | :zip | :product
165
+ metadata[:dropped_axes] = plan[:dropped_axes] # e.g., [:j] for reductions
166
+ metadata[:effective_signature] = plan[:effective_signature] # Normalized for lowering
167
+ metadata[:dim_env] = plan[:env] # Dimension bindings (for matmul)
168
+ metadata[:signature_score] = plan[:score] # Match quality
169
+ end
170
+
171
+ def attach_shape_contract(metadata, plan)
172
+ # Attach shape contract for lowering convenience
173
+ metadata[:shape_contract] = {
174
+ in: plan[:effective_signature][:in_shapes],
175
+ out: plan[:effective_signature][:out_shape],
176
+ join: plan[:effective_signature][:join_policy]
177
+ }
178
+ end
179
+
180
+ def registry_v2_enabled?
181
+ ENV["KUMI_FN_REGISTRY_V2"] == "1"
182
+ end
183
+
184
+ def registry_v2
185
+ @registry_v2 ||= Kumi::Core::Functions::RegistryV2.load_from_file
186
+ end
187
+
188
+ def nep20_flex_enabled?
189
+ ENV["KUMI_ENABLE_FLEX"] == "1"
190
+ end
191
+
192
+ def nep20_bcast1_enabled?
193
+ ENV["KUMI_ENABLE_BCAST1"] == "1"
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end