Why does a resolver record each reference's binding at compile time instead of searching the scopes again at run time?
answer
- decide once, from the text
- the reference carries its answer
- depth and slot, not a name
- meaning independent of the caller
- the alternative is dynamic scoping
basics
~20 sSo that a name's meaning is fixed by where it is written, not by who is running. Recording the binding turns each access into a direct slot reference and makes shadowing and capture predictable; searching by name at run time makes the same text mean different things on different call paths.
solid answer
~50 sOnce the outward walk has found a declaration, the resolver writes the result onto the reference — typically as a scope depth and a slot index in that scope's frame — and every later pass uses that instead of the name. Two things follow. Mechanically, an access becomes a fixed offset rather than a map lookup on a chain of tables, and a nested block that captures an enclosing variable can be told at compile time that the capture exists and must outlive the block. Semantically, and more importantly, the meaning of the reference stops depending on the call path: the binding was decided from the program text, so a helper block always reads the variable that lexically encloses it, never a same-named variable that happens to exist in whoever called it. That second behaviour — resolving by the run-time chain of callers — is **dynamic scoping**, and it is precisely what recording the binding rules out.
go deeper
Recall that a variable's meaning is decided by where it appears in the code, not by which part of the program happens to be calling at the time.
Explain the recorded pair of scope depth and slot index, how it replaces a per-access lookup, and how it reveals which enclosing variables a nested block captures.
Argue the semantic case: a caller's unrelated local can never change a callee's behaviour, so shadowing becomes a prediction a reader can make from the text alone.
Weigh the costs you inherit: run-time-constructed names need a separate searched environment, capture-by-storage versus capture-by-value must be chosen, and live redefinition needs an indirection.
## Two ways to answer "which variable is this?" When a reference to `timeout` executes, something has to say which storage it means. There are two families of answer: - **Decide it from the text, in advance.** The resolver walks the scope chain at compile time, finds the declaration that lexically encloses the reference, and records the answer on the reference. At run time nothing is searched. - **Decide it from the run-time state, on each access.** Keep an environment of name-to-value bindings alive while the program runs, and look the name up in it each time, following whatever chain exists at that moment — typically the chain of active calls. Both designs work and both have been shipped. The first is what essentially all mainstream language designs settle on, and the reasons are worth being able to state precisely. ## What the resolver writes down The recorded binding is usually not "a declaration" in the abstract but a pair that later passes can turn into an address: - **depth** — how many links outward the walk travelled (0 for the current scope, 1 for its parent, and so on); - **slot** — the index this declaration was assigned within that scope's storage. With both in hand, an access compiles to a fixed traversal of a known number of links and a fixed offset, instead of a hash lookup per table per access. The resolver also learns, for free, which enclosing variables a nested block refers to — a variable referenced at depth greater than zero is **captured** — which is the information a later pass needs in order to give that variable storage that outlives the block it was declared in. ## The property that actually matters The performance argument is real but secondary. The decisive argument is about **meaning being local**. Consider a workflow script where a helper block reads `timeout`, and two different steps call it — one of which happens to declare its own local `timeout`: | Design | What the helper reads | Consequence | |---|---|---| | Binding recorded from the text | The `timeout` that lexically encloses the helper, always | The helper's behaviour can be reasoned about by reading the helper and its enclosing scopes | | Name searched at run time along the call chain | Whichever `timeout` the current caller has | The same helper silently behaves differently depending on who called it | The second row is dynamic scoping, and its failure mode is the one that makes it unpopular: a caller can change a callee's behaviour by declaring an unrelated local variable that merely shares a spelling. Renaming a local becomes a semantically significant act at a distance, and no amount of reading the callee explains the bug. Recording the binding is what makes the shadowing rules of the previous section *predictions a reader can make*, rather than facts that depend on the run. ## What it costs Nothing is free: 1. **Names must be resolvable statically.** A construct that builds a variable name from a string at run time cannot be given a recorded binding, so a language that offers one needs a fallback path — usually an explicit, slower, dynamically searched environment kept separate from ordinary variables. 2. **Capture becomes a design decision.** Once the resolver knows a variable is captured, the language must say whether the nested block captures the storage (seeing later changes) or the value at capture time. Ecosystems genuinely differ here; some capture the storage by default, some require the captured variable to be immutable, some let the author choose. The resolver's record is the same either way; what differs is what the later pass builds from it. 3. **Reloading changed code is harder.** A binding baked into a reference has to be invalidated when a definition is replaced while the system is running, which is why designs that support live replacement usually keep an indirection at exactly the points where redefinition is allowed. ## How to say it The compact interview answer: resolution is a decision about text, so make it once and write it down. You get an access that costs a known offset rather than a lookup, you learn which variables a nested block captures, and above all you get the guarantee that a name means the same declaration no matter which caller is on the stack — which is the whole reason lexical scoping is easier to reason about than the alternative.
- What does the resolver learn about capture while doing this?That a reference resolved at a depth greater than zero names a variable of an enclosing scope. A nested block containing such references captures them, so a later pass knows that storage must outlive the block that declared it rather than being discarded when the block is popped.
- What happens to a language feature that builds a variable name at run time?It cannot be given a recorded binding, because there is no name in the text to resolve. Such languages keep a separate, searched environment for those accesses, which is slower and opaque to the checks; most designs restrict the feature or omit it for exactly that reason.
saying these in an interview costs you the question
- says the binding must be searched again on every access
- thinks a callee sees the caller's same-named local
- believes recording a binding is purely a speed optimisation
- confuses where a name is written with when it executes
- assumes every language resolves names before running