Activation Records

Intro

Some people have had a little trouble with activation records. Since we haven't had much time to digest the information, and since the midterm is coming up, I've written some quick notes that I hope will help.

Terminology (from the book)

A block is a region of program text, delimited by begin and end markers.

The scope of a variable is the region of text in which the binding of that variable is visible.

The lifetime of a variable is the amount of time that the variable occupies memory during program execution.

Activation records keep track of values as a program executes. More specificly, an activation record has a set of names that are bound to values. We link activation records together in two ways:

A control link from record A points to the previous record on the stack. The chain of control links traces the dynamic execution of the program.

An access link from record A points to the record of the closest enclosing block in the program. The chain of access links traces the static structure (think: scopes) of the program.

A closure is an entity that contains information about a function. Specifically, it is a pair that contains a pointer to the function's closest enclosing block (activation record) and a pointer to the function's code. Think of a closure as a "sleeping" function. When we define a function, we just make a closure. When we call the function, we "wake up" its closure, which then tells us what we need to do to execute the function.

Creating Activation Records

This section contains a few simple guidelines for working with activation records. Follow these guidelines as you work on examples, and you should begin to understand this topic pretty quickly.

Essentially, three actions that occur in a program can cause us to create an activation record:

Every time we create an activation record, we must set the control and access links, and populate the bindings with values. Let's see how to do that for each of the above actions.

Function definition:
Control Link: The previous record on the stack
Access Link: The previous record on the stack
Bindings:
Variable definition:
Control Link: The previous record on the stack
Access Link: The previous record on the stack
Bindings:
Function application:
Control Link: The record that called the function
Access Link: The record pointed to by the function's closure
Bindings:

Study these "rules" for creating activation records, and understand them. We will refer to them as we work through an example in the next section.

Example

OK, let's see what this really means by walking through a simple example. Here is the key for the notation I will use:

   (1)    Line number 1 
   [1]    Activation record 1
   [A]    Closure A
{expr}    A bit of code

Example code

 (1)   fun decr n = n - 1;
 (2)   fun add1 a = 
 (3)      let
 (4)         fun add2 b = a + b
 (5)      in
 (6)	     add2
 (7)      end
 (8)   val x = 1
 (9)   val addx = add1 x
(10)   val y = addx (decr x)
If you type this (without the line numbers) into an ML interpreter, you'll get a value of 1 for y. Let's see if we can reproduce that result by modeling the computation with activation records.

The first line

