# Function transitive model dependencies

This is a source addition after public **0.5.1**, not a new Mooncakes release.
Example index12 below is a syntax placeholder, not a measurement of your input:

```sh
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
moonsize app.wasm --dependencies 12 --strict
```

The full function index space includes preceding imports. Any valid local or
imported function can be the target; it need not be exported, model-root
reachable or cyclic. The closure includes the target. Only Top, own JSON or DOT (mutually exclusive), and
strict may combine; baseline, budgets, HTML and other query/report modes are
rejected before business I/O. JSON requires an explicit dependencies index and
a new, nonempty `.json` path. Existing argument value constraints, unknown
options, `--` and first-argument help routing are unchanged.

| Result | Exit |
| --- | ---: |
| Completed query, including only the target | 0 |
| Input, parsing, index, evidence or output failure | 1 |
| Missing/invalid parameter or mode conflict | 2 |
| Strict refusal of incomplete module graph | 3 |

A valid decimal index outside the real inventory is an input/index error1;
invalid numeric syntax is parameter error2. Empty modules have no valid target.
Strict examines the entire module's `graph_complete`, before output preflight
and creation. JSON and DOT destinations use existing unified path protection
and native exclusive creation. Existing files are not removed or truncated.
No rollback, new writer or multi-file transaction is introduced.

## Meaning and evidence

This is a directed transitive closure in the **current reference model**:

- Direct is a recorded `call` or `return_call` dependency.
- Reference is `ref.func`, a reference taken without necessarily calling.
- Indirect is a static table-call candidate, not a known runtime target.

The query prepares one inventory, builds once and performs one iterative BFS.
It does not call analyze, retained, SCC, per-target callers or extra opcode
decoding. Existing inventory/build routines retain their own parsing; this is
not a promise that the complete pipeline parses the file only once.

Before BFS, every original edge is checked: caller/callee index bounds, local
body caller, and file offset bounds. Bad evidence outside the target closure
also fails the query. Adjacency preserves all original edges and sorts by
callee, direct/reference/indirect, then file offset. Each function first enters
the head-index queue once. Self-loops, back edges and shared successors cannot
overwrite the target's depth0/null predecessor or duplicate weights.

Depth is the minimum number of model edges, **not runtime stack depth**. Equal
length routes choose the first-discovery edge in deterministic BFS order. A
node stores just that original predecessor, not a full copied path. Node output
sorts by depth, then index; traversal order and display order are separate.

After traversal, all original edges whose caller is in the closure are kept;
their callee must also be present. Full edges sort by caller, offset, callee,
kind. Parallel sites and kinds remain, including self/back/cross edges. They
are distinct from the BFS predecessor tree. Edge counts are not execution
counts, and candidate edges are not call-site counts.

`call_ref` and other unsolved targets keep the original full module diagnostics;
the query never guesses a new edge. Diagnostics can be outside this closure.
`unresolved_count` still counts table call sites without static candidates,
not all diagnostic entries. A complete graph flag does not certify full Wasm
semantics, absence of host dependencies or actual execution.

## Counts and local byte ledgers

Every full function index counts once. A local weight is `total_size`, including
its body length prefix. Imported nodes have `encoded_size: null`. For an import
target, `target_local_encoded_bytes = 0` is **only a local-file accounting
contribution**, not a measured zero-byte body or a claim that host calls are free.
Even import targets perform normal whole-module checks and graph construction.

```text
function_count = local_function_count + imported_function_count
function_count = reachable_function_count + other_function_count
reachable_function_count = reachable_local_count + reachable_imported_count
dependency_function_count = reachable_function_count - 1
local_encoded_bytes = reachable_local_encoded_bytes + other_local_encoded_bytes
reachable_local_encoded_bytes = target_local_encoded_bytes + dependency_local_encoded_bytes
edge_count = direct_edges + reference_edges + indirect_edges
```

Only the target is a valid closure, with zero other dependencies and zero
dependency bytes. Code section headers/count, type, data, name and other
sections are excluded from function weights. Outside this target's closure is
not dead code. Different target closures can overlap, so their byte totals
cannot simply be added. These metrics do not compute exclusive/shared
attribution, retained size, necessary retention or deletion/recompile/gzip
savings, and do not describe runtime heat.

## Reports and related queries

