intermediateSemantics
Two variables in the same function are both called `x`. How does the compiler decide which one a use refers to?
Whether the candidate has a working model of scope as a structure the compiler builds, not a rule it applies. The discriminator is whether they realise the answer is a data structure with a lifetime, and can say what happens after resolution.
What a strong answer covers
- Name resolution walks the AST maintaining a scope stack. Each block pushes a scope; each declaration inserts a symbol into the current scope; each use searches from the innermost scope outward and binds to the first match. The result is not a rule applied repeatedly — it is a link recorded on the AST node, from the identifier to the declaration, and every later phase follows the link rather than the name.
- That recorded link is the whole point. After resolution, the string
xis no longer meaningful to the compiler: two distinct symbols exist, they may have different types, and they will occupy different storage. A phase that goes back to comparing source names has thrown away the answer the resolver computed. - The details that vary are worth naming. Whether a declaration is visible before its own initializer —
let x = x;is an error in some languages and reads the outerxin others. Whether the scope starts at the declaration or spans the whole block, which is what makes hoisting and temporal dead zones observable. Whether shadowing is legal at all: Rust encourages it, some style guides ban it, and C warns on it. And whether resolution is lexical at all — dynamic scoping resolves by call stack at run time, which makes the answer depend on the caller. - The failure mode is not usually "the wrong variable is reported to the user". It is that a later phase keys something by source name — a storage slot, a debug entry, a cache — and two distinct symbols collide.
✓ Green flags
- Describes the scope stack and the innermost-first search concretely.
- Says that the outcome is a recorded symbol reference, not a repeated lookup.
- Names a language where the visibility rule differs and says how.
- Knows shadowing is a legality decision the language makes, and can argue both sides.
- Mentions that a shadowed variable still exists and is still live, which matters for debug info and for closures.
✗ Red flags
- "The second declaration overwrites the first." Nothing is overwritten; both symbols exist, both may be live, and the outer one is visible again after the inner block ends.
- "The compiler renames them x and x1." Some IRs do exactly that for display, but the renaming is a consequence of resolution, not the mechanism of it.
- "Shadowing is always a bug." It is a deliberate feature in several languages, and the alternative — inventing
x2— is worse for narrowing patterns. - "Scopes are decided at run time." They are decided statically in every mainstream language; dynamic scoping is the rare exception and it is a different language design.
Follow-up
A closure captures x inside the inner block. Which x does it capture, and when — at closure creation or at call?
Implementation challenge
What to ask them to write or trace on a whiteboard.
Draw the symbol table state at each of five program points for a function with two nested blocks that both declare x, and mark for each use which entry it binds to.