Introduction

Two questions decide what a procedure call does with names. Parameter passing answers how an actual parameter in the call is connected to the formal parameter in the procedure: by copying its value, by sharing its location, by copying in and out, or by re-evaluating the argument text. Scope rules answer which declaration a free name in a procedure body refers to: the one that encloses it in the program text (static scope) or the most recent one in the chain of calls (dynamic scope).

Both are favourites for output-prediction questions: a short program, a stated mechanism or scope rule, and the question "what is printed?". The answer follows mechanically once each rule is applied exactly.

Prerequisites

  • Runtime Environments and Activation Records: activation records, control links (the call chain) and access links (the lexical chain).
  • l-values (locations) and r-values (contents) of expressions, as in assignment (see the note Functions and Parameter Passing).

Core concepts

  • Formal and actual parameters. Formals are the names in the procedure declaration; actuals are the expressions in the call.
  • l-value and r-value. A[i] has an l-value (the address of the element) and an r-value (the number stored there). Mechanisms differ in which of these they pass and when they compute it.
  • Environment and state. The environment maps names to locations; the state maps locations to values. Scope rules fix the environment; assignments change the state.
  • Aliasing. Two names denote the same location. Call by reference creates aliases (a formal and a global, or two formals), which changes results.
  • Static (lexical) scope. A name refers to the declaration in the nearest enclosing block or procedure in the program text. Resolved at compile time; implemented with access links or displays.
  • Dynamic scope. A name refers to the declaration in the most recently called, still active, procedure that declares it. Resolved at run time by searching down the call stack.

How it works

The four mechanisms

MechanismWhat is passedEffect of assigning to the formal
Call by valuer-value of the actual, copied into a new localOnly the local copy changes
Call by referencel-value of the actual (its address)The actual's location changes immediately
Call by value-result (copy-restore)r-value copied in; the actual's l-value is noted at the callLocal copy changes; it is copied back to the noted location on return
Call by nameThe argument text, re-evaluated in the caller's environment at each use (a thunk)The location named by the text at that moment changes

If the actual is not an l-value, such as a + 1, call by reference typically passes the address of a temporary that holds its value. Call by value-result differs from call by reference only when the procedure can reach the same location by another path (an alias) during the call. Call by name differs from call by reference when the argument's location depends on something the procedure changes.

Example 1: one variable, four outcomes

Text
int a = 1;
void f(int x) { x = x + 1; a = a + 10; x = x + 100; }
main: f(a);  then a is ?
MechanismTraceFinal a
Valuex: 1, 2, 102; a: 1111
Referencex and a are one location: 2, 12, 112112
Value-resultx: 1, 2, 102 while a becomes 11; on return a = x102
Nameevery use of x means a: 2, 12, 112112

Value-result and reference disagree because a is changed through the global name while the copy x is separate.

Example 2: when the argument's location moves

Text
int i = 0; int A[2] = {5, 7};
void q(int x) { i = 1; x = x + 1; }
main: q(A[i]);
  • Value: A is unchanged, {5, 7}.
  • Reference: the address of A[0] is fixed at the call; A becomes {6, 7}.
  • Value-result: x starts as 5 and becomes 6; it is copied back to A[0], the location noted at the call: {6, 7}.
  • Name: x = x + 1 re-evaluates A[i] with i=1i = 1 on both sides: A[1] = A[1] + 1, so A becomes {5, 8}.

Some descriptions of value-result recompute the destination address on return; under that variant the copy-back goes to A[1]. Questions that rely on the difference say which rule applies.

Example 3: swap fails under call by name

Text
void swap(int x, int y) { int t = x; x = y; y = t; }
int i = 0; int A[2] = {1, 5};
main: swap(i, A[i]);

By reference, x is i and y is A[0]: t=0t = 0, i=1i = 1, A[0]=0A[0] = 0, giving i=1i = 1 and A = {0, 5}, a correct swap. By name, each use re-evaluates: t=i=0t = i = 0; i=A[i]=A[0]=1i = A[i] = A[0] = 1; then y is A[i] with the new i=1i = 1, so A[1]=0A[1] = 0. The result is i=1i = 1 and A = {1, 0}: A[0] never changed. By value nothing outside changes.

Example 4: aliasing between formals

Text
int a = 2;
void h(int x, int y) { x = x + 1; y = y + y; }
main: h(a, a);

By value, a stays 2. By reference both formals are a: a=3a = 3, then a=3+3=6a = 3 + 3 = 6. By value-result xx becomes 3 and yy becomes 4 independently; copying back left to right leaves a=4a = 4 (the last copy wins, so the order of copy-back matters).

Example 5: all four mechanisms on one call

Text
int i = 1; int A[5] = {10, 20, 30, 40, 50};
void p(int x, int y) { y = y + 1; x = x + 2; i = i + 1; y = y + x; }
main: p(i, A[i]);
MechanismTraceFinal iFinal A
Valuecopies x = 1, y = 20; only i = i + 1 reaches a global2unchanged
Referencex is i, y is A[1]: A[1] = 21; i = 3; i = 4; A[1] = 21 + 4 = 254A[1] = 25
Value-resultcopies x = 1, y = 20: y = 21, x = 3, global i = 2, y = 24; copy back i = 3, A[1] = 243A[1] = 24
NameA[1] = 21 (i is 1); i = 3; i = 4; then y = y + x is A[4] = A[4] + i = 544A[1] = 21, A[4] = 54

