Name resolution associates uses of names in source code with their declarations or candidate declarations. Symbol tables store these identities and related information, which are later relied upon for type checking, capture analysis, and code generation. In complex languages, these phases may be interleaved, and not all names can be resolved in a single pass without type information.
The most important distinction is between spelling and identity: two variables both named x can be different symbols, and the same symbol may be accessed via aliases or qualified paths.
Scope Determines Name Visibility
Lexical scope determines the lookup environment based on program structure. Assuming the language allows inner scopes to shadow outer ones, entering an inner scope prioritizes finding the inner declaration; leaving it restores the outer environment.
When declarations become visible is dictated by the language. Whether functions allow forward references, whether local variables are visible within their own initializers, and whether re-declaration in the same block is an error, cannot be replaced by the simplistic rule of "add to the table upon encountering the definition."
For example, in let x = x + 1, some languages allow the right-hand side to refer to the previous x, while others might place the new declaration in an uninitialized state. Implementations should distinguish between declaration, initialization, and readable states.
A Concise Implementation of Lexical Environments
Each scope maintains a mapping from names to SymbolId and a reference to its parent. insert only checks for duplicates within the current scope; lookup searches up the parent chain from the current environment; lookup_current is used in scenarios requiring a search limited to the current layer only.
Hash table queries per layer are typically expected to be O(1), while searching the parent chain depends on depth. Implementations can also use binding stacks per name, undo logs, or persistent maps; there is no single data structure that is "correct" solely by adopting a scope stack.
If the AST or IDE index still references SymbolId after analysis ends, the symbol records must persist. Popping the current active scope does not mean deleting declaration data referenced by subsequent phases. Separating temporary traversal stacks from long-term symbol storage avoids dangling indices.
Active environments can exit, but resolved references cannot disappear
The following defines rules for a teaching language: duplicate declarations with the same name in the same layer cause an error, while inner layers are allowed to shadow; it only handles value names and does not support imports or overloading. Symbol records are stored in the arena of the current analysis, and scopes only save the index of the records. This integer ID is stable within the current analysis, not a globally stable ID across file edits.
fromdataclassesimportdataclass@dataclass(frozen=True)classSymbol:name:strdeclaration:strclassScope:def__init__(self,parent=None):self.parent=parentself.names={}defdeclare(self,arena,name,location):ifnameinself.names:raiseValueError(f"duplicate {name}: {location}")identity=len(arena)arena.append(Symbol(name,location))self.names[name]=identityreturnidentitydeflookup(self,name):current=selfwhilecurrentisnotNone:ifnameincurrent.names:returncurrent.names[name]current=current.parentraiseNameError(name)arena=[]outer=Scope()a=outer.declare(arena,"x","line 1")inner=Scope(outer)assertinner.lookup("x")==a# Before inner declaration appears
b=inner.declare(arena,"x","line 3")resolved_use=inner.lookup("x")assertresolved_use==banda!=btry:inner.declare(arena,"x","line 4")exceptValueError:passelse:raiseAssertionError("duplicate accepted")assertlen(arena)==2# Failed insertion leaves no ghost records
active=outerassertactive.lookup("x")==aassertarena[resolved_use].declaration=="line 3"try:active.lookup("missing")exceptNameError:passelse:raiseAssertionError("unresolved name accepted")
The final read of the arena illustrates: after exiting the inner scope, resolved usage points still refer to the inner declaration. Code generation or "jump to definition" cannot re-lookup lookup("x") from the current outer environment, or it would incorrectly rebind inner references to the outer one.
Moment
Current Lookup Result
Recorded Inner Uses
Inner x not yet declared
Outer a
None yet
After declaring inner x
Inner b
Saves b
Exiting inner scope
Outer a
Still b
To support continuous shadowing within the same block, one can establish new visibility layers after each declaration or adjust binding stack rules; one cannot claim to have fully implemented Rust's let rules while simultaneously using the "duplicate error in same layer" approach from this example. Rustc Scopes
Does leaving a scope rebind existing references?
Connect each use to a declaration ID, not merely a shared color for spelling x. New lookup may find S1 inside and S0 after exit, while recorded uses keep their original targets. Closure uses also have a binding origin. The toggle rebuilds source analysis instead of pretending declaration edits rebind runtime variables.
Preparing the visual
Does leaving a scope rebind existing references?
Same spelling is not the same symbol. Scope exit changes later lookup, not recorded references. Binding identity and capture mode are separate.
{"id":"lexical-binding-identity","title":"Does leaving a scope rebind existing references?","summary":"Same spelling is not the same symbol. Scope exit changes later lookup, not recorded references. Binding identity and capture mode are separate.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"inner\" checked><span data-i18n=\"inner\"></span></label><div class=\"presets\" role=\"group\" data-i18n-label=\"use\"><button data-mode=\"outside\" data-i18n=\"outside\"></button><button data-mode=\"inside\" data-i18n=\"inside\"></button><button data-mode=\"after\" data-i18n=\"after\"></button><button data-mode=\"closure\" data-i18n=\"closure\"></button></div><div id=\"program\" class=\"source\"></div><svg id=\"binding\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\"></svg><div class=\"metrics\"><div><span data-i18n=\"bound\"></span><strong id=\"bound\"></strong></div><div><span data-i18n=\"value\"></span><strong id=\"value\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#program{min-height:14.4em}.presets button{flex-basis:40%}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let use='inside';function draw(){const inner=$('#inner').checked,id=use==='inside'&&inner?'S1':'S0',value=id==='S1'?20:10,lines=['let x = 10; // S0','let f = () => x;','use(x);','{',inner?' let x = 20; // S1':' // no inner declaration',' use(x);','}','use(x);'];const line={outside:2,inside:5,after:7,closure:1}[use];$('#program').innerHTML=lines.map((s,i)=>'<span class=\"code-line '+(i===line?'on':'')+'\">'+esc(s)+'</span>').join('');graph('binding',[{name:'S0',x:80,y:45,cls:id==='S0'?'on':'',sub:'x = 10'},{name:'S1',x:280,y:45,cls:id==='S1'?'on':'',sub:inner?'x = 20':'∅'},{name:'use',x:180,y:145,cls:'on'}],[[2,id==='S0'?0:1,true]]);set('bound',id);set('value',value);pressed(use);}$('#inner').onchange=draw;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{use=b.dataset.mode;draw()});draw();","audio":false,"strings":{"after":"Use after exit","bound":"Binding identity","closure":"Use in closure","heading":"Does leaving a scope rebind existing references?","inner":"Include inner x declaration","inside":"Inner use","intro":"Select a use site or remove the inner declaration. Colors connect uses to declaration identity; the closure keeps its resolved binding.","outside":"Outer use","reset":"Reset","scope":"Teaching language: inner shadowing, visibility after declaration. Closure is created before the inner declaration and reads outer x. Toggling rebuilds analysis, not runtime deletion.","scopeLabel":"Model scope and assumptions","use":"Use site","value":"Value at declaration"}}
What Symbol Records Should Store
Field
Purpose
Stable identity, declaration kind
Distinguish variables, types, functions with the same name
Subsequent constraint generation and type checking
Mutability, access rights, etc.
Assignment, import, and call restrictions
Definition and reference relationships
IDE reference queries and refactoring; can be stored separately in indices
Namespaces are also dictated by the language. In C, tag names and regular identifiers belong to different namespaces; labels have their own scope. Putting all spellings into a single unclassified table would cause false duplicate reports or wrong definition lookups.
Module Imports: Candidate Sources and Access Rights
Priorities differ among module paths, explicit imports, wildcard imports, aliases, and built-in names. Symbol tables should retain candidate sources to facilitate reporting ambiguities like "both imports provide this name," rather than arbitrarily selecting the last inserted one.
Finding a declaration and allowing access to it from the current location are two separate issues. Visibility checks may also depend on module hierarchy, packages, or other language rules, not just public/private values.
Module cycles are not necessarily illegal. Some languages allow collecting declarations first, then resolving function bodies or handling strongly connected components; others prohibit certain dependency cycles. Recursive imports should be prevented from causing infinite resolution, but unconditional errors for all cycles are incorrect. Rust's name resolution also interacts with macro expansion and different namespaces. Rustc Name Resolution
Overloading: Lookup Candidates and Target Selection Can Be Stepwise
Languages with overloading may first obtain a candidate set based on name and scope, then select the callable target based on argument count, types, implicit conversions, and generic constraints. Member lookup also depends on the receiver type.
Therefore, "lookup returns a single symbol" is an interface for simple languages. Complex implementations can return candidates, deferred resolution items, or error placeholders, retaining source information for diagnostics.
Closure Capture: Name Resolution Only Completes the First Step
When a closure references an outer local variable, the resolver needs to know which external binding it points to; capture methods, environment layout, and escaping require analyzing how the closure uses that value and the language's ownership rules.
Here, reading the length of the Vec only requires a shared borrow; it does not automatically move ownership just because Vec is not Copy. move capture does not imply the closure can only be called once; Fn, FnMut, and FnOnce depend on how the closure uses captured values upon invocation. Rust Closures
Being captured does not necessarily mean heap allocation. Non-escaping closures may use stack environments; capturing by value can place data into the closure object, and optimizations may even eliminate the environment. The true requirement is that captured values remain valid for the required duration; borrowing, copying, moving, boxing, or upvalues are different implementation strategies.
Closures Cannot Re-lookup by Same Name at Invocation
Once lexical binding is determined, subsequent declarations with the same name do not rewire existing closures to the new declaration. In the following Rust example, the closure always references the first value; the second value merely shadows the name for subsequent direct uses. Both bindings remain valid for their required durations.
fnmain(){let value =10;letread_old=||value;let value =20;assert_eq!(read_old(),10);assert_eq!(value,20);assert_eq!(read_old(),10);}
This is a separate dimension from "capture by value vs. by reference": first determine which binding is referenced, then decide how to provide that binding to the closure. If a language allows modifying the same binding being referenced, subsequent reads might observe its new value; but switching to another binding with the same name does not equate to modifying the original binding.
Generic Instantiation: Source Symbols and Backend Instances Managed Separately
A generic declaration has a stable identity, while specific type arguments form the instantiation context. The backend can cache instances using keys like "declaration identity + substituted parameters," avoiding the need to copy a set of source-language scope symbols for every use.
Whether code generation uses monomorphization, shared implementations, or dictionary passing is a subsequent strategy. Different type arguments do not guarantee different machine code in the final output; optimization, inlining, and link-time deduplication all affect the artifacts.
From Batch Compilation to IDE
Batch traversal can use an active environment stack; interactive systems must also handle modified symbol identities, dependency invalidation, and old location mappings. Editing a single function body should not arbitrarily change all symbol identities in the entire project; changes to public declarations may require re-checking dependencies.
Diagnostics should ideally indicate usage locations, candidate declarations, and specific failure rules, such as "this name comes from a private module" or "two wildcard imports cause ambiguity," rather than converting everything to "symbol not found."