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 x is 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 outer x in 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.

The lessons behind it