What must a build-time compiler see to turn state assignments into targeted UI updates, and what escapes it?
answer
- precision resolved before the program runs
- declaration, reads and writes in one unit
- the compiler must see the assignment
- uncompiled or dynamic writes escape silently
basics
~20 sIt must see the reactive declaration, the expressions that read it, and the assignment that writes it, inside one compiled unit. Writes it cannot analyse - through a passed reference, from uncompiled code, or via dynamic access - escape.
solid answer
~50 sA compiler analysing a component before it ships can do what no run-time mechanism can: pair a specific assignment with the specific bindings that read that value, and emit the update calls directly. For that it needs three things visible together - the declaration marked as reactive state, the expressions in the template that read it, and the statements that assign to it - within the unit it compiles. What escapes is everything static analysis cannot follow: a write made through a reference handed to code the compiler never saw, a field mutated on a plain object rather than reassigned, dynamic property access, or a value passed outside the component where its link to those bindings is gone. Because of that, such frameworks ship a run-time-tracked container as the escape hatch, and the mental discipline they impose is to write in a shape the compiler can actually prove.
go deeper
The core idea is enough here: a build step reads the component and writes the update code for you, so each assignment already knows which bindings to refresh.
Name the three things the compiler must see together, and at least two kinds of write that escape it - an uncompiled callee, or a mutation where instrumentation expected an assignment.
Emphasise the silent failure: no error, correct memory, stale screen, and no live dependency graph to interrogate. Say how you would spot it in review and where the escape hatch belongs.
The decision is about where the boundary falls in your codebase. Most code sits in the compiled fast path; shared and cross-module state lives on the escape hatch, and that boundary needs to be an explicit convention rather than folklore.
## What a compiler has that a runtime does not Read-time tracking discovers dependencies by watching a program run, and pays a small cost on every read for the privilege. A compiler gets the answer for free - but only for what it can *prove* from source. Given a component's text, it can determine that a declaration is reactive state, that three expressions in the template read it, and that two statements assign to it, and then emit code in which each assignment is followed by "mark those three bindings dirty". No edges are recorded at run time because they were resolved at build time, which is why this model can ship a very small runtime and do no per-read bookkeeping at all. ## What it must see Three things, in a unit it compiles: 1. **The declaration**, in a form that marks the value as reactive - a specific declaration shape, or a call the compiler recognises. Ordinary locals are not tracked; the compiler has to know which values are worth instrumenting. 2. **The reads** - the template expressions and derived declarations that use the value, so it knows which bindings to mark. 3. **The writes** - the assignments themselves, so it can attach the update call. This is the demanding one: the compiler rewrites the assignment, so the assignment has to be in the source it is rewriting. ## What escapes the static view - **A write through a reference the compiler cannot follow.** Hand the value, or an object holding it, to a function in another module the compiler did not instrument, and the write that happens in there is invisible. - **A mutation instead of a reassignment.** If instrumentation hangs off the assignment statement, editing a field inside an object, or pushing into a collection, is not an assignment the compiler saw. This is why such frameworks either insist on reassignment or wrap the value in something that tracks at run time. - **Dynamic access.** A property whose name is computed cannot be matched to a binding statically. - **Crossing the compilation boundary.** Pass the value out of the component and the static link to those bindings does not travel with it; the receiver holds a plain value. - **Values that never appear in a template.** Instrumentation is a map from assignment to binding; a value nothing renders has nowhere to point. ## Static versus run-time discovery | | Compile-time instrumentation | Read-time tracking | |---|---|---| | When dependencies are resolved | at build time, from source | while the program runs | | Per-read cost | none | records an edge | | Runtime size | small - the wiring is in the emitted code | must ship the tracking machinery | | Dynamic dependencies | only as the compiler can prove them | naturally, per run | | Failure mode | a write it never saw is simply not propagated | a read outside a tracked scope records no edge | | Escape hatch | a run-time-tracked container | an explicit untracked read | ## The escape hatch, and why it matters more than the happy path Every compile-time framework ships a way to opt into run-time tracking for values that leave the compiled shape - typically a container object whose reads and writes pass through accessors, which is read-time tracking again by another name. That is the interesting part of the model in practice. The compiler's fast path covers the component-local case, which is most code; the escape hatch covers shared state, values handed to libraries, and anything crossing a module boundary. A team that does not know which side of that line it is on writes code where a write sometimes propagates and sometimes does not, with no error at either the build or the run. The second thing to internalise: a compile-time failure is **silent**. There is no exception when a write is not instrumented. The value in memory is correct and the screen is stale, which is among the harder classes of bug to reason about from a stack trace - because there is no stack trace. A run-time tracking runtime can at least be inspected while running to ask who the dependents of a value are; static wiring has already been baked in. ## Where this model sits among the four It discovers the same thing fine-grained tracking discovers - which bindings depend on which value - but earlier and without bookkeeping, at the cost of only knowing what source analysis can show. It is unlike re-run-and-diff, which discovers nothing finer than "this component is stale", and unlike dirty checking, which discovers differences by comparing values after the fact and needs no knowledge of dependencies at all. Naming that trade cleanly - **precision for free, in exchange for a shape the compiler can prove** - is the answer interviewers are listening for.
- Why do compile-time frameworks still ship a run-time-tracked container?Because static wiring stops at the compilation unit. Shared state, values handed to uncompiled libraries, and anything crossing a module boundary need dependencies discovered while running, so the container's accessors record and notify at run time - read-time tracking used as the escape hatch for what the compiler cannot prove.
- What makes a missed write in this model harder to debug than a missed dependency in read-time tracking?It fails silently and statically. No exception is raised, the value in memory is right and the screen is stale, and there is no live graph to interrogate because the wiring was decided at build time. A tracking runtime can at least be asked, while running, who depends on a value.
- Does compile-time instrumentation remove the need for a scheduler?No. It decides what to mark, not when to flush. Several assignments in one handler still mark several bindings, and those are normally batched into one pass so no intermediate state reaches the screen. Discovery and flushing are separate stages under all four models.
saying these in an interview costs you the question
- Thinks the compiler can trace writes made in code it never compiled.
- Believes compile-time instrumentation removes the need for any runtime.
- Says a mutated field is equivalent to a reassignment for the compiler.
- Assumes a missed write raises an error rather than failing silently.
- Claims static wiring handles dynamic dependencies as well as run-time tracking.