Subprograms II: activation records, static and dynamic chains
What happens at call and return, the structure of an activation record, the run-time stack, recursion, nonlocal access through the static chain in nested subprograms (chain_offset, local_offset), blocks and the deep/shallow access implementation of dynamic scope.
Last week we talked about what subprograms do; this week how the compiler implements them. At the center there is a single structure: the activation record (ARI). Each time a subprogram is called, an ARI is placed on the run-time stack, and removed when it returns. Exam questions: the contents of an ARI, the picture of the stack for a given call sequence, where the static/dynamic links point, and (chain_offset, local_offset) pairs.
Golden rules:
- The dynamic link points to the caller, the static link to the static parent.
- The static parent is read from the text, not from the call order.
- chain_offset = depth of the place of reference − depth of the place of declaration.
- Every call is a new ARI: in recursion several ARIs of the same function are on the stack.
Call and return semantics
What must be done in a call (call semantics):
- Save the caller’s execution state.
- Allocate space for the local variables (if stack-dynamic).
- Pass the parameters (copy if value, address if reference).
- Save the return address.
- Set up the link needed for nonlocal access (static link).
- Transfer control to the callee.
At the return (return semantics): copy back the values of out and inout parameters, put the return value of a function where the caller can access it, restore the stack, and go to the return address.
Activation record
The form of the local data a subprogram needs while running is called an activation record, and its concrete instance belonging to a particular call is called an activation record instance (ARI).
┌──────────────────┐ ← top of the stack
│ local variables │
│ parameters │
│ dynamic link │ → the caller's ARI
│ static link │ → the static parent's ARI
│ return address │
└──────────────────┘ ← base of the ARI
The simple case: no recursion, no nesting
In early Fortran all locals were static. Each subprogram had a single ARI, placed in a fixed location at compile time. Fast, but recursion is impossible: a second activation overwrites the variables of the first.
Stack-dynamic locals: the run-time stack
In languages like C and Java, each call puts a new ARI on the stack. The environment pointer (EP) points to the base of the ARI of the currently running unit; locals are accessed with EP + a fixed offset. The dynamic link points to the caller’s ARI; on return EP is restored to this value. The chain formed by dynamic links is called the dynamic chain (call chain).
static int f(int n) {
System.out.print("in" + n + " ");
int r = (n <= 1) ? 1 : n * f(n - 1);
System.out.print("out" + n + " ");
return r;
}
// main: int v = f(3); System.out.println(); System.out.println(v);
in3 in2 in1 out1 out2 out3
6
While f(1) is running the stack is: main, f(3), f(2), f(1). Each ARI has its own n and r. If fact(4) is called from main, there are at most 5 ARIs on the stack.
Nested subprograms: the static chain
In languages like Ada, JavaScript and Python, a function can be defined inside another function. In static scope a nonlocal variable is in the ARI of one of the static ancestors. The problem: where is that ARI on the stack? The solution: a static link pointing to the static parent’s ARI is added to each ARI. The chain of static links is called the static chain.
- Static depth: how many levels inside the outermost a unit is. Main is 0.
- chain_offset (nesting depth): the static depth of the unit where the reference is made − the static depth of the unit where the variable is declared.
- local_offset: the position of the variable inside its own ARI.
Nonlocal access: follow chain_offset static links, and go to local_offset in the ARI reached. Both numbers are known at compile time.
Worked example
Main declares p, q, r static depth 0
Sub1 declares q, s static depth 1 (inside Main)
Sub2 declares t, p static depth 2 (inside Sub1)
Sub3 declares r, s static depth 1 (inside Main)
Call sequence: Main → Sub3 → Sub1 → Sub2
ARI layout: 0 return address, 1 static link, 2 dynamic link, locals from 3 onward
The stack while Sub2 is running (bottom to top):
# ARI static link → dynamic link →
3 Sub2 Sub1 Sub1
2 Sub1 Main Sub3 ← note: static parent Main, caller Sub3
1 Sub3 Main Main
0 Main - -
References inside Sub2:
| Name | Unit where found | chain_offset | local_offset | Pair |
|---|---|---|---|---|
| t | Sub2 | 2 − 2 = 0 | 3 | (0, 3) |
| p | Sub2 (hides Main.p) | 0 | 4 | (0, 4) |
| q | Sub1 | 2 − 1 = 1 | 3 | (1, 3) |
| s | Sub1 | 1 | 4 | (1, 4) |
| r | Main | 2 − 0 = 2 | 5 | (2, 5) |
Inside Sub3, q is in Main: (1, 4). Sub3’s own r: (0, 3).
PITFALL · the static link does not go to the caller. Sub3 called Sub1, but Sub1’s static link points to Main. The static link follows the nesting in the text. The point most often lost on exams is this: drawing the arrow to the ARI right below on the stack.
How is the static link set up?
At call time, the most recent active ARI of the callee’s static parent must be found. The compiler does this with offsets too: the static link is followed as many times as the difference between the static depth of the caller and the depth of the callee’s static parent. Sub3 (depth 1) calls Sub1 (parent Main, depth 0): follow 1 link from Sub3 → Main.
Criticism: access to a deep nonlocal requires following many links and the access time varies with depth; this makes prediction hard in time-sensitive code. The alternative display method keeps a pointer for each depth in an array. In practice, since nesting rarely exceeds 3, the static chain is sufficient.
Blocks
Blocks like { int c, d; ... } can be implemented in two ways:
- Like subprograms called without parameters, always from the same place: an ARI at every entry.
- More efficient: the block’s variables are placed statically in the ARI of the enclosing subprogram. Sibling blocks cannot be active at the same time and share the same cells.
void f() {
int a, b;
{ int c, d; ... }
{ int e, f, g; ... }
}
local cells needed: 2 + max(2, 3) = 5
Implementing dynamic scope
Deep access: a nonlocal name is searched for in the dynamic chain from top to bottom. The ARIs must also hold variable names (which variable is in which ARI is not known at compile time). The chain length is uncertain, access is slow.
If the program above had dynamic scope, r in Sub2: not in Sub2, not in Sub1, found in Sub3 → Sub3.r. In static scope it is Main.r. s, however, is Sub1.s under both rules (Sub1 is both the static parent and the caller).
Shallow access: a separate stack (or a central table) is kept for each variable name; the topmost one is the active one. Access is fast, but calls and returns are expensive (push/pop for each local).
Common mistakes
| Wrong | Correct |
|---|---|
| The static link points to the caller | To the static parent (the enclosing one in the text) |
| The dynamic link points to the static parent | To the caller |
| chain_offset = the difference of ARIs on the stack | The difference of static depths |
| For sibling blocks, 2 + 3 cells are added | They share: max |
| 4 ARIs on the stack for fact(4) | 5 including main |
| Recursion can be done with static locals | It cannot; a single ARI |
| Deep access follows the static chain | It follows the dynamic chain |
| In recursion exits are in the same order as entries | In reverse order (LIFO) |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
List the typical contents of an activation record for a subprogram in a language with nested subprograms (e.g. Ada, JavaScript).
Show answer
Return address: where execution resumes after the return. Static link: the ARI of the static parent (for nonlocal access). Dynamic link: the top of the caller's ARI (to unwind the stack on return). Parameters and locals. For a function there may also be space for the return value.
Use the program below. Main calls Sub3, Sub3 calls Sub1, Sub1 calls Sub2. What are the static depths of Main, Sub1, Sub2 and Sub3?
Main declares p, q, r
Sub1 (nested in Main) declares q, s
Sub2 (nested in Sub1) declares t, p
Sub3 (nested in Main) declares r, sShow answer
Static depth shows how many levels inside the outermost scope a unit is. Main is 0. Those inside Main (Sub1, Sub3) are 1. Sub2 inside Sub1 is 2. The call order does not affect static depth.
Same program. Each ARI has offset 0 = return address, 1 = static link, 2 = dynamic link, and locals from offset 3 in declaration order (Main too). Give the (chain_offset, local_offset) pair for each reference to t, q and r inside Sub2.
Show answer
chain_offset = the static depth of the unit where the reference is made − the static depth of the unit where the variable is declared. t is in Sub2: 2 − 2 = 0, the first local of Sub2 → 3. q: not in Sub2, it is in Sub1 (Sub2's p does not hide q, the name is different): 2 − 1 = 1, the first local of Sub1 → 3. r: not in Sub1 either, it is in Main: 2 − 0 = 2, the third local of Main → 5. (The p in Sub2 is (0, 4); it hides Main's p.)
Same program and call sequence. When Sub2 is executing, which ARI does the static link of Sub1's ARI point to, and which does its dynamic link point to?
Show answer
The static link points to the static parent: Sub1 is written inside Main in the text → Main's ARI. The dynamic link points to the caller: Sub3 called Sub1 → Sub3's ARI. The stack from bottom to top: Main, Sub3, Sub1, Sub2. Sub2's static link points to Sub1, and Sub3's static link points to Main.
Same program and call sequence, but suppose the language used dynamic scoping implemented with deep access. Which declaration does a reference to r inside Sub2 find? Under static scoping?
Show answer
Deep access scans the dynamic chain from top to bottom: there is no r in Sub2, none in Sub1, there is one in Sub3 → Sub3.r. Static scope follows the static chain: Sub2 → Sub1 → Main, Main.r. Sub3 is not a static ancestor of Sub2.
What is printed in Java?
static int f(int n) {
System.out.print("in" + n + " ");
int r = (n <= 1) ? 1 : n * f(n - 1);
System.out.print("out" + n + " ");
return r;
}
// main: System.out.println(f(3));Show answer
Each call puts a new ARI on the stack (with its own n and r). First all the entries: f(3), f(2), f(1). f(1) is the base case and returns; then the ARIs are removed in reverse order: out1, out2, out3. The result is 3 · 2 · 1 = 6.
With fact(n) { if (n <= 1) return 1; return n * fact(n - 1); }, what is the maximum number of ARIs on the run-time stack during fact(4) called from main (count main's ARI too)?
Show answer
main + fact(4) + fact(3) + fact(2) + fact(1) = 5. While fact(1) is running they are all on the stack, because none has returned yet (the multiplication is done after the return). That is why recursion requires stack-dynamic locals: every ARI has its own n.
How many calls to fib are made in total when computing fib(4) with fib(n) = n < 2 ? n : fib(n-1) + fib(n-2)?
Show answer
fib(4) → fib(3), fib(2). fib(3) → fib(2), fib(1). Each fib(2) → fib(1), fib(0). Count: fib(4) 1, fib(3) 1, fib(2) 2, fib(1) 3, fib(0) 2 → 9. Each call creates an ARI and removes it when it returns; the stack depth is at most 4 fib + main.
A function declares int a, b; and contains two sibling blocks { int c, d; ... } and { int e, f, g; ... }. If blocks are implemented by static allocation inside the function's ARI, how many local cells does the ARI need?
Show answer
Sibling blocks cannot be active at the same time, so they can share their space. a, b are always there: 2. For the blocks the largest: max(2, 3) = 3. Total 5 (not 7). The block's variables are accessed with a fixed offset; no separate ARI is created when entering the block.
In the static chain method, how is a nonlocal variable accessed at run time, given its (chain_offset, local_offset) pair?
Show answer
chain_offset says how many static links to follow, and local_offset says the position inside the target ARI. Both are computed at compile time. The cost: following more than one link for far (deep) nonlocals; the access time depends on the depth.
Compare the dynamic link and the static link: what does each point to and what is each used for?
Show answer
The chain formed by dynamic links (the dynamic chain) is the call history; dynamic scope also works over it. The chain of static links (the static chain) reflects the nesting in the text; nonlocal access in static scope is done with it. If the static parent called a subprogram, both point to the same ARI.
Why can't a language with only static local variables (like early Fortran) support recursion?
Show answer
With static locals, the ARI is allocated once at compile time, in a fixed place. In recursion, several activations of the same subprogram are alive at the same time, and each must have its own variables. This is possible only with a run-time stack that creates a new ARI at each call.
Verification: The static depths, the (chain_offset, local_offset) pairs, the static and dynamic link of every ARI on the stack in the call Main → Sub3 → Sub1 → Sub2, and the deep access results were computed in node from the nested unit model (verify/SEN2001/09.mjs). The entry/exit order of the recursive function was run with the JDK (java verify/SEN2001/W09.java); the maximum number of ARIs and the number of fib calls were counted with node.