Notes on Nested Functions and Non-local References

These notes are divided into four sections. The first section describes two approaches to resolving non-local references within functions, "dynamic scope" and "static scope". The second section looks at how an interpreter can implement static scope when there are nested functions (by using nesting links). The third section is about nested functions being passed as parameters to other nested functions (and the need for closures). The last section is about nested functions as return values from other nested functions (and the need for heap allocated activation records).




A.) Two approaches to resolving non-local references in functions.

The two approaches to resolving non-local variables can be summarized as:
1.) Use lexical scope.
2.) Use dynamic scope. That is, at run time, search the chain of environments for the most recent instance of the non-local variable.


Below is an example of a program that contains two functions, one of which, functionA(), makes a non-local reference. Analyze the function call made in the main() function using each of the two scope strategies for non-local references.

   int x = 10;      // global variable

   functionA()
   {
      print(x);     // non-local reference
   }

   functionB()
   {
      int x = 100;  // local variable
      functionA();
   }

   main()
   {
      functionB();
   }

If we use static scope, the output from this program is the number 10.
If we use dynamic scope, the output from this program is the number 100.



B.) How an compiler (or interpreter) can implement static scope when there are nested functions.

Note: If you do not have nested functions, as in C/C++ and Java, then, when using static scope, all non-local references are to "global" variables, and the compiler always knows how to find global variables.


Suppose from now on that we always use static scope.

The nested functions in a program can be organized into a tree, where each node of the tree represents a function that is nested in its parent node AND the nested function's local variables. We can also visualize these functions (and their scopes) as nested boxes. So f1 has three functions nested directly in it (and possibly some local variables), f2 has two functions nested directly within it, and f3 has no nested functions (but may have local variables).


           f1            +------------------f1-----------------------------+
         / | \           |                                                 |
       /   |   \         | +---------f2--------+                           |
     f2    f3   f4       | |                   | +----------f4-----------+ |
    /  \        |        | | +----f5--+ +-f6-+ | |                       | |
  /      \      |        | | |        | |    | | | +--------f7---------+ | |
 f5      f6     f7       | | | +-f8-+ | |    | | | |                   | | |
 |            / | \      | | | |    | | +----+ | | | +--f9---+ +-f10-+ | | |
 |           /  |  \     | | | |    | |        | | | |       | |     | | | |
 f8         f9 f10  f11  | | | +----+ |        | | | | +f12+ | |     | | | |
            |            | | |        |        | | | | |   | | +-----+ | | |
            |            | | +--------+        | | | | |   | |         | | |
           f12           | +-------------------+ | | | +---+ | +-f11-+ | | |
                         |                       | | |       | |     | | | |
                         |       +-f3-+          | | +-------+ |     | | | |
                         |       |    |          | |           +-----+ | | |
                         |       |    |          | +-------------------+ | |
                         |       +----+          +-----------------------+ |
                         |                                                 |
                         +-------------------------------------------------+


We implement static scope by putting in each activation record a "nesting link" that will point to the most recent activation record of a function's parent function.


For any given nested function we can ask:
Q1.) What functions can that function call (i.e., which other functions are in the scope of the given function)?
Q2.) How do we initialize the nesting link in an activation record of each function that the given function can call?
Q3.) What non-local references can the function potentially make (i.e., which other function's local variables are in the scope of the given function)?
Q4.) For each non-local reference that the function can make, how does the interpreter use the nesting links to access the non-local reference?


Q1: What other functions can a function call?

A1: There are four answers:
1.) Any child function (but not the descendents of the child functions).
2.) Any sibling function (including itself).
3.) Any ancestor function.
4.) The siblings of each ancestor function (but not the children of those ssiblings).


Q2: For each kind of function that can be called, how does the compiler set the nesting link in the activation record of the called function?

A2: The three cases are
1.) When calling a child function, the nesting link in the activation record of the callee points to the activation record of the caller.
2.) When calling a sibling function (or when calling itself), the nesting link in the activation record of the callee is equal to the nesting link from the activation record of the caller.
3.) When calling an ancestor function, or the sibling of an ancestor function, the compiler has to count how many levels up the tree it is from the caller to the parent function of the given ancestor function. Then the compiler will emit code that follows (de-references) that many nesting links, starting from the caller's nesting link, to get to the most recent activation record of the ancestor's parent, and then sets the callee's nesting link to that address.


Q3: What non-local references can a nested function potentially make?

A3: A nested function can make non-local references to the local variables of its ancestor functions.