Text shows the target separately, so it does not consume a Top row. Dependency
rows include index, depth, identity, encoded bytes or not-measured status, safe
name and predecessor caller/kind/file offset. The full ledger and graph warning
remain visible when Top hides rows. Top is not a traversal or memory limit.
Complete nodes and edges are held in memory; no constant-memory claim is made.

JSON is independent `moonsize-function-dependencies`, `schema_version: 1`.
It contains target identity, original input SHA-256, all nodes, predecessors,
all closure edges, full counts/ledgers and module diagnostics/warning/scope.
The target predecessor and imported encoded sizes are true nulls. Top never
truncates library results or JSON. Edge records reuse CallEdge serialization;
they contain kinds and offsets, not invented opcode names. To inspect one
opcode, separately run `--locate-offset OFFSET --locate-decode`.

| Query | Question |
| --- | --- |
| [calls](function-calls.md) | Which call instructions are encoded in one local body? |
| [callers](function-callers.md) | Which recorded one-hop instructions refer to this target? |
| [why-index](../README.md#explaining-a-functions-reference-path) | What is one root-to-target model path? |
| [entrypoints](entrypoints.md) | Which functions are reachable from exported entry groups? |
| dependencies | What is the transitive model closure from any chosen function? |

These are separate modes. No source attribution or new cross-build identity is
inferred. Direct, reference and candidate edges all count as model steps.

## API and migration

```moonbit
function_dependencies(Bytes, Int)
  -> Result[FunctionDependenciesReport, FunctionDependenciesError]
describe_function_dependencies_error(FunctionDependenciesError) -> String
render_function_dependencies(FunctionDependenciesReport, Int) -> String
render_function_dependencies_json(FunctionDependenciesReport) -> String
```

New types are FunctionDependencyNode, FunctionDependenciesReport and
FunctionDependenciesError. Nodes reuse FunctionPathNode with depth and optional
CallEdge predecessor. Input errors wrap WasmError; invalid targets retain index
and inventory count; invalid graph evidence retains actual index/offset/reason.
Options adds only dependencies=None and dependencies_json=None. Exhaustive
external Options construction must supply those fields. Existing public
signatures, errors and JSON formats are unchanged. The BFS helper is private.
No dependencies, package version or Mooncakes publication are changed.

## Actual local evidence

The fixed normal input is **130 B**, SHA-256
`bef050f03cb12940e1b153c40de0f2ba54cda02e658719bd87a79d220cc93dc8`.
Its import is index0; six local bodies start at file66/83/90/97/102/105.
Only isolated function5 is exported; the query is explicitly for function1,
without an export/root check. The exact output order and first-discovery edges:

| Index | Depth | Encoded bytes | Predecessor |
| ---: | ---: | ---: | --- |
| 1 | 0 | 17 | null (target) |
| 2 | 1 | 7 | 1→2 direct@67 |
| 3 | 1 | 7 | 1→3 reference@71 |
| 0 | 2 | null (imported) | 2→0 direct@86 |
| 4 | 2 | 5 | 2→4 direct@84 |

The earlier parallel direct@67 wins over direct@69; reference@71 wins over
candidate@76 for the same successor. Both2 and3 reach4, but BFS first discovers
it from2. Direct self@79 and back edge4→1@98 leave the target at depth0/null.
Full evidence has **10** edges (direct8/reference1/indirect1), while the
predecessor tree has **4**. In caller/offset order the full edge positions are
67/69/71/76/79/84/86/91/93/98; none is dropped to obtain the tree.

```text
7 functions = 6 local + 1 import = 5 in closure + 2 other
5 in closure = 4 local + 1 import
4 dependencies = 5 in closure - target1
42 local encoded B = 36 in closure + 6 other
36 in closure B = target17 + dependencies19
10 edges = direct8 + reference1 + indirect1
```

The import0 query is also unit-tested: one node with null encoded size, zero
target/dependency local contribution, and no outgoing model edge. Isolated
local5 has only itself, encoded3 B and zero dependency bytes. Neither result
proves absence of host dependencies or runtime cost.

The gap fixture is **134 B**, SHA-256
`4368f888598462ad6601e8cc1115ea15444d3a5487ad7507acb482489bf63bbe`.
Function6/file108 call_ref is outside target1's closure. Its module diagnostic
is preserved, unresolved_count stays0, and strict returns3 before creating
strict.json. Import0's query also retains the full-module gap in the unit test.

Three filtered new combined tests passed first run3/3. Exactly four analyzer
processes returned **0 / 0 / 3 / 1**: text Top1, full JSON with Top1, strict gap
refusal, existing sentinel refusal. The script checks fixed original edges,
depths, predecessor keys, encoded lengths, bytes, hashes and ledgers; input and
sentinel stay unchanged. No old query/script is run to generate expectations.

Evidence: `_build/function-dependencies-verification/run-eTi67B/` contains both
inputs, four logs, argv/status, `results.json`, `new-dependencies.json` and the
preserved sentinel. `review-5yzvgveg/` alongside it contains the current generated
interface plus a readable HEAD declaration audit. No historical build or ACL
change was needed. Only new tests and required interface/format/native checks,
one release build and four CLI calls ran locally; old suites/CLI, demos, probes,
V8, performance and browser/PDF validation were skipped. CI retains its full
flow and adds only this lightweight four-call script; final-SHA status is
reported separately. Version stays0.5.1; no Mooncakes release is performed.

## Incoming versus outgoing source queries (source addition after public 0.5.1)

`--callers INDEX` lists one-hop incoming instruction evidence; `--dependents INDEX`
lists the transitive incoming closure, with original caller-to-callee edges and
next_edge toward the target. `--dependencies INDEX` lists the outgoing closure;
`--why-index INDEX` gives one path from model roots. None is an execution trace
or source-change impact analysis. Use separate commands, not combined modes.
[Reverse closure contract and evidence](function-dependents.md).

## Explicit pair path (source addition after public 0.5.1)

`--path-between FROM TO` reconstructs one deterministic shortest path from one
complete dependencies query, allowing non-root/imported endpoints. It does not
run another graph search or relax why-root. Not-found has empty paths/null hops;
same-index has zero hops. No Top; only own JSON and strict combine. Workflow:
name search -> dependencies/dependents sets -> explicit pair path -> locate-decode
for actual instruction offsets. [Contract and real evidence](function-path-between.md).

## Two arbitrary starts in one input (source addition after public 0.5.1)

`--compare-dependencies LEFT RIGHT` compares two outgoing closures, each including
its start, partitioning the full index space into shared/left_only/right_only/
neither. It is not a cross-build comparison or global exclusive attribution.
entrypoints uses all function exports and start/element roots; this query does
not examine third-party roots or reuse that exclusive/shared definition.
Only Top, own JSON and strict combine. [Sets, actual ledgers and complete evidence](dependency-overlap.md).

## Complete dependency subgraph DOT (source addition after public 0.5.1)

`--dependencies-dot new.dot` requires `--dependencies INDEX`, a nonempty new
`.dot` target, and cannot combine with `--dependencies-json`. Top only affects
text: DOT includes every closure node and original edge. Imported or isolated
targets produce one-node graphs. The old whole-module `--call-graph`, text and
JSON format remain unchanged. CLI reads once and calls function_dependencies
once, then checks strict before unified preflight/exclusive creation. DOT mode
prints only a save confirmation. Non-strict incomplete graphs keep warnings;
strict rejects with3 before writing. Existing targets return1 unchanged;
parameter errors2, success0. No second query/build/instruction decode occurs.

API: `render_function_dependencies_dot(FunctionDependenciesReport) -> String`.
The pure renderer sorts copies by node index and caller/offset/callee/kind.
Ordinary digraph retains one statement per original edge, without strict or
concentration. IDs are fn_INDEX, never names. Query target is highlighted, not
called a model root. Labels include identity, local encoded bytes (imports:
not measured), and BFS depth. Direct/solid, reference/dashed and indirect
candidate/dotted labels carry actual file offsets, without guessing opcodes.

Graph attributes preserve input_sha256, file_size, target_index, graph_complete,
unresolved_count, modeling_scope, graph_warning and diagnostics_json (complete
existing diagnostic JSON as a safely quoted DOT string, including records
outside the closure). The visible title includes full counts, legend, model
limits and incomplete warning/diagnostic count. This is model evidence, not
execution, global dead/live status or removal savings. Layout cost is not
bounded. Generating DOT does not require or launch Graphviz.

User controls become visible `\uXXXX` text before DOT quote/backslash escaping.
Literal `\N`, `\G`, `\E`, `\n`, `\l`, `\r` stay literal; only program separators
create layout newlines. Chinese is preserved. No HTML-like/record labels, raw
user comments, URLs or image attributes are generated. Legacy render_dot is
unchanged. See [DOT grammar](https://graphviz.org/doc/info/lang.html) and
[escString rules](https://graphviz.org/docs/attr-types/escString/).

### Targeted evidence (Windows, 2026-10-29)

Toolchain: moon 0.1.20260915 (2e1a46d), Node24.18.0. The fixed134B input SHA-256:
`4bc13ffc04a8eefd9122f4f05a7c3ea3f09b138af45c6bbfb5141691152e0f76`.
Query1 exports nodes `[0,1,3,4]`, eight complete edges versus three predecessor
tree edges. Six direct, one reference and one indirect candidate remain:

| Caller -> callee | Kind | File offset |
| --- | --- | ---: |
| 1 -> 3 | direct | 68 |
| 1 -> 3 | direct | 70 |
| 1 -> 4 | reference | 72 |
| 1 -> 4 | indirect candidate | 77 |
| 1 -> 1 | direct | 80 |
| 3 -> 4 | direct | 92 |
| 3 -> 0 | direct | 94 |
| 4 -> 3 | direct | 99 |

Top1 removes none. Import0 is an ellipse marked not measured; target1 is
highlighted with17B/depth0. Closure-external function6 call_ref at file109 in
138B gap input remains a full diagnostic (one record, unresolved_count0) with
visible warning. Gap SHA-256:
`60f19584f36ac1f2fa5386d317f7fc1b16fc5c6f1b9d26780cad3df1e4ecdcea`.

Three new combined tests passed3/3 on first execution: exact escaping,
non-mutating sort, full edges, single-node graphs and parameter contracts.
Initial new-source syntax/type issues were corrected before running tests.
Exactly four CLI calls passed0/0/3/1: Top1 DOT, non-strict gap DOT, strict gap
refusal and existing sentinel refusal. Inputs and sentinel stayed unchanged.
Evidence: `_build/dependencies-dot-verification/run-XtfaaJ/` contains inputs,
new.dot, gap.dot, argv/status, logs and results.json. Sibling api-diff.txt shows
exactly one renderer and one Options field; old signatures/types unchanged.

Graphviz dot was not available on PATH. **No Graphviz parsing or actual rendering
was verified.** String checks are not visual acceptance. No tools installed.
Local checks omitted old suites/scripts, demos, probes, V8, performance and
browser/PDF. One release build and required interface/format/native checks ran.
CI preserves its full flow with one new four-case step; exact final-SHA status
is reported separately. Version remains0.5.1, no Mooncakes publication.

## Graphviz consumer contract (2026-10-30, source-only after 0.5.1)

The preceding134/138B Windows record belongs to923b9ba and remains historical:
its special name was on function2, outside the exported closure. This follow-up
moves the name to function3 in the actual `[0,1,3,4]` subgraph. The name includes
Chinese, a quote, literal `\N`, `\G`, `\E`, `\n`, `\l`, `\r`, actual newline/tab/ESC
and text resembling an injected DOT statement. The name section still follows
code; instruction offsets68/70/72/77/80/92/94/99 and gap diagnostic109 stay fixed.
The two inputs and resulting DOT have new identities; no old hashes are reused.

Only the test script understands `--require-graphviz`; it is not a product flag:

```sh
node scripts/test-dependencies-dot.mjs _build/native/release/build/cmd/main/main.exe
node scripts/test-dependencies-dot.mjs _build/native/release/build/cmd/main/main.exe --require-graphviz
node scripts/verify-dependencies-dot-graphviz.mjs <explicit-evidence-directory> [dot-executable]
```

The first command retains string checks and explicitly records Graphviz not
executed. CI uses the second, exactly once: the same four analyzer calls, then
the verifier gets that exact new directory. There is no latest-run search or
fixed historical consumer input. Standalone verification uses the third form,
with an optional explicit dot executable. It exclusively creates a graphviz/
subdirectory, never overwrites DOT or earlier consumer results. Missing tools,
unsupported format, startup/timeout/nonzero/invalid JSON and assertion failures
all fail the verification. Calls use shell:false, argv arrays,30s timeout and
8MiB output limit. Version output on stderr is permitted when status is0.

The verifier runs `dot -V` and once per new.dot/gap.dot runs `dot -Tjson`.
It checks the `_gvid` to node-name mapping (not Wasm indices), the exact node
set, eight-edge multiset with parallel edges/self-loop/kinds/offsets/styles,
import not-measured shape/label, and highlighted query target. File size/hash,
target/completeness/unresolved attributes and full diagnostic JSON are checked.
The gap retains closure-external function6/file109 call_ref even though fn_6
is absent. No coordinates, fonts, curve geometry or complete layout snapshots
are fixed. [Graphviz JSON documentation](https://graphviz.org/docs/outputs/json/)
distinguishes actual `-Tjson` layout drawing from input-only dot_json.

Checks read actual `_ldraw_` text operations, with independent fixed expected
lines for all four nodes and edge labels. fn_3's first text line must contain
the intact literal backslash sequences and visible control escapes; its only
remaining lines are local encoded7B and BFS depth1. User text must not create
nodes, substitution names or additional layout lines. diagnostics_json is
parsed once after decoding the Graphviz JSON envelope, without deleting
backslashes or ignoring parse errors. These checks establish the tested Linux
Graphviz version's parsing, layout processing and output text contract; they
do not establish manual visual, font glyph, browser, PDF or printer acceptance.

CI prepares Graphviz once in the existing native job, reusing dot or installing
Linux graphviz if absent. Preparation failures and consumer failures have
separate steps. CI requires the consumer and cannot downgrade to string-only
success. Its always-on upload, when a run directory exists, contains only that
run: two inputs/DOT, hashes, four CLI logs/argv/status, Graphviz JSON, version,
commands/status/stderr and assertions. Artifact name:
`dependencies-dot-consumer-<full-commit-SHA>`; graphviz/results.json records
actual version, executable path, final GITHUB_SHA, hashes, drawn text, diagnostic
and assertion results. The exact run link/version/artifact are reported after
CI succeeds; this source record does not predeclare an unexecuted Linux result.

### Local focused record

No product fix, public API/Options/.mbti/version/dependency change was required.
Local MoonBit tests:0; builds:0. Reused the native release CLI from the preceding
verified product-source build, SHA-256
`f78793ff4a494978202e0ea1b18e526f62f0c19c353f1c17101e03a1bbbb54a7`.
Both JS files passed node --check. The four analyzer calls ran only once and
passed0/0/3/1, preserving inputs/sentinel; Top1 still exports4 nodes/8 full edges
versus3 tree edges. Evidence: `_build/dependencies-dot-verification/run-USbbSF/`.

| Item | Size | SHA-256 |
| --- | ---: | --- |
| overlap.wasm | 180 B | `5ab8b5909382839922958a3371296c1bdce0e1efe20ee50246ade8771ae4777e` |
| gap.wasm | 184 B | `76123b7954a5cf57aa7dca5cc1db847d6011386288f62cd970f41c5a3261acc2` |
| new.dot | — | `90843808c22992153211ca247052122a83fec3331869b5195b5c96a9ff7b97b0` |
| gap.dot | — | `c54f728222b4d6ea77ce451b910183ea7ebd9695854445a8581cf7907b9e18b9` |

Local Graphviz was not installed, probed again or run; actual consumer execution
is required in target-SHA Linux CI. Old local suites/scripts, demos, V8, probes,
performance and browser/PDF checks were omitted. Version remains0.5.1; no
Mooncakes publication. The production CLI still has no Graphviz dependency.

### First Linux consumer observation and verifier adjustment

Run41055902945 (source1fb2746) used `/usr/bin/dot`, reporting
`dot - graphviz version 2.43.0 (0)`. Both -Tjson commands returned0 and retained
four nodes/eight edges. gap.dot passed every structure/layout-text/diagnostic
assertion. The complete new.dot failed a verifier assumption: Graphviz omits
the empty graph_warning attribute from JSON. Its product DOT string check still
requires graph_warning="". Only this known-empty complete-graph consumer value
now accepts omitted or empty; incomplete graphs still require a nonempty string
and exact diagnostic record. No backslash rewriting or product fix was needed.
Both archived real layouts are rechecked with the same corrected assertions;
this does not replace the final SHA's fresh CI consumption. The failed artifact
and logs remain preserved, and the final run/artifact are reported separately.
