///|
/// A named local definition, preserving its full function index and file range.
pub struct FunctionSearchMatch {
index : Int
name : String
raw_name : String
module_name : String
/// Start of the body-length prefix, not the first instruction.
offset : Int
body_size : Int
total_size : Int
matched_raw_name : Bool
matched_display_name : Bool
}
///|
pub struct FunctionSearchReport {
file_size : Int
sha256 : String
query : String
matching_rule : String
scope : String
local_function_count : Int
named_count : Int
unnamed_count : Int
match_count : Int
unmatched_named_count : Int
matched_body_bytes : Int
matched_prefix_bytes : Int
matched_encoded_bytes : Int
matches : Array[FunctionSearchMatch]
}
///|
pub enum FunctionSearchError {
InvalidFunctionSearchQuery
FunctionSearchInputError(WasmError)
}
///|
pub fn describe_function_search_error(error : FunctionSearchError) -> String {
match error {
InvalidFunctionSearchQuery =>
"function search query must not be empty or whitespace-only"
FunctionSearchInputError(error) => describe_error(error)
}
}
///|
/// Inspect available local names once, without decoding instructions or a graph.
pub fn search_local_functions(
data : Bytes,
query : String,
) -> Result[FunctionSearchReport, FunctionSearchError] {
if query.trim().is_empty() {
return Err(InvalidFunctionSearchQuery)
}
let parsed = match parse_module(data) {
Ok(value) => value
Err(error) => return Err(FunctionSearchInputError(error))
}
let stats = match collect_function_stats(parsed, data) {
Ok(value) => value
Err(error) => return Err(FunctionSearchInputError(error))
}
let matches : Array[FunctionSearchMatch] = []
let mut named_count = 0
let mut body_bytes = 0
let mut prefix_bytes = 0
let mut encoded_bytes = 0
for stat in stats {
let raw = match stat.raw_name {
None => continue
Some(value) => value
}
named_count += 1
let matched_raw_name = raw.contains(query)
let matched_display_name = stat.name.contains(query)
if !matched_raw_name && !matched_display_name {
continue
}
matches.push({
index: stat.index,
name: stat.name,
raw_name: raw,
module_name: stat.module_name,
offset: stat.offset,
body_size: stat.body_size,
total_size: stat.total_size,
matched_raw_name,
matched_display_name,
})
body_bytes += stat.body_size
prefix_bytes += stat.total_size - stat.body_size
encoded_bytes += stat.total_size
}
matches.sort_by(fn(a, b) {
if a.total_size != b.total_size {
b.total_size.compare(a.total_size)
} else {
a.index.compare(b.index)
}
})
Ok({
file_size: data.length(),
sha256: experiment_sha256(data),
query,
matching_rule: "Case-sensitive literal substring in the available raw name or display name; no regex, wildcards, Unicode normalization or path splitting. The query is not trimmed for matching.",
scope: "Local function definitions only, using existing best-effort name selection and display rules. Imported declarations and export aliases are not searched. Generated unnamed placeholders are excluded; real names equal to (unnamed) are searchable. Names are not unique identities and available names need not cover all definitions. No match does not prove a function is absent. Indices include preceding function imports. Offset points to the body-length prefix. Matched bytes include locals, instructions and actual body-length prefixes, excluding section headers, names and dependency closures; they do not predict removal, recompilation or gzip savings. No graph or instruction decoding. Top limits text only, not search cost. Run subsequent index queries explicitly.",
local_function_count: stats.length(),
named_count,
unnamed_count: stats.length() - named_count,
match_count: matches.length(),
unmatched_named_count: named_count - matches.length(),
matched_body_bytes: body_bytes,
matched_prefix_bytes: prefix_bytes,
matched_encoded_bytes: encoded_bytes,
matches,
})
}