///|
pub struct FunctionDependencyNode {
node : FunctionPathNode
depth : Int
predecessor : CallEdge?
}
///|
pub struct FunctionDependenciesReport {
target : FunctionPathNode
file_size : Int
sha256 : String
function_count : Int
local_function_count : Int
imported_function_count : Int
nodes : Array[FunctionDependencyNode]
edges : Array[CallEdge]
reachable_function_count : Int
other_function_count : Int
reachable_local_count : Int
reachable_imported_count : Int
dependency_function_count : Int
local_encoded_bytes : Int
reachable_local_encoded_bytes : Int
other_local_encoded_bytes : Int
target_local_encoded_bytes : Int
dependency_local_encoded_bytes : Int
edge_count : Int
direct_edges : Int
reference_edges : Int
indirect_edges : Int
graph_complete : Bool
unresolved_count : Int
diagnostics : Array[GraphDiagnostic]
graph_warning : String
modeling_scope : String
}
///|
pub enum FunctionDependenciesError {
DependenciesInputError(WasmError)
InvalidDependenciesTarget(Int, Int)
InvalidDependenciesEvidence(Int, Int, String)
}
///|
pub fn describe_function_dependencies_error(
error : FunctionDependenciesError,
) -> String {
match error {
DependenciesInputError(error) => describe_error(error)
InvalidDependenciesTarget(index, count) =>
"dependency target function index \{index} is outside the function index space of \{count}"
InvalidDependenciesEvidence(index, offset, reason) =>
"invalid dependency graph evidence for function \{index} at file offset \{offset}: " +
reason
}
}
///|
fn dependencies_scope() -> String {
"Transitive closure from any valid function index in the current reference model, including the target itself. No export or model-root reachability requirement. " +
"Direct calls, references taken by ref.func and static indirect candidates all count as model steps, not execution, runtime heat, necessary retention or deletion benefit. Unmodeled call_ref targets remain diagnostics without guessed edges. " +
"Depth is the minimum number of model edges from the target, not runtime stack depth. Each node keeps one deterministic first-discovery predecessor, not a copied path; complete closure edges preserve parallel positions, kinds and candidates beyond the predecessor tree. " +
"Local encoded bytes count each full function index once, including the body length prefix but excluding code header/count and type/data/name/other sections. An import's encoded_size remains null; target_local_encoded_bytes=0 for an imported target is only its local-file accounting contribution, not a measured host body or cost. " +
"Outside this target's closure does not mean dead code. Closures from different targets can overlap, so their bytes cannot be added as module size. No exclusive/shared attribution, retained, recompile or gzip savings are computed. " +
"Diagnostics and graph_complete describe the entire module, including sites outside this closure. Top limits dependency text rows, not traversal cost or complete JSON; complete evidence memory grows with the graph. To inspect actual opcodes, use edge offsets separately with --locate-offset --locate-decode. " +
graph_modeling_scope()
}
///|
/// Validate the whole graph, then one head-index BFS. Evidence is never reduced
/// to adjacency sets or tree edges. No per-node path copies or instruction decode.
fn dependencies_closure(
inventory : Array[FunctionPathNode],
edges : Array[CallEdge],
target : Int,
file_size : Int,
) -> Result[
(Array[FunctionDependencyNode], Array[CallEdge]),
FunctionDependenciesError,
] {
let count = inventory.length()
if target < 0 || target >= count {
return Err(InvalidDependenciesTarget(target, count))
}
let adjacency : Array[Array[CallEdge]] = Array::makei(count, fn(_) { [] })
for edge in edges {
for index in [edge.caller, edge.callee] {
if index < 0 || index >= count {
return Err(
InvalidDependenciesEvidence(
index,
edge.offset,
"index outside function inventory",
),
)
}
}
if inventory[edge.caller].imported {
return Err(
InvalidDependenciesEvidence(
edge.caller,
edge.offset,
"imported caller has no local body",
),
)
}
if edge.offset < 0 || edge.offset >= file_size {
return Err(
InvalidDependenciesEvidence(
edge.caller,
edge.offset,
"instruction offset outside input",
),
)
}
adjacency[edge.caller].push(edge)
}
for outgoing in adjacency {
outgoing.sort_by(fn(a, b) {
if a.callee != b.callee {
return a.callee - b.callee
}
let kind = path_edge_rank(a.kind) - path_edge_rank(b.kind)
if kind != 0 {
kind
} else {
a.offset - b.offset
}
})
}
let depth : Array[Int] = Array::make(count, -1)
let predecessor : Array[CallEdge?] = Array::make(count, None)
let queue = [target]
depth[target] = 0
let mut head = 0
while head < queue.length() {
let caller = queue[head]
head += 1
for edge in adjacency[caller] {
if depth[edge.callee] >= 0 {
continue
}
depth[edge.callee] = depth[caller] + 1
predecessor[edge.callee] = Some(edge)
queue.push(edge.callee)
}
}
let closure_edges : Array[CallEdge] = []
for edge in edges {
if depth[edge.caller] < 0 {
continue
}
if depth[edge.callee] < 0 {
return Err(
InvalidDependenciesEvidence(
edge.callee,
edge.offset,
"callee escapes computed closure",
),
)
}
closure_edges.push(edge)
}
closure_edges.sort_by(fn(a, b) {
if a.caller != b.caller {
return a.caller - b.caller
}
if a.offset != b.offset {
return a.offset - b.offset
}
if a.callee != b.callee {
return a.callee - b.callee
}
path_edge_rank(a.kind) - path_edge_rank(b.kind)
})
let nodes = queue.map(fn(index) {
FunctionDependencyNode::{
node: inventory[index],
depth: depth[index],
predecessor: predecessor[index],
}
})
nodes.sort_by(fn(a, b) {
if a.depth != b.depth {
a.depth - b.depth
} else {
a.node.index - b.node.index
}
})
Ok((nodes, closure_edges))
}
///|
/// One inventory and one build(data), not analyze, retained, SCC or callers.
/// Existing inventory/build parsing is reused; this is not a parse-once claim.
pub fn function_dependencies(
data : Bytes,
target_index : Int,
) -> Result[FunctionDependenciesReport, FunctionDependenciesError] {
let parsed = match parse_module(data) {
Ok(value) => value
Err(error) => return Err(DependenciesInputError(error))
}
let inventory = match path_inventory(data, parsed) {
Ok(value) => value
Err(error) => return Err(DependenciesInputError(error))
}
if target_index < 0 || target_index >= inventory.length() {
return Err(InvalidDependenciesTarget(target_index, inventory.length()))
}
let graph = match build(data) {
Ok(value) => value
Err(error) => return Err(DependenciesInputError(error))
}
let (nodes, edges) = match
dependencies_closure(inventory, graph.edges, target_index, data.length()) {
Ok(value) => value
Err(error) => return Err(error)
}
let mut imports = 0
let mut local_bytes = 0
for node in inventory {
if node.imported {
imports += 1
} else {
local_bytes += node.encoded_size.unwrap_or(0)
}
}
let mut reached_imports = 0
let mut reached_bytes = 0
for entry in nodes {
if entry.node.imported {
reached_imports += 1
} else {
reached_bytes += entry.node.encoded_size.unwrap_or(0)
}
}
let mut direct = 0
let mut reference = 0
let mut indirect = 0
for edge in edges {
match edge.kind {
Direct => direct += 1
Reference => reference += 1
Indirect => indirect += 1
}
}
let target = inventory[target_index]
let target_bytes = target.encoded_size.unwrap_or(0)
Ok({
target,
file_size: data.length(),
sha256: experiment_sha256(data),
function_count: inventory.length(),
local_function_count: inventory.length() - imports,
imported_function_count: imports,
nodes,
edges,
reachable_function_count: nodes.length(),
other_function_count: inventory.length() - nodes.length(),
reachable_local_count: nodes.length() - reached_imports,
reachable_imported_count: reached_imports,
dependency_function_count: nodes.length() - 1,
local_encoded_bytes: local_bytes,
reachable_local_encoded_bytes: reached_bytes,
other_local_encoded_bytes: local_bytes - reached_bytes,
target_local_encoded_bytes: target_bytes,
dependency_local_encoded_bytes: reached_bytes - target_bytes,
edge_count: edges.length(),
direct_edges: direct,
reference_edges: reference,
indirect_edges: indirect,
graph_complete: is_graph_complete(graph),
unresolved_count: graph.unresolved_indirect.length(),
diagnostics: sorted_diagnostics(graph),
graph_warning: render_graph_warning(graph),
modeling_scope: dependencies_scope(),
})
}