Formatting functions are registered at startup and invoked later from unrelated call sites - what does lexical resolution guarantee about their free names?
answer
- written once, run anywhere
- definition site settles the names
- path to the call is irrelevant
- audit the file, not the dispatcher
- declaration fixed, value not promised
basics
~20 sIt guarantees that each free name still refers to the declaration the text around the definition chose. Deferral, storage and an unfamiliar call site cannot re-point a name, so a registered function can be reviewed where it was written.
solid answer
~50 sBecause lexical resolution walks outward from the **definition**, a function that is built at startup and stored carries a settled answer for every free name in its body. Handing it to a registry, passing it across module boundaries, or invoking it much later from code that has never heard of it cannot change **which declaration** those names refer to. That is the property that makes deferred execution reviewable: you audit the registered function by reading the file it was written in. Under caller-driven resolution none of this holds - the free names would be answered by whatever call chain happened to be active at invocation time, so the same registered function would behave differently per path and could fail on a path that binds nothing. Note the boundary: lexical resolution fixes the declaration, not the value, so a binding that is later updated is a separate question.
code
pseudocode · 11 lines// module A, read once at startup
let separator = ' | '
function makeFormatter()
return function(fields)
return join(fields, separator) // separator is free; module A answers it
// module B, much later, during a request
let separator = ' ; '
let f = makeFormatter()
print(f(['a', 'b'])) // 'a | b'go deeper
Hold on to one sentence: a function's free names were decided where it was written, so calling it from somewhere new does not change what they mean.
Explain why deferral is safe under lexical resolution and what exactly is guaranteed - the declaration, not the contents - and be able to say what would break if the call chain decided instead.
Use it as a debugging heuristic on real systems: for a misbehaving registered callback, audit the module it was written in first, and treat moving it between modules as a change that needs the same review as an edit.
Make the rule explicit in how the team registers behaviour: a registered function's inputs should be its parameters plus its declaration-site surroundings, so that the set of things that can affect it stays enumerable as the system grows.
## What deferral actually threatens Registration splits a function's life in two. It is **written** in one place, at one time, surrounded by one set of declarations. It is **run** somewhere else, later, surrounded by nothing in particular - inside a dispatch loop, a handler table, a scheduler. Between those two moments, everything about the run-time surroundings has changed. The question a scoping rule has to answer is which of the two moments decides what the body's free names mean. Lexical resolution answers: the first one. The regions searched are the regions of text enclosing the definition, and text does not move. ## What the guarantee covers, and what it does not Be precise here, because the two halves are often collapsed: | Claim | True under lexical resolution? | |---|---| | A free name still refers to the same declaration when invoked later | Yes | | An unfamiliar call site can supply a different declaration for it | No, it cannot | | The function can be moved between modules without re-pointing names | Yes, as long as its text moves with it unchanged | | The **value** behind that declaration is the same as at registration | Not guaranteed - that is a separate question about the binding's contents | | A registration failure can be caused by the path that reached it | No; the path plays no part in resolution | The fourth row is the boundary of this leaf. Which declaration a name refers to is settled lexically; whether the thing behind that declaration has since changed is a question about what a function holds onto, and it is answered elsewhere. ## Why this is the reviewable property A registry is a place where code arrives from many modules and is invoked by machinery that knows nothing about any of them. If resolution depended on the call path, then reviewing a registered function would require reviewing every path that can reach the registry - a set that grows with the system and is usually not enumerable. Lexical resolution collapses that work: 1. Open the file where the function was written. 2. List its parameters and its free names. 3. Walk outward through the enclosing text for each free name. 4. You now have the complete list of what the body can refer to. Step 4 is only sound because no caller can add to that list. This is the concrete payoff of the rule, and it is why "where was this written?" is the first question to ask about a misbehaving registered callback, rather than "who called it?". ## The failure that the alternative produces Picture the same registry under caller-driven resolution. A formatting function registered by one module refers to a free name that its own module binds. Invoked through a path whose frames also bind that name, it picks up the path's binding; invoked through a path that binds nothing, it fails. Two properties are destroyed at once: - **Determinism per function.** The same registered function produces different results for identical arguments depending on the route. - **Testability in isolation.** A test cannot exercise the function by calling it with arguments; it must reconstruct a call chain, and the chain is part of the contract even though nothing states it. This is also why the idea of packaging a function with its surrounding bindings needs lexical resolution to mean anything at all. A package is only worth carrying if its contents were decided before the journey. ## Operating consequences worth stating - **Audit at the definition site.** For a misbehaving registered callback, read the module it was written in before reading the dispatcher. - **Moving a function between modules is a semantic change.** The enclosing declarations it resolves against move with the text. A free name that resolved one way in the old surroundings may resolve differently, or become unresolved, in the new ones - so the safe motion is the one that carries the declarations too, or turns them into parameters. - **Registration order does not affect resolution.** It may affect which function is selected for a job; it cannot affect what the selected function's names mean. - **Long-lived registrations do not decay.** A function registered at startup and invoked for the process lifetime keeps the same answers for its free names throughout. Whether the things behind those names should be held for that long is a different design question. The short version to say out loud in an interview: lexical resolution turns "what can this function refer to?" into a question about one file, and keeps the answer true no matter where or when the function is eventually called.
- Does the guarantee extend to the value behind the name?No. Lexical resolution fixes which declaration the name refers to, not what is stored there. If that binding is later updated, a body that reads the name may observe the update. Keep the two claims separate: the declaration is settled, the contents are a different question.
- What is the first place to look when a registered callback misbehaves?The module where it was written, and the declarations enclosing it. Because no call path can add to what its free names may refer to, the dispatcher and the caller are rarely the explanation - they choose when it runs, not what its names mean.
- Why does moving a registered function to another module count as a behaviour change?Its free names resolve against the text that now encloses it. A name that met one declaration in the old surroundings may meet a different one, or none, in the new. Move the declarations with it, or convert them into parameters before moving.
- How would the same registry behave if names were resolved from the call chain?Each registered function would become path-dependent: identical arguments would give different results by route, and some routes would fail because no active frame binds the name. Testing would require reconstructing chains rather than passing arguments.
saying these in an interview costs you the question
- Says the dispatcher's surroundings supply the function's free names.
- Claims the registered function's values are frozen at registration.
- Thinks invoking later re-runs name resolution from scratch.
- Believes moving a function between modules is always behaviour-preserving.
- Blames registration order for which declaration a name reaches.