Static and Dynamic Scoping
When we access a variable that is declared in the current scope, that variable is a bound variable. A variable that is not bound in the current scope is a free variable. This function h
accesses two variables:
function h() {
const angle = 360;
return angle / n;
}
Within h
, variable angle
is bound and variable n
is free. Is this reference to n
legal?
How should a programming language handle free variables? Some possible options include:
- A reference to a free variable could generate an error informing the programmer that the variable is not in scope. A language that chooses this option would permit only local variables and function parameters, which is quite limiting.
-
The compiler or interpreter could look for the variable in the parent scope. If it doesn't find it, it proceeds to the grandparent scope. And so on, all the way up to the global scope. For example, function
child
in this JavaScript program can see thex
declared ingrandparent
:JavaScriptResolving variable references by traversing the nesting hierarchy of the code is lexical scoping. The term lexical indicates that the resolution is based only on the source code. More commonly, this scheme is called static scoping because the variable references can be resolved ahead of time, before the program is run.function grandparent() { let x = 7; function parent() { function child() { console.log(x); } } }
-
The language runtime could look for the variable in the call stack. The caller's stack frame is checked first. Then the caller's caller. And so on, all the way back to the main function's stack frame. For example, function
f
in this program can see thex
from functiong
, which called it:Not JavaScriptSince variables are found in the call stack, and the call stack only exists at runtime, this resolution scheme is called dynamic scoping. Few languages support dynamic scoping. Note thatfunction f() { console.log(x); } function g() { let x = 7; f(); } g();
f
is not lexically nested ing
, so variablex
would be undefined under static scoping.
Let's examine the difference between static and dynamic scoping with this pseudocode:
function main()
int a = 1
int b = 2
function f()
int a = 0
b += 1
print(a, b)
function g()
int b = 4
print(a, b)
f()
print(a, b)
print(a, b)
g()
print(a, b)
With static scoping, we identify which variables are accessed by looking only at the nesting of scopes in the code. A compiler assembles an environment data structure, which is a table of all visible variables and the scope from which they are drawn.
With dynamic scoping, the nesting of scopes is not considered when determining the environment. Instead, we must traverse the call stack. In general, we can't determine the environment just by looking at the code, as the sequence of function calls may be unpredictable. However, this particular pseudocode is predictable.
Explore how the program behaves under static scoping by stepping through from print statement to print statement.
function main()
int a = 1
int b = 2
function f()
int a = 0
b += 1
print(a, b)
function g()
int b = 4
print(a, b)
f()
print(a, b)
print(a, b)
g()
print(a, b)
Stack
Output
-
When the highlighted line is run, only
main
has a stack frame on the stack. Since only the variables ofmain
are in play, they are what's printed. -
Function
g
gets called. It declares its ownb
variable, which is stored in the new stack frame. When it prints,g
must go looking for ana
variable. Because the program is statically scoped andg
is nested insidemain
, it uses thea
frommain
. -
Function
f
gets called. It declares its owna
variable, which is stored in the new stack frame. Variableb
is updated and printed, but it is a free variable. Becausef
is nested insidemain
, it uses theb
frommain
. -
Function
f
has returned, so its stack frame is wiped. Functiong
prints what it sees. -
Function
g
has returned, so its stack frame is wiped. Functionmain
prints what it sees.
Contrast this with dynamic scoping, which gives a different result.
function main()
int a = 1
int b = 2
function f()
int a = 0
b += 1
print(a, b)
function g()
int b = 4
print(a, b)
f()
print(a, b)
print(a, b)
g()
print(a, b)
Stack
Output
-
When the highlighted line is run, only
main
has a stack frame on the stack. Since only the variables ofmain
are in play, they are what's printed. -
Function
g
gets called. It declares its ownb
variable, which is stored in the new stack frame. When it prints,g
must go looking for ana
variable. Because the program is dynamically scoped andg
was called bymain
, it uses thea
frommain
. The fact thatg
is a lexical child ofmain
is irrelevant. Nevertheless, static and dynamic scoping exhibit the same behavior at this point. -
Function
f
gets called. It declares its owna
variable, which is stored in the new stack frame. Variableb
is updated and printed, but it is a free variable. Becausef
is called byg
, it uses theb
fromg
. Here the two scoping schemes behave differently. -
Function
f
has returned, so its stack frame is wiped. Functiong
prints what it sees. -
Function
g
has returned, so its stack frame is wiped. Functionmain
prints what it sees.
Dynamic scoping is harder to reason about than static scoping because it depends on runtime behavior. You won't see it in many languages. Perl supports it through the poorly named local
modifier. Unix environment variables are dynamically scoped. If one process sets an environment variable, any processes it spawns will see the same environment variables. Despite its unpopularity, we mention dynamic scoping here to emphasize that there's not just one way for programming languages to work and because it provides a counterpoint that helps illustrate the behavior of the more common static scoping.