kumi 0.0.13 → 0.0.15

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/BACKLOG.md +34 -0
  4. data/CHANGELOG.md +33 -0
  5. data/CLAUDE.md +4 -6
  6. data/README.md +0 -45
  7. data/config/functions.yaml +352 -0
  8. data/docs/dev/analyzer-debug.md +52 -0
  9. data/docs/dev/parse-command.md +64 -0
  10. data/docs/dev/vm-profiling.md +95 -0
  11. data/docs/features/README.md +0 -7
  12. data/docs/functions/analyzer_integration.md +199 -0
  13. data/docs/functions/signatures.md +171 -0
  14. data/examples/hash_objects_demo.rb +138 -0
  15. data/golden/array_operations/schema.kumi +17 -0
  16. data/golden/cascade_logic/schema.kumi +16 -0
  17. data/golden/mixed_nesting/schema.kumi +42 -0
  18. data/golden/simple_math/schema.kumi +10 -0
  19. data/lib/kumi/analyzer.rb +76 -22
  20. data/lib/kumi/compiler.rb +6 -5
  21. data/lib/kumi/core/analyzer/checkpoint.rb +72 -0
  22. data/lib/kumi/core/analyzer/debug.rb +167 -0
  23. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +1 -3
  24. data/lib/kumi/core/analyzer/passes/function_signature_pass.rb +199 -0
  25. data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +67 -0
  26. data/lib/kumi/core/analyzer/passes/load_input_cse.rb +120 -0
  27. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +72 -157
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +40 -36
  29. data/lib/kumi/core/analyzer/state_serde.rb +64 -0
  30. data/lib/kumi/core/analyzer/structs/access_plan.rb +12 -10
  31. data/lib/kumi/core/compiler/access_planner.rb +3 -2
  32. data/lib/kumi/core/function_registry/collection_functions.rb +3 -1
  33. data/lib/kumi/core/functions/dimension.rb +98 -0
  34. data/lib/kumi/core/functions/dtypes.rb +20 -0
  35. data/lib/kumi/core/functions/errors.rb +11 -0
  36. data/lib/kumi/core/functions/kernel_adapter.rb +45 -0
  37. data/lib/kumi/core/functions/loader.rb +119 -0
  38. data/lib/kumi/core/functions/registry_v2.rb +68 -0
  39. data/lib/kumi/core/functions/shape.rb +70 -0
  40. data/lib/kumi/core/functions/signature.rb +122 -0
  41. data/lib/kumi/core/functions/signature_parser.rb +86 -0
  42. data/lib/kumi/core/functions/signature_resolver.rb +272 -0
  43. data/lib/kumi/core/ir/execution_engine/interpreter.rb +110 -7
  44. data/lib/kumi/core/ir/execution_engine/profiler.rb +330 -0
  45. data/lib/kumi/core/ir/execution_engine.rb +6 -15
  46. data/lib/kumi/dev/ir.rb +75 -0
  47. data/lib/kumi/dev/parse.rb +105 -0
  48. data/lib/kumi/dev/profile_aggregator.rb +301 -0
  49. data/lib/kumi/dev/profile_runner.rb +199 -0
  50. data/lib/kumi/dev/runner.rb +85 -0
  51. data/lib/kumi/dev.rb +14 -0
  52. data/lib/kumi/frontends/ruby.rb +28 -0
  53. data/lib/kumi/frontends/text.rb +46 -0
  54. data/lib/kumi/frontends.rb +29 -0
  55. data/lib/kumi/kernels/ruby/aggregate_core.rb +105 -0
  56. data/lib/kumi/kernels/ruby/datetime_scalar.rb +21 -0
  57. data/lib/kumi/kernels/ruby/mask_scalar.rb +15 -0
  58. data/lib/kumi/kernels/ruby/scalar_core.rb +63 -0
  59. data/lib/kumi/kernels/ruby/string_scalar.rb +19 -0
  60. data/lib/kumi/kernels/ruby/vector_struct.rb +39 -0
  61. data/lib/kumi/runtime/executable.rb +108 -45
  62. data/lib/kumi/schema.rb +12 -6
  63. data/lib/kumi/support/diff.rb +22 -0
  64. data/lib/kumi/support/ir_render.rb +61 -0
  65. data/lib/kumi/version.rb +1 -1
  66. data/lib/kumi.rb +3 -0
  67. data/performance_results.txt +63 -0
  68. data/scripts/test_mixed_nesting_performance.rb +206 -0
  69. metadata +50 -6
  70. data/docs/features/analysis-cascade-mutual-exclusion.md +0 -89
  71. data/docs/features/javascript-transpiler.md +0 -148
  72. data/lib/kumi/js.rb +0 -23
  73. data/lib/kumi/support/ir_dump.rb +0 -491