Under value-result the copy of x overwrites the increment that i = i + 1 made through the global name, so ii ends at 3, not 4. Under name, the last statement touches A[4], an element the call never mentioned: the index is re-evaluated after ii has changed.

Static versus dynamic scope

Under static scope, a free name in a procedure is bound to the declaration that encloses the procedure's text. For C-like programs with all procedures at the outermost level, that means: own locals, then globals. Under dynamic scope, the binding is the most recent active declaration: own locals, then the caller's, then the caller's caller's, and so on down the stack, then globals.

Example 6: the basic contrast.

Text
int x = 1;
void f() { print(x); }
void g() { int x = 2; f(); }
main: g(); f();

Static: f always sees the global x, printing 1 1. Dynamic: when f is called from g, the most recent x is g's, so the first call prints 2; the second call is from main and prints 1. Output 2 1.

Static and dynamic scope resolution of x in f for the calls g() and f()
Figure 1. The program x = 1; f prints x; g declares x = 2 and calls f. Under static scope f finds the global x and g(); f() prints 1 1; under dynamic scope f finds the x of g first and the output is 2 1.

Example 7: assignments as well as reads.

Text
int n = 1;
void show() { print(n); }
void inc()  { n = n + 1; }
void two()  { int n = 5; inc(); show(); }
main: two(); show();

Static: inc changes the global n to 2 and both calls of show print the global: 2 2. Dynamic: inside two, the n that inc and show see is two's local, so inc makes it 6 and show prints 6; after two returns, show sees the global, still 1. Output 6 1.

Implementation. Static scope uses the access links or display of the runtime-environments note; the hop count is fixed at compile time. Dynamic scope can be implemented by deep access (search the control-link chain for the first record that declares the name) or shallow access (keep one current slot per name, saving the old value on entry to a procedure that declares it and restoring it on exit).

Static scope with nested procedures

With nested declarations, static scope looks outward through the enclosing procedures' texts, not through the callers. If p is declared inside main and q is declared inside p, a name used in q and declared in both main and the caller of q resolves to p's or main's declaration, whichever encloses q first, no matter who called q. Under dynamic scope the caller's declaration wins.

Example 8: nested procedures.

Text
int x = 10;
void p() {
    int x = 20;
    void q() { print(x); }
    void r() { int x = 30; q(); }
    r(); q();
}
main: p();
  • Static: q is declared inside p, so its free x is p's, whoever calls it. Both calls print 20: output 20 20.
  • Dynamic: the first call of q comes from r, whose x is the most recent: 30. The second comes from p: 20. Output 30 20.
  • If q were declared at the outer level instead (next to p), static scope would print the global: 10 10. Dynamic scope would still print 30 20, because the call history is unchanged.

Important properties and observations

  • Reference and name agree on a simple variable argument whose location cannot change (Example 1); they differ for A[i] when i changes (Examples 2 and 3).
  • Value-result and reference agree unless aliasing exists during the call (Examples 1 and 4).
  • Call by value never changes the actual; but passing a pointer by value still lets the callee change the pointed-to object, which is how C simulates reference parameters.
  • Call by name is lazy: an argument never used is never evaluated, and one used twice is evaluated twice.
  • Static scope is decided by program text; dynamic scope by the call history. The same program can print different results under the two rules only if some procedure has a free name declared in more than one place.
  • C, C++, Java and Pascal use static scope; C passes everything by value (arrays decay to a pointer passed by value); C++ adds reference parameters.

Common misconceptions.

  • "Value-result is the same as reference." Not when the procedure also reaches the actual through another name.
  • "Dynamic scope means the variable is allocated dynamically." It is about name resolution, not storage.
  • "Under static scope the callee sees the caller's locals." Only if the callee is textually nested in the caller.

GATE examination perspective

  • Output under a stated mechanism. Trace each assignment, keeping the formal's location (reference), a separate copy (value, value-result) or the argument text (name). For value-result, apply the copy-back order the question implies.
  • Output under static and dynamic scope. For each free name, find the enclosing declaration (static) or the most recent active one on the call chain (dynamic). Assignments to a free name change whichever variable that rule picks.
  • Aliasing traps. The same variable passed twice, or a global also passed as an argument.
  • Array arguments with changing indices. Separate name from reference.
  • Value-result copy-back can undo a global update made during the call. (Example 5: ii ends at 3, not 4).
  • Nested procedures under static scope. Find where the procedure is declared, not where it is called (Example 8).
  • Which needs what at run time. Static scope uses access links or displays; dynamic scope searches the control chain.

Summary

Sources

  1. A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers, Principles, Techniques, and Tools, 2nd ed., Pearson Addison-Wesley, 2007: Section 1.6.1 (the static/dynamic distinction), Section 1.6.2 (environments and states), Section 1.6.3 (static scope and block structure), Section 1.6.5 (dynamic scope), Section 1.6.6 (parameter-passing mechanisms: call by value, call by reference, call by name), Section 1.6.7 (aliasing), Section 7.3 (access links and displays for static scope).

Next steps