skip to content

In a language with nested blocks, how does the name-resolution pass decide which declaration an identifier refers to?

level: middleimportance: must knowfreq 65%

answer

  1. names live in a chain of tables
  2. one table per open scope
  3. innermost scope consulted first
  4. walk outward, first hit wins
  5. an inner declaration hides the outer

basics

~20 s

Name resolution keeps one symbol table per open scope, each linked to the table of the scope around it. An identifier binds to the first matching entry found walking that chain outward, so an inner declaration hides an outer one of the same name.

solid answer

~50 s

Every scope that is open at a given point in the program — the script, a block, a loop body, a step's parameter list — gets its own symbol table mapping a name to an entry: what it was declared as, where, its type once known, and a slot for later passes. Each table links to the table of its enclosing scope, so the resolver always holds a `scope chain`. To resolve a reference it looks the name up in the innermost table, and on a miss follows the link outward, stopping at the **first** table that has an entry; that entry becomes the reference's binding and is recorded on the tree. If the walk runs past the outermost table the name is unresolved, and the pass reports it at that reference. Because the walk stops at the first hit, an inner declaration **shadows** an identically named outer one for the whole of the inner scope.

code

pseudocode · 8 lines
pseudocode
resolve(name, scope):
    s = scope
    while s is not none:
        if s.table contains name:
            return s.table[name]        // first match wins, walk stops
        s = s.parent                    // step outward one scope
    report error "unresolved name: " + name
    return error_entry                  // keep checking the rest of the tree

go deeper

for a junior

Recall the shape of the answer: names are looked up starting in the closest enclosing block and the search moves outward, and the nearest declaration is the one that counts.

for a middle

Explain the mechanics: a symbol table per scope, a parent link making a chain, a first-hit walk, an entry holding kind, position, type and slot, and shadowing falling out of the stopping rule.

for a senior

Show what you do with the failure paths: how an unresolved name is reported once and poisoned so it does not cascade, and how a duplicate in a single scope differs from shadowing across scopes.

for a principal

Frame it as a design surface: whether shadowing is silent, warned or rejected, and what recording bindings on the tree buys the passes that follow versus re-resolving names later.

## What this pass is for A parser proves that source text has a legal **shape**. It does not know what any name in that text *means*. The pass that follows — name resolution, the first half of semantic analysis — walks the tree the parser produced and answers exactly one question for every identifier: **which declaration does this reference stand for?** Everything downstream depends on that answer. A type check needs to know which declaration's type to compare against. An optimisation needs to know that two occurrences of `total` are the same storage, or that they are two unrelated variables that merely share a spelling. A code generator needs a slot to read from. None of that is decidable from the tree's shape alone. ## One symbol table per scope, chained outward A **scope** is a region over which one set of declarations is visible: a whole script, a block, a loop body, a parameter list. The resolver gives each scope its own **symbol table** — a map from name to an entry recording: - the **kind** of thing declared (variable, parameter, step, constant); - the **position** of the declaration, which is what diagnostics point at; - the **type**, once it is known; - a **slot or index** that later passes use instead of the name. The tables are not independent. Each carries a link to the table of the scope that encloses it, so at any point in the walk the resolver holds a **scope chain**: innermost table first, then its parent, out to the outermost scope. Entering a block pushes a fresh table; leaving it pops that table, and the names it held stop being reachable by name. Resolving one identifier is then a fixed procedure: 1. Look the name up in the innermost table. 2. On a miss, follow the link to the enclosing table and look again. 3. Stop at the **first** table that has an entry — that entry is the binding, and the resolver records it on the reference. 4. If the walk runs past the outermost table, the name is unresolved: report it at this reference, bind it to an error entry, and keep checking the rest of the tree so one typo does not cascade into a hundred follow-on complaints. Step 3 is the whole of the rule people call **lexical scoping**: the meaning of a name is fixed by where the reference is written, not by what has run before it. ## Shadowing is a consequence, not a separate feature Because the walk stops at the first hit, a declaration in an inner scope makes an identically named outer declaration unreachable *by that name* for the whole of the inner scope. That is **shadowing**. Consider a workflow script whose outer block declares `retries`, and whose nested `on failure` block declares its own `retries`: | Reference site | Chain walked | Binds to | |---|---|---| | Outer block, before the nested block | outer -> script | the outer `retries` | | Inside the nested block | nested -> outer -> script | the nested `retries` | | Outer block, after the nested block | outer -> script | the outer `retries` again | | Inside the nested block, a name only the script declares | nested -> outer -> script | the script-level entry | Two things follow that candidates routinely get wrong. First, the inner declaration does **not** modify the outer variable: there are two entries in two tables, two identities, two slots. The outer one keeps its value and becomes reachable again once the inner scope is popped. Second, shadowing is not a shape error, so the parser has no opinion about it at all — it is legal text, and whether the tool stays silent, warns, or rejects is a language-design choice made in this pass. ## Collisions inside one scope A second name in the *same* table is a different matter. Two declarations of one name in one scope have no outward walk to disambiguate them, so the resolver reports a duplicate declaration when it inserts the second entry. Note the asymmetry worth stating aloud in an interview: **same scope is a collision; nested scopes are shadowing**, and the mechanism that produces both is the single rule that each scope owns its own table. ## What the pass leaves behind When the walk finishes, every reference in the tree carries a binding, and every scope's table carries the declarations it owns. That artefact — not the source text — is what the type checks read next: to compare an assignment's two sides, the checker asks the recorded binding for its type rather than searching for the name again. A resolver that resolved correctly but recorded nothing would force every later pass to repeat the same walk, and would leave the meaning of a name open to being re-decided differently later.

  • What does the pass do when the walk reaches the outermost scope with no match?
    It reports an unresolved name at that reference and binds the reference to an error entry rather than stopping. The error entry is treated as agreeing with anything, so later checks do not pile a cascade of false complaints on top of one misspelling, and the run still reports every other real problem in the file.
  • Does an inner declaration of the same name change the outer variable?
    No. They are two entries in two tables with two identities and two slots. Inside the inner scope the outer one is simply unreachable by that name; it keeps its value, and once the inner scope is popped the name resolves to it again. Shadowing hides, it does not assign.
  • Why is a repeated name inside one scope an error when the same repetition across nested scopes is not?
    Nested scopes are disambiguated by the outward walk: the inner entry is found first, so there is a defined answer. Two entries in one table have no such ordering rule, so the reference would be genuinely ambiguous. The resolver reports the collision when it tries to insert the second entry.

Looking up an extension in a stack of directories: your desk copy first, then the department's, then the company-wide one. The first listing you find is the one you dial, even if a wider directory lists the same name against a different number.

saying these in an interview costs you the question

  • thinks one flat table holds every name in the program
  • expects the outermost declaration to win the lookup
  • says the parser rejects a shadowed name as a syntax error
  • believes the walk continues past the first matching entry
  • says an inner declaration overwrites the outer variable's value
  • treats a repeated name in one scope and shadowing as the same case