Q4: For each non-local reference that a nested function can make, how does the compiler use the nesting links to access the non-local variable?

A4: Pretty much the same way that it created the nesting links when a nested function calls an ancestor function. The compiler has to count how many levels up the tree it is from the nested function to the given ancestor function. Then the compiler will need to emit code follows (de-references) that many nesting links, starting from the caller's nesting link, to get to the most recent activation record of the ancestor function containing the non-local reference.


Problem: The above rules for creating nesting links need not be optimal. For example, if function f9 above contains non-local references to local variables in f1, and contains no other non-local references, then it would be reasonable for the compiler to optimize the nesting link in an activation record for f9 by making the nesting link point to the most recent activation record of f1 (instead of f7). With this optimization, each non-local reference from within f9 would need only one pointer de-reference instead of three,
a.) What could stop the compiler from being able to make this optimization? (Hint: It has something to do with f12.)
b.) Based on your answer to part (a), come up with a single rule for creating nesting links that replaces the three rules given above and always creates optimal nesting links.



C.) Nested functions as parameters to other nested functions.

Let us consider nested functions that call other nested functions and pass to them references to other nested functions which make non-local references (if they are ever called). For example, using the functions described by the tree above, function f5 could call f1 and pass to f1 a reference to f6. Then f1 might call f6, or f1 could call f4 and pass it its reference to f6. Then f4 might call f7 and pass to it the reference to f6, and then f7 might call f6! Clearly, if f7 were to call f6, the above rules for setting nesting links are of no use. In fact, wherever a function calls another function by using a "functional parameter", there is no way that the compiler can know at compile time what the value of that parameter will be (that is, who the "passee" is), so without further information, there is no way that the compiler can set the nesting link for the "passee" when (and if) it is called. So we need another idea.

Note: When a function has a parameter that represents a function, we will call that a "functional parameter". The actual value passed in a functional parameter will be call the "passee function". So a caller function calls a callee function and passes, through a functional parameter in the callee, a reference to the passee function.

Notice that for any nested function, the set of potential passee functions is the same as the set of potential callee functions, that is, any function name that is in scope. So a nested function can either call or pass as a parameter (or both) any of its children, itself, its siblings, its ancestors and its ancestor's siblings.

When a function calls a callee, and attempts to pass a reference to a passee, we already know that the compiler knows how to determine the nesting link for the callee (and the compiler emits the code that computes and then sets the value of the callee's nesting link in the callee's environment). The compiler needs to do, more or less, the same thing for the passee. The compiler knows how to emit code that computes the nesting link for the passee. But there is no environment yet for the passee (it is not being called). What does the compiler do with the nesting link? What the compiler should do is create what we will call a "function object" which is an object that stores two values, a reference to a function and the value of that function's nesting link. The function object is then the value that is actually passed to the callee. (These "function objects" are often called "closures".)

When (and if) the callee decides to call the passee, the compiler notices that the function being called is really a function object. Instead of emitting code that computes the nesting link, the compiler emits code that extracts the nesting link out of the function object and then uses that value to initialize the nesting link in the new activation record that is being build for the function call. Notice that, at compile time, the compiler has no idea what the value for the code pointer is in the function object. This means that the compiler does not know for which function it is creating an activation record. This means that the compiler does not know how large the activation record needs to be (if it doesn't know which function it is calling, it doesn't know how many local variables there are (but the compiler does know how many parameters there are (why?))). One solution for this is to include in the function object the size of the needed environment (in fact, there may be other things that need to go in a function object).

Notice that this scheme means that there are two kinds of function calls, "regular" ones and function calls where the name used in the function call comes from a functional parameter. The compiler can tell the difference by looking up attributes of the identifier used in the function call. The compiler maintains a symbol table which stores attributes for all of a program's identifiers. When the compiler sees an identifier used in a function call, the compiler looks in the symbol table to see if the identifier has the attribute of a function object or the attribute of a function definition. For a regular function call (the attributes of the identifier are the attributes of a function definition), the compiler emits code that computes the value of the nesting link at runtime (and the compiler gets all the information that it needs for the environment from the text of the function definition). For a functional parameter function call. the compiler emits code that extracts the nesting link from the function object. The compiler may also need to emit code that extracts other needed information from the functional object (such as activation record size). In some languages (i.e., interpreted languages) the "function pointer" part of the function object may actually be a text string containing the actual code of the function (or it could be a link to the abstract syntax tree of a partially compiled function).


D.) Nested functions as return values from other nested functions.

