///|
pub struct FunctionCallerGroup {
caller : FunctionPathNode
sources : Array[FunctionPathStep]
}
///|
/// Complete recorded one-hop instruction sources, without reachability filtering.
pub struct FunctionCallersReport {
target : FunctionPathNode
file_size : Int
sha256 : String
callers : Array[FunctionCallerGroup]
caller_count : Int
source_count : Int
graph_complete : Bool
diagnostics : Array[GraphDiagnostic]
graph_warning : String
modeling_scope : String
}
///|
pub enum FunctionCallersError {
CallersInputError(WasmError)
InvalidCallersTarget(Int, Int)
InvalidCallersEvidence(Int, String)
}
///|
pub fn describe_function_callers_error(error : FunctionCallersError) -> String {
match error {
CallersInputError(error) => describe_error(error)
InvalidCallersTarget(index, count) =>
"target function index " +
index.to_string() +
" is outside the function index space of " +
count.to_string()
InvalidCallersEvidence(offset, reason) =>
"invalid incoming instruction evidence at file offset " +
offset.to_string() +
": " +
reason
}
}
///|
fn function_callers_scope() -> String {
"All one-hop instruction references recorded in the current model, including sources in unreachable functions. " +
"Direct calls, references taken and indirect candidates are distinct; these are not execution traces or predictions of recompiled or gzip savings. " +
"Exports, start and element roots are not incoming body instructions; use --why-index to explain model roots. " +
"Diagnostics describe the whole module and need not concern this target. graph_complete does not prove full semantics or removability. " +
"Top only limits displayed caller groups, not parsing or graph-building costs. " +
graph_modeling_scope()
}
///|
/// Only matching offsets need evidence decoding; shared offsets are cached.
fn callers_sources(
data : Bytes,
graph : CallGraph,
inventory : Array[FunctionPathNode],
target : Int,
) -> Result[Array[FunctionPathStep], FunctionCallersError] {
let sources : Array[FunctionPathStep] = []
let decoded : Map[Int, Instruction] = Map([])
for edge in graph.edges {
if edge.callee != target {
continue
}
if edge.caller < 0 ||
edge.caller >= inventory.length() ||
inventory[edge.caller].imported {
return Err(
InvalidCallersEvidence(edge.offset, "invalid body caller index"),
)
}
let instruction = match decoded.get(edge.offset) {
Some(instruction) => instruction
None => {
let instruction = match decode_instruction(data, edge.offset) {
Ok(instruction) => instruction
Err(error) =>
return Err(
InvalidCallersEvidence(edge.offset, describe_error(error)),
)
}
decoded.set(edge.offset, instruction)
instruction
}
}
let (type_index, table_index) = match (edge.kind, instruction.opcode) {
(Direct, Call | ReturnCall) | (Reference, RefFunc) => {
if instruction.operands.length() != 1 ||
instruction.operands[0] != target {
return Err(
InvalidCallersEvidence(
edge.offset,
"target operand does not match edge",
),
)
}
(None, None)
}
(Indirect, CallIndirect | ReturnCallIndirect) => {
if instruction.operands.length() != 2 {
return Err(
InvalidCallersEvidence(edge.offset, "missing type or table index"),
)
}
(Some(instruction.operands[0]), Some(instruction.operands[1]))
}
_ =>
return Err(
InvalidCallersEvidence(edge.offset, "opcode does not match edge kind"),
)
}
sources.push({
caller: edge.caller,
callee: edge.callee,
kind: edge.kind,
opcode: instruction.opcode,
offset: edge.offset,
type_index,
table_index,
})
}
sources.sort_by(fn(a, b) {
let caller = a.caller - b.caller
if caller != 0 {
return caller
}
let offset = a.offset - b.offset
if offset != 0 {
offset
} else {
path_edge_rank(a.kind) - path_edge_rank(b.kind)
}
})
let unique : Array[FunctionPathStep] = []
for source in sources {
if !unique.is_empty() {
let previous = unique[unique.length() - 1]
// Target is fixed; cached instruction determines opcode/type/table.
if previous.caller == source.caller &&
previous.offset == source.offset &&
path_edge_rank(previous.kind) == path_edge_rank(source.kind) {
continue
}
}
unique.push(source)
}
Ok(unique)
}
///|
/// Parse, inventory and build directly. No analyze, dominators, BFS or truncation.
pub fn function_callers(
data : Bytes,
target_index : Int,
) -> Result[FunctionCallersReport, FunctionCallersError] {
let parsed = match parse_module(data) {
Ok(parsed) => parsed
Err(error) => return Err(CallersInputError(error))
}
let inventory = match path_inventory(data, parsed) {
Ok(inventory) => inventory
Err(error) => return Err(CallersInputError(error))
}
if target_index < 0 || target_index >= inventory.length() {
return Err(InvalidCallersTarget(target_index, inventory.length()))
}
let graph = match build(data) {
Ok(graph) => graph
Err(error) => return Err(CallersInputError(error))
}
let sources = match callers_sources(data, graph, inventory, target_index) {
Ok(sources) => sources
Err(error) => return Err(error)
}
let groups : Array[FunctionCallerGroup] = []
for source in sources {
if !groups.is_empty() &&
groups[groups.length() - 1].caller.index == source.caller {
groups[groups.length() - 1].sources.push(source)
} else {
groups.push({ caller: inventory[source.caller], sources: [source], })
}
}
Ok({
target: inventory[target_index],
file_size: data.length(),
sha256: experiment_sha256(data),
callers: groups,
caller_count: groups.length(),
source_count: sources.length(),
graph_complete: is_graph_complete(graph),
diagnostics: sorted_diagnostics(graph),
graph_warning: render_graph_warning(graph),
modeling_scope: function_callers_scope(),
})
}