///|
pub struct EntrypointGroup {
/// Full function index, also the stable group identifier within this artifact.
index : Int
aliases : Array[String]
reachable : Array[Int]
local_function_count : Int
imported_function_count : Int
reachable_model_bytes : Int
exclusive_model_bytes : Int
shared_model_bytes : Int
}
///|
/// One inventory node per function; groups and shared lists refer to its index.
pub struct EntrypointFunction {
node : FunctionPathNode
entrypoints : Array[Int]
other_root_reachable : Bool
}
///|
pub struct EntrypointsLedger {
local_function_count : Int
total_local_bytes : Int
export_union_local_count : Int
export_union_bytes : Int
not_export_reachable_bytes : Int
exclusive_model_bytes : Int
shared_model_bytes : Int
}
///|
pub struct EntrypointsReport {
file_size : Int
sha256 : String
export_name_count : Int
entrypoint_count : Int
other_exports : Array[Export]
entrypoints : Array[EntrypointGroup]
/// Full function index order. Imports retain encoded_size=None.
functions : Array[EntrypointFunction]
other_roots : Array[FunctionRoot]
other_root_reachable : Array[Int]
/// Export-reachable local functions shared with another group or other roots.
shared_local_functions : Array[Int]
ledger : EntrypointsLedger
graph_complete : Bool
diagnostics : Array[GraphDiagnostic]
graph_warning : String
modeling_scope : String
}
///|
pub enum EntrypointsError {
EntrypointsInputError(WasmError)
/// Actual export name, export ordinal, function index, function count.
InvalidEntrypointExport(String, Int, Int, Int)
/// Actual root description (start/element ordinal), index, function count.
InvalidEntrypointsRoot(String, Int, Int)
/// Actual caller/callee and instruction file offset from the graph.
InvalidEntrypointsEdge(Int, Int, Int)
InvalidEntrypointsExportKind(String, Int, Int)
}
///|
pub fn describe_entrypoints_error(error : EntrypointsError) -> String {
match error {
EntrypointsInputError(error) => describe_error(error)
InvalidEntrypointExport(name, ordinal, index, count) =>
"function export " +
Json::string(name).stringify() +
" at export ordinal \{ordinal} references index \{index} outside function count \{count}"
InvalidEntrypointsRoot(source, index, count) =>
"\{source} references index \{index} outside function count \{count}"
InvalidEntrypointsEdge(caller, callee, offset) =>
"invalid function edge \{caller} -> \{callee} at file offset \{offset}"
InvalidEntrypointsExportKind(name, ordinal, kind) =>
"export " +
Json::string(name).stringify() +
" at export ordinal \{ordinal} has unsupported kind \{kind}"
}
}
///|
fn entrypoints_scope() -> String {
"Function exports are grouped by function index; aliases do not repeat traversal. " +
"Exclusive/shared bytes describe reachability sets in the current reference model, not execution frequency or savings from deleting an export. " +
"Start and all element functions remain conservative roots, even when exported. " +
"Indirect candidates and ref.func references do not imply inevitable runtime calls. Module diagnostics need not concern every entry. " +
"Weights are local total_size bytes (including body length prefixes), excluding code section header/count and data/name/type/other sections. " +
"Imports have no local body measurement; null does not mean host calls have no cost. " +
"Per-entry reachable/shared bytes overlap and must not be added as module size. Not reached by function exports does not mean dead or removable. " +
"No prediction of recompiled or gzip savings; graph_complete and strict success do not establish full semantics. " +
"Top limits display only, not parsing or traversal costs; more unique entries require more reachability computations. " +
graph_modeling_scope()
}
///|
fn entrypoints_sorted(set : Set[Int]) -> Array[Int] {
let indices = set.to_array()
indices.sort_by(fn(a, b) { a.compare(b) })
indices
}
///|
fn entrypoints_add_root(
roots : Map[Int, Array[FunctionRootReason]],
reason : FunctionRootReason,
) -> Unit {
let reasons = roots.get(reason.function_index).unwrap_or([])
reasons.push(reason)
roots.set(reason.function_index, reasons)
}
///|
/// One graph, one traversal per unique function export, one for all other roots.
/// Parsing/building uses existing safe entry points; no analyze or dominators.
pub fn inspect_entrypoints(
data : Bytes,
) -> Result[EntrypointsReport, EntrypointsError] {
let parsed = match parse_module(data) {
Ok(m) => m
Err(e) => return Err(EntrypointsInputError(e))
}
let inventory = match path_inventory(data, parsed) {
Ok(v) => v
Err(e) => return Err(EntrypointsInputError(e))
}
let count = inventory.length()
let sections = find_sections(parsed)
let exports = match sections.exports {
None => []
Some(section) =>
match parse_export_section(data, section) {
Ok(v) => v
Err(e) => return Err(EntrypointsInputError(e))
}
}
let aliases : Map[Int, Array[String]] = Map([])
let other_exports : Array[Export] = []
let mut export_name_count = 0
for ordinal, item in exports {
if item.kind == 0 {
if item.index < 0 || item.index >= count {
return Err(
InvalidEntrypointExport(item.name, ordinal, item.index, count),
)
}
let names = aliases.get(item.index).unwrap_or([])
names.push(item.name)
aliases.set(item.index, names)
export_name_count += 1
} else if item.kind >= 1 && item.kind <= 4 {
other_exports.push(item)
} else {
return Err(InvalidEntrypointsExportKind(item.name, ordinal, item.kind))
}
}
let reasons : Map[Int, Array[FunctionRootReason]] = Map([])
if sections.start is Some(section) {
let index = match parse_start_section(data, section) {
Ok(n) => n
Err(e) => return Err(EntrypointsInputError(e))
}
if index < 0 || index >= count {
return Err(InvalidEntrypointsRoot("start section", index, count))
}
entrypoints_add_root(reasons, {
function_index: index,
kind: StartRoot,
name: None,
section_offset: section.offset,
ordinal: None,
table_index: None,
element_mode: None,
})
}
if sections.elements is Some(section) {
let segments = match parse_element_section(data, section) {
Ok(v) => v
Err(e) => return Err(EntrypointsInputError(e))
}
for ordinal, segment in segments {
let seen : Set[Int] = Set([])
for index in segment.functions {
if index < 0 || index >= count {
return Err(
InvalidEntrypointsRoot("element segment \{ordinal}", index, count),
)
}
if seen.contains(index) {
continue
}
seen.add(index)
entrypoints_add_root(reasons, {
function_index: index,
kind: ElementRoot,
name: None,
section_offset: section.offset,
ordinal: Some(ordinal),
table_index: if segment.table_index < 0 {
None
} else {
Some(segment.table_index)
},
element_mode: Some(
if segment.passive {
"passive-or-declarative"
} else {
"active"
},
),
})
}
}
}
let graph = match build(data) {
Ok(g) => g
Err(e) => return Err(EntrypointsInputError(e))
}
for edge in graph.edges {
if edge.caller < 0 ||
edge.caller >= count ||
edge.callee < 0 ||
edge.callee >= count {
return Err(InvalidEntrypointsEdge(edge.caller, edge.callee, edge.offset))
}
}
let root_indices = reasons.keys().to_array()
root_indices.sort_by(fn(a, b) { a.compare(b) })
let other_roots = root_indices.map(fn(index) {
{ index, reasons: reasons.get(index).unwrap_or([]), }
})
let other_reachable = reachable_from_roots(
graph,
Set::from_array(root_indices),
)
let owners : Array[Array[Int]] = Array::makei(count, fn(_) { [] })
let indices = aliases.keys().to_array()
indices.sort_by(fn(a, b) { a.compare(b) })
let reaches : Map[Int, Array[Int]] = Map([])
for index in indices {
let reachable = entrypoints_sorted(
reachable_from_roots(graph, Set([index])),
)
for function in reachable {
owners[function].push(index)
}
reaches.set(index, reachable)
}
let functions : Array[EntrypointFunction] = inventory.map(fn(node) {
{
node,
entrypoints: owners[node.index],
other_root_reachable: other_reachable.contains(node.index),
}
})
let shared : Array[Int] = []
let mut local_function_count = 0
let mut total_local_bytes = 0
let mut export_union_local_count = 0
let mut export_union_bytes = 0
let mut exclusive_model_bytes = 0
let mut shared_model_bytes = 0
for function in functions {
if function.node.encoded_size is Some(size) {
local_function_count += 1
total_local_bytes += size
if !function.entrypoints.is_empty() {
export_union_local_count += 1
export_union_bytes += size
if function.entrypoints.length() > 1 || function.other_root_reachable {
shared.push(function.node.index)
shared_model_bytes += size
} else {
exclusive_model_bytes += size
}
}
}
}
shared.sort_by(fn(a, b) {
let order = inventory[b].encoded_size
.unwrap_or(0)
.compare(inventory[a].encoded_size.unwrap_or(0))
if order == 0 {
a.compare(b)
} else {
order
}
})
let groups : Array[EntrypointGroup] = []
for index in indices {
let reachable = reaches.get(index).unwrap_or([])
let mut local_count = 0
let mut imported = 0
let mut exclusive = 0
let mut common = 0
for target in reachable {
let function = functions[target]
match function.node.encoded_size {
None => imported += 1
Some(size) => {
local_count += 1
if function.entrypoints.length() > 1 || function.other_root_reachable {
common += size
} else {
exclusive += size
}
}
}
}
groups.push({
index,
aliases: aliases.get(index).unwrap_or([]),
reachable,
local_function_count: local_count,
imported_function_count: imported,
reachable_model_bytes: exclusive + common,
exclusive_model_bytes: exclusive,
shared_model_bytes: common,
})
}
groups.sort_by(fn(a, b) {
let order = b.reachable_model_bytes.compare(a.reachable_model_bytes)
if order == 0 {
a.index.compare(b.index)
} else {
order
}
})
Ok({
file_size: data.length(),
sha256: experiment_sha256(data),
export_name_count,
entrypoint_count: groups.length(),
other_exports,
entrypoints: groups,
functions,
other_roots,
other_root_reachable: entrypoints_sorted(other_reachable),
shared_local_functions: shared,
ledger: {
local_function_count,
total_local_bytes,
export_union_local_count,
export_union_bytes,
not_export_reachable_bytes: total_local_bytes - export_union_bytes,
exclusive_model_bytes,
shared_model_bytes,
},
graph_complete: is_graph_complete(graph),
diagnostics: sorted_diagnostics(graph),
graph_warning: render_graph_warning(graph),
modeling_scope: entrypoints_scope(),
})
}