A whole-program pass deletes a type only ever constructed from a name computed at run time — why?
answer
- roots first, then follow references
- only edges the build can see
- a name is data, not structure
- deleted at build, fails at run time
- the plain build never reproduces it
basics
~20 sReachability is computed from edges the build can see: call sites, references, overrides. A target named by data at run time creates no such edge, so the analysis concludes nothing reaches the type and removes it — at build time, with the failure surfacing only later.
solid answer
~40 sA removal pass starts from a root set and walks every reference it can see in the compiled code, keeping what it reaches and deleting the rest. That walk is only as complete as the edges it is given. When the only thing that ever creates the type is a lookup performed on a name assembled at run time, the build sees a call into a generic lookup and a string that it does not evaluate — no edge to the type at all. So the type is judged unreachable and stripped, silently, with no error, because from the analysis's point of view nothing is wrong. The defect appears much later: the optimized artifact runs, the lookup asks for something that is no longer there, and it fails on whichever path first needs it.
code
pseudocode · 9 lines// an edge the walk can follow: the target is named in the code
handler = new SettlementHandler()
// no edge: the only mention of the target is data the build does not evaluate
name = settings.read("handler.type")
handler = construct(lookupType(name))
// an edge again: the table names every target, the string only chooses
handler = table[name]() // table holds new SettlementHandler, new RefundHandler, ...go deeper
Remember that a build can legitimately delete code nothing appears to use, and that choosing a target from text at run time hides the use from it.
Explain the walk: roots, visible references, delete the rest — and say why a name held as data is not a reference the walk can follow.
Diagnose it in the field: the optimized artifact fails where the plain one does not, and the fix is an explicit root list plus a suite that runs against the stripped build.
Decide how much dynamic surface the platform tolerates at all, since the answer sets what every team must declare and what the optimization is allowed to keep buying.
## How reachability is decided A whole-program removal pass answers one question: which parts of this program can possibly be used? It answers it structurally, not by running anything: 1. **Pick a root set** — the entry points, plus anything the build has been told to treat as always reachable. 2. **Walk the edges.** From each reachable piece of code, follow every reference the compiled form actually contains: the targets of call sites, the types named in signatures and fields, the implementations that could satisfy a call through an abstraction. 3. **Keep what was reached, delete the rest.** Everything the walk never arrived at is considered dead and removed. Step 3 is the whole point. Programs carry a great deal of code that a given deployment cannot execute, and deleting it is what buys the size and startup budget the pass exists for. ## Why a name computed at run time is not an edge An edge exists when the compiled code **names its target**. A construction written directly leaves a reference behind; so does a signature that mentions a type, and so does an implementation of an abstraction the walk reaches. A lookup by name leaves nothing of the kind. What is in the compiled code is a call into a general-purpose lookup and, somewhere, a piece of data. The analysis does not execute the program, so it does not learn what that data will say. Even when the string is a literal sitting right next to the lookup, treating it as a reference would mean interpreting arbitrary data as program structure, which no removal pass can do in general. Where the name is assembled from configuration, from an external document, or from two fragments joined together, there is nothing to interpret at all. So the missing edge is not a bug in the analysis; it is the exact price of the mechanism. Deciding the target at run time means the build cannot know the target. ## What the failure looks like | Build | What happens to the type | When you find out | |---|---|---| | Plain, unoptimized | Present, because nothing was stripped | Never — everything works | | Optimized or ahead-of-time | Deleted at build time, no error | At run time, when the lookup finds nothing | That table is the reason this class of defect is so unpleasant: - **The build is green.** Removing unreachable code is the pass doing its job, so there is nothing to report. - **The development build cannot reproduce it.** Tests run against the unstripped artifact pass perfectly, which is why the failure arrives after the pipeline, not during it. - **The failure is far from the cause.** The symptom is a lookup that cannot find its target, often on a rarely taken path, hours or days after the decision that deleted it. ## Closing the gap There are only two honest moves, and they are opposites: - **Give the analysis the edge it is missing.** Declare the dynamically reached types and members as additional roots, in a form the build reads before the walk runs. This keeps the mechanism and pays for it with an explicit, reviewable list. - **Make the edge real.** Route construction through code that references the target statically — a table of constructors the program itself contains, selected by the name. The walk then sees ordinary references and keeps everything, and the name only chooses among them. Two practices decide whether either move works: - **Test the artifact you ship, not the one that is convenient.** An acceptance suite run against the stripped build is the only cheap way to find these. - **Keep the dynamic surface enumerable.** A design where the set of targets reachable only by name is small and listable can be declared. A design where any type may be summoned by any string cannot be, and a broad declaration that keeps everything simply cancels the optimization it was meant to survive. ## The sentence to leave with Static analysis sees structure; a name decided at run time is data. The moment a program chooses its target from data, it has stepped outside what any build-time walk can prove, and the only way back is to hand the build the edge by hand.
- Why does this defect surface so late?Nothing fails at build time: deleting unreachable code is the pass succeeding. The first signal is a run-time lookup that finds nothing, in the optimized artifact only, often on a path that ordinary smoke traffic never takes.
- How would you give the analysis the edge it is missing?Either declare the dynamically reached types and members as extra roots in a form the build reads before it walks, or route construction through code that references each target statically, letting the name merely choose among references the walk can already see.
- Why not just turn the removal pass off?Because the pass is what pays for the size and startup budget it was enabled for; turning it off retains the large unreachable majority along with the handful you needed. Enumerating the dynamic surface keeps the win and removes the surprise.
saying these in an interview costs you the question
- Says the analyser has a bug rather than a missing edge it was never given
- Assumes code present in the source cannot be absent from the artifact
- Expects the plain development build to reproduce the deletion
- Thinks a name in a configuration file keeps the target reachable
- Declares everything reachable, cancelling the optimization it needed