SEN2001 · Week 9 · Subprograms II

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.

call semanticsreturn semanticsactivation recordactivation record instancerun-time stackreturn addressdynamic linkstatic linkdynamic chainstatic chainstatic depthchain offsetlocal offsetenvironment pointerrecursionblockdeep accessshallow access

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:

  1. The dynamic link points to the caller, the static link to the static parent.
  2. The static parent is read from the text, not from the call order.
  3. chain_offset = depth of the place of reference − depth of the place of declaration.
  4. 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):

  1. Save the caller’s execution state.
  2. Allocate space for the local variables (if stack-dynamic).
  3. Pass the parameters (copy if value, address if reference).
  4. Save the return address.
  5. Set up the link needed for nonlocal access (static link).
  6. 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:

NameUnit where foundchain_offsetlocal_offsetPair
tSub22 − 2 = 03(0, 3)
pSub2 (hides Main.p)04(0, 4)
qSub12 − 1 = 13(1, 3)
sSub114(1, 4)
rMain2 − 0 = 25(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.

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:

  1. Like subprograms called without parameters, always from the same place: an ARI at every entry.
  2. 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

WrongCorrect
The static link points to the callerTo the static parent (the enclosing one in the text)
The dynamic link points to the static parentTo the caller
chain_offset = the difference of ARIs on the stackThe difference of static depths
For sibling blocks, 2 + 3 cells are addedThey share: max
4 ARIs on the stack for fact(4)5 including main
Recursion can be done with static localsIt cannot; a single ARI
Deep access follows the static chainIt follows the dynamic chain
In recursion exits are in the same order as entriesIn reverse order (LIFO)

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

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, static link, dynamic link, parameters, local variables

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.

Question 2easy

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, s
Show answer
Main 0, Sub1 1, Sub2 2, Sub3 1

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.

Question 3medium

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
t: (0, 3), q: (1, 3), r: (2, 5)

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.)

Question 4hard

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
static link: Main; dynamic link: Sub3

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.

Question 5hard

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
dynamic (deep access): Sub3's r; static: Main's r

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.

Question 6medium

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
in3 in2 in1 out1 out2 out3 then 6

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.

Question 7easy

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
5

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.

Question 8medium

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
9

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.

Question 9medium

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
5

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.

Question 10medium

In the static chain method, how is a nonlocal variable accessed at run time, given its (chain_offset, local_offset) pair?

Show answer
follow chain_offset static links starting from the current ARI, then add local_offset

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.

Question 11easy

Compare the dynamic link and the static link: what does each point to and what is each used for?

Show answer
dynamic link: caller's ARI, used to restore the stack on return; static link: static parent's ARI, used for nonlocal access

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.

Question 12medium

Why can't a language with only static local variables (like early Fortran) support recursion?

Show answer
each local has only one memory cell, so a recursive call would overwrite the caller's values

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.

Review card

at the call: create an ARI, pass the parameters, save the return address, transfer control at the return: copy out/inout values, carry the function value, unwind the stack, go to the return address ARI: return address | static link | dynamic link | parameters | locals static local (Fortran I): fixed ARI → NO recursion stack-dynamic: a new ARI at every call → recursion POSSIBLE dynamic link → CALLER. static link → STATIC PARENT (enclosing in the text) static depth: outermost = 0, +1 for each nesting chain_offset = depth(place of reference) - depth(place of declaration) access: follow chain_offset static links, add local_offset stack Main, Sub3, Sub1, Sub2 → Sub1's static link is Main (NOT Sub3) blocks: sibling blocks share space → space = outer locals + max(blocks) dynamic scope: deep access = scan the dynamic chain; shallow access = central table/stack for each name recursion: entry order 3,2,1 ; exit order 1,2,3