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.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/BACKLOG.md +34 -0
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +4 -6
- data/README.md +0 -45
- data/config/functions.yaml +352 -0
- data/docs/dev/analyzer-debug.md +52 -0
- data/docs/dev/parse-command.md +64 -0
- data/docs/dev/vm-profiling.md +95 -0
- data/docs/features/README.md +0 -7
- data/docs/functions/analyzer_integration.md +199 -0
- data/docs/functions/signatures.md +171 -0
- data/examples/hash_objects_demo.rb +138 -0
- data/golden/array_operations/schema.kumi +17 -0
- data/golden/cascade_logic/schema.kumi +16 -0
- data/golden/mixed_nesting/schema.kumi +42 -0
- data/golden/simple_math/schema.kumi +10 -0
- data/lib/kumi/analyzer.rb +76 -22
- data/lib/kumi/compiler.rb +6 -5
- data/lib/kumi/core/analyzer/checkpoint.rb +72 -0
- data/lib/kumi/core/analyzer/debug.rb +167 -0
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +1 -3
- data/lib/kumi/core/analyzer/passes/function_signature_pass.rb +199 -0
- data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +67 -0
- data/lib/kumi/core/analyzer/passes/load_input_cse.rb +120 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +72 -157
- data/lib/kumi/core/analyzer/passes/toposorter.rb +40 -36
- data/lib/kumi/core/analyzer/state_serde.rb +64 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +12 -10
- data/lib/kumi/core/compiler/access_planner.rb +3 -2
- data/lib/kumi/core/function_registry/collection_functions.rb +3 -1
- data/lib/kumi/core/functions/dimension.rb +98 -0
- data/lib/kumi/core/functions/dtypes.rb +20 -0
- data/lib/kumi/core/functions/errors.rb +11 -0
- data/lib/kumi/core/functions/kernel_adapter.rb +45 -0
- data/lib/kumi/core/functions/loader.rb +119 -0
- data/lib/kumi/core/functions/registry_v2.rb +68 -0
- data/lib/kumi/core/functions/shape.rb +70 -0
- data/lib/kumi/core/functions/signature.rb +122 -0
- data/lib/kumi/core/functions/signature_parser.rb +86 -0
- data/lib/kumi/core/functions/signature_resolver.rb +272 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +110 -7
- data/lib/kumi/core/ir/execution_engine/profiler.rb +330 -0
- data/lib/kumi/core/ir/execution_engine.rb +6 -15
- data/lib/kumi/dev/ir.rb +75 -0
- data/lib/kumi/dev/parse.rb +105 -0
- data/lib/kumi/dev/profile_aggregator.rb +301 -0
- data/lib/kumi/dev/profile_runner.rb +199 -0
- data/lib/kumi/dev/runner.rb +85 -0
- data/lib/kumi/dev.rb +14 -0
- data/lib/kumi/frontends/ruby.rb +28 -0
- data/lib/kumi/frontends/text.rb +46 -0
- data/lib/kumi/frontends.rb +29 -0
- data/lib/kumi/kernels/ruby/aggregate_core.rb +105 -0
- data/lib/kumi/kernels/ruby/datetime_scalar.rb +21 -0
- data/lib/kumi/kernels/ruby/mask_scalar.rb +15 -0
- data/lib/kumi/kernels/ruby/scalar_core.rb +63 -0
- data/lib/kumi/kernels/ruby/string_scalar.rb +19 -0
- data/lib/kumi/kernels/ruby/vector_struct.rb +39 -0
- data/lib/kumi/runtime/executable.rb +108 -45
- data/lib/kumi/schema.rb +12 -6
- data/lib/kumi/support/diff.rb +22 -0
- data/lib/kumi/support/ir_render.rb +61 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +3 -0
- data/performance_results.txt +63 -0
- data/scripts/test_mixed_nesting_performance.rb +206 -0
- metadata +50 -6
- data/docs/features/analysis-cascade-mutual-exclusion.md +0 -89
- data/docs/features/javascript-transpiler.md +0 -148
- data/lib/kumi/js.rb +0 -23
- data/lib/kumi/support/ir_dump.rb +0 -491
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::
|
19
|
-
Core::Analyzer::Passes::
|
20
|
-
Core::Analyzer::Passes::
|
21
|
-
Core::Analyzer::Passes::
|
22
|
-
Core::Analyzer::Passes::
|
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
|
-
|
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 =
|
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)
|
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
|
-
|
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
|