Line (1) is a function definition, so we need to create an activation frame. Looking at our rules above, we know we need a control link, an access link, and a binding for the name decr to a closure (let's call it [A]) for the function.

[1] {(1) fun decr n = n - 1;}

    Control: [0]
     Access: [0]
       decr: [A]

Let's go through this step by step. The first part:

[1] {( 1) fun decr n = n - 1;}
indicates that this is activation record [1], which was created in response to the code in line (1). This part isn't really needed for an activation record. The number [1] gives us a name for the record, to which other records can refer. The code snippet is to remind us what part of the program corresponds to this activation record.

The control and access links are set to the previous frame on the stack (in this case, the non-existent [0], because there is no previous frame):

    Control: [0]
     Access: [0]

The last part:

      decr: [A]
indicates that this record binds the name decr to the closure [A]. Remember that when we define a function, we just bind its name to a closure. We do not do anything with the body of the function. That happens only when we wake up the closure to call the function.

The only thing left to do for line (1) is to define closure [A] (Notice that the access link for the closure points to record [1].):

[A]   ([1], code for decr @ line 1)

One more thing: When I make a closure, I usually like to draw a template for the activation record that will be created when we wake up the closure to call the function:

[A]   ([1], code for decr @ line 1) 
[???] {???}

    Control: ???
     Access: [1]
          n: ???
The access link for the woken-up frame will always be [1], but the values for the control link and n will be bound when the closure gets woken up at function-call time. This visualization also emphasises the idea of a closure as a "sleeping function".

To recap, line 1 causes us to write:

[1] {(1) fun decr n = n - 1;}

    Control: [0]
     Access: [0]
       decr: [A]
[A]   ([1], code for decr @ line 1) 
[???] {???}

   Control: ???
     Access: [1]
          n: ???

The rest of the program

Line (2) is also a function definition, so we need to create an activation frame and a closure for the function:

[2] {(2) fun add1 a = ...}

    Control: [1]
     Access: [1]
       add1: [B]
[B]   ([2], code for add1 @ line 2) 
[???] {???}

    Control: ???
     Access: [2]
          a: ???
       add2: ???
The access and control links for the new record ([2]) just point to the previous record on the stack ([1]). Closure [B] references [2]. One interesting thing to look at is the template record inside closure [B]. Notice this follows the rules for a function application activation record. We need a control and access link and bindings for the function's formal parameter (a). We also need a binding for the local variable add2 created inside add1. Notice that--when we create [B]--we don't do anything with the definition of add2 (i.e., we're not making a closure for add2 now). That's because add2's definition is inside add1 and will be evaluated only when add1 is called. We'll see how that works below. For now, we have completed the definition of add1, and we can skip down to line (8).

Line (8) is a variable definition, so we need to create an activation record. Before we do that, however, we need to evaluate the right-hand side of the assignment, so we can bind x to that value. For line (8), the evaluation is simple: it's just the integer 1. Now we can create an activation record and bind the name x to 1:

[3] {(8) val x = 1}

    Control: [2]
     Access: [2]
          x: 1

Line (9) is another variable definiton. First, we evaluate the right-hand side of the assignment. To do this, we need to call a function, which means (you guessed it!) we have to make an activation record. To figure out how to create the new record, we have to look up the value add1 by following access links from [3] until we find a binding for the function. This binding exists in [2], and the value of add1 is the closure [B]. Now we can look in [B] at our template for add1, and use the template to instantiate the activation record for this function call:

[4] {(9) ... add1 x}

    Control: [3]
     Access: [2]
          a: 1
       add2: [C]
[C]   ([4], code for add2 @ line 4) 
[???] {???}

    Control: ???
     Access: [4]
          b: ???
The values for the access and control links are easy to determine from our rules above. For the value of a, we must look up the value of x, which we find to be 1. Function add1 defines a local value add2 in line (4). This value is a function, so we create a closure ([C]) for it.

Now that we have created the activation record for the call to add1, we can evaluate the body of the function, and bind its value to addx in a new activation record. The body of add1 simply returns the value of add2, which is [C]. So, we can create an activation record for line (9):

[5] {(9) val addx = add1 x}

    Control: [3]
     Access: [3]
       addx: [C]
Notice that the control and access links refer to [3], because [3] is the most recent record on the stack.

Now for the final line. We want to bind a value for y, but to do so, must call addx. And to do that, we have to call decr. We follow the access links to find that decr is bound to [A] in [1]. We look inside the closure, and instantiate the record activation template:

[6] {(10) ... (decr x)}

    Control: [5]
     Access: [1]
          n: 1

We now evaluate the body of decr, and get a value of 1-1=0. We use this value in the call to addx. Waking up the closure [C], to which addx is bound and plugging in 0 for formal parameter b, we get:

[7] {(10) ... addx (decr x)}

    Control: [5]
     Access: [4]
          b: 0

Finally, we can evaluate the body of [C], to get 0+1=1, which we can bind to y in our final, variable definition activation record:

[8] {(10) val y = addx (decr x)}

    Control: [5]
     Access: [5]
          y: 1

Last Thoughts

These were mostly conceptual ideas to get you ready for the midterm. After reading it, you should be able to work through examples on your own reasonably well. The visual notation I used here is not the same as the one used in class. You should translate this notation to the one we used in class (i.e., with arrows, etc.) when you study for and take the midterm. I'll leave you with a bonus question:

Assuming the current activation record is [8], which (if any) of the above structures can be garbage collected? Answer: [6], [7].