@@ -0,0 +1,10 @@
1
+ schema do
2
+ input do
3
+ integer :x
4
+ integer :y
5
+ end
6
+
7
+ value :sum, input.x**input.y
8
+ value :product, input.x * input.y
9
+ value :difference, input.x - input.y
10
+ end
data/lib/kumi/analyzer.rb CHANGED
@@ -15,11 +15,14 @@ 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
25
+ Core::Analyzer::Passes::IRDependencyPass # 19. Extracts IR-level dependencies for VM execution optimization
23
26
  ].freeze
24
27
 
25
28
  def self.analyze!(schema, passes: DEFAULT_PASSES, **opts)
@@ -32,33 +35,93 @@ module Kumi
32
35
  end
33
36
 
34
37
  def self.run_analysis_passes(schema, passes, state, errors)
35
- passes.each do |pass_class|
38
+ # Resume from a saved state if configured
39
+ state = Core::Analyzer::Checkpoint.load_initial_state(state)
40
+
41
+ debug_on = Core::Analyzer::Debug.enabled?
42
+ resume_at = Core::Analyzer::Checkpoint.resume_at
43
+ stop_after = Core::Analyzer::Checkpoint.stop_after
44
+ skipping = !!resume_at
45
+
46
+ passes.each_with_index do |pass_class, idx|
47
+ pass_name = pass_class.name.split("::").last
48
+
49
+ if skipping
50
+ skipping = false if pass_name == resume_at
51
+ next if skipping
52
+ end
53
+
54
+ Core::Analyzer::Checkpoint.entering(pass_name:, idx:, state:)
55
+
56
+ before = state.to_h if debug_on
57
+ Core::Analyzer::Debug.reset_log(pass: pass_name) if debug_on
58
+
59
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
60
  pass_instance = pass_class.new(schema, state)
37
61
  begin
38
- state = pass_instance.run(errors)
62
+ state = Dev::Profiler.phase("analyzer.pass", pass: pass_name) do
63
+ pass_instance.run(errors)
64
+ end
39
65
  rescue StandardError => e
40
66
  # TODO: - GREATLY improve this, need to capture the context of the error
41
67
  # and the pass that failed and line number if relevant
42
- pass_name = pass_class.name.split("::").last
43
68
  message = "Error in Analysis Pass(#{pass_name}): #{e.message}"
44
69
  errors << Core::ErrorReporter.create_error(message, location: nil, type: :semantic, backtrace: e.backtrace)
45
70
 
71
+ if debug_on
72
+ logs = Core::Analyzer::Debug.drain_log
73
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
74
+
75
+ Core::Analyzer::Debug.emit(
76
+ pass: pass_name,
77
+ diff: {},
78
+ elapsed_ms: elapsed_ms,
79
+ logs: logs + [{ level: :error, id: :exception, message: e.message, error_class: e.class.name }]
80
+ )
81
+ end
82
+
46
83
  raise
47
84
  end
85
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
86
+
87
+ if debug_on
88
+ after = state.to_h
89
+
90
+ # Optional immutability guard
91
+ if ENV["KUMI_DEBUG_REQUIRE_FROZEN"] == "1"
92
+ (after || {}).each do |k, v|
93
+ 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?)
94
+ next
95
+ end
96
+ raise "State[#{k}] not frozen: #{v.class}" unless v.frozen?
97
+ end
98
+ end
99
+
100
+ diff = Core::Analyzer::Debug.diff_state(before, after)
101
+ logs = Core::Analyzer::Debug.drain_log
102
+
103
+ Core::Analyzer::Debug.emit(
104
+ pass: pass_name,
105
+ diff: diff,
106
+ elapsed_ms: elapsed_ms,
107
+ logs: logs
108
+ )
109
+ end
110
+
111
+ Core::Analyzer::Checkpoint.leaving(pass_name:, idx:, state:)
112
+
113
+ break if stop_after && pass_name == stop_after
48
114
  end
