← board

Emission size — reachability-gated dead-code elimination (umbrella)

Goal

Shrink emitted binaries by emitting only reachable code. Two overlapping fronts, unified here:

  1. Routine-level DCE for uses-unit bodies (was feature-lazy-standard-unit- emission): a uses textfile/builtin pulls in whole unit bodies even when only one routine is called. hello.pas still emits ~31.6 KB vs a ~29 KB reachable baseline. Emit a unit routine only if reached from the program entry.

  2. Finer runtime-support emission (was chore-runtime-emission-size): the implicit runtime helpers (string/dynarray/managed/exception support) are emitted coarsely; gate each on actual use.

Both are the same mechanism: build a call/reachability graph from the entry point and skip unreferenced routine bodies. Do it once, cover both fronts.

Acceptance

hello (and other minimal programs) shrink measurably (toward the ~29 KB baseline) with no behavior change; self-host byte-identical; make test + cross green. Ideally lands as an -O-gated pass under [[feature-optimization-levels]].

Notes

Done — 2026-08-21 (--dce, and -O3 implies it)

hello went from 60298 to 17380 bytes; the ticket asked for "toward the ~29 KB baseline". Code: 58522B -> 15604B, 113 emitted routines -> 41 live.

program before after
hello 60298 17380 -72%
arrays 103832 19268 -82%
test_class_is_as 107941 23704 -79%
test_dynarray_copy_managed_elems 109164 27313 -75%
lib_classes_tthread 358975 147233 -59%
lib_httpjson 1737321 717798 -59%

Both fronts, as the ticket predicted, turned out to be one mechanism — and it is front 1's mechanism: the "implicit runtime helpers emitted coarsely" of front 2 ARE unit routines, so a reachability walk over routine bodies drops them by the same rule. What is left of hello outside a routine body is ~1000 bytes of entry stub and lazily emitted stubs; there is no coarse-grained runtime blob left to gate.

Not lazy emission — a post-pass

BodyAddr := CodeLen sits at the prologue: the compiler emits a body the moment it parses it, so at the point where you would want to skip one, nothing yet knows who calls it. Lazy emission means replaying parser state per routine per frontend; compacting afterwards is one language-agnostic pass over tables that already exist. compiler/dce.inc.

The actual work was making the references enumerable

Moving a body invalidates every PC-relative reference that crosses the gap, and a stale rel32 does not crash — it calls into the middle of whatever slid up into the hole. So most of this change is not the pass:

A range holding a CodeRef target is never dropped, whoever emitted it. That rule is what keeps the pass honest against a stub kind nobody listed: an unreferenced stub costs its bytes, a wrongly dropped one costs a jump into hyperspace.

Refuses rather than guesses

Two bodies sharing a start address (the C frontend aliases one), a body that never recorded its end, -c / --shared (their writers carry their own code-offset tables), -g, a non-x86-64 target, a non-Pascal frontend. --dce-report prints which.

Evidence

-O3 enables it, per the convention that a new pass lands in the free tier first. That also buys the breadth the pass cannot buy itself: tools/optdiff.sh sweeps ~900 programs demanding identical behaviour at -O0/-O2/-O3, so Track T's opt tier is now a whole-corpus --dce differential.

What is deliberately left

Log