A name’s spelling is not its identity. Scope determines which declaration a use refers to.
By the end: Distinguish lexical scope, shadowing, binding identity, and runtime storage.
Find the declaration, not just a matching string
A symbol table records declarations and relevant information about them. With lexical scope, a name use resolves according to its surrounding source structure. An inner declaration can shadow an outer declaration with the same spelling. They remain two bindings, not one variable that changes identity.
The scope experiment shows two declarations named x, with identities A and B. The outer uses refer to A; the use inside the block refers to B. This pseudocode is not accepted by the expression editor. It demonstrates a responsibility that our fixed expression environment only models in a simpler form.
Resolve each use in its surrounding scope
Treat a declaration as introducing an identity and a use as asking which identity applies at that source position. In the separate block pseudocode, the outer declaration x = 10 introduces binding A. Entering the inner block introduces another declaration x = 20 with identity B. The letters A and B are explanatory labels, not extra names written in the source. Both declarations have the spelling x. Lookup must consider where a use appears, because spelling alone cannot choose between two visible declaration candidates.
One implementation strategy keeps a stack of scope tables while traversing source structure. Entering a block pushes a table. Declaring a local name adds an entry in that table, and leaving the block pops it. To resolve a use, search the innermost eligible table first, then continue outward until finding the declaration. In the displayed example, the inner print finds B immediately. After the block, the table containing B is no longer part of lookup, so the final print finds A again rather than remembering the most recent matching spelling.
Scope-table changes belong to the static traversal in this explanation. They are not writes to the variable's runtime value. The resolver can associate all three use sites with their declarations without executing any print statement. If execution then follows the straight-line pseudocode, the printed values are ten, twenty, and ten. The middle output does not replace the outer declaration's value. Confusing the traversal's table stack with runtime storage makes shadowing look like assignment even though the source introduced a distinct local binding.
All three uses have stable declaration links
Separate lexical-scope pseudocode. Nested outlines are source scopes, not memory regions. Lines connect each read to declaration A or B; selecting a use in the lab does not change these identities.
Contrast an inner declaration with an assignment under an explicit alternative rule. Suppose the block contains x = 20 without introducing another x, and assignment updates the nearest enclosing binding. There is then no B. Both the assignment and the final outer read refer to A, whose value changes from ten to twenty during execution. The same numeral twenty appears in both examples, but the declaration structure differs. Before predicting the final read, identify whether the statement creates an identity or updates storage associated with an existing identity.
The order of lookup depends on language policy as well as nesting. Our displayed block example places each use after the declaration it intends to illustrate, so it does not decide every forward-reference question. A language can make declarations visible only after their source position, throughout a block with an initialization restriction, or under another defined rule. The scope-table strategy must encode that policy. Searching inward first is insufficient when the innermost table contains a declaration that is not eligible at the particular use position.
A declaration and an assignment change different things
Question
Inner declaration
Assignment to outer x
Block statement
let x = 20
x = 20
Binding introduced
B, distinct from A
None
Inner read resolves to
B
A
Runtime value of A afterward
10
20
Outer read after block
10
20
Separate pseudocode models. The assignment column explicitly assumes updates target the nearest enclosing binding. Only the declaration column is shown by the interactive scope experiment.
In the scope experiment, select each of the three use buttons and predict its binding label before reading the highlighted connection. Check the source boundary that contains the middle use but excludes the final one. Then describe the assignment alternative on paper, crossing out B and redirecting the affected use to A. The controls only select a use in the fixed declaration example. They do not edit statements, execute the pseudocode, or implement the alternative. The comparison asks you to apply the binding rule yourself.
Separate lexical scope from a call stack
Lexical scope is determined by source nesting, not by whichever function happened to call another. A runtime call stack records active invocations and their local storage. Recursive calls can have distinct storage for the same source-level binding. Binding identity and a particular activation’s value are therefore different concepts.
A closure combines executable behavior with access to its required lexical environment. A returned function may still need a variable after the creating call has finished. Resolving the name identifies what it refers to; runtime representation must then preserve the necessary storage or value.
Separate a declaration from its activations
A local declaration is one source-level binding even when its function runs repeatedly. Each invocation can need a different instance of storage for that binding. Consider pseudocode for a function whose parameter is n and whose local v is initialized from n. Calling it with three and later with eight uses the same declaration of v in both executions. Nevertheless, a read during the first call obtains three and a read during the second obtains eight. Binding identity alone does not select an invocation's current value.
Recursion makes the distinction visible while both instances coexist. Suppose an outer call with n = 2 stores v = 2 and calls the same function with n = 1. The inner call has its own v = 1. When it returns, a read in the suspended outer call still needs two. A call-frame representation can keep the two storage instances distinct while using the same local-slot offset within each frame. The slot number identifies a position within an activation, not one address shared by every call.
Lexical lookup also differs from searching callers. In a separate pseudocode example, an outer scope declares x = 10 and defines readX whose body returns x. Another function declares its own x = 20 and calls readX. Under the assumed lexical scope, readX's use refers to the outer declaration, so the result is ten. The caller's local x is not an enclosing declaration around readX's source body. Searching the active call stack for a matching spelling would implement a different lookup rule and yield twenty in this example.
A caller is not necessarily a lexical parent
Separate function pseudocode under lexical scope. The source link gives readX access to A. The call edge transfers control from caller but does not redirect readX's x to B. Geometry is schematic, not an address or stack-depth scale.
The runtime still needs a way to access the resolved declaration's appropriate environment. A closure can retain a reference to its defining environment, and a direct nested call may use a link to an enclosing activation. A compiler can also replace some accesses with simpler storage operations when it proves the replacement equivalent. These are representation choices. The lexical rule states which declaration supplies the value; it does not require every implementation to search a dictionary by spelling whenever the function reads that value during execution.
The static resolver might record a declaration identifier, or a lexical distance and slot number. For example, distance zero can mean the current lexical environment and distance one its enclosing environment under a chosen representation. That convention works only when runtime environment links correspond to the resolver's scope structure. An implementation that inserts extra runtime environments must adjust access information or preserve the correspondence another way. The numerical distance is an implementation coordinate, not an intrinsic property of the language's variable or a count of callers.
One declaration can have simultaneous storage instances
Property
Outer recursive call
Inner recursive call
Parameter n
2
1
Source declaration of v
The same declaration
The same declaration
Runtime storage instance
Outer activation’s v cell
Inner activation’s v cell
Stored v
2
1
After the inner call returns
Outer read still obtains 2
Ordinary inner frame can finish
Separate recursive-function pseudocode. Each call initializes v from n and the outer call reads its v after the inner call. No addresses, frame sizes, or target ABI layout are specified.
Draw the lexical caller example with source containment on the left and two active call frames on the right. Link readX's x to its source declaration first. Only then choose the runtime storage that realizes that declaration. Do the recursion example separately, labeling its two v cells by call rather than inventing two source declarations. These drawings answer different questions. One identifies what a source use means; the other distinguishes simultaneous runtime instances. Neither is a memory feature hidden inside the SmallExpr editor, which has no functions or call frames.
Work through the mechanism
Connect a use to its declaration
Line 2 resolves to declaration on line 1: binding A, value 10.
Nested outlines represent lexical scopes, not memory addresses or elapsed time. Selecting a use replaces the highlighted binding link; it does not execute the program. Declarations in this pseudocode introduce distinct bindings.
Use site
Binding
Value in this example
Outer use before block
A
10
Use within block
B
20
Outer use after block
A
10
Source nesting determines these links. This is not a demonstration of dynamic scoping.
Preserve the environment a closure needs
A closure matters when a function uses a binding from an enclosing function and can run after that enclosing call returns. Consider separate pseudocode for makeReader, which initializes a local x to ten and returns a function that reads x. Resolution links the returned function's use to that local declaration. Returning from makeReader does not change the link to some unrelated x in its future caller. The runtime must retain enough information to produce ten later, despite the end of the original activation's ordinary lifetime.
For this read-only example, preserving the value ten may be sufficient. If the language also allows two returned functions to share and update x, copying ten independently into both closures can change behavior. Assume one closure increments the captured binding and another reads that same binding. Starting at ten, one increment followed by a read must produce eleven under shared mutable capture. Two independent copied cells would let the read remain ten. The capture rule must therefore specify whether the functions share a binding or receive separate values.
A possible shared-binding representation uses an environment object containing one x cell, with references from both closures to that object. The returned functions keep the environment reachable after the creating call returns. Their access paths reach the same cell, so an update through one is visible through the other. This is an object-graph explanation, not an implementation in the expression lab. It also does not require retaining every local from the old frame. Only the state needed for the language's later behavior must remain represented.
Shared mutable capture requires shared state
Separate closure model. Both reference paths reach one retained x cell. A completed increment changes its value from 10 to 11, so the reader obtains 11. Shapes and spacing do not encode memory addresses or allocation sizes.
Later declarations introduce a separate correctness problem. Assume a language where a use sees the nearest preceding declaration in its lexical surroundings. An outer x is ten. Inside a block, define readX before a later local declaration x = 20, and call readX both before and after that declaration. Its source use resolves to the outer x under the stated policy. Both calls should therefore read ten. A naive runtime search through a mutable block table could incorrectly find the newly added inner x on the second call.
Resolving the use once can prevent that accidental change of declaration. The runtime may follow recorded access information instead of repeating an unrestricted spelling search. Another design can preserve distinct environment versions as declarations become visible. Either approach must still allow changes to the value of an already captured mutable binding when the language requires them. Freezing which declaration a use denotes is not the same as freezing that declaration's value. This distinction explains why copying a whole table of values is not a universal closure implementation.
A later declaration must not change an earlier lexical link
Source event
Resolved preceding declaration
Naive mutable-table search
Outer x = 10 exists
A
A
Define readX inside block
Its x resolves to A
Store reference to current block table
First call to readX
Read A: 10
No block x yet; find A: 10
Later declare block x = 20
Introduce B; link stays A
Add B to the captured block table
Second call to readX
Read A: 10
Incorrectly find B: 20
Separate language-policy example: a use denotes the nearest preceding lexical declaration. The failing column describes a hypothetical implementation error, not permitted behavior or an implemented SmallExpr closure.
Trace the shared-cell example on paper by marking the single cell before and after an increment. Then replace it with two copied cells and identify the first read whose result changes. For the later-declaration example, leave all numeric values unchanged and change only the lookup strategy. The second call then exposes the wrong declaration choice. These controlled contrasts isolate two independent obligations, shared state and stable lexical identity. Neither can be checked by merely observing that one initial call returned the expected value in a single execution.
Make language policies explicit
Languages differ on forward references, declaration order, namespaces, imports, and whether a variable is visible in its own initializer. A compiler must implement those policies rather than guess from spelling. Built-in names and module-qualified names also need defined lookup rules.
A binding error is distinct from a type error. absent + 1 cannot resolve its left name in our fixed environment. flag + 1 resolves both operands but fails their type requirement. Real implementations may combine these analyses while still maintaining the distinction in their reasoning and diagnostics.
State the binding policy before diagnosing a use
A symbol table can record information besides a name and declaration location. An entry might identify whether the declaration is a parameter, local, imported name, or function, and which type information is available. These facts answer related but separate questions. Finding a declaration for flag does not make flag an integer. Finding a declaration for a function does not prove that a particular call passes compatible arguments. Distinguishing these questions explains diagnostics even when one traversal performs several checks.
SmallExpr uses a simpler environment than the scope pseudocode. Its only predefined names are x, y, and flag, with values four, seven, and true. It has no declarations to traverse or nested scope stack to construct. The check function looks up a name and obtains its type from that fixed environment value. For absent + 1, lookup fails. For flag + 1, lookup succeeds and the arithmetic type requirement fails. For x + 1, both responsibilities succeed, and execution returns five under the stated arithmetic semantics.
Identifiers also follow a defined spelling policy before binding starts. SmallExpr's names use ASCII letters or underscore initially, followed by ASCII letters, digits, or underscores, within the teaching length bound. Lookup is case-sensitive, so X and x are different spellings. X + 1 parses but fails binding in the fixed environment. The lexer recognizes true as a boolean literal, not an ordinary name that could resolve to a declaration. Consequently changing capitalization can affect which responsibility rejects an expression rather than merely changing its displayed appearance.
Spelling, binding, and typing answer different questions
SmallExpr source
Name or literal classification
Full checking outcome
x + 1
x is a declared int name
int; execution returns 5
X + 1
X is a name, distinct from x
Binding error
flag + 1
flag is a declared bool name
Typing error; no coercion
absent + 1
absent is a name
Binding error
flag
Declared bool name
bool; execution returns true
true
Boolean literal, not a name
bool; execution returns true
Exact outcomes in the fixed x = 4, y = 7, flag = true environment. Parsing alone accepts all these structures; the full pipeline adds the later checks.
Initialization policy needs its own rule in a richer language. Imagine outer x = 10 and an inner declaration written let x = x + 1. If the inner binding becomes visible only after its initializer, that read can denote the outer binding and initialize the inner one to eleven. If the new binding is already visible but unavailable until initialized, the same source can be rejected as a premature read. Both policies are coherent when stated precisely. The block experiment does not choose between these hypothetical initializer rules.
Try x + 1, X + 1, and flag + 1 in a view that runs the full checking pipeline, such as the types lesson. Predict successful integer typing, a binding error, and a type error respectively before entering them. Then compare x and flag alone. Each name resolves, but one has type int and the other bool. Reading a boolean alone is permitted. Arithmetic on that boolean is not, because SmallExpr provides no implicit conversion. This comparison locates the failing rule more precisely than saying the name is invalid.
Initializer lookup needs an explicit language policy
Assumed policy
Meaning of x in let x = x + 1
Outcome with outer x = 10
Inner binding becomes visible after initializer
Read the outer binding A
Initialize inner binding B to 11
Inner binding visible but unavailable during initializer
Refers to unavailable inner binding B
Reject the premature read
Scope experiment on this page
No self-referencing initializer shown
Does not choose either policy
Alternative pseudocode semantics, not SmallExpr syntax. Both hypothetical policies require more than matching the nearest name spelling; they differ in when a declaration becomes eligible for reading.
When diagnosing unfamiliar code, write down the use site, candidate declarations, visibility policy, and selected identity before reasoning about storage. Next ask which runtime activation or retained environment supplies the value. Finally check the operation's type requirements. A resolver proves facts about declaration choice under the source rules; a runtime trace shows values for one execution. If a trace happens to agree while lookup chooses the wrong declaration, changing later values can expose the error. Correct name resolution requires the rule to hold beyond coincidentally equal example values.
Exercises
Does shadowing overwrite the outer variable?
In the scope diagram, an inner block declares a new x = 20 while outer x = 10 remains in scope outside it. What does the outer use after the block read?
Hint
A declaration creates a different binding; assignment would be a different statement.
Worked answer
Operation
Effect on A
Declare inner x
Creates B; A stays ten
Read after block
Resolves to A
Assign outer x instead
Would modify A under the assumed language
The same spelling does not imply the same binding.
It reads ten from binding A. The inner declaration B shadows A within its scope but does not assign to A. If the block instead assigned to the existing outer binding, the answer could differ. Distinguish the declaration rule from an update to storage.
A returned closure
A function returns another function that uses a local variable. Can the implementation discard every trace of that variable when the outer call returns?
Hint
The returned function may still need the captured binding or its value.
Worked answer
Required meaning
Possible implementation
Later read of captured value
Stored value or reconstructed equivalent
Shared mutable captured binding
Shared cell or equivalent state
Unused local data
May be discarded
Capture of a binding and capture of a value are language-dependent.
Not if later execution must read it. The implementation needs a representation that preserves the required environment, for example a heap-allocated captured cell, an environment object, or an optimized equivalent. It need not preserve the entire stack frame, but it must preserve the language’s capture semantics.
Sources and further reading
The explanations, examples, and worked answers are complete without these optional links. The sources describe specific implementations and language contracts, not one universal compiler pipeline. SmallExpr and the later pseudocode models are constructed teaching examples. Source notes identify the selected material reviewed; this is not an exhaustive compiler survey, production conformance test, or independent specialist review.