49
115
  state
50
116
  end
51
117
 
52
118
  def self.handle_analysis_errors(errors)
53
119
  type_errors = errors.select { |e| e.type == :type }
54
- semantic_errors = errors.select { |e| e.type == :semantic }
55
120
  first_error_location = errors.first.location
56
121
 
57
122
  raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
58
123
 
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))
124
+ raise Errors::SemanticError.new(format_errors(errors), first_error_location)
62
125
  end
63
126
 
64
127
  def self.create_analysis_result(state)
@@ -76,16 +139,7 @@ module Kumi
76
139
  def self.format_errors(errors)
77
140
  return "" if errors.empty?
78
141
 
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
142
+ errors.map(&:to_s).join("\n")
89
143
  end
90
144
  end
91
145
  end
data/lib/kumi/compiler.rb CHANGED
@@ -3,18 +3,19 @@
3
3
  module Kumi
4
4
  # Compiles an analyzed schema into executable lambdas
5
5
  class Compiler < Core::CompilerBase
6
- def self.compile(schema, analyzer:)
7
- new(schema, analyzer).compile
6
+ def self.compile(schema, analyzer:, schema_name: nil)
7
+ new(schema, analyzer, schema_name: schema_name).compile
8
8
  end
9
9
 
10
- def initialize(schema, analyzer)
11
- super
10
+ def initialize(schema, analyzer, schema_name: nil)
11
+ super(schema, analyzer)
12
12
  @bindings = {}
13
+ @schema_name = schema_name
13
14
  end
14
15
 
15
16
  def compile
16
17
  # Switch to LIR: Use the analysis state instead of old compilation
17
- Runtime::Executable.from_analysis(@analysis.state)
18
+ Runtime::Executable.from_analysis(@analysis.state, schema_name: @schema_name)
18
19
  end
19
20
  end
20
21
  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
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Extract IR-level dependencies for VM execution optimization
8
+ # DEPENDENCIES: :ir_module from LowerToIRPass
9
+ # PRODUCES: :ir_dependencies - Hash mapping declaration names to referenced bindings
10
+ # :name_index - Hash mapping stored binding names to producing declarations
11
+ # INTERFACE: new(schema, state).run(errors)
12
+ #
13
+ # NOTE: This pass extracts actual IR-level dependencies by analyzing :ref operations
14
+ # in the generated IR, providing the dependency information needed for optimized VM scheduling.
15
+ class IRDependencyPass < PassBase
16
+ def run(errors)
17
+ ir_module = get_state(:ir_module, required: true)
18
+
19
+ ir_dependencies = build_ir_dependency_map(ir_module)
20
+ name_index = build_name_index(ir_module)
21
+
22
+ state.with(:ir_dependencies, ir_dependencies).with(:name_index, name_index)
23
+ end
24
+
25
+ private
26
+
27
+ # Build a map of declaration -> [stored_bindings_it_references] from the IR
28
+ def build_ir_dependency_map(ir_module)
29
+ deps_map = {}
30
+
31
+ ir_module.decls.each do |decl|
32
+ refs = []
33
+ decl.ops.each do |op|
34
+ if op.tag == :ref
35
+ refs << op.attrs[:name]
36
+ end
37
+ end
38
+ deps_map[decl.name] = refs
39
+ end
40
+
41
+ deps_map.freeze
42
+ end
43
+
44
+ # Build name index to map stored binding names to their producing declarations
45
+ def build_name_index(ir_module)
46
+ name_index = {}
47
+
48
+ ir_module.decls.each do |decl|
49
+ # Map the primary declaration name
50
+ name_index[decl.name] = decl
51
+
52
+ # Also map any vectorized twin names produced by this declaration
53
+ decl.ops.each do |op|
54
+ if op.tag == :store
55
+ stored_name = op.attrs[:name]
56
+ name_index[stored_name] = decl
57
+ end
58
+ end
59
+ end
60
+
61
+ name_index.freeze
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end