moonsize

    A Wasm size profiler for MoonBit — section breakdown, function ranking, call graph and retained-size analysis

    wasm
    webassembly
    size
    profiling
    cli
    Download zip
    Version
    0.6.0
    License
    MIT
    Last updated
    5 days ago
    Downloads
    98

    #moonsize

    Older published library (0.5.1):

    moon add BigSaltyMan/moonsize@0.5.1

    A size analyzer for WebAssembly, built for MoonBit's output. It reads a .wasm file, measures every section, attributes the code section to named functions, follows a reference graph, and highlights candidates for investigation. It does not predict the exact bytes a source deletion will save after rebuilding.

    The analysis model follows Twiggy: sizes are attributed to individual functions, functions are grouped by package, and reachability is calculated from the current model's roots. Read the retained-size boundaries and measured rebuild cases before interpreting a removal candidate.

    Chinese version: README.zh.md.

    Version 0.5.0 adds data-entry inspection, exact-payload group comparison and opt-in HTML details. It also changes report writes to exclusive creation; read the 0.5 migration guide for behavior and API changes. The 0.4 migration guide remains historical. The moon add command adds a library dependency; it does not install a standalone CLI. For the CLI, clone this source repository and run the build below.

    The review evidence index links measured rebuild cases, real PDFs and explicit model boundaries. The 0.4.0 release record records the exact source SHA, target Linux CI, registry checksum and clean consumer verification; historic devlog counts are snapshots.

    The install command above selects the published 0.5.1 library, not the newest source features or a standalone CLI. The current source declares 0.6.0, an unpublished release candidate, including the additions developed after public 0.5.1. Build the latest CLI from an explicit full source commit; record that SHA when sharing results. The historic 0.5.1 release record identifies that public release and is not evidence that later source changes were published. Source delivery and task-oriented guide links current features and the final-commit package audit. Read the 0.6 migration guide for cumulative API and CLI changes. Candidate preparation does not publish Mooncakes or create a tag or GitHub Release; registry consumption of 0.6.0 has not been verified.

    Patch 0.5.1 removes the obsolete JSON-manifest dependency chain that blocked Mooncakes documentation for 0.4.0/0.5.0. It ships the pinned synchronous gzip implementation with source hashes and Apache-2.0 attribution, keeping its algorithm and defaults. See toolchain compatibility.

    #Building

    Reproducible CLI performance baseline: run the built native CLI against four bounded representative inputs, or use the two-input --smoke check. Raw times, hashes, failures and environment are retained. CI runs smoke only, without a millisecond gate; 50k functions and peak memory are not claimed as measured by this local baseline.

    moon build --target native --release ./_build/native/release/build/cmd/main/main.exe app.wasm

    The module declares supported_targets = "+native", so a project that depends on it builds with --target native as well.

    #Data segment encodings

    moonsize app.wasm --data-segments --top 20 moonsize app.wasm --data-json new-data.json moonsize app.wasm --html new-report.html --data-json new-data.json

    The 0.5.0 data-segment feature accounts for data-entry payload, encoding metadata and section overhead in file bytes. Active offset expressions are recorded without evaluation; no source attribution or deletion savings are inferred. Text Top does not truncate the JSON or totals. It supports ordinary text, strict, max-size and baseline (single-file details unless --diff-data is enabled), and rejects graph, experiment, path, budget and function-diff combinations. HTML accepts current-input details explicitly: a static first 20 rows remain readable without JavaScript, and the button appends 20 at a time. HTML and JSON share the same ledger. Printing includes the displayed subset; --top remains a text-only limit. HTML does not accept --diff-data. See encoding ledger, real sample and API migration.

    With --baseline before.wasm --diff-data, the text report also compares exact payload content groups, including duplicate counts and encoding overhead. Add --data-json new-diff.json for the independent moonsize-data-segments-diff format; without --diff-data, JSON keeps the original single-file format. Groups do not establish source identity, equivalent initialization or safe deduplication, and raw differences do not predict rebuild or gzip savings.

    This source addition follows public 0.5.1; it has not been republished.

    moonsize app.wasm --find-data "assets/logo" --top 20 moonsize app.wasm --find-data "assets/logo" --find-data-json new-search.json moonsize app.wasm --find-data-hex 00FF00 --find-data-json new-hex-search.json --top 1

    Search exact UTF-8 bytes inside each data payload, retaining overlapping hits. Spaces matter; no normalization or cross-entry matching is performed. Results record original segment indices and payload/file byte offsets, not memory addresses. Query text is included in reports. Top limits text only; JSON and counts remain complete, and memory grows with occurrences. Hits do not prove standalone strings, resource use or deletion savings; compiler encodings can differ. This independent mode accepts only Top and its own new JSON target. Alternatively, --find-data-hex accepts compact, nonempty, even-length ASCII hex pairs; case is equivalent and reports use lowercase. It matches actual bytes, including NUL and invalid UTF-8, without text conversion or encoding detection. The two queries are mutually exclusive. Shared --find-data-json exports moonsize-data-search for text and moonsize-data-hex-search for hex. No spaces, 0x prefixes, separators or wildcards. See matching rules, API and verified positions.

    #Bounded data payload preview

    This source addition follows public 0.5.1 and has not been republished. Coordinates below are illustrative; use your own search result:

    moonsize app.wasm --find-data "assets/logo" moonsize app.wasm --data-preview 1 --data-offset 8 --data-limit 64 moonsize app.wasm --data-preview 1 --data-preview-json new-preview.json moonsize after.wasm --data-preview 2 --data-offset 8 --data-limit 32 \ --baseline before.wasm --baseline-data-preview 1 --baseline-data-offset 4 \ --data-preview-json new-window-diff.json

    Preview uses the original entry index and payload byte offsets. Default offset is 0 and limit is 256; valid limits are 1..4096. It clips at payload end and permits an empty end window, without crossing entries or padding data. Text shows hex plus printable ASCII (other bytes become dots); JSON contains the complete returned window, not a full payload export. Both expose actual content. No encoding or memory address is inferred. This independent mode rejects Top and other analysis/report modes. An explicit baseline preview pair compares equal window-relative positions: both indices are required; offsets independently default to zero and a common limit clips each side. Missing bytes are null (--), not 00. No resource matching or insertion/deletion alignment. --diff-data instead groups complete payload content. See byte contract, API and actual ranges.

    #Compare two function dependency sets

    Source addition after public0.5.1: compare two arbitrary full function indices in one input, including imports and each start itself.

    moonsize app.wasm --compare-dependencies 12 47 --top 20 moonsize app.wasm --compare-dependencies 12 47 --compare-dependencies-json new-overlap.json --top 1

    Indices are placeholders. shared/left_only/right_only/neither partition the full index space with count and local-byte ledgers. Single-side is relative to these starts, not global exclusive attribution; neither is not dead code or removal savings. Only Top, own JSON and strict combine; no baseline. Top applies to each of three text tables; JSON retains all groups and both complete edges. One build, at most two old closure BFS calls (one for equal starts). Actual sets, byte ledgers, entrypoint distinction and API.

    #A model path between any two functions

    Source addition after public0.5.1: explicitly choose FROM and TO in the full function index space, including imports. The source need not be a model root.

    moonsize app.wasm --path-between 12 47 moonsize app.wasm --path-between 12 47 --path-between-json new-path.json --strict

    Indices12/47 are placeholders. Only own JSON and strict combine; explicit Top is rejected. Found, zero-step and within-model not-found all return0; strict checks full-module diagnostics before writing. The complete dependencies closure is prepared once, then predecessors are reconstructed without another search. This is a model path, not execution, stack depth or removal benefit. Exact path, preparation cost, null states and API.

    #Transitive function model referrers

    Source addition after public 0.5.1: --dependents INDEX lists every modeled source that can reach any valid function, including imported targets. Edges retain caller-to-callee direction; each node keeps one next edge toward the target and a minimum model depth. Complete internal edges remain separate.

    moonsize app.wasm --dependents 12 --top 20 moonsize app.wasm --dependents 12 --dependents-json new-dependents.json --top 1 moonsize app.wasm --dependents 12 --dependents-dot new-referrers.dot --top 1

    Index12 is a placeholder. Only Top, strict and own JSON or DOT combine; strict checks full-module diagnostics. Top limits text rows, not traversal or JSON. This is not source-change impact or removal savings. Contrast callers (one-hop incoming), dependencies (outgoing closure), and why-index (one root path). Direction, actual accounting, API and limits.

    Incoming DOT keeps original caller → callee arrows; depth to target counts the minimum model steps toward the query target. Top never clips the graph; own JSON and DOT are mutually exclusive. Generation needs no Graphviz, while Linux CI checks actual layout text. Incoming DOT contract.

    #Transitive function model dependencies

    Source addition after public 0.5.1: start at any full function index, including imports, and list its transitive reference-model closure. The target is included once; other nodes keep minimum model depth and one first-discovery edge. Full closure evidence retains parallel/direct/reference/candidate edges separately from the predecessor tree. Local bytes are unique per index, not retained or removal savings; imported encoded sizes remain null.

    moonsize app.wasm --dependencies 12 --top 20 moonsize app.wasm --dependencies 12 --dependencies-json new-dependencies.json --top 1 moonsize app.wasm --dependencies 12 --dependencies-dot new-dependencies.dot --top 1

    Index12 is a placeholder. Only Top, strict and own JSON or DOT combine; JSON and DOT are mutually exclusive. Strict uses whole-module diagnostics, including outside the target closure. Top limits text, never the complete JSON or DOT. DOT generation needs no Graphviz; parallel edges, candidates and diagnostics remain. This local graph does not determine global dead code. Windows checks for 923b9ba did not include Graphviz. Linux CI now requires Graphviz -Tjson structure and layout-text checks, with per-commit consumer artifacts; this is not manual visual, font-glyph, browser or print acceptance. Actual depths, byte ledgers, related queries and migration.

    #Reference cycle components

    Add --cycle-witness to cycles or cycles-json for one deterministic closed model path per component, starting at its minimum index. It may show only some members and does not promise a globally shortest cycle or actual recursion. Original edges and diagnostics remain complete; Top never truncates shown steps or JSON. Without the modifier the old query/format is unchanged.

    moonsize app.wasm --cycles --cycle-witness --top 10 moonsize app.wasm --cycles-json new-witnesses.json --cycle-witness --top 1

    Source addition after public 0.5.1: query strongly connected components in the current reference graph, including unreachable functions and self-loops. Direct calls, references taken and indirect candidates remain distinct. Model cycles do not prove runtime recursion; component encoded bytes are not retained or deletion savings. Only Top, own JSON and strict may combine.

    moonsize app.wasm --cycles --top 10 moonsize app.wasm --cycles-json new-cycles.json --top 1

    Top limits components in text, not their members or complete JSON. Strict refuses incomplete graphs before output creation. Actual components, edge positions, ledgers and API migration.

    #File range ownership

    Source addition after public 0.5.1: --locate-range OFFSET LENGTH explains a half-open file range, without opcode decoding. Empty ranges through EOF are valid; out-of-bounds requests fail rather than clip. Each row separates full owner_range from actual overlap_range; all intersections continuously cover the query. Only Top and own JSON combine; Top never truncates JSON or coverage.

    moonsize app.wasm --locate-range 40 32 --top 20 moonsize app.wasm --locate-range 40 32 --locate-range-json new-range.json --top 1

    Coordinates above are placeholders, not measurements. No runtime addresses or optimization benefit is inferred. Actual 13-part ledger, API and errors.

    #File offset ownership

    A source addition after public 0.5.1; not republished. Example 31 is a placeholder:

    moonsize app.wasm --locate-offset 31 moonsize app.wasm --locate-offset 31 --locate-offset-json new-location.json

    Locate one decimal file byte in its physical section and, for a selected code or data payload, its count field, function prefix/body or data metadata/payload. Ranges are half-open; EOF has no byte and fails. Custom sections count toward physical ordinals, imported functions toward function indices. Other payload interiors stay undivided. This independent mode has no Top or strict and accepts its own optional new JSON and --locate-decode. Default locating does not decode instructions; the modifier refines only a hit local body to locals or one whole instruction. Non-body bytes are not_applicable; a bad target fails even for a locals query. Decoded JSON uses moonsize-file-offset-decoded with the base location nested; default JSON is unchanged. No runtime-address lookup or full semantic validation is claimed. Base structure checks and whole-file SHA still scan the input. See nine regions, API and actual ranges.

    #Find opcode locations

    Source addition after public 0.5.1; not republished:

    moonsize app.wasm --find-opcode i32.const --top 20 moonsize app.wasm --find-opcode i32.const --find-opcode-json new-matches.json --top 1

    Match a complete decoded mnemonic, case-sensitively, in all local functions. Results retain full function indices, original instruction ordinals and file/body positions. Immediates, initializer expressions, names and payload text do not become opcode sites. Every body is decoded once; any bad body fails the query. Top limits global location rows, not scan cost or complete JSON. Only query, Top and own new JSON may be combined; no strict or baseline. Empty results do not describe other module regions. Rules, actual positions and API. Use separate commands to go from opcode statistics to this query, then to --instructions or --locate-offset --locate-decode. No automatic multi-pass mode.

    #Module-wide function opcode statistics

    Source addition after public 0.5.1; not republished:

    moonsize app.wasm --opcode-stats --top 20 moonsize app.wasm --opcode-stats-json new-opcodes.json --top 1 moonsize after.wasm --baseline before.wasm --diff-opcodes --top 20

    Decode every local body once and aggregate mnemonic counts, complete instruction encoding bytes and distinct-function counts. Bytes include subopcodes/immediates; function_count is deduplicated within each function and cannot be summed to a function total. Imports have no synthetic body; initializer expressions are excluded. One bad body fails the whole query, naming its full index. The result keeps only opcode groups and compact function summaries, without instruction lists or a matrix. Top limits text only, not all-body decoding cost or JSON. Single-input statistics accept Top and own new JSON. For two complete modules, --diff-opcodes requires baseline and reuses --opcode-stats-json for a complete comparison. It separates instruction deltas from code header/count/prefix/locals overhead, without matching functions. Zero statistical deltas do not prove equal bytes or semantics. Neither mode accepts strict, HTML or budgets. Static statistics are not runtime hotspots or savings. Code byte ledger, API and real output.

    #Local access instruction encodings

    Source addition after public 0.5.1; not republished. Index 3 is illustrative:

    moonsize app.wasm --local-accesses 3 --top 20 moonsize app.wasm --local-accesses 3 --local-accesses-json new-accesses.json --top 1

    List actual local.get/set/tee positions and index encoding widths in one fully decoded local body, with sparse numeric-index groups. Repeated sites remain; tee counts once. The encoded local index includes the parameter index space, unlike parameter-excluding --locals ordinals. No signature, parameter/declaration binding, variable names/types or actual index-range validation is provided. The decoder currently limits operands to nonnegative Int, not full u32. Static counts are not execution frequency; encoded bytes do not measure runtime memory or savings. Top independently limits each text table, keeping totals, members and JSON complete. This standalone mode accepts only Top and own new JSON, without strict. Scope, API and measured access ledger.

    #Local declaration groups in one function

    Source addition after public 0.5.1; not republished. Index 3 is illustrative:

    moonsize app.wasm --locals 3 --top 20 moonsize app.wasm --locals 3 --locals-json new-locals.json --top 1

    Inspect original locals groups, quantities and exact type bytes without decoding instructions or expanding variables. Zero groups and zero-quantity groups differ; ordinals exclude parameters and are not complete local indices. Quantities use u32; cumulative counts/ordinals use decimal strings in JSON. Limited type labels do not validate types, and the body remainder stays undecoded. Top limits text rows only; JSON is complete. The single-input query accepts Top and its own new JSON, without strict. An explicit comparison additionally requires both --baseline and --baseline-locals; targets are never matched automatically. Scope, API and measured ledger.

    #Explicit locals comparison

    moonsize after.wasm --locals 5 --baseline before.wasm --baseline-locals 3 --top 20 moonsize after.wasm --locals 5 --baseline before.wasm --baseline-locals 3 --locals-json new-locals-diff.json --top 1

    Indices are placeholders in each file's complete function index space. Both targets are explicit; full raw type_hex buckets describe encoding statistics, not variable identity or semantic type equivalence. Zero-quantity groups count as present. Cumulative quantities and signed quantity deltas use exact decimal strings; actual encoding widths are measured separately. Remainders stay undecoded. Top limits only displayed type rows, keeping all member indices and nested JSON complete. The existing single-input JSON stays moonsize-function-locals; the comparison uses moonsize-function-locals-diff/schema 1. Zero statistical differences do not prove equal group sequences, remainder contents or semantics. This is an unpublished source increment after 0.5.1, without deletion/runtime/gzip estimates.

    #Calls inside one function

    Source addition after public 0.5.1; not republished. Index 3 is illustrative:

    moonsize app.wasm --calls 3 --top 20 moonsize app.wasm --calls 3 --calls-json new-calls.json --top 1

    List call/return_call, table calls and reference calls at their original positions in one local body. Direct targets retain full indices and identities; dynamic targets remain null. ref.func is excluded. A local function with no calls succeeds; imports have no local body and are rejected. Top limits text rows, not the full JSON or decode cost. This independent mode has no strict and only accepts Top and its own new JSON. Calls are static instruction facts, not execution counts or savings. Callers queries incoming model references; calls inspects body call instructions, so these are not a simple graph reversal. Scope, API and actual call-site ledger.

    #Function and package comparison JSON

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize after.wasm --baseline before.wasm --diff-functions-json new-function-diff.json --top 20 moonsize after.wasm --baseline before.wasm --diff-functions --diff-functions-json new-growth.json --max-size 5KB

    The JSON flag implies --diff-functions and exports that same existing comparison. Top never truncates the complete functions, packages or matching coverage. The independent moonsize-function-diff/schema_version 1 format records both input hashes, code/function/overhead totals, signed current-minus-baseline deltas and all five symbol states. Absent sides remain null; ambiguous and missing names retain one row per function per side. Even self comparison can have nonzero one-sided uncertain rows whose deltas cancel. A unique raw-symbol match is not semantic identity, and added/removed symbols do not prove source changes.

    This JSON option allows baseline, Top, retained, dead-code, strict, max-size and the explicit text flag. Other reports, compression, independent queries, budget configuration and data details are rejected before I/O. Old text-only combinations stay unchanged. Strict checks the current input graph only and refuses before writing. With baseline, max-size gates file growth; a failure can still save the comparison before exit 3. Existing targets are refused with exit 1. No retained, graph completeness, gzip or future source-edit savings are inferred. See fields, coverage ledgers and JSON.parse example.

    #Complete analysis JSON

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --summary-json new-summary.json --top 1 --retained --dead-code moonsize app.wasm --summary-json new-strict-summary.json --strict --max-size 100KB

    An additional export of the same ordinary Analysis, with every physical section, local function, symbol module group, model root and diagnostic. Top affects text only. The versioned moonsize-summary/schema_version 1 format keeps numeric byte ledgers, nullable raw names/retained results, booleans and complete provenance. Module names are existing symbol attribution labels, not verified source ownership. Retained dominated bytes include the function itself; other dominated function counts exclude it. Missing results stay null. Model-unreachable is not safe removal; retained sets can overlap and must not be summed. Function/module bytes are already contained in section bytes, so those views must not be added together.

    Only Top, retained, dead-code, strict and max-size may accompany this export. Strict refusal returns 3 before creating a file. Non-strict incomplete models keep all diagnostics. A max-size violation still saves the report before returning 3; JSON contains analysis, not a budget conclusion. Existing destinations are refused with exit 1. No compression, baseline, independent queries, budget configuration, data details, HTML or complete call edges; use --call-graph for edges. See field definitions, real evidence and JSON.parse example.

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --find-functions 'demo::sample' --top 20 moonsize app.wasm --find-functions 'classify' --find-functions-json new-search.json

    Searches available raw symbols and display names of local definitions using a case-sensitive literal substring, preserving the original query, including spaces. No regex, wildcards or Unicode normalization. Generated unnamed placeholders are excluded; a real raw name equal to (unnamed) remains searchable. Duplicate names keep their distinct full function indices, including preceding function imports. Imports and export aliases are outside this search; use --imports for declarations.

    Top limits text only; counts, byte totals and JSON remain complete. No match, no available names or no local functions all return 0, with distinct explanations. This independent mode accepts only Top and its own JSON, rejects strict and other modes before I/O, and uses exclusive output creation. Choose an index from the results and explicitly run --instructions, --callers or --why-index next. A name is not a unique identity; no match does not prove absence. Matched encoding bytes exclude section headers, names and dependency closures, and are not savings. See measured results, examples, coverage and API.

    #Single-function instruction encoding

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --instructions 3 --top 20 moonsize app.wasm --instructions 3 --instructions-json new-instructions.json

    Uses the full function index space, with imports first; an imported target has no local body and returns an input error. Only the selected body is decoded. The report separates its length prefix, locals encoding and instructions, and lists opcode counts, encoded bytes and original-file instruction ranges. Top limits both text tables; JSON contains every group and instruction. This independent mode accepts Top, its own JSON and an optional explicit baseline/index pair, rejects strict and other modes before I/O, and refuses to overwrite existing output. Counts and bytes describe static encoding, not execution frequency, timing or savings. To compare two explicitly selected local functions:

    moonsize after.wasm --instructions 7 --baseline before.wasm --baseline-instructions 5 --top 20 moonsize after.wasm --instructions 7 --baseline before.wasm --baseline-instructions 5 --instructions-json new-instructions-diff.json

    Both indices are required, even when numerically equal. Each target is inspected once. Exact opcode mnemonics compare counts and encoded bytes; full prefix, locals and instruction ledgers use current minus baseline. Different names are allowed; names and indices do not establish cross-build identity. Zero aggregate deltas do not prove equal sequences, immediate values, semantics or compatibility. Top limits text rows only. The independent moonsize-function-instructions-diff JSON nests both complete reports as objects; single-file JSON remains unchanged. Baseline/current errors retain their target and original cause. No automatic matching, instruction alignment, performance or complete-build savings prediction. See scope, measured ledgers and API.

    #Duplicate data payloads

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --data-duplicates --top 20 moonsize app.wasm --data-duplicates-json new-data-duplicates.json

    Groups exact payload bytes within one input, excluding modes, memory indices, offset expressions and length prefixes. SHA-256 and length select candidate buckets; every match is confirmed against the bytes. Member indices refer to the complete original data report, retaining each encoding and unevaluated expression. Empty payloads participate, including zero-byte groups. This differs from function --duplicates and cross-build --diff-data; it does not infer safe deduplication.

    Top limits text groups and each group previews at most 20 members, with displayed and total counts. JSON retains all groups and entries. The measured 74 B fixture has 3 duplicate groups, 7 members and 1 singleton: payload 16 = all duplicate-group members 13 + singletons 3 B; data section 56 = header 3 + count 2 + entries 51 B. These are file-byte facts, not estimated savings or runtime memory usage. The independent mode accepts only Top and its own JSON, rejects strict/other modes before input I/O, and inherits exclusive output creation. See full ledgers, collision checks, scope and API.

    #Visual report development

    Start the local HTTP server before visual acceptance, screenshots, printing, or browser tests:

    npm run dev

    Then open http://localhost:5173/. The root serves examples/report.html; use this HTTP URL for all browser-based checks rather than opening the report with file://.

    #Usage

    #Help by task

    No Wasm input is needed. This is a source addition after public 0.5.1; build the current native CLI. Help examples use placeholder paths/indices, not measurements.

    moonsize --help moonsize -h moonsize --help-topic functions

    Topic IDTaskExisting guides
    overviewOrdinary report, Top, HTML and summary JSONHTML delivery, summary JSON
    functionsFind an index, inspect instructions and model referencesSearch, instructions, callers
    dataData entries, duplicate payloads and comparisonsData encodings and comparisons
    interfacesImports/exports and their baseline comparisonsImports, exports
    metadataCustom/name overhead, outer comparison and partial detailsMetadata
    baselineFile, function/package and selected-function comparisonsFunction/package JSON, experiments
    ciSize gates, budget evidence and supported strict modesBudgets

    Only the first user argument triggers help; flags inside business arguments are left to the existing parser. Help returns 0; malformed help requests return 2 with available topics. No input is read and no report is created. See help syntax and dispatch rules. The full option table remains below.

    Unknown ASCII dash-prefixed options now return 2 before file/config I/O. End option parsing with -- to read a dash-prefixed filename, including a file named --help:

    moonsize -- --help moonsize --top 1 --summary-json new-summary.json -- --app.wasm

    After the separator, tokens are positional paths only; at most one input is accepted overall. Place all options before it. Known option-value constraints are unchanged; use an explicit path such as ./--name where needed. A single - is a filename, not stdin. This source behavior follows public 0.5.1; earlier implicit dash-prefixed input calls must use -- or an explicit relative path.

    usage: moonsize <file.wasm> [--top <n>] [--retained] [--dead-code] [--compress] [--strict] [--call-graph <path>] [--html <path>] [--max-size <size>] [--baseline <path.wasm>] [--diff-functions] [--diff-functions-json <new-path.json>] [--summary-json <new-path.json>] [--data-segments] [--diff-data] [--data-json <new-path.json>] [--budget <config.json>] [--budget-json <path.json>] [--budget-md <path.md>] [--experiment <report.html|report.json>] [--experiment-context <context.json>] moonsize <file.wasm> --find-functions <query> [--top <n>] [--find-functions-json <new.json>] moonsize <file.wasm> --instructions <n> [--top <n>] [--instructions-json <new.json>] moonsize <after.wasm> --instructions <n> --baseline <before.wasm> --baseline-instructions <n> [--top <n>] [--instructions-json <new.json>] moonsize <file.wasm> --data-duplicates [--top <n>] [--data-duplicates-json <new.json>] moonsize <file.wasm> --imports [--top <n>] [--imports-json <new.json>] moonsize <file.wasm> --baseline <before.wasm> --diff-imports [--top <n>] [--imports-json <new.json>] moonsize <file.wasm> --metadata [--top <n>] [--metadata-json <new.json>] moonsize <file.wasm> --duplicates [--top <n>] [--duplicates-json <new.json>] moonsize <file.wasm> --entrypoints [--top <n>] [--entrypoints-json <new.json>] [--strict] moonsize <file.wasm> --generics [--top <n>] [--generics-json <new.json>] moonsize <file.wasm> --callers <n> [--top <n>] [--callers-json <new.json>] [--strict] moonsize <file.wasm> --why-index <n> [--why-root <n>] [--why-json <path.json>] [--strict] Report the section table of a WebAssembly binary, rank its heaviest functions, roll the bytes up by module, and follow the call graph to identify candidates for investigation. --top <n> how many functions to rank (default 10) --retained dominated function bytes in the current graph model --dead-code functions no root can reach --compress whole-file gzip and independent section gzip sizes --data-segments append data-entry file bytes (payload-ranked by --top) --data-json <path> enable details and write all entries to a new .json file offset expressions are recorded, not evaluated --diff-data compare exact payload content groups; requires --baseline --data-json then writes the data comparison format --strict refuse to report when the call graph is incomplete, instead of reporting it with a warning --call-graph <path> write a new .dot or .json call graph; refuses existing paths --html <path> write a new self-contained HTML report; refuses existing paths --data-segments or --data-json adds current-input data details --max-size <size> fail with exit code 3 above this size, where size is bytes or a KB/MB/GB count such as 10KB or 1.5MB --budget <path> validated multi-metric byte budgets; growth needs --baseline --budget-json <path> write budget JSON; requires --budget, refuses existing paths --budget-md <path> write CI Markdown summary; requires --budget, refuses existing paths --baseline <path> compare against another module and print the change; --max-size then bounds the growth, not the file --diff-functions append function and package changes; requires --baseline --experiment <path> write measured before/after results; requires --baseline refuses to overwrite an existing path --experiment-context <path> attach declared build settings and validation --locate-range <offset> <length> decompose file ownership; EOF empty allowed --locate-range-json <path> complete ownership JSON to a new target --locate-offset <n> locate one decimal file byte; EOF fails; no Top --locate-decode opt-in whole target-body decoding to locals/instruction; requires query --locate-offset-json <path> location to a new .json; requires query --data-preview <index> bounded payload bytes (no Top); original entry index --data-offset <n> payload byte offset, default 0; requires --data-preview --data-limit <n> 1..4096 bytes, default 256; requires --data-preview --baseline-data-preview <index> explicit baseline entry, requires baseline/current preview --baseline-data-offset <n> independent baseline payload offset, default 0; comparison only --data-preview-json <path> returned window or explicit comparison to a new .json --find-data <query> exact UTF-8 byte search within data payloads, including overlaps --find-data-hex <hex> exact payload bytes from compact hex pairs; excludes --find-data --find-data-json <path> save every hit to a new .json; requires either query --find-functions <query> case-sensitive literal search of local raw/display names --find-functions-json <path> save all matches to a new .json; requires a query --diff-functions-json <path> save full function/package comparison; implies diff-functions and requires baseline --summary-json <path> save full ordinary analysis to a new .json; Top never truncates it --instructions <n> inspect a local function in the full function index space --instructions-json <path> save complete groups/instructions or explicit comparison to a new .json --baseline-instructions <n> explicit baseline function index; requires instructions/baseline --data-duplicates group exact data payloads within the current input --data-duplicates-json <path> write full groups and source entries to a new .json only Top and own JSON; text previews 20 members per group --imports list host import declarations and their encoding bytes --imports-json <path> write all declarations to a new .json file; implies --imports unsupported descriptors fail; baseline requires --diff-imports --diff-imports compare import names/kinds/counts; requires --baseline type compatibility not evaluated; differences still return 0 --exports list exact host-visible declarations and declared-target aliases --diff-exports compare exact export names/kinds; requires --baseline --exports-json <path> write complete exports to a new .json file; implies --exports --metadata account for custom/name physical file bytes --diff-metadata compare outer custom encodings; requires --baseline --metadata-json <path> write full metadata to a new .json file; implies --metadata partial optional name details still return 0 with diagnostics --duplicates group exact local body bytes, excluding length prefixes --duplicates-json <path> write complete groups to a new .json file --entrypoints show model-reachable, exclusive and shared function bytes --entrypoints-json <path> write complete results to a new .json file --generics group supported raw symbols by encoded function bytes --generics-json <path> write full groups to a new .json file; implies --generics --find-opcode <mnemonic> exact decoded locations in all local bodies --find-opcode-json <path> all matching sites to a new .json; requires query --opcode-stats aggregate all local function instruction encodings; Top/own JSON --diff-opcodes compare both module aggregates; requires --baseline --opcode-stats-json <path> full opcode/function summaries to a new .json; implies mode --local-accesses <n> inspect local.get/set/tee encodings; Top/own JSON only --local-accesses-json <path> save full accesses/groups to a new .json; requires query --locals <n> inspect local declaration groups; Top/own JSON or explicit baseline pair --locals-json <path> save all groups or explicit comparison to a new .json --baseline-locals <n> baseline target; requires locals and baseline --calls <n> list call instructions in one local body; Top/own JSON only --calls-json <path> save all call sites to a new .json; requires calls --compare-dependencies <left> <right> two outgoing closures in one input; Top/strict --compare-dependencies-json <path> full sets to a new .json; requires pair --path-between <from> <to> one shortest model path; strict/own JSON only, no Top --path-between-json <path> full path to a new .json; requires explicit pair --dependents <n> transitive model referrers to any function; Top/strict --dependents-json <path> full reverse closure to a new .json; requires dependents --dependents-dot <path> full incoming subgraph with original arrows; exclusive with JSON; Top only limits text --dependencies <n> transitive model closure from any function; Top/strict allowed --dependencies-json <path> full closure to a new .json; requires dependencies --dependencies-dot <path> full subgraph to a new .dot; exclusive with JSON; Top only limits text --cycles query reference-model cyclic components; Top/strict allowed --cycle-witness add one closed model path per component; requires cycles --cycles-json <path> complete cycles JSON to a new .json; implies cycles --callers <n> list recorded one-hop incoming instruction sources --callers-json <path> write full callers JSON to a new .json file; requires --callers --why-index <n> explain a shortest model reference path to function n --why-root <n> search from one actual model root (requires --why-index) --why-json <path> write path JSON; refuses existing paths query mode allows only these flags and --strict element functions are conservative roots; paths are not execution traces or predictions of savings All report outputs refuse existing files, directories and link aliases. Targets are checked together; each file is created exclusively. Output errors use exit 1. Multiple files are not a transaction; a later failure may leave earlier new outputs. Exit codes: 0 completed, 1 input/query/output/unavailable measurement, 2 bad arguments/config, 3 over budget, incomplete strict graph, or path search limited.

    Without flags it prints the section table, the heaviest functions and the per-package roll-up. --retained and --dead-code add their sections and can be combined. --call-graph and --html each write a file instead of printing a report, so they are used on their own:

    moonsize app.wasm --retained --dead-code # the full text report moonsize app.wasm --html report.html # charts, opens in a browser moonsize app.wasm --call-graph graph.dot # for Graphviz

    All report destinations, including HTML and DOT/JSON graphs, refuse existing files, directories and link aliases. Output protection failures return 1. Targets are checked together after parsing and strict refusal; each write uses native exclusive creation. This prevents truncation if a target appears after preflight, but is not a multi-file transaction or crash-atomic write. Later failure can leave earlier new files or a partial new file; hostile parent-directory changes remain outside this guarantee. See protection and concurrency boundaries. Generate an update at a fresh staging path, verify it, then explicitly replace the published copy yourself. No production Node dependency or overwrite flag is added.

    #Optimization experiments

    moonsize after.wasm --baseline before.wasm --experiment experiment.html \ --experiment-context context.json --top 20

    The offline HTML report (or .json for CI) combines artifact SHA-256, measured file and whole-file gzip changes, section/function/package differences, graph diagnostics, and declared build/validation records. Different build settings, failed checks, and missing records remain visible; smaller output does not prove equivalent behavior. Context commands are recorded, never executed. Existing output paths are refused. See the format and reproducible workflow and sample report.

    #Multi-metric budgets and CI summaries

    Use a versioned JSON configuration to check several measured byte limits:

    moonsize after.wasm --baseline before.wasm --budget moonsize-budget.json \ --budget-json budget-result.json --budget-md budget-summary.md

    Version 1 has a nonempty rules array. Every rule needs a unique nonempty id, metric, mode and integer max_bytes (0 through 2147483647). Supported metrics are file.raw, whole-file file.gzip, complete code.raw, and package.raw; the last also requires an exact package label from the report. Unknown fields, fractional/exponent tokens and invalid values are rejected before reading Wasm. absolute checks current bytes; growth checks current minus baseline and requires --baseline. Equality passes and shrinkage retains its negative delta.

    Package bytes sum local function total_size under existing symbol attribution, including body size prefixes. Missing names or unresolved ownership make package rules unavailable. A fully attributed growth comparison can record a newly observed/disappeared label as zero on the missing side; a label absent on both sides is unavailable. Successful file/code/gzip measurements are retained. Package totals are not total source-package costs or deletion savings; gzip is one whole-file measurement with the current compressor defaults, not predicted network cost. Independent gzip streams are never added.

    Each rule reports passed, exceeded or unavailable, its measurements and reason. Exits are 0 for all passed, 3 for evaluable rules with an exceeded limit, 1 for input/measurement/attribution/output errors (taking precedence over exceeded), and 2 for arguments/configuration. Strict retains its existing refusal code 3 before normal reports are written. JSON/Markdown refuse existing destinations; they include artifact hashes, coverage and metric scope. Both outputs can be requested together. --budget conflicts with --max-size, path queries, experiments, HTML and graph export; ordinary text options remain available.

    Configuration, APIs, migration and exact semantics include a reproducible source demo and the actual demo configuration. Run it before this example, whose package label belongs to that demo:

    node scripts/demo-budget.mjs _build/native/release/build/cmd/main/main.exe _build/native/release/build/cmd/main/main.exe _build/budget-demo/after.wasm \ --baseline _build/budget-demo/before.wasm --budget examples/budget.json

    CI runs the budget regression and source/rollback demo, saves JSON/Markdown, and applies file/gzip/code budgets to the freshly built example. Its generated Markdown is also appended to the CI step summary.

    #Incoming function references

    This is a source-checkout addition after public 0.5.1; rebuild the native CLI. No new Mooncakes version is published by this change.

    moonsize app.wasm --callers 42 --top 20 moonsize app.wasm --callers 42 --callers-json new-callers.json

    --why-index finds one root-to-target model path; --callers lists all recorded one-hop body sources, including unreachable callers. Distinct positions are retained; direct calls, references taken and indirect candidates are labeled separately. Top limits text caller groups; JSON and counts remain complete. Only Top, callers JSON and strict may accompany this independent query. Empty results do not prove removability. Strict gaps return 3 before output checks; JSON uses exclusive creation. See actual sample, API and scope.

    #Host import declarations

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --imports --top 20 moonsize app.wasm --imports-json new-imports.json

    Lists physical function, table, memory, global and tag import declarations, with exact module/field names, per-kind indices and original file ranges. Repeated names remain separate. The measured fixture's import section is 138 B = 3 B header + 2 B count prefix + 133 B declarations; declaration bytes decompose into 64 B module names + 39 B field names + 30 B descriptors. Module summaries exclude section overhead. These are declaration encoding bytes, not host/package sizes.

    The supported subset covers short funcref/externref table types, 32-bit limits (including shared memory with a maximum), the listed global scalar/vector/reference types, and tag attribute 0. Typed references, other GC forms, memory64/table64 and unsupported flags fail explicitly. Core import errors never become partial success. Top limits text entries only; summaries and declaration-order JSON remain complete. Single-file mode allows Top and its own JSON; baseline requires --diff-imports. Strict and other report modes are rejected before input I/O. Output inherits exclusive creation. To compare two builds:

    moonsize after.wasm --baseline before.wasm --diff-imports --top 20 moonsize after.wasm --baseline before.wasm --diff-imports --imports-json new-imports-diff.json

    Exact (module_name, field, kind) groups compare multiplicities, retaining both complete inventories. Overlap is a quantity intersection, not physical member matching. same_count does not mean unchanged or type-compatible; type indices, limits and mutability can differ. Top limits text groups only; comparison JSON is moonsize-imports-diff/schema_version 1, while single-file JSON remains unchanged. The measured pair has 9 declarations on each side: overlap 6, removed 3, added 3. Its import section delta is 11 B = 1 header + 1 count prefix + 9 declaration bytes, all current minus baseline. Reordering creates no quantity change. Differences still return 0; parse errors retain side, path and reason. Declarations do not prove runtime use, permissions, host compatibility or removal savings. See the support matrix, comparison contract and actual ledgers.

    #Export declarations and aliases

    This is a source addition after public 0.5.1; build the native CLI from source.

    moonsize app.wasm --exports --top 20 moonsize app.wasm --exports-json new-exports.json

    Lists function, table, memory, global and tag declarations with exact UTF-8 host names, full u32 target indices and original file ranges. Names are not demangled internal symbols. Duplicate names across any kinds are rejected. Targets group by both kind and index: function #0 and table #0 are separate. Singleton targets are retained too. This does not validate target existence, signatures, origin or reachability; re-exported functions can be imports without a local body.

    The measured five-kind fixture has 8 declarations, 5 declared targets, 2 alias groups and 5 alias members. Export bytes: 57 = headers 3 + count 2 + declarations 52; declaration bytes: 52 = name encodings 35 + descriptors 17. These are declaration costs, not body sizes or removal savings. Top limits each text table, while JSON retains all entries/targets using moonsize-exports/schema_version 1. Only Top and own JSON may be combined; invalid combinations return 2 before input I/O, input/ output failures return 1 and successful empty results return 0. New JSON paths use the existing exclusive-create protection. Use --entrypoints separately for model reachability. See scope, actual ledgers and API migration.

    Compare two export inventories by exact full name:

    moonsize after.wasm --baseline before.wasm --diff-exports --top 20 moonsize after.wasm --baseline before.wasm --diff-exports --exports-json new-exports-diff.json

    The four states are added, removed, kind_changed and same_kind. Same-kind targets expose a boolean target_index_changed; other rows use null. This compares index numbers, not cross-build identities or ABI compatibility. Renumbering stays same_kind; alias groups remain complete on each side and are not matched across builds. Top limits text rows only; moonsize-exports-diff/schema_version 1 retains complete nested inventories, all rows and signed ledgers. The measured pair has removed 1, kind_changed 1, added 1, same_kind 5; file +23 B = export +8 B + non-export +15 B. Only baseline, Top, exports and own JSON are allowed. Changes still return 0; input/output failures return 1, invalid combinations return 2. This is a source increment after public 0.5.1, not a new Mooncakes release.

    #Custom sections and symbol-name overhead

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --metadata --top 20 moonsize app.wasm --metadata-json new-metadata.json

    The report accounts for every physical custom section, including repeated names, actual length prefixes, name bytes and opaque content. Classification uses only reliably decoded full UTF-8 names: name, producers, the .debug_ prefix, or other; unavailable names retain their ranges and a separate accounting bucket. All recognized name sections retain physical subsections and function-name records. Indices are metadata declarations, not proof that those functions exist.

    Optional name-content errors produce partial details, diagnostics and remainder ranges while preserving the byte ledger; successful exit 0 does not certify complete interpretation. Outer parse errors still return 1. Top limits details only; summaries and JSON remain complete. This independent mode allows only Top and its own JSON, rejects strict, and inherits exclusive output creation. The measured fixture accounts for 115 custom bytes as 15 header + 48 custom-name encoding + 52 content bytes; these are decompositions, not additive parent/child totals. Metadata is not necessarily removable, and file bytes do not predict recompiled or transfer savings. See the scope, actual ledgers and API.

    Compare outer custom encodings by category and exact full name:

    moonsize after.wasm --baseline before.wasm --diff-metadata --top 20 moonsize after.wasm --baseline before.wasm --diff-metadata --metadata-json new-metadata-diff.json

    Repeated names retain all instances; unavailable names use a separate accounting bucket. Both coverage states and partial name details remain visible. Top limits only text name groups; JSON retains both complete reports and all groups using moonsize-metadata-diff/schema_version: 1. This compares counts and outer bytes, not contents, name internals, DWARF or semantics. Equal names or zero deltas do not establish equal contents or safe removal. The measured fixture changes file bytes by +2 = custom -3 + non-custom +5. Comparison accepts only baseline, Top, metadata and its JSON; exits 0/1/2 mean success (including partial), input/output failure and argument error. This is a source addition after public 0.5.1.

    #Identical function bodies

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --duplicates --top 20 moonsize app.wasm --duplicates-json new-duplicates.json

    Only exact local body bytes (locals declarations plus instructions) determine membership. Length prefixes, names and declared type indices do not. SHA-256 and length select candidates; every match is confirmed against the actual bytes. The actual fixture groups functions #8/#9 despite different type indices and 1/2-byte prefixes, retaining their measured 5/6-byte encodings separately. No names are required. Top limits text groups only; JSON keeps all members and refuses existing targets. This independent mode rejects strict and other modes. Identical bytes do not establish semantic equivalence, safe merging or savings; absence of identical bodies does not establish absence of semantic duplication. See the byte ledger, actual sample and API.

    #Exported entrypoint dependencies

    This source addition follows public 0.5.1; build the native CLI from source.

    moonsize app.wasm --entrypoints --top 20 moonsize app.wasm --entrypoints-json new-entrypoints.json moonsize app.wasm --entrypoints --strict

    Aliases of the same function form one entry group. The report separates each entry's model-reachable local function bytes into exclusive and shared portions, including sharing with start/all conservative element roots. Imports retain null body sizes. One graph is built; each unique entry requires one reachability walk, and Top limits display, not analysis costs. JSON keeps complete membership and aliases and refuses existing targets. Strict gaps return 3 before file checks. The actual shared example has 34 local encoded bytes: 31 in the export union, 3 outside it; the union is 18 exclusive + 13 shared bytes counted once. These are overlapping model sets, not deletion or gzip savings. See the actual sample, ledger, API and limits.

    #MoonBit generic-instance sizes

    This source addition is not part of the published 0.5.1 package. Build the native CLI from source to use it:

    moonsize app.wasm --generics --top 20 moonsize app.wasm --generics-json new-generics.json

    The independent mode accepts only Top and generics JSON; even strict is rejected because no call graph is built. Families use fully parsed raw symbols, never matching display names. The first supported shape is numeric-package free functions/methods with Int or Double type arguments. The real probe identifies four definitions in two families, totaling 26 encoded bytes. Counts distinguish function definitions from distinct raw type-argument encodings. Top limits text only; JSON preserves every family and instance and refuses existing targets. Missing/unreadable names and unsupported symbols are reported, not guessed. Family keys apply within one artifact; totals do not predict removable bytes. See the observed encoding, size ledger and limits.

    #Explaining a function's reference path

    For an arbitrary non-root FROM -> TO query use path-between; why-index and why-root retain their model-root rules.

    --cycles --cycle-witness instead closes a representative path inside each component, without selecting roots or claiming all members are visited. It is a separate query; inspect real opcodes manually with the returned offsets. Closed witness rules.

    For whole-module mutually reachable groups instead of one root path, run --cycles separately. It includes unreachable functions and model self-loops; reference/candidate cycles do not prove runtime recursion. Component weights are local encoded bytes, not retained size. Cycle query.

    After locating a large function's index in the report, ask which modeled entry can introduce it:

    moonsize app.wasm --why-index 47 moonsize app.wasm --why-index 47 --why-root 0 moonsize app.wasm --why-index 47 --why-json function-path.json --strict

    Indices above are illustrative. The query returns one deterministic path with the fewest edges. Roots are ordered by function index; outgoing edges by target index, then direct/reference/indirect kind, then original instruction offset. Every step retains its actual opcode (call, return_call, ref.func, call_indirect, or return_call_indirect) and file offset. Indirect edges are known candidates; ref.func takes a reference rather than proving a call. Unmodeled call_ref targets are not guessed, and graph diagnostics remain visible even when a path is found.

    The default searches all existing model roots: function exports (including all aliases), the start function, and conservative element references. If the target is itself an element root, a zero-edge path is expected and its source is shown. --why-root selects one actual model root without changing global reachability or retained sizes. Failure from that entry does not imply global unreachability. Root evidence records the section start and export/element ordinal; these are explicitly section positions, separate from instruction offsets.

    Results distinguish found, not-found-within-model, and search-limited. An incomplete graph may still yield either of the first two; an absent path is then particularly inconclusive. Imports have encoded_size: null because their bodies are outside this module. Path JSON has its own format moonsize-function-path and schema_version: 1, structured root reasons, ordered nodes/steps, diagnostics, limits and search counters. Existing JSON paths are refused. Strict graph refusal occurs before creating a report.

    Query mode accepts only --why-index, --why-root, --why-json and --strict; other report, comparison, experiment and budget flags conflict before input is read (including an explicit --top). Exit codes are 0 for a completed query (found or absent), 1 for input/index/root/output errors, 2 for invalid arguments, and 3 for a limited search or strict refusal of an incomplete graph.

    The library entry is explain_function(data, target_index, root?, limits?); render with render_function_path or render_function_path_json, and describe errors with describe_function_path_error. It builds the canonical graph from bytes without computing dominators. PathLimits::new() defaults to 100,000 nodes, 1,000,000 edges and depth 256. The node limit separately bounds prepared roots and visited nodes; the edge limit separately bounds root-source records, indexed edges and examined edges. Preparation limits return search-limited too. These limits do not bound binary parsing or construction of the existing call graph; sorting also has its own cost. Existing parsing/analysis signatures are unchanged; Options gains the three optional query fields. Clients inspecting options or matching the new status/error enums should handle the added fields/variants.

    This is a path in the current reference model, not an execution trace or a prediction of recompiled or gzip savings. Measure actual changes by rebuilding both complete artifacts.

    node scripts/test-function-path.mjs _build/native/release/build/cmd/main/main.exe node scripts/demo-function-path.mjs _build/native/release/build/cmd/main/main.exe

    The source demo discovers emitted indices instead of hardcoding them, queries an exported entry through path_upstream to path_target, removes the upstream call, rebuilds, writes an optimization experiment, then restores the source and checks exact bytes and path evidence. Base output is preserved; optional output is intentionally removed, so this is not a claim of full behavioral equivalence. Sources, paths, hashes, outputs and before/after experiment reports stay under _build/function-path-demo/; CI uploads this evidence. Path bytes are never summed as a deletion benefit.

    #Using the library

    The parsing, the analysis and every report are ordinary packages in this module, so a program can take the numbers instead of the text. moon addBigSaltyMan/moonsize and import it; Options::default is the way in, because Options is read-only and no other package may write one.

    fn main {
    let bytes : Bytes = read("app.wasm")
    match @moonsize.parse_module(bytes) {
    Ok(parsed) =>
    match @moonsize.analyze(bytes, parsed) {
    Ok(analysis) => {
    let options = @moonsize.Options::default("app.wasm")
    // What the five heaviest functions cost, as text or as numbers:
    // `analysis.stats` and `analysis.retained` are there to be read.
    println(@moonsize.render_top_functions(analysis.stats, analysis.data.length(), 5))
    println(@moonsize.render_graph_warning(analysis.graph))
    println(@moonsize.render_dead_code(analysis, 5))
    println(@moonsize.render_retained(analysis, 5))
    println(@moonsize.render_html(analysis, options.path))
    }
    Err(error) => println(@moonsize.describe_error(error))
    }
    Err(error) => println(@moonsize.describe_error(error))
    }
    }

    The library does no I/O, so reading the file and choosing an exit code stay with the caller. supported_targets = "+native" applies to a program that depends on this module too: build it with --target native.

    #Analysis

    Everything below is built on one idea: a function's bytes only matter if something can call it.

    #Symbol names

    Attribution starts with the names in the binary's name section, and MoonBit does not publish how it mangles them, so the rules here were read back from the name section of real builds (moonc 0.1.20260920, the wasm backend):

    • a symbol is _M0<kind> and then a path: F a function, M a method, I a trait implementation;
    • the package is B for the builtin package, C for moonbitlang/core, or P and an index — which runs into the first component's own length, so P55probe is index 5 followed by 5probe;
    • a component is its length and then its name, and the length counts the mangled name;
    • anything that cannot appear in an identifier is escaped as _ and the byte in hex, and a literal underscore is doubled: to__string_2einner is to_string.inner, _24default__impl is $default_impl, and a non-ASCII character is escaped one byte at a time, so 中 is _e4_b8_ad;
    • a generic instantiation carries G...E with one code per argument, where a builtin is a single letter, a tuple is U...E, and a named type is R and a path;
    • a closure is either an environment named __moonbit_<fn> or a body carrying C<id>l<line>, the id of the closure and the line it was written on;
    • a trait implementation names two packages, its type's and its trait's.

    The reports print the path through display_name. demangle_full is the fuller decoder, for callers that want escapes resolved, arguments spelled out and closures marked. Both are display aids rather than a codec: when one cannot decode a symbol exactly it returns it untouched, because a report that invents a name is worse than one that shows a raw symbol. The known limits are where that happens — a generic argument whose type code is not one of the letters a build produced (Int, Double, String, Bool, Char, Byte, Float, Int64, UInt64, Unit) is left undecoded rather than guessed, a named argument is only decoded as the last one in its list because its path has no terminator to separate it from what follows, and a symbol over 4096 bytes is refused outright so a forged name cannot drive the walk into a deep recursion.

    #The call graph

    The decoder walks every function body instruction by instruction — the full MVP instruction set, the 0xFC saturating and bulk-memory opcodes, the exception handling and tail-call opcodes MoonBit emits, and the 0xFB garbage-collection opcodes its wasm-gc backend uses. Each body contributes:

    • call x and return_call x — precise edges to x;
    • ref.func x — a reference, which counts as reachable because the function may run later as a closure;
    • call_indirect and return_call_indirect — conservative edges to static element candidates in the selected table;
    • call_ref and return_call_ref — recognised call sites with an UnmodeledReferenceCall diagnostic. Target analysis is not implemented, including when ref.func immediately precedes the call. No target is guessed.

    Static element segments supply known candidates, not proof of a complete target set. Imports, exports, runtime writes or unmodelled explicit initializers can make that set incomplete. This version conservatively diagnoses each such risk for ordinary and tail indirect calls, even when candidates are nonempty; known candidate edges remain in the graph. Calls with no static candidates also retain their separate diagnostic. Dead-code and retained-size results may be inaccurate. The report says so above everything else:

    WARNING: the call graph has 1 analysis gap. function 0, offset 42: call_ref: reference-call target analysis is not implemented. Dead-code and retained-size results may be inaccurate; do not use them as proof that a function can be removed.

    --dead-code and --retained repeat it under their titles, and --strict turns these recognised incomplete cases into a refusal — no report, exit code 3.

    Writes are collected from all functions before checking any table call: table.set, table.init, table.copy (destination only), table.grow and table.fill. Reads via table.get and table.size are not writes. A writer that appears after the call, or in an unreachable function, still causes a conservative refusal: this version does not prove whether that write executes. Imported tables precede local tables in their own index space, independently of function imports. Explicit initializer expressions are consumed completely and reported as unmodelled; their targets are not guessed.

    call_ref and return_call_ref make the graph incomplete even if every table call has candidates. analyze still returns Ok(Analysis): a modelling gap is distinct from malformed input. Diagnostics retain their category, full caller index, file offset, actual opcode, type index and optional table index.

    graph_complete means no gaps were detected within the current scope: analysis.graph.diagnostics is empty. Dynamic table writes, host-provided contents and explicit initializers are diagnosed but not solved. Other Wasm modelling limitations remain. A successful --strict run is not a completeness certificate.

    A function is a root if the outside world can reach it: it is exported, it is the start function, or it is referenced by an element segment. Reachability and retained sizes describe paths in the current graph model.

    #Parser bounds and Reader API

    Reader::read_u32_leb() returns Result[UInt, WasmError] across the full 0..4294967295 range. Non-shortest encodings remain accepted within five bytes; illegal fifth-byte high bits or continuation produce LebOverflow(offset). Lengths, counts, indices and integer instruction operands pass through one checked internal conversion and currently support 0..2147483647. Larger valid u32 encodings produce ValueOutOfRange(offset, value), an implementation limit rather than a claim that the unsigned encoding is invalid Wasm.

    Reader::new(data) still covers the whole buffer. The new Reader::new_range(data, start, length) returns Result[Reader, WasmError] and restricts reads to [start, start + length). Negative or out-of-buffer requests return InvalidReadRange(offset, length, start, end). seek(offset) now returns Result[Unit, WasmError]; handle that result when migrating callers. Seeking to the end is allowed; reading there returns UnexpectedEnd. Failed seek and read_bytes operations leave the cursor unchanged. Every reported offset is absolute within the file.

    Section payloads, function bodies, locals, instruction immediates and optional name subsections use bounded readers. The outer custom-section name is structural and must fit its payload. Optional name metadata remains tolerant of damaged entries, preserving earlier names without borrowing following bytes. Parsing and range failures return CLI exit code 1, including with --strict, and never create or overwrite JSON, DOT or HTML reports. They are distinct from call-graph gaps refused with code 3.

    parse_module accepts Core Wasm binary version 1 only and checks standard section uniqueness and order. Custom sections may repeat, including with the same name, anywhere between standard sections. The explicit order is Type, Import, Function, Table, Memory, Tag, Global, Export, Start, Element, DataCount, Code, Data; numeric section ids alone do not give this order.

    Local function declarations and code bodies must have equal counts; imports are excluded. Both payloads must be consumed completely, and every code body must fit its declared range. Missing sections count as zero. Current inputs and --baseline use the same checks before reports are printed or written. analyze(data, parsed) also revalidates the bytes and derives its section table from them, so stale or modified metadata cannot bypass these checks.

    These are the basic structure checks needed for size analysis. The tool does not yet provide full WebAssembly type and instruction-semantic validation. Type/call index validity, signed LEB numeric semantics and full 64-bit constant storage remain limited; dynamic table analysis also retains its documented limitations.

    Parsing function signatures are unchanged. External exhaustive matches on WasmError must handle five new variants:

    VariantFields
    UnsupportedVersionversion-field offset, raw unsigned UInt value
    DuplicateSectionsecond section's id-byte offset, kind
    InvalidSectionOrdercurrent id-byte offset, kind, preceding standard kind
    FunctionCodeCountMismatchrelated section's id-byte offset, kind, local declaration count, body count
    TrailingSectionBytesfirst unconsumed payload byte offset, kind

    Version errors point to offset 4; mismatch errors use the code section when present, otherwise the function section. These failures return input error 1, including under --strict, rather than graph-incompleteness code 3.

    #Diagnostic API and export migration

    build(data, bodies, elements) is replaced by build(data). Pass complete module bytes: the public entry parses canonical bodies, elements and table context, so callers cannot omit writers or imply completeness with an empty context. analyze(data, parsed) keeps its signature and uses the same internal rules. count_imported_functions still counts function imports only.

    CallGraph.diagnostics counts reason entries, not call sites. One call can have multiple reasons or distinct writer sources. Export aliases collapse to one host-exposure reason per table; runtime writers deduplicate by file offset. Analysis.unresolved_count is unchanged: only table calls with no static candidates, even when other reasons make the graph incomplete.

    External exhaustive matches on GraphDiagnosticKind need the new variants:

    VariantStable JSON code
    NoStaticTableTargetsno_static_table_targets
    UnmodeledReferenceCallunmodeled_reference_call
    ImportedTableContentsimported_table_contents
    ExportedTableContentsexported_table_contents
    RuntimeTableMutationruntime_table_mutation
    UnmodeledTableInitializerunmodeled_table_initializer
    UnknownTableContentsunknown_table_contents

    The last reason covers a target absent from the parsed context, without claiming full index/type validation. GraphDiagnostic.source : GraphDiagnosticSource? records provenance separately from the call's own caller, offset, opcode. Writer sources have their full function index, opcode and absolute file offset. Import/export sources point to their descriptor kind byte; initializer sources point to the expression start, with no function/opcode. Unsupported or malformed table encodings return InvalidTableEncoding(offset, reason) (CLI 1), while analysis gaps return Ok(Analysis) and strict refusal 3. Table metadata handles multi-byte reference types and i32 limits; table64/shared limits are explicitly unsupported rather than silently misread. Exhaustive WasmError matches must add InvalidTableEncoding.

    scan_calls now returns CallKind::Reference for call_ref and return_call_ref, with type_index = Some(type) and the original offset. External exhaustive matches on CallKind must add this branch. ref.func continues to create a reference edge, not a call site.

    Use render_graph_warning(analysis.graph), render_dead_code(analysis, top) and render_retained(analysis, top). The last two read diagnostics internally; the old separate arrays, file-size argument and optional unresolved count are removed. The library example above and _probe/ use these signatures.

    JSON exports retain module, functions, roots, edges and indirect_sites, and add schema_version: 1, graph_complete, modeling_scope and diagnostics. Each diagnostic has code, caller, offset, opcode, type_index and table_index (null for reference calls). Provenance-bearing reasons add source: {caller, offset, opcode}; metadata sources have null caller/opcode. Existing no-target/reference diagnostics retain their original shape. Complete graphs carry an empty array. Diagnostics sort by caller, call offset, stable reason code, then source offset. JSON and HTML keep schema version 1 with these additive reason/source fields.

    Non-strict JSON, DOT and HTML exports carry their own limitations: JSON contains structured diagnostics and scope, DOT has a graph-level warning label, and HTML shows diagnostics at the top and limits alongside dead-code results. Sharing an export preserves this information. Strict refusal occurs before writing a new file or changing an existing one, for all three formats.

    #Retained size

    SIZE is a defined function's encoded size, including its body-length prefix. RETAINED sums those encoded sizes for the reachable defined functions dominated by the selected function in the current graph/root model, including itself. DIES counts the other functions in that model set.

    The dominator computation answers a question about a fixed graph: every path from a root to a dominated function passes through the selected function. Its graph result does not model editing callers, recompiling, changing indices or removing data and metadata. Element functions are conservatively rooted even for private/passive/declarative segments. A clean diagnostic list or successful --strict therefore does not turn this result into exact deletion savings.

    Reading the table:

    • RETAINED == SIZE — the model attributes no other function's encoded bytes to this dominator; an actual source change may affect other sections and callers.
    • RETAINED ≫ SIZE with a high DIES — investigate the corresponding dependency branch, then rebuild to measure the actual effect.
    • IND — the function can be reached through a call_indirect, so removing it requires checking every dependent table entry and indirect call before removal.

    Actual raw-file reduction is before.wasm size minus rebuilt after.wasm size. Whole-file gzip reduction is measured separately on the two complete files with the same compressor settings. Neither is predicted by retained size or by summing independently compressed functions. Reproducible cases and the exact definitions are in the boundary report.

    #Dead code

    The list contains functions unreachable from roots within the current model. Inspect diagnostics and modelling limits before considering removal; an incomplete graph does not prove a function can be removed.

    MoonBit's compiler does dead-code elimination well, so a small program usually reports none — that is a finding too.

    #Compressed size

    --compress measures the complete Wasm file as one gzip stream using the current compressor defaults. Actual servers may use gzip with other settings, another content encoding, or no compression. Section measurements are independent gzip streams for observing local compressibility.

    Measured from examples/fib.wasm with --compress:

    COMPRESSED SIZE raw 10675 B gzip 5207 B (48.7% of raw, ratio 2.05x) BY SECTION — INDEPENDENT GZIP STREAMS SECTION RAW GZIP RATIO code 5166 2712 1.90x custom (name) 4975 1983 2.50x data 252 174 1.44x custom (producers) 71 95 0.74x type 59 66 0.89x function 50 63 0.79x import 37 61 0.60x export 21 45 0.46x global 13 34 0.38x element 8 32 0.25x table 7 31 0.22x memory 5 29 0.17x datacount 3 27 0.11x Section gzip sizes are measured independently and are not additive. They do not represent shares of the whole-file gzip size.

    Each section range includes its id, length prefix and payload, including a custom section's name. HTML function gzip covers the encoded function range including its length prefix (total_size / HTML size); its ratio uses that same raw range, not bodySize. RATIO is raw bytes divided by gzip bytes. Small streams may grow because each has its own header and trailer; their actual size and ratio below 1 remain visible.

    Independent streams are not additive and do not represent whole-file transfer shares or the bytes saved by removing a function. Gzip can exploit redundancy across ranges when compressing the whole file. To measure an optimization's benefit, compare the complete build artifacts before and after the change.

    A failed measurement stays a failure: text shows failed and a reason for the specific section; a failed whole-file card shows N/A with its own reason. Other successful measurements remain available. HTML sections, functions and treemap leaves pair gzip with gzipError: success is a number and null; failure is null and the local reason. This additive HTML data extension and its compression_scope explanation are separate from call-graph JSON exports. Public parsing and compression signatures are unchanged; compress_sections now returns Err for invalid metadata ranges instead of measuring empty bytes.

    #Judging what can be deleted

    1. Check graph diagnostics, root assumptions and the runtime integration contract. --dead-code lists functions unreachable in this model; it is not a safe-deletion list.
    2. Then look at --retained for large RETAINED values with DIES above zero. These identify modelled dependency branches to investigate, not promised savings.
    3. Treat IND rows with care: an indirect candidate can only be removed when the table entry and the call sites go with it.

    #Example output

    A 10,675-byte MoonBit program (--target wasm, debug), reduced to the analysis sections:

    Retained size # INDEX SIZE RETAINED DIES SHARE IND FUNCTION 1 47 158 5162 46 48.3% ____moonbit__main 2 39 301 1773 10 16.6% int::Int::to__string_2einner 3 37 24 887 8 8.3% println 4 34 9 819 5 7.6% moonbit.println 5 33 206 810 4 7.5% moonbit.fprintln 6 28 49 717 7 6.7% moonbit.decref 7 29 409 668 6 6.2% moonbit.gc.free 8 43 633 633 0 5.9% int__to__string__dec DIES counts the other functions that become unreachable with this one. IND marks a function an indirect call could reach. Dead code 0 of 47 functions are unreachable from the roots 0 bytes (0.0% of file) every function is reachable

    ____moonbit__main retains 5,162 bytes — 48.3% of the file, 46 functions — which is what a program with a single entry point looks like: everything hangs off it. int__to__string__dec is the largest single function at 633 bytes but retains only itself, so shrinking it is a compiler problem rather than a deletion.

    --call-graph graph.dot writes the same graph for Graphviz, with dead functions dashed and indirect edges dotted; --call-graph graph.json writes it with the roots, every edge and every indirect site's candidate set.

    #HTML report

    moonsize examples/fib.wasm --html new-report.html

    The original analysis page starts with the full input path, section navigation and four exact byte counts: File size, Code section, Whole-file gzip and Unreachable in model. The analysis status follows the cards. Detected graph gaps and their actual diagnostic positions remain visible; “No detected graph gaps” means only that no gaps were detected within this model. Model results identify investigation candidates, not guaranteed deletion savings.

    Then read the sections and module composition, Top 20 functions, module map, and expandable methods. Section and function bars have independent row heights. The section legend and keyboard-accessible comparison button share one byte axis for raw and independent gzip, including small streams that grow. Independent gzip sizes are not additive and are not whole-file transfer shares. Each failed measurement retains its own reason.

    Full function names without hovering. Expand or collapse the function table below the ranking. Its same Top 20 local functions expose index, full name/module, body bytes, encoded bytes, independent gzip and model reachability. Long names wrap and can be selected/copied; the table scrolls locally on narrow screens. Ranking uses body bytes, while independent gzip ratios use the full encoded range, including its size prefix. Function indices include preceding imports; the displayed function count explicitly counts local functions.

    Module shares and colors. The ring divides attributed function encodings, excluding code-section overhead. Its tooltip separately computes a share of the complete code section. The ring and module→function treemap use the same identity-based module colors, independent of rank. Treemap area remains raw function encoding; unreachable functions also have text and outlined cells, which do not imply removal savings.

    The native CLI always embeds pinned ECharts 5.6.0 from its executable, regardless of the working directory. It never reads a nearby assets/ file or downloads a chart library. The report contains its library, styles, scripts and data without remote fonts or sibling files. Copying that one HTML file is sufficient. The library API remains compatible: render_html without an echarts source still selects CDN mode; pass a source explicitly for an offline library-generated page. Missing/broken chart code still leaves a clear fallback and readable cards, function table and methods.

    A4 printing redraws charts at paper width, hides hover controls, opens required disclosures, repeats table headers and includes a complete module legend. It restores the screen layout and disclosure states afterwards. Actual relocated offline rendering and two browser-generated PDFs have been checked; see delivery evidence and reproduction. Node and browser test tools are not required to generate reports with the native CLI.

    The current generated example uses the unchanged stored input, SHA-256 0e6f10e56434b67adb162cdb56a46553ef38b8de1abd0d0deb1fe56d7173d10c. It measures 10,675 B file, 5,166 B code, 5,207 B whole-file gzip and 0 B unreachable local function encodings. Page usage and verification limits record the real HTTP-browser checks, screenshots and tested offline/print scope.

    Current report at 1440 px

    Current report at 390 px

    Both screenshots were captured from the generated report in a real browser at http://localhost:5173/.

    #CI integration

    --max-size turns the report into a gate, and --baseline turns it into a comparison, so a build can fail on growth instead of on a number somebody has to keep updating by hand.

    moonsize app.wasm --max-size 100KB # a ceiling on the file moonsize app.wasm --baseline main.wasm --max-size 5KB # at most 5 KB of growth

    With a baseline, --max-size bounds the change, not the file: the question a pull request asks is what it costs, not how big the program already was. A run with a baseline prints the comparison after the report, largest current group first, with the change as bytes and as a share of what it was:

    SIZE COMPARISON baseline current delta total 10675 11039 +364 (+3.4%) code 5166 5299 +133 (+2.5%) custom (name) 4975 5205 +230 (+4.6%) data 252 252 0 (0.0%) custom (producers) 71 71 0 (0.0%) ...

    Standard sections compare by kind. Custom sections group by their complete, case-sensitive names: repeated names produce one row containing their summed encoded sizes. Empty names are valid, and a custom section named code remains separate from the standard code section. Sizes include each section's actual id, length encoding and full payload, including the custom name encoding; legal non-shortest length encodings therefore count their extra bytes.

    For example, two x sections of 4 and 6 bytes compared with two of 5 and 8 bytes produce one row:

    baseline current delta custom (x) 10 13 +3 (+30.0%)

    This compares a name's total encoded size without attempting to pair individual same-name custom sections across builds. Reordering sections does not change the result. Ties use descending baseline size, then standard binary section order followed by custom name order (String::compare). Deleted groups remain visible against zero.

    total is the entire file, including its eight-byte header. Group increases and decreases may cancel, and equal group sizes do not imply equal content. With a baseline, --max-size still limits only the whole-file growth, including when some groups grow while the total is unchanged.

    #Function and package changes

    moonsize current.wasm --baseline previous.wasm --diff-functions --top 20

    This appends FUNCTION SIZE CHANGES, PACKAGE SIZE CHANGES, MATCHING SUMMARY and CODE OVERHEAD. Without --diff-functions, baseline output stays at the existing file/section comparison. The option requires --baseline and rejects --html or --call-graph with exit 2 before reading inputs. Other text options combine normally. --top 0 keeps totals and matching coverage but hides both detail rankings; positive limits apply independently to functions and packages.

    Matching uses the exact, nonempty original symbol only when it appears once among local functions on each side. Function indices and offsets only locate records within a build. Display names, sizes and order never establish identity. Unique one-sided symbols are added-symbol or removed-symbol; a duplicate on either side makes all related records ambiguous, even if absent on the other side. Missing or empty names are missing-symbol. Both unmatched categories retain separate per-side records and byte counts; neither is also called an addition/removal. The actual symbol (unnamed) remains distinct from no symbol. Added/removed symbols may reflect renames or compiler changes rather than source functions being added/deleted. Stripped binaries still have accurate totals and package summaries, with an explicit lack of function identity coverage.

    Function sizes include the body length prefix, locals and instruction bytes (total_size); package summaries independently aggregate all functions by the existing best-effort module_name, including (unnamed) and (top-level). Ranking uses absolute byte delta, then case-sensitive UTF-16 lexical symbol or package order; duplicate identity ties use status and per-build indices. It does not depend on Map traversal. Raw symbols are quoted and escaped in text.

    The structured results reconcile:

    package encoded-byte sums = local function total_size sums code section = function encoding + code overhead (header/count encoding) function delta = matched delta + added bytes - removed bytes + unmatched current bytes - unmatched baseline bytes

    Name/data and other section changes belong to the existing section comparison; function deltas do not sum to the whole-file delta. --max-size still budgets only whole-file growth, including growth outside code. Current-input strict behavior is unchanged; comparison itself does not build a baseline call graph. Both files are read once, and fallible comparison preparation finishes before any normal report is printed.

    Library callers use compare_modules(baseline_data, current_data) returning Result[ModuleSizeDiff, ComparisonError] (Baseline(WasmError) / Current(WasmError)). The result exposes file totals, baseline_code / current_code (CodeSizeSummary: section, function count/bytes, overhead), functions (FunctionSizeChange with status, raw symbol, optional per-side FunctionStat, delta), packages (PackageSizeChange) and matching (MatchingSummary, per-category count/byte coverage and matched delta). render_function_diff(diff,top) renders the same text. Validation covers basic module structure and bounded function/import parsing, without full Wasm type/instruction validation.

    API migration: FunctionStat.raw_name : String? preserves a nonempty symbol before display_name, alongside unchanged display, package and size fields. Internal record constructors must supply it; never reconstruct it from name. Malformed UTF-8 in optional function-name metadata stops name collection at that entry, retaining earlier names and leaving the damaged suffix unmatched; it is not lossily converted into a fabricated shared identity. Valid Unicode names, including a literal replacement character or BOM, retain their exact identity. Options adds diff_functions, defaulting to false. Existing parse, analyze, compression and budget signatures remain unchanged.

    For a source-built demonstration, run:

    moon build --target native --release bash scripts/demo-function-diff.sh _build/native/release/build/cmd/main/main.exe

    The script copies before.mbt and after.mbt into the same temporary project, builds both with --target wasm --debug --no-strip, verifies real name sections with V8, runs comparison, then restores the source and checks the complete artifact and symbol differences return to zero. Artifacts and measured reports remain under _build/function-diff-demo.*. With MoonBit 0.10.13+cbb11c36f, the measured example is 4,977 → 10,785 file bytes; classify is 29 → 55 encoded bytes, the new weighted_score symbol is 103 bytes, and demo::functiondiff is 29 → 158 bytes. All function encoding grows by 2,125 bytes, including runtime helpers; this differs from the 5,808-byte whole-file growth. These are measured demo results for that toolchain, not fixed cross-version expectations.

    The exit codes are 0 ok, 1 unreadable input, 2 bad command line and 3 over budget or refused by --strict, so a job can tell a broken input apart from a broken build.

    This repository gates itself in .github/workflows/ci.yml: formatting, moon check --target native --deny-warn, moon test, and the freshly built debug Wasm example held to a budget. The local setup action pins MoonBit 0.10.13+cbb11c36f to match local formatting and tests, and runs moon update before dependency resolution. A toolchain upgrade should update local checks and CI together.

    - uses: ./.github/actions/setup-moonbit - name: Build the example and CLI run: | moon build --target wasm moon build --target native --release - name: The example stays inside its budget env: EXAMPLE: _build/wasm/debug/build/examples/examples.wasm CLI: _build/native/release/build/cmd/main/main.exe run: | test -s "$EXAMPLE" printf 'budget input: %s\nbytes: %s\n' "$EXAMPLE" "$(wc -c < "$EXAMPLE")" sha256sum "$EXAMPLE" "$CLI" "$EXAMPLE" --max-size 100KB

    The budget job reads that build artifact directly; examples/fib.wasm remains the published example copy. Its reverse check uses 0B, explicitly captures the native CLI's status, and requires exactly 3 plus the budget-failure reason. Missing files, launch errors and command-line errors fail the job. Debug and release artifact paths must track the build mode if it changes.

    For another project that produces a .wasm, install the compatible toolchain with the official installer before applying its size budget:

    - name: Setup MoonBit run: | curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s '0.10.13+cbb11c36f' echo "$HOME/.moon/bin" >> "$GITHUB_PATH" - run: moon update - run: moon build --target wasm --release - name: Size budget run: moonsize _build/wasm/release/build/app/app.wasm --max-size 250KB

    To catch growth rather than an absolute size, build the base branch too and pass it as the baseline; --max-size then reads as the allowance:

    - name: Build the base branch run: | git worktree add "$RUNNER_TEMP/base" "origin/${{ github.base_ref }}" (cd "$RUNNER_TEMP/base" && moon build --target wasm --release) - name: Size budget run: | moonsize _build/wasm/release/build/app/app.wasm \ --baseline "$RUNNER_TEMP/base/_build/wasm/release/build/app/app.wasm" \ --max-size 5KB

    .github/workflows/size-check.yml does that on every pull request. Both sides use the same installed toolchain and default debug Wasm mode. Each build artifact must exist and be nonempty before copying; the workflow logs source paths, comparison paths, byte counts and SHA-256 hashes. The native CLI compares $RUNNER_TEMP/current.wasm against $RUNNER_TEMP/baseline.wasm. If a historical baseline has no examples/moon.pkg, the log explicitly says it uses the repository's historical example without rebuilding that branch's source. A baseline with that package must build successfully; build failures do not fall back to the stored binary.

    #Development

    The key decisions, the known limits of the symbol rules, the role AI tools played, and the provenance of everything vendored are written down in docs/devlog.md.

    moon check --target native --deny-warn moon test moon build --target native --release bash scripts/test-tail-calls.sh _build/native/release/build/cmd/main/main.exe bash scripts/test-graph-diagnostics.sh _build/native/release/build/cmd/main/main.exe bash scripts/test-parser-boundaries.sh _build/native/release/build/cmd/main/main.exe

    The diagnostic CLI script requires Node.js to validate its Wasm fixtures with V8 and assert fields in parsed JSON. The parser script also uses Node.js for a five-second timeout per CLI process, distinguishing timeouts, crashes and expected input errors. All three scripts run in Git Bash or Linux after building the native executable. Complete-module fixtures are cross-checked with V8; Reader-only and optional metadata tests are kept separate.

    The test suite covers the instruction table one opcode at a time — every entry carries a canonical encoding and the length the specification gives it — and pins three real MoonBit function bodies, including one whose locals use two-byte garbage-collection reference types, so a wrong immediate width fails a test instead of quietly producing a wrong call graph.

    #Regenerating the example

    The numbers quoted in examples/report.html and in this README — the file size, the section shares, the retained sizes — are read out of a real build rather than written by hand. Changing examples/fib.mbt or moving to a new toolchain makes them stale. Build, copy the artifact, then generate the report:

    moon build --target wasm stage=$(mktemp -d examples/report-update.XXXXXX) cp _build/wasm/debug/build/examples/examples.wasm "$stage/fib.wasm" moon run cmd/main -- \ --html "$stage/report.html" \ "$stage/fib.wasm" sha256sum _build/wasm/debug/build/examples/examples.wasm "$stage/fib.wasm"

    The build and staged input hashes must match. Check the measurements against that input before replacing either checked-in artifact. Start npm run dev, open http://127.0.0.1:5173/, then inspect the staged page at http://127.0.0.1:5173/report-update.XXXXXX/report.html (use the actual directory). The report records the actual staged input path. After accepting the new report, run these explicit replacement steps; the CLI never removes the old report:

    cp "$stage/fib.wasm" examples/fib.wasm cp "$stage/report.html" examples/report.html

    Update report-desktop.jpg (1440 × 1000) and report-mobile.jpg (390 × 844) only after reviewing the accepted page. Do not overwrite an unrelated report.png. Keep or explicitly clean up only your own staging directory after review.

    .moonignore keeps the rendered page out of the published package — it is 1.2 MB of what would otherwise be a 2.4 MB module, and nobody moon adds this for a screenshot — while the source, the example binary and the chart library all ship.

    Analysis

    pub struct Analysis {
    data : Bytes
    parsed : WasmModule
    stats : Array[FunctionStat]
    graph : CallGraph
    roots :
    Set
    [Int]
    reachable :
    Set
    [Int]
    retained : Array[RetainedSize]
    indirect :
    Set
    [Int]
    graph_complete : Bool
    unresolved_count : Int
    }

    A parsed module with its sizes, call graph and reachability resolved.

    BudgetArtifact

    pub struct BudgetArtifact {
    sha256 : String
    raw_bytes : Int
    code_bytes : Int
    gzip : Result[Int, String]?
    coverage : BudgetCoverage?
    }

    BudgetCoverage

    pub struct BudgetCoverage {
    functions : Int
    function_bytes : Int
    unattributed_functions : Int
    unattributed_bytes : Int
    }

    BudgetError

    pub enum BudgetError {
    BudgetCurrentInput(WasmError)
    BudgetBaselineInput(WasmError)
    BudgetBaselineRequired
    }

    BudgetProblem

    pub struct BudgetProblem {
    rule_index : Int?
    field : String
    reason : String
    }

    BudgetReport

    pub struct BudgetReport {
    config_version : Int
    current : BudgetArtifact
    baseline : BudgetArtifact?
    rules : Array[BudgetRuleResult]
    status : BudgetStatus
    exit_code : Int
    }

    BudgetRuleResult

    pub struct BudgetRuleResult {
    id : String
    metric : String
    package_name : String?
    mode : String
    current : Int?
    baseline : Int?
    delta : Int?
    max_bytes : Int
    status : BudgetStatus
    reason : String
    current_package_present : Bool?
    baseline_package_present : Bool?
    }

    BudgetStatus

    pub enum BudgetStatus {
    BudgetPassed
    BudgetExceeded
    BudgetUnavailable
    } derive(Eq)

    CallEdge

    pub struct CallEdge {
    caller : Int
    callee : Int
    kind : EdgeKind
    offset : Int
    }

    One edge of the call graph.

    CallGraph

    pub struct CallGraph {
    calls : Map[Int,
    Set
    [Int]]
    called_by : Map[Int,
    Set
    [Int]]
    indirect_sites : Array[IndirectSite]
    unresolved_indirect : Array[IndirectSite]
    diagnostics : Array[GraphDiagnostic]
    edges : Array[CallEdge]
    }

    The reference graph between functions.

    CallKind

    pub enum CallKind {
    Direct(Int)
    Indirect(Int)
    Reference
    }

    How a call site reaches its target.

    CallSite

    pub struct CallSite {
    kind : CallKind
    type_index : Int?
    offset : Int
    }

    One call found in a function body.

    CodeSizeSummary

    pub struct CodeSizeSummary {
    code_size : Int
    function_count : Int
    function_bytes : Int
    overhead : Int
    }

    ComparisonError

    pub enum ComparisonError {
    Baseline(WasmError)
    Current(WasmError)
    }

    Which input failed structural parsing or function-size collection.

    CompiledBudget

    type CompiledBudget

    Opaque validated rules. No rule array is exposed outside this package.

    DataByteRange

    pub struct DataByteRange {
    offset : Int
    size : Int
    } derive(Eq,
    Debug
    )

    Half-open encoded range in the original file, never a memory address.

    DataGroupTotals

    pub struct DataGroupTotals {
    count : Int
    payload_size : Int
    encoded_size : Int
    metadata_size : Int
    } derive(Eq,
    Debug
    )

    Totals on either side, or signed current-minus-baseline deltas.

    DataHexSearchError

    pub enum DataHexSearchError {
    InvalidDataHexQuery
    DataHexSearchInputError(WasmError)
    }

    DataHexSearchReport

    pub struct DataHexSearchReport {
    file_size : Int
    sha256 : String
    query_hex : String
    query_size : Int
    matching_rule : String
    scope : String
    data_segments : DataSegmentsReport
    segment_count : Int
    matched_segment_count : Int
    unmatched_segment_count : Int
    occurrence_count : Int
    matches : Array[DataSearchMatch]
    }

    DataPayloadGroup

    pub struct DataPayloadGroup {
    id : Int
    sha256 : String
    payload_length : Int
    kind : DataPayloadGroupKind
    baseline_indices : Array[Int]
    current_indices : Array[Int]
    baseline : DataGroupTotals
    current : DataGroupTotals
    delta : DataGroupTotals
    }

    A content group, never an assertion of cross-build entry identity. The local id distinguishes even different byte groups with an identical full digest.

    DataPayloadGroupKind

    pub enum DataPayloadGroupKind {
    SharedPayload
    BaselineOnly
    CurrentOnly
    } derive(Eq,
    Debug
    )

    DataPreviewComparison

    pub struct DataPreviewComparison {
    baseline : DataPreviewReport
    current : DataPreviewReport
    same_count : Int
    different_count : Int
    baseline_only_count : Int
    current_only_count : Int
    compared_positions : Int
    returned_size_delta : Int
    positions : Array[DataPreviewPosition]
    scope : String
    }

    DataPreviewComparisonError

    pub enum DataPreviewComparisonError {
    BaselineDataPreviewError(DataPreviewError)
    CurrentDataPreviewError(DataPreviewError)
    }

    DataPreviewError

    pub enum DataPreviewError {
    InvalidDataPreviewLimit(Int)
    InvalidDataPreviewIndex(Int, Int)
    InvalidDataPreviewOffset(Int, Int)
    DataPreviewInputError(WasmError)
    }

    DataPreviewPosition

    pub struct DataPreviewPosition {
    window_relative_offset : Int
    status : String
    baseline_byte : Int?
    current_byte : Int?
    baseline_file_offset : Int?
    current_file_offset : Int?
    } derive(Eq,
    Debug
    )

    Relative positional evidence, with absent bytes/file offsets kept distinct from actual zero bytes. status has four stable encoding-fact values.

    DataPreviewReport

    pub struct DataPreviewReport {
    file_size : Int
    sha256 : String
    segment_count : Int
    segment : DataSegment
    payload_offset : Int
    requested_limit : Int
    window : DataByteRange
    returned_size : Int
    remaining_bytes : Int
    bytes : Bytes
    scope : String
    }

    One bounded payload window. File offsets never represent memory addresses.

    DataSearchError

    pub enum DataSearchError {
    EmptyDataSearchQuery
    DataSearchInputError(WasmError)
    }

    DataSearchMatch

    pub struct DataSearchMatch {
    segment_index : Int
    payload_offset : Int
    file_offset : Int
    size : Int
    } derive(Eq,
    Debug
    )

    DataSearchReport

    pub struct DataSearchReport {
    file_size : Int
    sha256 : String
    query : String
    query_size : Int
    matching_rule : String
    scope : String
    data_segments : DataSegmentsReport
    segment_count : Int
    matched_segment_count : Int
    unmatched_segment_count : Int
    occurrence_count : Int
    matches : Array[DataSearchMatch]
    }

    Full results, ordered by original segment index and payload byte offset.

    DataSegment

    pub struct DataSegment {
    index : Int
    mode : DataSegmentMode
    memory_index : Int?
    encoding : DataByteRange
    payload : DataByteRange
    offset_expression : DataByteRange?
    mode_size : Int
    memory_index_size : Int
    payload_length_size : Int
    } derive(Eq,
    Debug
    )

    Metadata only: no payload copy and no evaluation of the offset expression.

    DataSegmentMode

    pub enum DataSegmentMode {
    Active
    Passive
    } derive(Eq,
    Debug
    )

    DataSegmentsComparison

    pub struct DataSegmentsComparison {
    baseline_sha256 : String
    current_sha256 : String
    baseline : DataSegmentsReport
    current : DataSegmentsReport
    groups : Array[DataPayloadGroup]
    delta : DataSegmentsDelta
    }

    DataSegmentsDelta

    pub struct DataSegmentsDelta {
    section_size : Int
    payload_size : Int
    entry_metadata_size : Int
    section_overhead_size : Int
    } derive(Eq,
    Debug
    )

    DataSegmentsReport

    pub struct DataSegmentsReport {
    file_size : Int
    section : Section?
    datacount : Int?
    section_size : Int
    header_size : Int
    count_size : Int
    payload_size : Int
    entry_metadata_size : Int
    entries : Array[DataSegment]
    }

    DataCount is separate and excluded from the data-section byte ledger. Absent data has section=None and all ledger sizes zero; an empty section still has a header and an encoded zero count. Entries remain in file order.

    DependencyOverlapError

    pub enum DependencyOverlapError {
    OverlapInputError(WasmError)
    InvalidOverlapTarget(String, Int, Int)
    OverlapEvidenceError(FunctionDependenciesError)
    }

    DependencyOverlapGroup

    pub struct DependencyOverlapGroup {
    kind : String
    indices : Array[Int]
    function_count : Int
    local_count : Int
    imported_count : Int
    local_encoded_bytes : Int
    }

    DependencyOverlapReport

    pub struct DependencyOverlapReport {
    file_size : Int
    sha256 : String
    left_index : Int
    right_index : Int
    functions : Array[FunctionPathNode]
    left_reachable : Array[Int]
    right_reachable : Array[Int]
    left_edges : Array[CallEdge]
    right_edges : Array[CallEdge]
    groups : Array[DependencyOverlapGroup]
    function_count : Int
    local_function_count : Int
    imported_function_count : Int
    local_encoded_bytes : Int
    left_function_count : Int
    left_local_count : Int
    left_imported_count : Int
    left_local_encoded_bytes : Int
    right_function_count : Int
    right_local_count : Int
    right_imported_count : Int
    right_local_encoded_bytes : Int
    union_function_count : Int
    union_local_count : Int
    union_imported_count : Int
    union_local_encoded_bytes : Int
    graph_complete : Bool
    unresolved_count : Int
    diagnostics : Array[GraphDiagnostic]
    graph_warning : String
    modeling_scope : String
    }

    DuplicateDataGroup

    pub struct DuplicateDataGroup {
    id : Int
    sha256 : String
    payload_size : Int
    member_count : Int
    member_indices : Array[Int]
    member_payload_bytes : Int
    member_metadata_bytes : Int
    member_encoded_bytes : Int
    }

    A content group in this input only; id is its smallest original entry index.

    DuplicateDataReport

    pub struct DuplicateDataReport {
    file_size : Int
    sha256 : String
    comparison_scope : String
    data_segments : DataSegmentsReport
    groups : Array[DuplicateDataGroup]
    summary : DuplicateDataSummary
    }

    DuplicateDataSummary

    pub struct DuplicateDataSummary {
    segment_count : Int
    duplicate_group_count : Int
    duplicate_member_count : Int
    singleton_count : Int
    total_payload_bytes : Int
    duplicate_member_payload_bytes : Int
    singleton_payload_bytes : Int
    total_entry_encoded_bytes : Int
    total_entry_metadata_bytes : Int
    duplicate_member_metadata_bytes : Int
    duplicate_member_encoded_bytes : Int
    singleton_metadata_bytes : Int
    singleton_encoded_bytes : Int
    data_section_bytes : Int
    section_header_bytes : Int
    count_prefix_bytes : Int
    } derive(Eq,
    Debug
    )

    Duplicate-member bytes count ALL members, never estimated removable bytes.

    DuplicateFunctionGroup

    pub struct DuplicateFunctionGroup {
    sha256 : String
    body_size : Int
    member_count : Int
    member_body_bytes : Int
    member_prefix_bytes : Int
    member_total_bytes : Int
    members : Array[DuplicateFunctionMember]
    }

    DuplicateFunctionMember

    pub struct DuplicateFunctionMember {
    index : Int
    name : String
    raw_name : String?
    type_index : Int
    offset : Int
    body_offset : Int
    body_size : Int
    prefix_size : Int
    total_size : Int
    }

    DuplicateFunctionsReport

    pub struct DuplicateFunctionsReport {
    file_size : Int
    sha256 : String
    comparison_scope : String
    summary : DuplicateFunctionsSummary
    groups : Array[DuplicateFunctionGroup]
    }

    DuplicateFunctionsSummary

    pub struct DuplicateFunctionsSummary {
    local_function_count : Int
    duplicate_group_count : Int
    duplicate_member_count : Int
    singleton_function_count : Int
    total_body_bytes : Int
    duplicate_body_bytes : Int
    singleton_body_bytes : Int
    total_prefix_bytes : Int
    total_encoded_bytes : Int
    }

    EdgeKind

    pub enum EdgeKind {
    Direct
    Indirect
    Reference
    }

    How one function refers to another.

    ElementSegment

    pub struct ElementSegment {
    table_index : Int
    passive : Bool
    functions : Array[Int]
    }

    One entry of the element section.

    EntrypointFunction

    pub struct EntrypointFunction {
    node : FunctionPathNode
    entrypoints : Array[Int]
    other_root_reachable : Bool
    }

    One inventory node per function; groups and shared lists refer to its index.

    EntrypointGroup

    pub struct EntrypointGroup {
    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
    }

    EntrypointsError

    pub enum EntrypointsError {
    EntrypointsInputError(WasmError)
    InvalidEntrypointExport(String, Int, Int, Int)
    InvalidEntrypointsRoot(String, Int, Int)
    InvalidEntrypointsEdge(Int, Int, Int)
    InvalidEntrypointsExportKind(String, Int, Int)
    }

    EntrypointsLedger

    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
    }

    EntrypointsReport

    pub struct EntrypointsReport {
    file_size : Int
    sha256 : String
    export_name_count : Int
    entrypoint_count : Int
    other_exports : Array[Export]
    entrypoints : Array[EntrypointGroup]
    functions : Array[EntrypointFunction]
    other_roots : Array[FunctionRoot]
    other_root_reachable : Array[Int]
    shared_local_functions : Array[Int]
    ledger : EntrypointsLedger
    graph_complete : Bool
    diagnostics : Array[GraphDiagnostic]
    graph_warning : String
    modeling_scope : String
    }

    Export

    pub struct Export {
    name : String
    kind : Int
    index : Int
    }

    One entry of the export section.

    ExportByteRange

    pub struct ExportByteRange {
    offset : Int
    size : Int
    } derive(Eq,
    Debug
    )

    Half-open original file range; never a runtime address.

    ExportComparisonRow

    pub struct ExportComparisonRow {
    name : String
    status : String
    baseline : ExportDeclaration?
    current : ExportDeclaration?
    declaration_bytes_delta : Int
    target_index_changed : Bool?
    }

    Exact host-visible name match, not a cross-build target or source identity.

    ExportDeclaration

    pub struct ExportDeclaration {
    ordinal : Int
    name : String
    kind : String
    kind_code : Int
    target_index : UInt
    encoding : ExportByteRange
    name_encoding : ExportByteRange
    name_length_prefix_bytes : Int
    name_utf8_bytes : Int
    descriptor : ExportByteRange
    target_index_prefix_bytes : Int
    }

    ExportKindCounts

    pub struct ExportKindCounts {
    functions : Int
    tables : Int
    memories : Int
    globals : Int
    tags : Int
    } derive(Eq,
    Debug
    )

    ExportTarget

    pub struct ExportTarget {
    kind : String
    kind_code : Int
    target_index : UInt
    entry_ordinals : Array[Int]
    member_count : Int
    declaration_bytes : Int
    is_alias : Bool
    }

    ExportsComparison

    pub struct ExportsComparison {
    baseline : ExportsReport
    current : ExportsReport
    counts : ExportsComparisonCounts
    delta : ExportsLedger
    baseline_non_export_bytes : Int
    current_non_export_bytes : Int
    delta_non_export_bytes : Int
    delta_file_bytes : Int
    rows : Array[ExportComparisonRow]
    comparison_scope : String
    }

    ExportsComparisonCounts

    pub struct ExportsComparisonCounts {
    added_count : Int
    removed_count : Int
    kind_changed_count : Int
    same_kind_count : Int
    row_count : Int
    } derive(Eq,
    Debug
    )

    ExportsLedger

    pub struct ExportsLedger {
    export_section_bytes : Int
    section_header_bytes : Int
    count_prefix_bytes : Int
    declaration_bytes : Int
    name_encoding_bytes : Int
    name_length_prefix_bytes : Int
    name_utf8_bytes : Int
    descriptor_bytes : Int
    kind_bytes : Int
    target_index_prefix_bytes : Int
    } derive(Eq,
    Debug
    )

    Parent and child byte counts are decompositions, not additional costs.

    ExportsReport

    pub struct ExportsReport {
    file_size : Int
    sha256 : String
    section : ExportByteRange?
    count_encoding : ExportByteRange?
    summary : ExportsSummary
    ledger : ExportsLedger
    entries : Array[ExportDeclaration]
    targets : Array[ExportTarget]
    supported_scope : String
    }

    ExportsSummary

    pub struct ExportsSummary {
    export_count : Int
    kind_counts : ExportKindCounts
    distinct_target_count : Int
    alias_group_count : Int
    alias_member_count : Int
    singleton_target_count : Int
    } derive(Eq,
    Debug
    )

    FileOffsetDecodeDetail

    pub struct FileOffsetDecodeDetail {
    kind : FileOffsetDecodeDetailKind
    file_offset : Int
    size : Int
    relative_offset : Int
    instruction : FunctionInstructionRecord?
    }

    FileOffsetDecodeDetailKind

    pub enum FileOffsetDecodeDetailKind {
    LocalsEncoding
    Instruction
    } derive(Eq,
    Debug
    )

    FileOffsetDecodeError

    pub enum FileOffsetDecodeError {
    FileOffsetDecodeLocationError(FileOffsetError)
    FileOffsetDecodeInstructionsError(FunctionInstructionsError)
    InconsistentFileOffsetDecode(Int, String)
    }

    FileOffsetDecodeStatus

    pub enum FileOffsetDecodeStatus {
    NotApplicable
    Decoded
    } derive(Eq,
    Debug
    )

    FileOffsetDecodedReport

    pub struct FileOffsetDecodedReport {
    location : FileOffsetReport
    decode_status : FileOffsetDecodeStatus
    detail : FileOffsetDecodeDetail?
    scope : String
    }

    FileOffsetError

    pub enum FileOffsetError {
    FileOffsetOutOfRange(Int, Int)
    FileOffsetInputError(WasmError)
    InconsistentFileOffsetRange(Int, String)
    }

    FileOffsetKind

    pub enum FileOffsetKind {
    ModuleHeader
    SectionHeader
    SectionPayload
    CodeCount
    FunctionSizePrefix
    FunctionBodyBytes
    DataEntryCount
    DataEntryMetadata
    DataPayload
    } derive(Eq,
    Debug
    )

    FileOffsetRegion

    pub struct FileOffsetRegion {
    kind : FileOffsetKind
    file_offset : Int
    size : Int
    relative_offset : Int
    } derive(Eq,
    Debug
    )

    A half-open file range and the query's byte position within that range.

    FileOffsetReport

    pub struct FileOffsetReport {
    file_size : Int
    sha256 : String
    offset : Int
    section_index : Int?
    section : Section?
    function : FunctionStat?
    data_segment : DataSegment?
    region : FileOffsetRegion
    scope : String
    }

    FileRangeError

    pub enum FileRangeError {
    FileRangeOutOfRange(Int, Int, Int)
    FileRangeOwnershipError(Int, Int, Int, FileOffsetError)
    InconsistentFileRange(Int, Int, Int, String)
    }

    FileRangePart

    pub struct FileRangePart {
    kind : FileOffsetKind
    owner_range : DataByteRange
    overlap_range : DataByteRange
    query_relative_offset : Int
    owner_relative_offset : Int
    section_index : Int?
    section : Section?
    function : FunctionStat?
    data_segment : DataSegment?
    }

    FileRangeReport

    pub struct FileRangeReport {
    file_size : Int
    sha256 : String
    query : DataByteRange
    covered_size : Int
    parts : Array[FileRangePart]
    scope : String
    }

    FunctionBody

    pub struct FunctionBody {
    index : Int
    body_size : Int
    total_size : Int
    offset : Int
    }

    One function body decoded out of the code section.

    The spec stores a body as a size-prefixed blob, so an entry costs its own length prefix on top of the body itself. Both numbers are kept: the body is what a compiler can shrink, the total is what the file actually spends.

    FunctionCallKind

    pub enum FunctionCallKind {
    DirectCall
    IndirectCall
    ReferenceCall
    } derive(Eq,
    Debug
    )

    Call instruction categories, independent of call-graph EdgeKind.

    FunctionCallRecord

    pub struct FunctionCallRecord {
    instruction_ordinal : Int
    opcode : Opcode
    kind : FunctionCallKind
    is_tail : Bool
    file_offset : Int
    body_relative_offset : Int
    encoded_size : Int
    target_index : Int?
    type_index : Int?
    table_index : Int?
    target : FunctionPathNode?
    }

    FunctionCallerGroup

    pub struct FunctionCallerGroup {
    caller : FunctionPathNode
    sources : Array[FunctionPathStep]
    }

    FunctionCallersError

    pub enum FunctionCallersError {
    CallersInputError(WasmError)
    InvalidCallersTarget(Int, Int)
    InvalidCallersEvidence(Int, String)
    }

    FunctionCallersReport

    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
    }

    Complete recorded one-hop instruction sources, without reachability filtering.

    FunctionCallsError

    pub enum FunctionCallsError {
    CallsInputError(WasmError)
    InvalidCallsTarget(Int, Int)
    ImportedCallsTarget(Int)
    InvalidCallsEvidence(Int?, String)
    }

    FunctionCallsReport

    pub struct FunctionCallsReport {
    file_size : Int
    sha256 : String
    caller : FunctionPathNode
    entry : FunctionInstructionRange
    prefix : FunctionInstructionRange
    body : FunctionInstructionRange
    locals_encoding : FunctionInstructionRange
    instruction_count : Int
    instruction_bytes : Int
    call_count : Int
    direct_count : Int
    indirect_count : Int
    reference_call_count : Int
    tail_count : Int
    call_encoding_bytes : Int
    other_instruction_bytes : Int
    calls : Array[FunctionCallRecord]
    scope : String
    }

    FunctionCoverage

    pub struct FunctionCoverage {
    count : Int
    bytes : Int
    }

    FunctionCycleComponent

    pub struct FunctionCycleComponent {
    component_id : Int
    members : Array[FunctionPathNode]
    internal_edges : Array[CallEdge]
    node_count : Int
    internal_edge_count : Int
    direct_edges : Int
    reference_edges : Int
    indirect_edges : Int
    local_encoded_bytes : Int
    }

    FunctionCycleWitness

    pub struct FunctionCycleWitness {
    component_id : Int
    start_index : Int
    nodes : Array[Int]
    edges : Array[CallEdge]
    edge_count : Int
    direct_edges : Int
    reference_edges : Int
    indirect_edges : Int
    }

    FunctionCycleWitnessError

    pub enum FunctionCycleWitnessError {
    CycleWitnessQueryError(FunctionCyclesError)
    InvalidCycleWitness(Int, Int?, String)
    }

    FunctionCycleWitnessReport

    pub struct FunctionCycleWitnessReport {
    cycles : FunctionCyclesReport
    witnesses : Array[FunctionCycleWitness]
    scope : String
    }

    FunctionCyclesError

    pub enum FunctionCyclesError {
    CyclesInputError(WasmError)
    InvalidCyclesEvidence(Int, Int, String)
    }

    FunctionCyclesReport

    pub struct FunctionCyclesReport {
    file_size : Int
    sha256 : String
    function_count : Int
    imported_function_count : Int
    local_function_count : Int
    cyclic_function_count : Int
    non_cyclic_function_count : Int
    component_count : Int
    components : Array[FunctionCycleComponent]
    local_encoded_bytes : Int
    cyclic_local_encoded_bytes : Int
    other_local_encoded_bytes : Int
    graph_complete : Bool
    unresolved_count : Int
    diagnostics : Array[GraphDiagnostic]
    graph_warning : String
    modeling_scope : String
    }

    FunctionDependenciesError

    pub enum FunctionDependenciesError {
    DependenciesInputError(WasmError)
    InvalidDependenciesTarget(Int, Int)
    InvalidDependenciesEvidence(Int, Int, String)
    }

    FunctionDependenciesReport

    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
    }

    FunctionDependencyNode

    pub struct FunctionDependencyNode {
    node : FunctionPathNode
    depth : Int
    predecessor : CallEdge?
    }

    FunctionDependentNode

    pub struct FunctionDependentNode {
    node : FunctionPathNode
    depth : Int
    next_edge : CallEdge?
    }

    FunctionDependentsError

    pub enum FunctionDependentsError {
    DependentsInputError(WasmError)
    InvalidDependentsTarget(Int, Int)
    InvalidDependentsEvidence(Int, Int, String)
    }

    FunctionDependentsReport

    pub struct FunctionDependentsReport {
    target : FunctionPathNode
    file_size : Int
    sha256 : String
    function_count : Int
    local_function_count : Int
    imported_function_count : Int
    nodes : Array[FunctionDependentNode]
    edges : Array[CallEdge]
    reaching_function_count : Int
    other_function_count : Int
    reaching_local_count : Int
    reaching_imported_count : Int
    dependent_function_count : Int
    local_encoded_bytes : Int
    reaching_local_encoded_bytes : Int
    other_local_encoded_bytes : Int
    target_local_encoded_bytes : Int
    dependent_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
    }

    FunctionInstructionRange

    pub struct FunctionInstructionRange {
    file_offset : Int
    size : Int
    }

    Half-open original-file range; not a memory address.

    FunctionInstructionRecord

    pub struct FunctionInstructionRecord {
    ordinal : Int
    mnemonic : String
    file_offset : Int
    body_relative_offset : Int
    encoded_size : Int
    }

    FunctionInstructionsComparison

    pub struct FunctionInstructionsComparison {
    baseline : FunctionInstructionsReport
    current : FunctionInstructionsReport
    delta : FunctionInstructionsDelta
    opcodes : Array[FunctionOpcodeDelta]
    scope : String
    }

    FunctionInstructionsComparisonError

    pub enum FunctionInstructionsComparisonError {
    BaselineInstructionsError(Int, FunctionInstructionsError)
    CurrentInstructionsError(Int, FunctionInstructionsError)
    }

    FunctionInstructionsDelta

    pub struct FunctionInstructionsDelta {
    entry_bytes : Int
    prefix_bytes : Int
    body_bytes : Int
    locals_bytes : Int
    instruction_bytes : Int
    instruction_count : Int
    }

    Signed current-minus-baseline quantities, never file ranges.

    FunctionInstructionsError

    pub enum FunctionInstructionsError {
    InstructionsInputError(WasmError)
    InvalidInstructionsTarget(Int, Int)
    ImportedInstructionsTarget(Int)
    InvalidInstructionsRange(Int, String)
    }

    FunctionInstructionsReport

    pub struct FunctionInstructionsReport {
    file_size : Int
    sha256 : String
    index : Int
    name : String
    raw_name : String?
    entry : FunctionInstructionRange
    prefix : FunctionInstructionRange
    body : FunctionInstructionRange
    locals_encoding : FunctionInstructionRange
    instruction_bytes : Int
    instruction_count : Int
    groups : Array[FunctionOpcodeGroup]
    instructions : Array[FunctionInstructionRecord]
    scope : String
    }

    FunctionLocalAccess

    pub struct FunctionLocalAccess {
    instruction_ordinal : Int
    opcode : Opcode
    local_index : Int
    file_offset : Int
    body_relative_offset : Int
    encoded_size : Int
    index_encoding : FunctionInstructionRange
    }

    One encoded access; ordinal belongs to the full instruction sequence.

    FunctionLocalAccessGroup

    pub struct FunctionLocalAccessGroup {
    local_index : Int
    access_count : Int
    get_count : Int
    set_count : Int
    tee_count : Int
    encoded_bytes : Int
    index_encoding_bytes : Int
    instruction_ordinals : Array[Int]
    }

    Sparse numeric index group; members retain all original instruction ordinals.

    FunctionLocalAccessesError

    pub enum FunctionLocalAccessesError {
    LocalAccessesInputError(WasmError)
    InvalidLocalAccessesTarget(Int, Int)
    ImportedLocalAccessesTarget(Int)
    InvalidLocalAccessesEvidence(Int?, String)
    }

    FunctionLocalAccessesReport

    pub struct FunctionLocalAccessesReport {
    file_size : Int
    sha256 : String
    target : FunctionPathNode
    entry : FunctionInstructionRange
    prefix : FunctionInstructionRange
    body : FunctionInstructionRange
    locals_encoding : FunctionInstructionRange
    instruction_count : Int
    instruction_bytes : Int
    access_count : Int
    get_count : Int
    set_count : Int
    tee_count : Int
    distinct_local_index_count : Int
    access_encoding_bytes : Int
    index_encoding_bytes : Int
    other_instruction_bytes : Int
    accesses : Array[FunctionLocalAccess]
    groups : Array[FunctionLocalAccessGroup]
    scope : String
    }

    FunctionLocalGroup

    pub struct FunctionLocalGroup {
    group_index : Int
    quantity : UInt
    declared_ordinal_start : UInt64
    declared_ordinal_end : UInt64
    encoding : FunctionInstructionRange
    quantity_encoding : FunctionInstructionRange
    type_encoding : FunctionInstructionRange
    type_hex : String
    type_label : String?
    }

    One encoded declaration group, never expanded into individual variables. Declared ordinals exclude parameters and use half-open intervals.

    FunctionLocalsComparison

    pub struct FunctionLocalsComparison {
    baseline : FunctionLocalsReport
    current : FunctionLocalsReport
    delta : FunctionLocalsDelta
    type_encodings : Array[FunctionLocalsTypeEncoding]
    scope : String
    }

    FunctionLocalsComparisonError

    pub enum FunctionLocalsComparisonError {
    BaselineLocalsError(Int, FunctionLocalsError)
    CurrentLocalsError(Int, FunctionLocalsError)
    }

    FunctionLocalsDelta

    pub struct FunctionLocalsDelta {
    entry_bytes : Int
    prefix_bytes : Int
    body_bytes : Int
    locals_bytes : Int
    group_count_encoding_bytes : Int
    quantity_encoding_bytes : Int
    type_encoding_bytes : Int
    body_remainder_bytes : Int
    group_count : Int
    declared_count_delta_decimal : String
    }

    Signed current-minus-baseline encoding deltas, never file ranges.

    FunctionLocalsError

    pub enum FunctionLocalsError {
    LocalsInputError(WasmError)
    InvalidLocalsTarget(Int, Int)
    ImportedLocalsTarget(Int)
    InvalidLocalsEncoding(Int?, String)
    }

    FunctionLocalsReport

    pub struct FunctionLocalsReport {
    file_size : Int
    sha256 : String
    target : FunctionPathNode
    entry : FunctionInstructionRange
    prefix : FunctionInstructionRange
    body : FunctionInstructionRange
    locals_encoding : FunctionInstructionRange
    group_count_encoding : FunctionInstructionRange
    body_remainder : FunctionInstructionRange
    group_count : Int
    declared_count : UInt64
    quantity_encoding_bytes : Int
    type_encoding_bytes : Int
    groups : Array[FunctionLocalGroup]
    scope : String
    }

    FunctionLocalsTypeDelta

    pub struct FunctionLocalsTypeDelta {
    group_count : Int
    declared_count_delta_decimal : String
    quantity_encoding_bytes : Int
    type_encoding_bytes : Int
    group_bytes : Int
    }

    FunctionLocalsTypeEncoding

    pub struct FunctionLocalsTypeEncoding {
    type_hex : String
    type_label : String?
    presence : String
    baseline : FunctionLocalsTypeTotals
    current : FunctionLocalsTypeTotals
    delta : FunctionLocalsTypeDelta
    }

    Full raw type encoding is the key; presence depends on groups, not quantity.

    FunctionLocalsTypeTotals

    pub struct FunctionLocalsTypeTotals {
    group_indices : Array[Int]
    group_count : Int
    declared_count : UInt64
    quantity_encoding_bytes : Int
    type_encoding_bytes : Int
    group_bytes : Int
    }

    FunctionMatchStatus

    pub enum FunctionMatchStatus {
    Matched
    AddedSymbol
    RemovedSymbol
    AmbiguousSymbol
    MissingSymbol
    } derive(Eq)

    Symbol changes describe binary identities, not source additions/deletions. Ambiguous and missing symbols have one record per function per side.

    FunctionOpcodeDelta

    pub struct FunctionOpcodeDelta {
    mnemonic : String
    baseline_count : Int
    current_count : Int
    count_delta : Int
    baseline_encoded_bytes : Int
    current_encoded_bytes : Int
    encoded_bytes_delta : Int
    }

    FunctionOpcodeGroup

    pub struct FunctionOpcodeGroup {
    mnemonic : String
    count : Int
    encoded_bytes : Int
    }

    FunctionPathBetweenError

    pub enum FunctionPathBetweenError {
    BetweenDependenciesError(FunctionDependenciesError)
    InvalidBetweenTarget(Int, Int)
    InvalidBetweenEvidence(Int?, Int?, Int?, String)
    }

    FunctionPathBetweenReport

    pub struct FunctionPathBetweenReport {
    source : FunctionPathNode
    target_index : Int
    file_size : Int
    sha256 : String
    function_count : Int
    status : FunctionPathBetweenStatus
    path_nodes : Array[FunctionPathNode]
    path_edges : Array[CallEdge]
    hop_count : Int?
    prepared_closure_function_count : Int
    graph_complete : Bool
    unresolved_count : Int
    diagnostics : Array[GraphDiagnostic]
    graph_warning : String
    modeling_scope : String
    }

    FunctionPathBetweenStatus

    pub enum FunctionPathBetweenStatus {
    BetweenPathFound
    BetweenPathNotFound
    } derive(Eq)

    FunctionPathError

    pub enum FunctionPathError {
    PathInputError(WasmError)
    InvalidPathTarget(Int, Int)
    InvalidPathRoot(Int)
    InvalidPathLimits(String)
    InvalidPathRootReference(Int, Int)
    InvalidPathEdgeIndex(Int, Int)
    InvalidPathEdgeEvidence(Int)
    }

    FunctionPathNode

    pub struct FunctionPathNode {
    index : Int
    name : String
    raw_name : String?
    imported : Bool
    encoded_size : Int?
    import_module : String?
    import_name : String?
    }

    FunctionPathReport

    pub struct FunctionPathReport {
    target : FunctionPathNode
    requested_root : Int?
    roots : Array[FunctionRoot]
    selected_roots : Array[Int]
    status : FunctionPathStatus
    path_root : Int?
    nodes : Array[FunctionPathNode]
    steps : Array[FunctionPathStep]
    graph_complete : Bool
    diagnostics : Array[GraphDiagnostic]
    graph_warning : String
    limits : PathLimits
    search_stats : PathSearchStats
    }

    FunctionPathStatus

    pub enum FunctionPathStatus {
    PathFound
    PathNotFound
    PathSearchLimited
    } derive(Eq)

    FunctionPathStep

    pub struct FunctionPathStep {
    caller : Int
    callee : Int
    kind : EdgeKind
    opcode : Opcode
    offset : Int
    type_index : Int?
    table_index : Int?
    }

    FunctionRoot

    pub struct FunctionRoot {
    index : Int
    reasons : Array[FunctionRootReason]
    }

    FunctionRootKind

    pub enum FunctionRootKind {
    ExportRoot
    StartRoot
    ElementRoot
    } derive(Eq)

    FunctionRootReason

    pub struct FunctionRootReason {
    function_index : Int
    kind : FunctionRootKind
    name : String?
    section_offset : Int
    ordinal : Int?
    table_index : Int?
    element_mode : String?
    }

    Positions are section starts, with export/segment ordinals when available; never presented as member positions or instruction offsets.

    FunctionSearchError

    pub enum FunctionSearchError {
    InvalidFunctionSearchQuery
    FunctionSearchInputError(WasmError)
    }

    FunctionSearchMatch

    pub struct FunctionSearchMatch {
    index : Int
    name : String
    raw_name : String
    module_name : String
    offset : Int
    body_size : Int
    total_size : Int
    matched_raw_name : Bool
    matched_display_name : Bool
    }

    A named local definition, preserving its full function index and file range.

    FunctionSearchReport

    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]
    }

    FunctionSizeChange

    pub struct FunctionSizeChange {
    status : FunctionMatchStatus
    raw_name : String?
    baseline : FunctionStat?
    current : FunctionStat?
    delta : Int
    }

    FunctionStat

    pub struct FunctionStat {
    index : Int
    name : String
    raw_name : String?
    module_name : String
    body_size : Int
    total_size : Int
    offset : Int
    }

    One function's byte budget, with the name and package the binary gives it.

    GenericCoverage

    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
    }

    GenericFamily

    pub struct GenericFamily {
    family_key : String
    display_name : String
    definition_count : Int
    type_encoding_count : Int
    total_size : Int
    instances : Array[GenericInstance]
    }

    GenericInstance

    pub struct GenericInstance {
    index : Int
    raw_name : String
    raw_type_arguments : String
    type_labels : Array[String]
    total_size : Int
    }

    GenericInstancesReport

    pub struct GenericInstancesReport {
    file_size : Int
    sha256 : String
    name_status : String
    recognition_scope : String
    coverage : GenericCoverage
    families : Array[GenericFamily]
    }

    GraphDiagnostic

    pub struct GraphDiagnostic {
    kind : GraphDiagnosticKind
    caller : Int
    offset : Int
    opcode : Opcode
    type_index : Int?
    table_index : Int?
    source : GraphDiagnosticSource?
    }

    One reason a call site's dependencies may be incomplete. A site can have multiple distinct reasons and multiple runtime writer positions.

    GraphDiagnosticKind

    pub enum GraphDiagnosticKind {
    NoStaticTableTargets
    UnmodeledReferenceCall
    ImportedTableContents
    ExportedTableContents
    RuntimeTableMutation
    UnmodeledTableInitializer
    UnknownTableContents
    }

    A stable category of a detected gap in call-target analysis.

    GraphDiagnosticSource

    pub struct GraphDiagnosticSource {
    caller : Int?
    offset : Int
    opcode : Opcode?
    }

    Provenance of a gap, distinct from the call instruction's own position.

    ImportByteRange

    pub struct ImportByteRange {
    offset : Int
    size : Int
    } derive(Eq,
    Debug
    )

    Half-open range in the original file, never a runtime address.

    ImportDeclaration

    pub struct ImportDeclaration {
    ordinal : Int
    kind : String
    kind_index : Int
    module_name : String
    field : String
    encoding : ImportByteRange
    module_encoding : ImportByteRange
    field_encoding : ImportByteRange
    descriptor : ImportByteRange
    module_length_prefix_bytes : Int
    module_utf8_bytes : Int
    field_length_prefix_bytes : Int
    field_utf8_bytes : Int
    type_index : UInt?
    limits : ImportLimits?
    value_type : String?
    mutability : Int?
    }

    ImportInventoryCounts

    pub struct ImportInventoryCounts {
    baseline_entry_count : Int
    current_entry_count : Int
    overlap_count : Int
    removed_count : Int
    added_count : Int
    count_delta : Int
    } derive(Eq,
    Debug
    )

    ImportInventoryGroup

    pub struct ImportInventoryGroup {
    module_name : String
    field : String
    kind : String
    status : String
    baseline_ordinals : Array[Int]
    current_ordinals : Array[Int]
    baseline_count : Int
    current_count : Int
    overlap_count : Int
    removed_count : Int
    added_count : Int
    count_delta : Int
    baseline_entry_bytes : Int
    current_entry_bytes : Int
    entry_bytes_delta : Int
    }

    A multiset key group, not a physical declaration correspondence.

    ImportKindCounts

    pub struct ImportKindCounts {
    functions : Int
    tables : Int
    memories : Int
    globals : Int
    tags : Int
    } derive(Eq,
    Debug
    )

    ImportLimits

    pub struct ImportLimits {
    flags : Int
    minimum : UInt
    maximum : UInt?
    } derive(Eq,
    Debug
    )

    Supported 32-bit limits only. Values retain the full unsigned range.

    ImportModule

    pub struct ImportModule {
    module_name : String
    entry_count : Int
    kind_counts : ImportKindCounts
    entry_bytes : Int
    ordinals : Array[Int]
    }

    ImportsComparison

    pub struct ImportsComparison {
    baseline : ImportsReport
    current : ImportsReport
    groups : Array[ImportInventoryGroup]
    counts : ImportInventoryCounts
    inventory_changed : Bool
    delta : ImportsLedger
    comparison_scope : String
    }

    ImportsComparisonError

    pub enum ImportsComparisonError {
    ImportsBaselineError(WasmError)
    ImportsCurrentError(WasmError)
    }

    Side-specific wrapper; the original parser error and position are preserved.

    ImportsLedger

    pub struct ImportsLedger {
    import_section_total_bytes : Int
    section_header_bytes : Int
    count_prefix_bytes : Int
    entry_bytes : Int
    module_name_encoding_bytes : Int
    module_length_prefix_bytes : Int
    module_utf8_bytes : Int
    field_name_encoding_bytes : Int
    field_length_prefix_bytes : Int
    field_utf8_bytes : Int
    descriptor_bytes : Int
    } derive(Eq,
    Debug
    )

    Parent and child totals are decompositions, not additional costs.

    ImportsReport

    pub struct ImportsReport {
    file_size : Int
    sha256 : String
    supported_scope : String
    section : ImportByteRange?
    count_encoding : ImportByteRange?
    entry_count : Int
    kind_counts : ImportKindCounts
    ledger : ImportsLedger
    modules : Array[ImportModule]
    entries : Array[ImportDeclaration]
    }

    IndirectSite

    pub struct IndirectSite {
    caller : Int
    table_index : Int
    type_index : Int
    offset : Int
    candidates : Array[Int]
    }

    A call_indirect or return_call_indirect, with its table's candidates.

    Instruction

    pub struct Instruction {
    opcode : Opcode
    offset : Int
    size : Int
    operands : Array[Int]
    }

    A decoded instruction: what it is, where it starts and what it carries.

    MatchingSummary

    pub struct MatchingSummary {
    matched_baseline : FunctionCoverage
    matched_current : FunctionCoverage
    added_symbols : FunctionCoverage
    removed_symbols : FunctionCoverage
    ambiguous_baseline : FunctionCoverage
    ambiguous_current : FunctionCoverage
    missing_baseline : FunctionCoverage
    missing_current : FunctionCoverage
    matched_delta : Int
    }

    Disjoint per-side categories. Missing/ambiguous are the unmatched reasons; neither category is also counted as an added or removed symbol.

    MetadataCategory

    pub struct MetadataCategory {
    category : String
    custom_ordinals : Array[Int]
    total_bytes : Int
    }

    MetadataComparison

    pub struct MetadataComparison {
    baseline : MetadataReport
    current : MetadataReport
    delta : MetadataEncodingTotals
    baseline_non_custom_bytes : Int
    current_non_custom_bytes : Int
    delta_non_custom_bytes : Int
    delta_file_bytes : Int
    categories : Array[MetadataComparisonGroup]
    name_groups : Array[MetadataComparisonGroup]
    comparison_scope : String
    }

    MetadataComparisonGroup

    pub struct MetadataComparisonGroup {
    kind : String
    name : String?
    baseline_ordinals : Array[Int]
    current_ordinals : Array[Int]
    baseline : MetadataEncodingTotals
    current : MetadataEncodingTotals
    delta : MetadataEncodingTotals
    }

    Ordinals identify physical instances within each input, never matched pairs.

    MetadataCustom

    pub struct MetadataCustom {
    ordinal : Int
    section_ordinal : Int
    encoding : MetadataRange
    section_header_bytes : Int
    name_encoding : MetadataRange
    name_length_prefix_bytes : Int
    name_bytes : MetadataRange
    name : String?
    category : String
    content : MetadataRange
    status : String
    name_subsection_bytes : Int
    unbounded_tail : MetadataRange?
    }

    MetadataDiagnostic

    pub struct MetadataDiagnostic {
    offset : Int
    custom_ordinal : Int
    subsection_ordinal : Int?
    reason : String
    }

    MetadataEncodingTotals

    pub struct MetadataEncodingTotals {
    custom_count : Int
    custom_encoded_bytes : Int
    section_header_bytes : Int
    name_encoding_bytes : Int
    content_bytes : Int
    } derive(Eq,
    Debug
    )

    Outer custom encoding accounting only; counts/deltas are not file ranges.

    MetadataFunctionName

    pub struct MetadataFunctionName {
    custom_ordinal : Int
    subsection_ordinal : Int
    ordinal : Int
    function_index : UInt
    encoding : MetadataRange
    index_prefix_bytes : Int
    name_length_prefix_bytes : Int
    name_bytes : MetadataRange
    name : String?
    }

    MetadataNameGroup

    pub struct MetadataNameGroup {
    name : String?
    custom_ordinals : Array[Int]
    total_bytes : Int
    }

    MetadataNameSubsection

    pub struct MetadataNameSubsection {
    custom_ordinal : Int
    ordinal : Int
    id : Int
    encoding : MetadataRange
    header_bytes : Int
    payload : MetadataRange
    status : String
    declared_count : UInt?
    count_prefix_bytes : Int
    parsed_record_bytes : Int
    unparsed : MetadataRange?
    }

    MetadataRange

    pub struct MetadataRange {
    offset : Int
    size : Int
    } derive(Eq,
    Debug
    )

    Half-open original-file range. Offsets are never memory addresses.

    MetadataReport

    pub struct MetadataReport {
    file_size : Int
    sha256 : String
    byte_ledger_complete : Bool
    custom_name_status : String
    name_details_status : String
    modeling_scope : String
    summary : MetadataSummary
    categories : Array[MetadataCategory]
    name_groups : Array[MetadataNameGroup]
    customs : Array[MetadataCustom]
    subsections : Array[MetadataNameSubsection]
    function_names : Array[MetadataFunctionName]
    diagnostics : Array[MetadataDiagnostic]
    }

    MetadataSummary

    pub struct MetadataSummary {
    custom_count : Int
    custom_bytes : Int
    section_header_bytes : Int
    custom_name_encoding_bytes : Int
    content_bytes : Int
    unavailable_custom_count : Int
    unavailable_custom_bytes : Int
    name_custom_count : Int
    name_content_bytes : Int
    name_subsection_bytes : Int
    unbounded_name_tail_bytes : Int
    function_name_payload_bytes : Int
    function_name_count_prefix_bytes : Int
    parsed_function_record_count : Int
    parsed_function_record_bytes : Int
    function_name_unparsed_bytes : Int
    function_index_prefix_bytes : Int
    function_name_length_prefix_bytes : Int
    function_name_utf8_bytes : Int
    unavailable_function_name_count : Int
    }

    ModuleOpcodeChange

    pub struct ModuleOpcodeChange {
    mnemonic : String
    status : String
    baseline_count : Int
    current_count : Int
    count_delta : Int
    baseline_encoded_bytes : Int
    current_encoded_bytes : Int
    encoded_bytes_delta : Int
    baseline_function_count : Int
    current_function_count : Int
    function_count_delta : Int
    }

    status is shared_opcode, baseline_only or current_only. Missing-side counts are zero; neither rows nor function_count deltas establish function identity.

    ModuleOpcodeComparison

    pub struct ModuleOpcodeComparison {
    baseline : ModuleOpcodeReport
    current : ModuleOpcodeReport
    delta : ModuleOpcodeDelta
    opcodes : Array[ModuleOpcodeChange]
    scope : String
    }

    ModuleOpcodeComparisonError

    pub enum ModuleOpcodeComparisonError {
    BaselineModuleOpcodeError(ModuleOpcodeError)
    CurrentModuleOpcodeError(ModuleOpcodeError)
    }

    ModuleOpcodeDelta

    pub struct ModuleOpcodeDelta {
    file_bytes : Int
    code_bytes : Int
    non_code_bytes : Int
    code_header_bytes : Int
    count_encoding_bytes : Int
    body_prefix_bytes : Int
    locals_encoding_bytes : Int
    instruction_bytes : Int
    instruction_count : Int
    imported_function_count : Int
    local_function_count : Int
    }

    Signed current-minus-baseline aggregate statistics; never file ranges.

    ModuleOpcodeError

    pub enum ModuleOpcodeError {
    ModuleOpcodeInputError(WasmError)
    ModuleOpcodeBodyError(Int, WasmError)
    InvalidModuleOpcodeEvidence(Int?, Int?, String)
    }

    ModuleOpcodeFunction

    pub struct ModuleOpcodeFunction {
    stat : FunctionStat
    locals_encoding : FunctionInstructionRange
    instruction_count : Int
    instruction_bytes : Int
    }

    Compact local-function facts, without instructions or an opcode matrix.

    ModuleOpcodeGroup

    pub struct ModuleOpcodeGroup {
    mnemonic : String
    count : Int
    encoded_bytes : Int
    function_count : Int
    }

    Complete instruction encoding bytes include opcode, subopcode and immediates.

    ModuleOpcodeReport

    pub struct ModuleOpcodeReport {
    file_size : Int
    sha256 : String
    code_section : Section?
    count_encoding : FunctionInstructionRange?
    imported_function_count : Int
    local_function_count : Int
    code_header_bytes : Int
    count_encoding_bytes : Int
    body_prefix_bytes : Int
    locals_encoding_bytes : Int
    instruction_bytes : Int
    instruction_count : Int
    groups : Array[ModuleOpcodeGroup]
    functions : Array[ModuleOpcodeFunction]
    scope : String
    }

    ModuleSizeDiff

    pub struct ModuleSizeDiff {
    baseline_size : Int
    current_size : Int
    delta : Int
    baseline_code : CodeSizeSummary
    current_code : CodeSizeSummary
    functions : Array[FunctionSizeChange]
    packages : Array[PackageSizeChange]
    matching : MatchingSummary
    }

    All sizes are raw encoded bytes. Function/package sums explain local function encoding, not file deltas (name/data/etc. remain in the section comparison). Records are ranked by absolute delta, then raw symbol/package identity.

    Opcode

    pub enum Opcode {
    Unreachable
    Nop
    Block
    Loop
    If
    Else
    Try
    Catch
    Throw
    Rethrow
    ThrowRef
    End
    Br
    BrIf
    BrTable
    Return
    Call
    CallIndirect
    ReturnCall
    ReturnCallIndirect
    CallRef
    ReturnCallRef
    Drop
    Select
    Delegate
    CatchAll
    SelectT
    LocalGet
    LocalSet
    LocalTee
    GlobalGet
    GlobalSet
    TableGet
    TableSet
    I32Load
    I64Load
    F32Load
    F64Load
    I32Load8S
    I32Load8U
    I32Load16S
    I32Load16U
    I64Load8S
    I64Load8U
    I64Load16S
    I64Load16U
    I64Load32S
    I64Load32U
    I32Store
    I64Store
    F32Store
    F64Store
    I32Store8
    I32Store16
    I64Store8
    I64Store16
    I64Store32
    MemorySize
    MemoryGrow
    I32Const
    I64Const
    F32Const
    F64Const
    I32Eqz
    I32Eq
    I32Ne
    I32LtS
    I32LtU
    I32GtS
    I32GtU
    I32LeS
    I32LeU
    I32GeS
    I32GeU
    I64Eqz
    I64Eq
    I64Ne
    I64LtS
    I64LtU
    I64GtS
    I64GtU
    I64LeS
    I64LeU
    I64GeS
    I64GeU
    F32Eq
    F32Ne
    F32Lt
    F32Gt
    F32Le
    F32Ge
    F64Eq
    F64Ne
    F64Lt
    F64Gt
    F64Le
    F64Ge
    I32Clz
    I32Ctz
    I32Popcnt
    I32Add
    I32Sub
    I32Mul
    I32DivS
    I32DivU
    I32RemS
    I32RemU
    I32And
    I32Or
    I32Xor
    I32Shl
    I32ShrS
    I32ShrU
    I32Rotl
    I32Rotr
    I64Clz
    I64Ctz
    I64Popcnt
    I64Add
    I64Sub
    I64Mul
    I64DivS
    I64DivU
    I64RemS
    I64RemU
    I64And
    I64Or
    I64Xor
    I64Shl
    I64ShrS
    I64ShrU
    I64Rotl
    I64Rotr
    F32Abs
    F32Neg
    F32Ceil
    F32Floor
    F32Trunc
    F32Nearest
    F32Sqrt
    F32Add
    F32Sub
    F32Mul
    F32Div
    F32Min
    F32Max
    F32Copysign
    F64Abs
    F64Neg
    F64Ceil
    F64Floor
    F64Trunc
    F64Nearest
    F64Sqrt
    F64Add
    F64Sub
    F64Mul
    F64Div
    F64Min
    F64Max
    F64Copysign
    I32WrapI64
    I32TruncF32S
    I32TruncF32U
    I32TruncF64S
    I32TruncF64U
    I64ExtendI32S
    I64ExtendI32U
    I64TruncF32S
    I64TruncF32U
    I64TruncF64S
    I64TruncF64U
    F32ConvertI32S
    F32ConvertI32U
    F32ConvertI64S
    F32ConvertI64U
    F32DemoteF64
    F64ConvertI32S
    F64ConvertI32U
    F64ConvertI64S
    F64ConvertI64U
    F64PromoteF32
    I32ReinterpretF32
    I64ReinterpretF64
    F32ReinterpretI32
    F64ReinterpretI64
    I32Extend8S
    I32Extend16S
    I64Extend8S
    I64Extend16S
    I64Extend32S
    RefNull
    RefIsNull
    RefFunc
    RefEq
    RefAsNonNull
    BrOnNull
    BrOnNonNull
    I32TruncSatF32S
    I32TruncSatF32U
    I32TruncSatF64S
    I32TruncSatF64U
    I64TruncSatF32S
    I64TruncSatF32U
    I64TruncSatF64S
    I64TruncSatF64U
    MemoryInit
    DataDrop
    MemoryCopy
    MemoryFill
    TableInit
    ElemDrop
    TableCopy
    TableGrow
    TableSize
    TableFill
    StructNew
    StructNewDefault
    StructGet
    StructGetS
    StructGetU
    StructSet
    ArrayNew
    ArrayNewDefault
    ArrayNewFixed
    ArrayNewData
    ArrayNewElem
    ArrayGet
    ArrayGetS
    ArrayGetU
    ArraySet
    ArrayLen
    ArrayFill
    ArrayCopy
    ArrayInitData
    ArrayInitElem
    RefTest
    RefTestNull
    RefCast
    RefCastNull
    BrOnCast
    BrOnCastFail
    AnyConvertExtern
    ExternConvertAny
    RefI31
    I31GetS
    I31GetU
    }

    Every instruction the decoder recognises, one variant per mnemonic.

    The set covers the MVP, the sign-extension, saturating-truncation, bulk-memory and reference-type extensions MoonBit's linear-memory backend emits, and the garbage-collection instructions its wasm-gc backend emits.

    Opcode::name

    fn Opcode::name(self : Opcode) -> String

    The spec mnemonic for an opcode, spelled as the text format writes it.

    Opcode::operands

    fn Opcode::operands(self : Opcode) -> Operands

    The immediate operands each opcode carries.

    OpcodeSearchError

    pub enum OpcodeSearchError {
    InvalidOpcodeSearchQuery
    OpcodeSearchInputError(ModuleOpcodeError)
    }

    Existing module opcode errors retain structure/body/evidence distinctions.

    OpcodeSearchFunction

    pub struct OpcodeSearchFunction {
    stat : FunctionStat
    match_count : Int
    matched_encoded_bytes : Int
    sites : Array[FunctionInstructionRecord]
    }

    Only matching instructions, retaining ordinals from the complete sequence.

    OpcodeSearchReport

    pub struct OpcodeSearchReport {
    file_size : Int
    sha256 : String
    query : String
    matching_rule : String
    scope : String
    imported_function_count : Int
    scanned_local_function_count : Int
    matched_function_count : Int
    unmatched_function_count : Int
    instruction_count : Int
    instruction_bytes : Int
    match_count : Int
    matched_encoded_bytes : Int
    other_instruction_bytes : Int
    functions : Array[OpcodeSearchFunction]
    }

    Operands

    pub enum Operands {
    None_
    BlockType
    Label
    LabelTable
    FunctionIndex
    CallIndirect
    LocalIndex
    GlobalIndex
    TableIndex
    TagIndex
    MemoryIndex
    MemoryArg
    I32Const
    I64Const
    F32Const
    F64Const
    HeapType
    RefType
    TypeIndex
    TypeFieldPair
    TypePair
    TypeCountPair
    TypeDataPair
    TypeElemPair
    DataIndex
    ElemIndex
    DataMemoryPair
    MemoryPair
    ElemTablePair
    TablePair
    CastPair
    TypeVector
    }

    How the bytes after an opcode are laid out.

    The shapes named <a><b>Pair read a then b, in encoding order. Naming them here rather than inlining the reads keeps every immediate width in one table that can be reviewed — and tested — against the specification.

    Options

    pub struct Options {
    path : String
    top : Int
    retained : Bool
    dead_code : Bool
    call_graph : String?
    html : String?
    summary_json : String?
    max_size : Int?
    baseline : String?
    diff_functions : Bool
    diff_functions_json : String?
    experiment : String?
    experiment_context : String?
    why_index : Int?
    why_root : Int?
    why_json : String?
    compare_dependencies : (Int, Int)?
    compare_dependencies_json : String?
    path_between : (Int, Int)?
    path_between_json : String?
    dependents : Int?
    dependents_json : String?
    dependents_dot : String?
    dependencies : Int?
    dependencies_json : String?
    dependencies_dot : String?
    cycles : Bool
    cycle_witness : Bool
    cycles_json : String?
    callers_index : Int?
    callers_json : String?
    calls_index : Int?
    calls_json : String?
    local_accesses_index : Int?
    local_accesses_json : String?
    find_opcode : String?
    find_opcode_json : String?
    diff_opcodes : Bool
    opcode_stats : Bool
    opcode_stats_json : String?
    locals_index : Int?
    locals_json : String?
    baseline_locals_index : Int?
    generics : Bool
    generics_json : String?
    entrypoints : Bool
    entrypoints_json : String?
    duplicates : Bool
    duplicates_json : String?
    exports : Bool
    diff_exports : Bool
    exports_json : String?
    metadata : Bool
    diff_metadata : Bool
    metadata_json : String?
    imports : Bool
    imports_json : String?
    diff_imports : Bool
    data_duplicates : Bool
    data_duplicates_json : String?
    instructions_index : Int?
    instructions_json : String?
    baseline_instructions_index : Int?
    find_functions : String?
    find_functions_json : String?
    find_data_hex : String?
    find_data : String?
    find_data_json : String?
    baseline_data_preview_index : Int?
    baseline_data_offset : Int?
    data_preview_index : Int?
    data_offset : Int?
    data_limit : Int?
    data_preview_json : String?
    locate_range_offset : Int?
    locate_range_length : Int?
    locate_range_json : String?
    locate_offset : Int?
    locate_offset_json : String?
    locate_decode : Bool
    budget : String?
    budget_json : String?
    budget_md : String?
    compress : Bool
    data_segments : Bool
    data_json : String?
    diff_data : Bool
    strict : Bool
    }

    What to analyze, how much of it to show, and which reports to produce.

    Options::default

    fn Options::default(path : String) -> Options

    The defaults parse_options starts from, with one flat report for the module at path. For callers that link this module as a library instead of running the CLI: every flag the command line can set is left at its absent value.

    PackageSizeChange

    pub struct PackageSizeChange {
    name : String
    baseline_functions : Int
    current_functions : Int
    baseline_size : Int
    current_size : Int
    delta : Int
    }

    Best-effort package attribution includes every local function, even unmatched.

    PathLimits

    pub struct PathLimits {
    max_nodes : Int
    max_edges : Int
    max_depth : Int
    }

    Limits apply separately to roots/visited nodes, root-source records, indexed edges and BFS edge examinations. Parsing/building the existing graph is not bounded by these query limits; deterministic sorting has its own cost.

    PathLimits::new

    fn PathLimits::new(max_nodes? : Int, max_edges? : Int, max_depth? : Int) -> PathLimits

    PathSearchStats

    pub struct PathSearchStats {
    root_sources : Int
    prepared_roots : Int
    indexed_edges : Int
    visited_nodes : Int
    checked_edges : Int
    max_depth : Int
    phase : String
    limit_reason : String?
    }

    Reader

    pub struct Reader {
    data : Bytes
    start : Int
    end : Int
    pos : Int
    }

    An absolute cursor restricted to [start, end) within a byte buffer.

    The shape follows jtenner/starshine's src/binary decoder, where a decode step takes (bytes, offset) and reports how far it advanced, so callers can walk a stream without hidden global state.

    Reader::at_end

    fn Reader::at_end(self : Reader) -> Bool

    Reader::new

    fn Reader::new(data : Bytes) -> Reader

    Reader::new_range

    fn Reader::new_range(data : Bytes, start : Int, length : Int) -> Result[Reader, WasmError]

    Construct a bounded reader, checking subtraction before any addition.

    Reader::position

    fn Reader::position(self : Reader) -> Int

    Reader::read_bits

    fn Reader::read_bits(self : Reader, count : Int) -> Result[Int, WasmError]

    Read a little-endian bit pattern of count bytes as a single integer.

    Floating point constants have no structural meaning for a size analyzer, so they are kept as the bits the file holds rather than converted and back.

    Reader::read_byte

    fn Reader::read_byte(self : Reader) -> Result[Int, WasmError]

    Read a single byte, or report the offset that ran out of input.

    Reader::read_bytes

    fn Reader::read_bytes(self : Reader, count : Int) -> Result[BytesView, WasmError]

    Read count raw bytes and advance past them.

    Negative or excessive counts leave the cursor unchanged. A view is enough: names are decoded straight from it and section payloads are skipped by size rather than copied.

    Reader::read_reftype

    fn Reader::read_reftype(self : Reader) -> Result[Int, WasmError]

    Consume a reference type.

    A reference type is either a one-byte shorthand such as funcref (0x70), or one of the two constructors 0x63/0x64 followed by a heap type. The shorthand byte is returned, or 0 when the two-byte form was used.

    Reader::read_signed_leb

    fn Reader::read_signed_leb(self : Reader, bits : Int) -> Result[Int, WasmError]

    Read a signed LEB128 integer of at most bits significant bits.

    Block types, heap types and the i32.const/i64.const immediates are all signed, and the sign lives in the last group's bit 6, so leaving it out would turn a small negative constant into a large positive one. The bits bound is what makes a non-terminating encoding malformed: a group may only continue while fewer than bits bits have been consumed.

    Reader::read_u32_leb

    fn Reader::read_u32_leb(self : Reader) -> Result[UInt, WasmError]

    Read an unsigned LEB128 integer of at most 32 significant bits.

    WebAssembly encodes section payload lengths, vector counts and custom section name lengths this way. Five groups of seven bits cover u32, so a fifth byte must terminate and may carry only four payload bits. Non-shortest encodings are accepted within these limits.

    Reader::read_valtype

    fn Reader::read_valtype(self : Reader) -> Result[Unit, WasmError]

    Consume a value type, whose width is not always one byte.

    Numeric and vector types are a single byte, but a garbage-collection reference type is the two-byte 0x63/0x64 form followed by a heap type. Reading one byte per type desynchronises the stream on the first struct-typed local, so every value type goes through here.

    Reader::remaining

    fn Reader::remaining(self : Reader) -> Int

    Reader::seek

    fn Reader::seek(self : Reader, offset : Int) -> Result[Unit, WasmError]

    Move to [start, end], including the end. Failure leaves the cursor unchanged.

    RetainedSize

    pub struct RetainedSize {
    index : Int
    own_size : Int
    retained : Int
    retained_functions : Int
    }

    Encoded function weights in a dominator subtree of the current graph model.

    Section

    pub struct Section {
    id : Int
    kind : SectionId
    offset : Int
    payload_size : Int
    total_size : Int
    custom_name : String?
    }

    One decoded section header, with the byte budget it accounts for.

    SectionCompression

    pub struct SectionCompression {
    label : String
    raw : Int
    gzip : Result[Int, String]
    }

    A section's complete encoding (id, length prefix and payload), measured as its own gzip stream. raw is the length of that same encoding.

    SectionId

    pub enum SectionId {
    Custom
    Type
    Import
    Function
    Table
    Memory
    Global
    Export
    Start
    Element
    Code
    Data
    DataCount
    Tag
    }

    The section identifiers defined by the WebAssembly core specification.

    SectionId::from_id

    fn SectionId::from_id(id : Int) -> SectionId?

    Map a raw section id byte to its meaning, or None when unassigned.

    SectionId::is_custom

    fn SectionId::is_custom(self : SectionId) -> Bool

    Custom sections are the only ones that carry a name, and they are worth telling apart from the numbered sections.

    SectionId::name

    fn SectionId::name(self : SectionId) -> String

    The lower-case name used by wasm-objdump and friends.

    Sections

    pub struct Sections {
    code : Section?
    imports : Section?
    names : Section?
    exports : Section?
    elements : Section?
    tables : Section?
    start : Section?
    }

    The sections the analysis passes need, picked out of the section table once.

    parse_module validates standard-section uniqueness before returning a module. Several custom sections are legal; the last name custom section is selected.

    TableType

    pub struct TableType {
    element_type : Int
    minimum : Int
    maximum : Int?
    }

    A table declared by the table section.

    WasmError

    pub enum WasmError {
    UnexpectedEnd(Int, Int)
    BadMagic
    InvalidSectionId(Int)
    LebOverflow(Int)
    InvalidReadRange(Int, Int, Int, Int)
    ValueOutOfRange(Int, UInt)
    UnsupportedVersion(Int, UInt)
    DuplicateSection(Int, SectionId)
    InvalidSectionOrder(Int, SectionId, SectionId)
    FunctionCodeCountMismatch(Int, SectionId, Int, Int)
    TrailingSectionBytes(Int, SectionId)
    InvalidTableEncoding(Int, String)
    InvalidDataEncoding(Int, String)
    InvalidImportKind(Int)
    InvalidImportEncoding(Int, String)
    InvalidExportEncoding(Int, String)
    UnknownOpcode(Int, Int)
    UnknownPrefixedOpcode(Int, Int, Int)
    }

    Every failure mode the decoder can report, with the offset that explains it.

    WasmModule

    pub struct WasmModule {
    version : Int
    file_size : Int
    sections : Array[Section]
    }

    A decoded module preamble plus its section table.

    DEFAULT_TOP

    let DEFAULT_TOP : Int

    How many functions --top ranks when the flag is absent.

    EXIT_INPUT

    let EXIT_INPUT : Int

    The input could not be read, or is not a WebAssembly module.

    EXIT_OK

    let EXIT_OK : Int

    The analysis ran and nothing was over budget.

    EXIT_OVER_SIZE

    let EXIT_OVER_SIZE : Int

    The run produced something the caller should not accept: the module is over the size budget the invocation set, or --strict found the call graph incomplete. Both mean "this run's answer is refused", which is why they share a code — a job branches on it either way.

    EXIT_USAGE

    let EXIT_USAGE : Int

    The command line did not make sense.

    analyze

    fn analyze(data : Bytes, _parsed : WasmModule) -> Result[Analysis, WasmError]

    Analyse actual bytes after revalidating their structure. The parsed argument is retained for API compatibility; stale or mutated metadata cannot bypass checks. Revalidation builds no analysis or call graph and no body records.

    budget_requires_baseline

    fn budget_requires_baseline(budget : CompiledBudget) -> Bool

    build

    fn build(data : Bytes) -> Result[CallGraph, WasmError]

    Build from complete module bytes, parsing canonical bodies, elements and table context internally. Callers cannot supply an empty table context or omit a writer body. Use analyze when sizes and retention are also needed.

    Every body is decoded once: call and return_call give precise edges, ref.func gives a reference edge, and call_indirect and return_call_indirect give conservative edges to static table candidates plus a record of each site, including sites with no candidates. Reference calls are kept as diagnostics without guessing a target, even when the preceding instruction is ref.func.

    check_experiment_context

    fn check_experiment_context(context : Json) -> Result[Unit, String]

    Validate optional context before reading Wasm inputs or creating output. Null means no context was recorded. Unknown fields fail rather than silently accepting a typo in a build setting or validation declaration.

    collect_function_stats

    fn collect_function_stats(parsed : WasmModule, data : Bytes) -> Result[Array[FunctionStat], WasmError]

    Read a module's function names and sizes.

    A convenience over analyze for callers that only want the size table; the two share function_stats, so they cannot disagree about attribution.

    compare_data_previews

    fn compare_data_previews(baseline_data : Bytes, baseline_index : Int, baseline_offset : Int, current_data : Bytes, current_index : Int, current_offset : Int, limit : Int) -> Result[DataPreviewComparison, DataPreviewComparisonError]

    Preview each explicit side once, then compare at most 4096 relative positions. No resource matching, prior search/diff-data, edit alignment or ordinary analysis.

    compare_data_segments

    fn compare_data_segments(baseline : Bytes, current : Bytes) -> Result[DataSegmentsComparison, ComparisonError]

    Exact payload groups after safe inspection of both inputs. No offset evaluation, stable entry identity, semantic-equivalence or savings inference.

    compare_exports

    fn compare_exports(baseline : Bytes, current : Bytes) -> Result[ExportsComparison, ComparisonError]

    Each strict inventory is obtained once. No rehashing, target matching, graph or type analysis; any side error aborts the whole comparison.

    compare_function_dependencies

    fn compare_function_dependencies(data : Bytes, left_index : Int, right_index : Int) -> Result[DependencyOverlapReport, DependencyOverlapError]

    One build; different starts invoke the old helper twice, equal starts once.

    compare_function_instructions

    fn compare_function_instructions(baseline : Bytes, baseline_index : Int, current : Bytes, current_index : Int) -> Result[FunctionInstructionsComparison, FunctionInstructionsComparisonError]

    Each side inspects its explicit target once. No identity matching or alignment.

    compare_function_locals

    fn compare_function_locals(baseline_data : Bytes, baseline_index : Int, current_data : Bytes, current_index : Int) -> Result[FunctionLocalsComparison, FunctionLocalsComparisonError]

    Inspect each explicitly selected target once; no identity or group alignment.

    compare_imports

    fn compare_imports(baseline : Bytes, current : Bytes) -> Result[ImportsComparison, ImportsComparisonError]

    Parse once per side. No analyze, graph, body inventory or type comparison.

    compare_metadata

    fn compare_metadata(baseline : Bytes, current : Bytes) -> Result[MetadataComparison, ComparisonError]

    Each side is inspected exactly once. Optional name-detail partial results remain successful; neither contents nor internal name records are compared.

    compare_module_opcodes

    fn compare_module_opcodes(baseline_data : Bytes, current_data : Bytes) -> Result[ModuleOpcodeComparison, ModuleOpcodeComparisonError]

    Inspect each side exactly once. No per-function matching or re-decoding.

    compare_modules

    fn compare_modules(baseline_data : Bytes, current_data : Bytes) -> Result[ModuleSizeDiff, ComparisonError]

    Validate both inputs and collect raw symbols and encoded function sizes. This does not build either call graph or require graph completeness. It is basic structure checking, not full Wasm type/instruction validation.

    compile_budget

    fn compile_budget(config : Json) -> Result[CompiledBudget, Array[BudgetProblem]]

    Preflight all fields, reporting every configuration problem before I/O.

    compile_budget_text

    fn compile_budget_text(text : String) -> Result[CompiledBudget, Array[BudgetProblem]]

    JSON byte thresholds use decimal integer tokens. Check the source too: the generic JSON parser rounds tiny fractional parts when converting to Double.

    compress_gzip

    fn compress_gzip(data : Bytes) -> Result[Bytes, String]

    Compress data as a gzip stream.

    Keep the compressor's current defaults. A server may use another gzip configuration, another content encoding, or no compression at all.

    compress_sections

    fn compress_sections(data : Bytes, parsed : WasmModule) -> Array[SectionCompression]

    Compress each section of parsed on its own, out of the file it was read from, heaviest raw section first.

    Each section becomes its own gzip stream, so each pays its own container — roughly twenty bytes of magic, header, CRC and length. That is invisible for a section of kilobytes and dominant for one of a handful of bytes, which is why a tiny section can come back larger than it went in.

    This is what render_compression is handed instead of bare Sections: a Section records where its bytes are, not what they are, and a section cannot be compressed without them.

    compressed_size

    fn compressed_size(data : Bytes) -> Result[Int, String]

    The compressed size of data in bytes, or why the compressor would not take it.

    The failure comes back as a failure rather than being swallowed into the raw size. Returning data.length() would render as a ratio of 1.00x, which reads as "this stream does not compress" when the truth is that nothing was measured at all — a report that cannot tell those apart is worse than one that admits it is missing a number.

    count_imported_functions

    fn count_imported_functions(data : Bytes, section : Section) -> Result[Int, WasmError]

    Count the function imports of a module.

    Imported functions occupy the first slots of the function index space, so this count is what turns a code-section position into the index that the name section and every call instruction actually use.

    decode_instruction

    fn decode_instruction(bytes : Bytes, offset : Int) -> Result[Instruction, WasmError]

    Decode the instruction that starts at offset.

    An unassigned opcode is an error rather than a skip: silently stepping over one byte would turn a decoding bug into a wrong call graph, and every number downstream of this function is a size.

    decode_instructions

    fn decode_instructions(bytes : Bytes, body_offset : Int, body_size : Int) -> Result[Array[Instruction], WasmError]

    Skip a function body's local declaration and decode the expression.

    body_offset is the first byte after the body's size prefix, so the locals vector comes first. Locals and instructions share a bounded reader, so no immediate can consume bytes from the next body before reporting an error, which is what keeps a wrong immediate width from quietly becoming a wrong call graph.

    demangle_full

    fn demangle_full(symbol : String) -> String

    The full decoded name for a symbol: escapes resolved, generic arguments spelled out, lifted closures marked.

    This is a display transform, not a codec. Every step is allowed to give up, and when one does the original symbol is returned unchanged, because a size report that invents a name is worse than one that shows a raw symbol.

    Known limits, all of which fall back to the raw symbol: a type code outside the builtin letters, a named type argument that is not the last one in a list (its path has no terminator, so it cannot be told apart from what follows), and any symbol that is not mangled by MoonBit in the first place.

    describe_budget_error

    fn describe_budget_error(error : BudgetError) -> String

    describe_data_hex_search_error

    fn describe_data_hex_search_error(error : DataHexSearchError) -> String

    describe_data_preview_comparison_error

    fn describe_data_preview_comparison_error(error : DataPreviewComparisonError) -> String

    describe_data_preview_error

    fn describe_data_preview_error(error : DataPreviewError) -> String

    describe_data_search_error

    fn describe_data_search_error(error : DataSearchError) -> String

    describe_dependency_overlap_error

    fn describe_dependency_overlap_error(error : DependencyOverlapError) -> String

    describe_entrypoints_error

    fn describe_entrypoints_error(error : EntrypointsError) -> String

    describe_error

    fn describe_error(error : WasmError) -> String

    A one-line explanation for a failed parse.

    describe_file_offset_decode_error

    fn describe_file_offset_decode_error(error : FileOffsetDecodeError) -> String

    describe_file_offset_error

    fn describe_file_offset_error(error : FileOffsetError) -> String

    describe_file_range_error

    fn describe_file_range_error(error : FileRangeError) -> String

    describe_function_callers_error

    fn describe_function_callers_error(error : FunctionCallersError) -> String

    describe_function_calls_error

    fn describe_function_calls_error(error : FunctionCallsError) -> String

    describe_function_cycle_witness_error

    fn describe_function_cycle_witness_error(error : FunctionCycleWitnessError) -> String

    describe_function_cycles_error

    fn describe_function_cycles_error(error : FunctionCyclesError) -> String

    describe_function_dependencies_error

    fn describe_function_dependencies_error(error : FunctionDependenciesError) -> String

    describe_function_dependents_error

    fn describe_function_dependents_error(error : FunctionDependentsError) -> String

    describe_function_instructions_comparison_error

    fn describe_function_instructions_comparison_error(error : FunctionInstructionsComparisonError) -> String

    describe_function_instructions_error

    fn describe_function_instructions_error(error : FunctionInstructionsError) -> String

    describe_function_local_accesses_error

    fn describe_function_local_accesses_error(error : FunctionLocalAccessesError) -> String

    describe_function_locals_comparison_error

    fn describe_function_locals_comparison_error(error : FunctionLocalsComparisonError) -> String

    describe_function_locals_error

    fn describe_function_locals_error(error : FunctionLocalsError) -> String

    describe_function_path_between_error

    fn describe_function_path_between_error(error : FunctionPathBetweenError) -> String

    describe_function_path_error

    fn describe_function_path_error(error : FunctionPathError) -> String

    describe_function_search_error

    fn describe_function_search_error(error : FunctionSearchError) -> String

    describe_imports_comparison_error

    fn describe_imports_comparison_error(error : ImportsComparisonError) -> String

    describe_module_opcode_comparison_error

    fn describe_module_opcode_comparison_error(error : ModuleOpcodeComparisonError) -> String

    describe_module_opcode_error

    fn describe_module_opcode_error(error : ModuleOpcodeError) -> String

    describe_opcode_search_error

    fn describe_opcode_search_error(error : OpcodeSearchError) -> String

    describe_size_failure

    fn describe_size_failure(options : Options, current_size : Int, baseline_size : Int?) -> String

    Why the size gate failed, in one line, and an empty string when it passed.

    It asks the gate rather than re-deriving the answer, so the message and the exit code can never disagree about whether the run was over budget.

    display_name

    fn display_name(name : String) -> String

    The name to print for a symbol: demangled when the binary mangles it, otherwise left exactly as the producer wrote it.

    evaluate_budget

    fn evaluate_budget(budget : CompiledBudget, current_data : Bytes, baseline_data? : Bytes) -> Result[BudgetReport, BudgetError]

    One evaluation per side/metric. No graph, dominators or independent gzip streams.

    experiment_report

    fn experiment_report(baseline_data : Bytes, current_data : Bytes, baseline_path : String, current_path : String, context : Json, top? : Int) -> Result[Json, String]

    Compare two artifacts, retaining both graph diagnostics and measured deltas. Deltas are current minus baseline. Function identity is raw-symbol based; missing/duplicate names remain unmatched. Context is explicitly self-reported. This neither rebuilds code nor establishes behavioral equivalence.

    explain_function

    fn explain_function(data : Bytes, target_index : Int, root? : Int, limits? : PathLimits) -> Result[FunctionPathReport, FunctionPathError]

    A deterministic shortest reference-model path. Builds a canonical graph from bytes without computing dominators. Never changes global roots or graph gaps.

    find_sections

    fn find_sections(parsed : WasmModule) -> Sections

    Pick the interesting sections out of a parsed section table.

    format_report

    fn format_report(parsed : WasmModule, path : String) -> String

    The user-facing report: one line per section, each with its byte count.

    The result carries no trailing newline, so the caller decides how to end it.

    function_callers

    fn function_callers(data : Bytes, target_index : Int) -> Result[FunctionCallersReport, FunctionCallersError]

    Parse, inventory and build directly. No analyze, dominators, BFS or truncation.

    function_cycle_witnesses

    fn function_cycle_witnesses(data : Bytes) -> Result[FunctionCycleWitnessReport, FunctionCycleWitnessError]

    Exactly one call to the existing cycles query. No parsing, graph building, SCC or instruction decoding is repeated here.

    function_cycles

    fn function_cycles(data : Bytes) -> Result[FunctionCyclesReport, FunctionCyclesError]

    One inventory and one graph build; no analyze, retained, BFS or opcode evidence recovery. build owns decoding; SCC consumes its original edges.

    function_dependencies

    fn function_dependencies(data : Bytes, target_index : Int) -> Result[FunctionDependenciesReport, FunctionDependenciesError]

    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.

    function_dependents

    fn function_dependents(data : Bytes, target_index : Int) -> Result[FunctionDependentsReport, FunctionDependentsError]

    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.

    function_path_between

    fn function_path_between(data : Bytes, source_index : Int, target_index : Int) -> Result[FunctionPathBetweenReport, FunctionPathBetweenError]

    Exactly one dependencies query. No second parse, build, BFS or opcode lookup.

    function_stats

    fn function_stats(bodies : Array[FunctionBody], names : Map[Int, String]?) -> Array[FunctionStat]

    Attribute a body to a name and a package.

    Attribution is best effort by design: a release binary keeps no function names, and the sizes are still worth reporting, so unnamed functions come back labelled (unnamed) rather than failing the run.

    help_request

    fn help_request(args : Array[String]) -> Result[String?, String]

    Args include the program name. Inspect only the first user argument; business option values later in argv never trigger this entry point.

    indirect_targets

    fn indirect_targets(graph : CallGraph) ->
    Set
    [Int]

    The functions any ordinary or tail indirect call could reach, for reports.

    inspect_data_segments

    fn inspect_data_segments(data : Bytes) -> Result[DataSegmentsReport, WasmError]

    Inspect data encodings after the ordinary module structure checks. This is not type/constant-expression validation, memory-index validation or an instantiation check. Expression ranges include their final end instruction.

    inspect_duplicate_data

    fn inspect_duplicate_data(data : Bytes) -> Result[DuplicateDataReport, WasmError]

    Single inspection and one digest per entry. No self-comparison, analyze, graph, expression evaluation, similarity search or retained payload copies.

    inspect_duplicate_functions

    fn inspect_duplicate_functions(data : Bytes) -> Result[DuplicateFunctionsReport, WasmError]

    Structural size-only inspection; no analyze, graph or path query.

    inspect_entrypoints

    fn inspect_entrypoints(data : Bytes) -> Result[EntrypointsReport, EntrypointsError]

    One graph, one traversal per unique function export, one for all other roots. Parsing/building uses existing safe entry points; no analyze or dominators.

    inspect_exports

    fn inspect_exports(data : Bytes) -> Result[ExportsReport, WasmError]

    Inspect only export declarations after ordinary module framing checks. No graph, body decoding, target/type validation or optional partial results.

    inspect_function_calls

    fn inspect_function_calls(data : Bytes, index : Int) -> Result[FunctionCallsReport, FunctionCallsError]

    Full instruction decode of one local body only. No graph, analyze, BFS or second decode for operands; ref.func is not a call instruction.

    inspect_function_instructions

    fn inspect_function_instructions(data : Bytes, index : Int) -> Result[FunctionInstructionsReport, FunctionInstructionsError]

    Decode only the selected local body, once. Other bodies are range-scanned.

    inspect_function_local_accesses

    fn inspect_function_local_accesses(data : Bytes, index : Int) -> Result[FunctionLocalAccessesReport, FunctionLocalAccessesError]

    Decode the selected body once. No graph, locals inspection, prior instruction report, signature parsing or secondary operand decoding.

    inspect_function_locals

    fn inspect_function_locals(data : Bytes, index : Int) -> Result[FunctionLocalsReport, FunctionLocalsError]

    Consume only the selected local body's declarations once; never decode any function's instructions or signature and never allocate by variable count.

    inspect_generic_instances

    fn inspect_generic_instances(data : Bytes) -> Result[GenericInstancesReport, WasmError]

    Size-only inspection: never constructs a graph or invokes analyze/retained.

    inspect_imports

    fn inspect_imports(data : Bytes) -> Result[ImportsReport, WasmError]

    Inspect core import declarations only after the ordinary module checks. No analyze, graph, function inventory or optional-content partial success.

    inspect_metadata

    fn inspect_metadata(data : Bytes) -> Result[MetadataReport, WasmError]

    Inspect all physical custom/name records, without changing effective-name selection, invoking analyze, building a graph, or copying opaque contents.

    inspect_module_opcodes

    fn inspect_module_opcodes(data : Bytes) -> Result[ModuleOpcodeReport, ModuleOpcodeError]

    Collect ranges first, then decode each local body once. No repeated module inspection per function, graph, retained or initializer-expression decoding.

    is_graph_complete

    fn is_graph_complete(graph : CallGraph) -> Bool

    Whether no gaps were detected within the current analysis scope.

    Missing edges can make dead-code and retained-size results inaccurate. Callers such as --dead-code, --retained and --strict report this hole. A true result does not prove every Wasm call mechanism is fully modelled. Dynamic table risks and reference calls are diagnosed, not solved.

    locate_file_offset

    fn locate_file_offset(data : Bytes, offset : Int) -> Result[FileOffsetReport, FileOffsetError]

    Parse structural ranges, then refine only the selected code/data payload. No function instruction decoding, graph construction or ordinary analysis.

    locate_file_offset_decoded

    fn locate_file_offset_decoded(data : Bytes, offset : Int) -> Result[FileOffsetDecodedReport, FileOffsetDecodeError]

    One base location; only a function-body hit invokes the existing complete single-function decoder, exactly once. A target decode error is never hidden.

    locate_file_range

    fn locate_file_range(data : Bytes, offset : Int, length : Int) -> Result[FileRangeReport, FileRangeError]

    Basic module validation still scans section/code structure. Refine only intersected code/data payloads, once each; never decode instructions.

    module_of

    fn module_of(name : String) -> String

    The package a symbol belongs to.

    Everything in the name except the final segment is the package, so demo::sample::src::fib lands in demo::sample::src and tlsf/searchBlock in tlsf. Names with no package at all are grouped under (top-level), which keeps the roll-up honest instead of inventing one bucket per function.

    parse_code_section

    fn parse_code_section(data : Bytes, section : Section, base_index? : Int) -> Result[Array[FunctionBody], WasmError]

    Decode the code section into one record per function body.

    base_index is the number of imported functions, so the returned index values line up with the function index space. Bodies are measured and skipped, never interpreted, which keeps this a structural pass like parse_module.

    parse_count

    fn parse_count(text : String) -> Int?

    Parse a non-negative decimal integer, or None for anything else.

    This toolchain's core library ships no string-to-Int parser, and the only number the CLI accepts is --top, so the grammar stays deliberately narrow: one or more ASCII digits, saturating into a rejection rather than an overflow.

    parse_element_section

    fn parse_element_section(data : Bytes, section : Section) -> Result[Array[ElementSegment], WasmError]

    Decode the element section.

    The eight encodings differ only in whether a table index, an offset expression, an element kind or a reference type is present, so the shared tail — a vector of function indices or of constant expressions — is read once after the mode byte has been handled.

    parse_export_section

    fn parse_export_section(data : Bytes, section : Section) -> Result[Array[Export], WasmError]

    Decode the export section.

    parse_module

    fn parse_module(data : Bytes) -> Result[WasmModule, WasmError]

    Decode the module preamble and walk every section header.

    Check version, standard-section uniqueness/order and function/code structure. Other payloads remain outside full type and instruction-semantic validation.

    parse_name_section

    fn parse_name_section(data : Bytes, section : Section) -> Map[Int, String]?

    Decode the function names out of a name custom section.

    The result is keyed by function index. None means the section is not a name section at all; Some of an empty map is a real answer, because names are optional producer metadata and a stripped binary simply has none. Subsection ids other than 1 (locals, labels, types) are skipped whole, and a malformed subsection ends the walk without discarding earlier names.

    parse_options

    fn parse_options(args : Array[String]) -> Result[Options, String]

    Parse a full argv, including the program name in slot zero.

    The grammar is moonsize <file.wasm> [--top <n>] [--retained] [--dead-code][--call-graph <path>] [--html <path>] [--max-size <size>] [--baseline<path.wasm>] [--diff-functions] [--compress] [--strict]: exactly one positional path, and a repeat of a flag keeps the last value. Anything else is reported as an error so a typo never silently analyses the wrong thing.

    --call-graph and --html each write a file rather than a report, so combining either with another output mode is rejected instead of quietly ignoring one of them.

    parse_size

    fn parse_size(text : String) -> Int?

    Parse a byte size: a decimal count with an optional KB, MB or GB suffix, so --max-size 10KB and --max-size 1.5MB both work.

    The suffixes are binary — a KB is 1024 bytes — which is what a size report means by the word. A fraction has to be followed by a suffix: --max-size1.5 is rejected, because half a byte is not a size.

    parse_start_section

    fn parse_start_section(data : Bytes, section : Section) -> Result[Int, WasmError]

    The function the module runs at instantiation, from the start section.

    parse_table_section

    fn parse_table_section(data : Bytes, section : Section) -> Result[Array[TableType], WasmError]

    Decode the table section.

    preview_data_segment

    fn preview_data_segment(data : Bytes, index : Int, offset : Int, limit : Int) -> Result[DataPreviewReport, DataPreviewError]

    Inspect all data encodings once, then copy at most 4096 bytes from one payload. Does not analyze functions, build a graph or run content search.

    reachable_from_roots

    fn reachable_from_roots(graph : CallGraph, roots :
    Set
    [Int]) ->
    Set
    [Int]

    Everything reachable from roots, the roots included.

    An explicit stack rather than recursion: real modules nest thousands of calls deep, and the depth of the graph should not be the depth of the C stack.

    render_budget

    fn render_budget(report : BudgetReport) -> String

    render_budget_json

    fn render_budget_json(report : BudgetReport, current_path : String, baseline_path? : String) -> String

    render_budget_markdown

    fn render_budget_markdown(report : BudgetReport, current_path : String, baseline_path? : String) -> String

    render_compression

    fn render_compression(raw_size : Int, gzip : Result[Int, String], sections : Array[SectionCompression]) -> String

    Whole-file gzip and independent full-section streams. Each ratio divides that row's raw bytes by its own gzip bytes. Independent streams neither add up to nor represent shares of whole-file gzip. Failures retain their reasons.
    fn render_data_hex_search(report : DataHexSearchReport, top : Int) -> String

    render_data_hex_search_json

    fn render_data_hex_search_json(report : DataHexSearchReport) -> String

    Independent schema: nested original ledger and complete ordered matches.

    render_data_preview

    fn render_data_preview(report : DataPreviewReport) -> String

    render_data_preview_comparison

    fn render_data_preview_comparison(report : DataPreviewComparison) -> String

    render_data_preview_comparison_json

    fn render_data_preview_comparison_json(report : DataPreviewComparison) -> String

    render_data_preview_json

    fn render_data_preview_json(report : DataPreviewReport) -> String

    fn render_data_search(report : DataSearchReport, top : Int) -> String

    render_data_search_json

    fn render_data_search_json(report : DataSearchReport) -> String

    Independent schema: nested original ledger and complete ordered matches.

    render_data_segments

    fn render_data_segments(report : DataSegmentsReport, top : Int) -> String

    Payload descending, then original index. Sorting and Top affect display only.

    render_data_segments_diff

    fn render_data_segments_diff(report : DataSegmentsComparison, baseline_path : String, current_path : String, top : Int) -> String

    Paths are display labels; complete file digests identify the compared bytes.

    render_data_segments_diff_json

    fn render_data_segments_diff_json(report : DataSegmentsComparison, baseline_path : String, current_path : String) -> String

    render_data_segments_json

    fn render_data_segments_json(report : DataSegmentsReport) -> String

    The original single-file format and signature are unchanged.

    render_dead_code

    fn render_dead_code(analysis : Analysis, top : Int) -> String

    The dead-code section: functions no root can reach.

    Unreachable means no path within the current model. Detected gaps are always shown, so a caller cannot omit the warning by forgetting an optional count.

    render_dependency_overlap

    fn render_dependency_overlap(report : DependencyOverlapReport, top : Int) -> String

    render_dependency_overlap_json

    fn render_dependency_overlap_json(report : DependencyOverlapReport) -> String

    render_dot

    fn render_dot(stats : Array[FunctionStat], graph : CallGraph, reachable :
    Set
    [Int]) -> String

    Render the call graph as Graphviz DOT.

    Functions unreachable within the model are dashed. Diagnostics and modelling limits travel in the graph label. Indirect edges are dotted candidates.

    render_duplicate_data

    fn render_duplicate_data(report : DuplicateDataReport, top : Int) -> String

    render_duplicate_data_json

    fn render_duplicate_data_json(report : DuplicateDataReport) -> String

    render_duplicate_functions

    fn render_duplicate_functions(report : DuplicateFunctionsReport, top : Int) -> String

    render_duplicate_functions_json

    fn render_duplicate_functions_json(report : DuplicateFunctionsReport) -> String

    render_entrypoints

    fn render_entrypoints(report : EntrypointsReport, top : Int) -> String

    render_entrypoints_json

    fn render_entrypoints_json(report : EntrypointsReport) -> String

    render_experiment_html

    fn render_experiment_html(report : Json) -> String

    Standalone HTML with no scripts or external resources. All supplied strings, including paths, raw symbols and context, are escaped before entering HTML.

    render_exports

    fn render_exports(report : ExportsReport, top : Int) -> String

    render_exports_diff

    fn render_exports_diff(report : ExportsComparison, top : Int) -> String

    render_exports_diff_json

    fn render_exports_diff_json(report : ExportsComparison) -> String

    render_exports_json

    fn render_exports_json(report : ExportsReport) -> String

    render_file_offset

    fn render_file_offset(report : FileOffsetReport) -> String

    render_file_offset_decoded

    fn render_file_offset_decoded(report : FileOffsetDecodedReport) -> String

    render_file_offset_decoded_json

    fn render_file_offset_decoded_json(report : FileOffsetDecodedReport) -> String

    render_file_offset_json

    fn render_file_offset_json(report : FileOffsetReport) -> String

    Preserve the original standalone format; refinement nests the same object.

    render_file_range

    fn render_file_range(report : FileRangeReport, top : Int) -> String

    render_file_range_json

    fn render_file_range_json(report : FileRangeReport) -> String

    render_function_callers

    fn render_function_callers(report : FunctionCallersReport, top : Int) -> String

    render_function_callers_json

    fn render_function_callers_json(report : FunctionCallersReport) -> String

    Independent format; Top never truncates this result.

    render_function_calls

    fn render_function_calls(report : FunctionCallsReport, top : Int) -> String

    render_function_calls_json

    fn render_function_calls_json(report : FunctionCallsReport) -> String

    render_function_cycle_witnesses

    fn render_function_cycle_witnesses(report : FunctionCycleWitnessReport, top : Int) -> String

    render_function_cycle_witnesses_json

    fn render_function_cycle_witnesses_json(report : FunctionCycleWitnessReport) -> String

    render_function_cycles

    fn render_function_cycles(report : FunctionCyclesReport, top : Int) -> String

    render_function_cycles_json

    fn render_function_cycles_json(report : FunctionCyclesReport) -> String

    Preserve the standalone schema and serialization; witnesses nest this value.

    render_function_dependencies

    fn render_function_dependencies(report : FunctionDependenciesReport, top : Int) -> String

    render_function_dependencies_dot

    fn render_function_dependencies_dot(report : FunctionDependenciesReport) -> String

    Render complete forward-closure evidence without analysing or mutating it. Top is deliberately absent. Parallel edges remain separate DOT statements.

    render_function_dependencies_json

    fn render_function_dependencies_json(report : FunctionDependenciesReport) -> String

    render_function_dependents

    fn render_function_dependents(report : FunctionDependentsReport, top : Int) -> String

    render_function_dependents_dot

    fn render_function_dependents_dot(report : FunctionDependentsReport) -> String

    Render complete incoming-closure evidence in original caller-to-callee direction without analysing or mutating it. Top is deliberately absent. Parallel edges remain separate DOT statements.

    render_function_dependents_json

    fn render_function_dependents_json(report : FunctionDependentsReport) -> String

    render_function_diff

    fn render_function_diff(diff : ModuleSizeDiff, top : Int) -> String

    Text view of the shared comparison; full JSON is rendered separately. Ranking uses absolute delta then exact symbol/package identity; indices locate records within a build and never establish a match. top=0 hides both detail rankings while retaining all totals and coverage.

    render_function_diff_json

    fn render_function_diff_json(diff : ModuleSizeDiff, baseline_sha256 : String, current_sha256 : String) -> String

    Serialize the existing comparison only, with no parsing, matching or graph. Callers must supply SHA-256 strings from exactly the inputs used for diff. Every original row is retained, in the comparison's existing ranked order.

    render_function_instructions

    fn render_function_instructions(report : FunctionInstructionsReport, top : Int) -> String

    render_function_instructions_diff

    fn render_function_instructions_diff(report : FunctionInstructionsComparison, top : Int) -> String

    render_function_instructions_diff_json

    fn render_function_instructions_diff_json(report : FunctionInstructionsComparison) -> String

    render_function_instructions_json

    fn render_function_instructions_json(report : FunctionInstructionsReport) -> String

    render_function_local_accesses

    fn render_function_local_accesses(report : FunctionLocalAccessesReport, top : Int) -> String

    render_function_local_accesses_json

    fn render_function_local_accesses_json(report : FunctionLocalAccessesReport) -> String

    render_function_locals

    fn render_function_locals(report : FunctionLocalsReport, top : Int) -> String

    render_function_locals_comparison

    fn render_function_locals_comparison(report : FunctionLocalsComparison, top : Int) -> String

    render_function_locals_comparison_json

    fn render_function_locals_comparison_json(report : FunctionLocalsComparison) -> String

    render_function_locals_json

    fn render_function_locals_json(report : FunctionLocalsReport) -> String

    render_function_path

    fn render_function_path(report : FunctionPathReport) -> String

    render_function_path_between

    fn render_function_path_between(report : FunctionPathBetweenReport) -> String

    render_function_path_between_json

    fn render_function_path_between_json(report : FunctionPathBetweenReport) -> String

    render_function_path_json

    fn render_function_path_json(report : FunctionPathReport) -> String

    Independent format version; existing call-graph JSON is unchanged.
    fn render_function_search(report : FunctionSearchReport, top : Int) -> String

    render_function_search_json

    fn render_function_search_json(report : FunctionSearchReport) -> String

    render_generic_instances

    fn render_generic_instances(report : GenericInstancesReport, top : Int) -> String

    render_generic_instances_json

    fn render_generic_instances_json(report : GenericInstancesReport) -> String

    Independent format; all families and instances survive text Top limits.

    render_graph

    fn render_graph(path : String, analysis : Analysis, target : String) -> String

    Render a graph export, choosing the format from the target file's extension.

    The extension is the only signal available and the one a user already thinks in: .json means JSON, anything else means DOT, which is what Graphviz tooling expects from a file called graph.dot.

    render_graph_warning

    fn render_graph_warning(graph : CallGraph) -> String

    The warning a report carries when the call graph could not follow every call, or "" when there is nothing to warn about.

    It belongs above the whole report rather than beside the sections it undermines: those sections are spread through the output, and a reader who skips one of them still has to see this. Saying it is the point — a tool that silently reports an incomplete graph as a fact is how a function in use gets deleted.

    render_html

    fn render_html(analysis : Analysis, file_path : String, echarts? : String) -> String

    Render the whole analysis as one HTML document.

    echarts is the chart library's source. Passing it inlines the library, so the report opens from any directory with no network and no sibling file; leaving it empty selects the compatible library CDN mode. This library does no I/O. The CLI always supplies its compiled-in vendored source; a library caller can explicitly supply a source or retain the CDN default.

    render_html_with_data_segments

    fn render_html_with_data_segments(analysis : Analysis, file_path : String, report : DataSegmentsReport, echarts? : String) -> String

    Opt-in current-input details from inspect_data_segments(analysis.data). No parsing or extra compression measurements are performed here.

    render_imports

    fn render_imports(report : ImportsReport, top : Int) -> String

    render_imports_diff

    fn render_imports_diff(report : ImportsComparison, top : Int) -> String

    render_imports_diff_json

    fn render_imports_diff_json(report : ImportsComparison) -> String

    render_imports_json

    fn render_imports_json(report : ImportsReport) -> String

    render_json

    fn render_json(path : String, stats : Array[FunctionStat], graph : CallGraph, roots :
    Set
    [Int], reachable :
    Set
    [Int], retained : Array[RetainedSize]) -> String

    Render the call graph, the roots and the retained sizes as JSON.

    render_metadata

    fn render_metadata(report : MetadataReport, top : Int) -> String

    render_metadata_diff

    fn render_metadata_diff(report : MetadataComparison, top : Int) -> String

    render_metadata_diff_json

    fn render_metadata_diff_json(report : MetadataComparison) -> String

    Complete reports remain nested objects with the original single-file schema.

    render_metadata_json

    fn render_metadata_json(report : MetadataReport) -> String

    render_module_opcode_comparison

    fn render_module_opcode_comparison(report : ModuleOpcodeComparison, top : Int) -> String

    render_module_opcode_comparison_json

    fn render_module_opcode_comparison_json(report : ModuleOpcodeComparison) -> String

    render_module_opcodes

    fn render_module_opcodes(report : ModuleOpcodeReport, top : Int) -> String

    render_module_opcodes_json

    fn render_module_opcodes_json(report : ModuleOpcodeReport) -> String

    render_module_summary

    fn render_module_summary(stats : Array[FunctionStat], file_size : Int) -> String

    The per-package roll-up of every function in the binary.
    fn render_opcode_search(report : OpcodeSearchReport, top : Int) -> String

    render_opcode_search_json

    fn render_opcode_search_json(report : OpcodeSearchReport) -> String

    render_retained

    fn render_retained(analysis : Analysis, top : Int) -> String

    The retained-size table, computed within the current graph model.

    SIZE is the function's encoded bytes. RETAINED sums the encoded bytes of reachable defined functions it dominates in this graph/root model, including itself. DIES counts the other members of that model set. None of these numbers predicts a source edit's recompiled file or gzip reduction.

    render_size_comparison

    fn render_size_comparison(baseline : WasmModule, current : WasmModule) -> String

    The size comparison table: the whole file and every section group, against the module it is being compared with.

    Standard sections match by kind; custom sections group by full name. Sizes include actual headers. Current size, baseline size, then identity determine row order. The whole-file total comes first and remains the budget's input.

    render_summary_json

    fn render_summary_json(analysis : Analysis) -> String

    Render only existing analysis results. No parsing, inspectors, graph, instruction decoding, compression or retained calculation; hash once.

    render_top_functions

    fn render_top_functions(stats : Array[FunctionStat], file_size : Int, top : Int) -> String

    The Top-N function table, heaviest first.

    Shares are of the whole file, so a row here can be read against the section table above it. top is clamped to the number of functions available.

    retained_sizes

    fn retained_sizes(graph : CallGraph, sizes : Map[Int, Int], roots :
    Set
    [Int]) -> Array[RetainedSize]

    The retained size of every reachable function.

    Within the fixed graph/root model, removing a node disconnects its dominated nodes: those every path from a root has to pass through. This is a graph operation, not source removal, relinking or gzip prediction. Computing it with an explicit "remove it and see what dies" walk would be correct but quadratic, and it would still need cycle handling; the dominator tree answers the same question for every function at once, and cycles fall out of the fixpoint without a special case.

    roots_of

    fn roots_of(exports : Array[Export], elements : Array[ElementSegment], start : Int?) ->
    Set
    [Int]

    The roots of a module: what the outside world can reach without going through code the module already has.

    Exported functions and start are roots. Every function referenced by an element segment is also conservatively rooted, including private, passive and declarative segments. This does not assert that each can actually run.

    same_kind

    fn same_kind(a : SectionId, b : SectionId) -> Bool

    True when two section ids are the same variant.

    Section ids carry no payload and this toolchain derives no Eq for them, so the comparison is written out once here rather than spelled at each call site that has to pick a section out of the table.

    scan_calls

    fn scan_calls(bytes : Bytes, body_offset : Int, body_size : Int) -> Result[Array[CallSite], WasmError]

    Collect direct, table and reference calls, including tail calls, from a body.

    Collect call, return_call, call_indirect and return_call_indirect. Also collect call_ref and return_call_ref, without resolving their targets. ref.func is a reference rather than a call; the graph handles it separately.

    search_data_segments

    fn search_data_segments(data : Bytes, query : String) -> Result[DataSearchReport, DataSearchError]

    One structural inspection and query encoding. Linear in query bytes, payload bytes and occurrences; retains every hit, including overlaps. Payload bytes are neither copied nor decoded as text. No function decoding or graph analysis is performed beyond the inspector's module checks.

    search_data_segments_hex

    fn search_data_segments_hex(data : Bytes, query_hex : String) -> Result[DataHexSearchReport, DataHexSearchError]

    Decode ASCII pairs directly into bytes; never use UTF-8 as an intermediate.

    search_local_functions

    fn search_local_functions(data : Bytes, query : String) -> Result[FunctionSearchReport, FunctionSearchError]

    Inspect available local names once, without decoding instructions or a graph.

    search_opcode

    fn search_opcode(data : Bytes, query : String) -> Result[OpcodeSearchReport, OpcodeSearchError]

    Inspect query first; collect module/function/name ranges once, then decode each local body once. No prior statistics report, graph or per-function inspector.

    size_exit_code

    fn size_exit_code(options : Options, current_size : Int, baseline_size : Int?) -> Int

    The exit code a module of current_size bytes earns, measured against baseline_size when the invocation named a baseline.

    Without one, --max-size is a ceiling on the file. With one it is the growth the file may show over the baseline, because that is the question a CI job is asking when it compares a branch against the branch it merges into — an absolute ceiling there would be answering something nobody asked.

    strict_exit_code

    fn strict_exit_code(options : Options, graph_complete : Bool) -> Int

    The exit code --strict earns, or EXIT_OK when the graph is complete or the flag is absent. Completeness includes every unified graph diagnostic, including reference calls.

    It lives here rather than in the entry point so the rule can be tested without a process: a job that passes --strict is saying it cannot judge a warning itself, and the run has to fail rather than print one.

    usage

    fn usage() -> String

    Printed when the tool is invoked without a file, or with arguments it cannot make sense of.

    Source Files