///|
pub struct GenericInstance {
index : Int
raw_name : String
raw_type_arguments : String
type_labels : Array[String]
total_size : Int
}
///|
pub struct GenericFamily {
/// Exact raw stem, including kind and package identity; artifact-local only.
family_key : String
display_name : String
definition_count : Int
type_encoding_count : Int
total_size : Int
instances : Array[GenericInstance]
}
///|
pub struct GenericCoverage {
total_functions : Int
total_size : Int
grouped_functions : Int
grouped_size : Int
named_ungrouped_functions : Int
named_ungrouped_size : Int
unnamed_functions : Int
unnamed_size : Int
}
///|
pub struct GenericInstancesReport {
file_size : Int
sha256 : String
/// missing, unreadable, or available. Unreadable metadata is not trusted.
name_status : String
recognition_scope : String
coverage : GenericCoverage
families : Array[GenericFamily]
}
///|
priv struct GenericSymbol {
stem : String
label : String
encoding : String
types : Array[String]
}
///|
/// Forward length parsing, separate from the heuristic display demangler.
/// Try each package-index/first-length boundary; ambiguous paths are rejected.
fn generic_path(bytes : Bytes, stop : Int) -> Array[String]? {
if stop < 7 || bytes[4] != b'P' {
return None
}
let mut digit_end = 5
while digit_end < stop && is_ascii_digit(bytes[digit_end]) {
digit_end += 1
}
let mut answer : Array[String]? = None
for split in 6.. stop {
valid = false
break
}
}
if !valid || pos == start || length == 0 || length > stop - pos {
valid = false
break
}
let end = pos + length
if !is_ident_start(bytes[pos]) {
valid = false
break
}
let name = @utf8.decode_lossy(bytes[pos:end])
while pos < end && valid {
if bytes[pos] == b'_' {
if pos + 1 < end && bytes[pos + 1] == b'_' {
pos += 2
} else if pos + 2 < end &&
hex_value(bytes[pos + 1]) is Some(_) &&
hex_value(bytes[pos + 2]) is Some(_) {
pos += 3
} else {
valid = false
}
} else if is_ascii_letter(bytes[pos]) || is_ascii_digit(bytes[pos]) {
pos += 1
} else {
valid = false
}
}
parts.push(decode_escapes(name))
}
let minimum = if bytes[3] == b'M' { 3 } else { 2 }
if valid && pos == stop && parts.length() >= minimum {
if answer is Some(_) {
return None
}
answer = Some(parts)
}
}
answer
}
///|
/// Observed on the checked-in toolchain probe: F/M + numeric P path + G[i|d]+E.
/// No inferred named/tuple/closure/trait encodings; the entire symbol must fit.
fn generic_symbol(raw : String) -> GenericSymbol? {
let bytes = @utf8.encode(raw)
let end = bytes.length()
if end > 4096 ||
end < 9 ||
!raw.has_prefix("_M0") ||
(bytes[3] != b'F' && bytes[3] != b'M') ||
bytes[end - 1] != b'E' {
return None
}
let mut found : GenericSymbol? = None
for split in 5..<(end - 2) {
if bytes[split] != b'G' {
continue
}
let types : Array[String] = []
let mut supported = true
for i in (split + 1)..<(end - 1) {
match bytes[i] {
b'i' => types.push("Int")
b'd' => types.push("Double")
_ => {
supported = false
break
}
}
}
if !supported || types.is_empty() {
continue
}
if generic_path(bytes, split) is Some(parts) {
if found is Some(_) {
return None
}
found = Some({
stem: @utf8.decode_lossy(bytes[0:split]),
label: parts.join("::"),
encoding: @utf8.decode_lossy(bytes[split:end]),
types,
})
}
}
found
}
///|
/// Check the selected name section fully instead of trusting the legacy parser's
/// best-effort partial map. Other name subsections remain opaque bounded bytes.
fn generic_names_readable(data : Bytes, section : Section) -> Bool {
let reader = match reader_at_payload(data, section) {
Ok(r) => r
Err(_) => return false
}
if skip_name(reader) is Err(_) {
return false
}
let mut seen_functions = false
while !reader.at_end() {
let id = match reader.read_byte() {
Ok(n) => n
Err(_) => return false
}
let size = match read_u32_as_int(reader) {
Ok(n) => n
Err(_) => return false
}
let sub = match Reader::new_range(data, reader.position(), size) {
Ok(r) => r
Err(_) => return false
}
if size > reader.remaining() {
return false
}
if id == 1 {
if seen_functions {
return false
}
seen_functions = true
let count = match read_u32_as_int(sub) {
Ok(n) => n
Err(_) => return false
}
let indices : Set[Int] = Set([])
for _ in 0.. n
Err(_) => return false
}
if indices.contains(index) {
return false
}
indices.add(index)
let length = match read_u32_as_int(sub) {
Ok(n) => n
Err(_) => return false
}
let name = match sub.read_bytes(length) {
Ok(b) => b
Err(_) => return false
}
let _text = @utf8.decode(name) catch { _ => return false }
}
if !sub.at_end() {
return false
}
}
if reader.read_bytes(size) is Err(_) {
return false
}
}
true
}
///|
fn generic_scope() -> String {
"Recognizes only fully parsed numeric-package F/M symbols with Int (i) or Double (d) type arguments (G...E), observed in the probe. " +
"Closures, trait implementations, other types and ambiguous encodings remain ungrouped. Ungrouped named functions are not necessarily non-generic. " +
"Compiler symbol conventions are not a complete official specification; family keys identify groups only within this artifact, not across toolchains. " +
"Weights are local function total_size bytes including body length prefixes, excluding the code section header/count. " +
"Inlined, merged or eliminated source instances are invisible. Encoded totals are not removable redundancy or predictions of recompiled or gzip savings."
}
///|
fn generic_aggregate(
stats : Array[FunctionStat],
file_size : Int,
sha256 : String,
name_status : String,
) -> GenericInstancesReport {
let groups : Map[String, Array[GenericInstance]] = Map([])
let labels : Map[String, String] = Map([])
let mut total_size = 0
let mut grouped_functions = 0
let mut grouped_size = 0
let mut named_ungrouped_functions = 0
let mut named_ungrouped_size = 0
let mut unnamed_functions = 0
let mut unnamed_size = 0
for stat in stats {
total_size += stat.total_size
let raw = if name_status == "available" { stat.raw_name } else { None }
match raw {
Some(raw) =>
match generic_symbol(raw) {
Some(symbol) => {
let instances = groups.get(symbol.stem).unwrap_or([])
instances.push({
index: stat.index,
raw_name: raw,
raw_type_arguments: symbol.encoding,
type_labels: symbol.types,
total_size: stat.total_size,
})
groups.set(symbol.stem, instances)
labels.set(symbol.stem, symbol.label)
grouped_functions += 1
grouped_size += stat.total_size
}
None => {
named_ungrouped_functions += 1
named_ungrouped_size += stat.total_size
}
}
None => {
unnamed_functions += 1
unnamed_size += stat.total_size
}
}
}
let families : Array[GenericFamily] = []
for key, instances in groups {
instances.sort_by(fn(a, b) { a.index.compare(b.index) })
let encodings : Set[String] = Set([])
let mut size = 0
for instance in instances {
encodings.add(instance.raw_type_arguments)
size += instance.total_size
}
families.push({
family_key: key,
display_name: labels.get(key).unwrap_or(key),
definition_count: instances.length(),
type_encoding_count: encodings.length(),
total_size: size,
instances,
})
}
families.sort_by(fn(a, b) {
let order = b.total_size.compare(a.total_size)
if order == 0 {
a.family_key.compare(b.family_key)
} else {
order
}
})
{
file_size,
sha256,
name_status,
recognition_scope: generic_scope(),
families,
coverage: {
total_functions: stats.length(),
total_size,
grouped_functions,
grouped_size,
named_ungrouped_functions,
named_ungrouped_size,
unnamed_functions,
unnamed_size,
},
}
}
///|
/// Size-only inspection: never constructs a graph or invokes analyze/retained.
pub fn inspect_generic_instances(
data : Bytes,
) -> Result[GenericInstancesReport, WasmError] {
let parsed = match parse_module(data) {
Ok(m) => m
Err(e) => return Err(e)
}
let stats = match collect_function_stats(parsed, data) {
Ok(s) => s
Err(e) => return Err(e)
}
let status = match find_sections(parsed).names {
None => "missing"
Some(section) =>
if generic_names_readable(data, section) {
"available"
} else {
"unreadable"
}
}
Ok(generic_aggregate(stats, data.length(), experiment_sha256(data), status))
}