The most important part of understanding "functions as return values" is to step back and look again at the idea of an activation record. We need to realize that "activation record" is not synonymous with "stack frame". That is, there are other ways of creating and storing activation records besides (the very common) way of using a stack. A second important idea is the notion of automatic garbage collection of dynamically allocated heap memory.

When a function is called, an activation record needs to be created in order to store the values of the parameters and the local variables (among a few other things). Instead of keeping a stack of these activation record, we could think of them as being nodes in a dynamically allocated linked list. So when the compiler needs to generate code for a function call, instead of issuing code to decrement (or increment, whichever the case may be) a stack pointer register by whatever amount is needed to make room for the activation record, the compiler could issue code that calls a library function that dynamically allocates, from a heap, enough memory to store the activation record. The compiler would then issue code to link this new "activation record node object" into a linked list of active activation records (if you want, you can think of the activation record as a "heap frame" instead of a "stack frame"). And instead of a stack pointer register that points to the current activation record, the compiler has to set up a runtime variable that keeps track of the head node of the linked list of activation records. When a function returns, instead of popping a stack frame off the stack by restoring the stack pointer (one of the things in a stack frame is the old value of the stack pointer), the runtime would execute code that de-allocates the first node from the linked list of activation records. (We will assume that the runtime just unlinks the head node from the linked list and then lets a garbage collector do the actual de-allocation.) Allocating and de-allocating memory from a heap is a lot more work than incrementing and decrementing a stack pointer, but we need this extra work in order to allow functions to return functions (that make non-local references).

So now let us assume that all of our activation records are in a dynamically allocated (and automatically garbage collected) linked list of "heap frames". In every other way, the activation records will be the same as the previous "stack frames". In particular, we will use the same rules for creating nesting links. So a nesting link is a link from one node in the chain of activation records to another node in the chain of activation records and the nesting links are in addition to the links that are used to maintain the linked list data structure.


Problem: Using the functions described by the tree above, sketch a picture of the linked list of activation records, including nesting links, when function f1 calls f2 which calls f5 and then function f5 calls f1 and passes to f1 a reference to f6 and then f1 calls f4 and passes to it its reference to f6, and then f4 calls f7 and passes to it the reference to f6, and then f7 calls f6.


Now let us look at a simple example of a function that returns another function as a value. Suppose that, using the functions from the tree above, that f1 calls f2 and f2 returns to f1 a reference to function f6, f1 calls f6, and f6 contains a non-local reference to a variable in f2. Let us step through these function calls, one by one, keeping track of activation records and nesting links. When f1 is called, an activation record is allocated at the front of the list of activation records. The nesting link in this activation record is null, since f1 is a top level function. When f1 calls f2, another activation record is allocated at the front of the list. The nesting link points to the activation record for f1 (so the activation record at the front of the linked list contains two pointers to the activation record for f1, one being the nesting link, the other being the linked list data structure's next node link). When f2 executes, it creates a function object that it can use as a return value to f1. The function object contains two pointers, one to the code for f6, the other pointer being the nesting link for f6, which is a pointer to the activation record for f2. When f2 returns, its activation record is unlinked from the front of the linked list of activation records. But the environment for f1 will store a reference to the function object that is the return value from f2. So inside f1's activation record is a pointer to a function object that contains a pointer to f2's previous activation record. So this previous activation record in not garbage and cannot be de-allocated by the garbage collector. (DRAW A PICTURE OF THE ACTIVATION RECORDS AND THEIR INTERCONNECTING LINKS!). Now f1 calls f6 by using the function object. This creates a new activation record at the front of the linked list and the nesting link points to (...what?, think hard, don't peek ahead). The activation record uses the function object to initialize its nesting link, and as we mentioned, the function object points to the old activation record from the call to f2 and this old activation record is still around because it can't be garbage collected. (DRAW ANOTHER PICTURE OF THE ACTIVATION RECORDS AND THEIR INTERCONNECTING LINKS!) When f6 executes it now has access to the non-local variables that it needs inside the old activation record from the (long gone) call to f2. When f6 returns, its activation record is unlinked from the front of the linked list. When f1 decides that it no longer needs to make any more calls to f6, f1 can set its reference to the function object to null. This will make the function object get garbage collected. At that point, f2's old activation record looses its claim to be active memory and it also (finally) gets garbage collected.



Problem: Using the functions described by the tree above, is the following scenario possible? If so, draw a picture of the activation records, including nesting links. If not, explain why. Function f1 calls f2 which returns to f1 a reference to function f8, which contains a non-local reference to a variable in f5, and then f1 calls f8 using the return value.