Notes on Nested Functions and Non-local References

These notes are divided into four sections. The first section describes three approaches to resolving non-local references within functions, dynamic binding, shallow binding and deep binding. The second section looks at how a compiler can implement deep binding when there are nested functions. The third section is about nested functions being passed as parameters to other nested functions. The last section is about nested functions as return values from other nested functions.




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

The three approaches to resolving non-local variables can be summarized as:
1.) At compile time, use lexical scoping.
2.) At compile time, think of a call to a function that makes a non-local reference as a "macro expansion" into the body of the calling function and then use lexical scoping with the modified caller.
3.) At run time, search the chain of activation records (e.g., runtime stack) for the most recent instance of the non-local variable.

The names commonly given to these three approaches are:
1.) deep binding (which is really lexical scoping, a form of static scoping)
2.) shallow binding (which is really another form of static scoping)
3.) dynamic binding

Note: References to variables are not bindings, references to variables make use of bindings. That is, to resolve a variable reference, a particular binding (between a variable name and a memory location) must be chosen. So the names "deep binding", "shallow binding" and "dynamic binding" are unfortunate. These are strategies for *choosing* bindings, not for making bindings.


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

   int x = 10;      // global variable

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

   functionB()
   {
      int x = 100;
      functionA(); // shallow binding and dynamic binding
   }               // will both use the x=100 binding

   functionC()
   {
      int x = 1000;
      functionD(); // What if we think of this call as a "macro expansion"?
   }

   functionD()
   {
      functionA(); // dynamic binding will use the x=1000 binding;
   }               // shallow binding will use x=10 binding

   main()
   {
      functionB();
      functionC();
   }

If we use deep binding, the output from this program is the numbers 10, 10.
If we use shallow binding, the output from this program is the numbers 100, 10.
If we use dynamic binding, the output from this program is the numbers 100, 1000.

NOTE: If we think of ALL function calls as a kind of "macro expansion" into the body of the calling function, and if we always use static scoping in the resulting text, we get the equivalent of dynamic binding. You can get s sense of this by "macro expanding" the function call functionD() above.



B.) How a compiler can implement deep binding when there are nested functions.

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


Suppose from now on that we always use deep binding.

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.


           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 "deep binding" by putting in each activation record a "nesting link" that points to the most recent activation record of a function's parent function.


For any given nested function we can ask
1.) What functions can that function call (i.e., which other functions are in the scope of the given function)?
2.) How do we initialize the nesting link in the activation record of each function that the given function can call?
3.) 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)?
4.) For each non-local reference that the function makes, how does the compiler use the nesting links to access the non-local reference?

Q: What other functions can a function call?

A: There are three answers:
1.) Any child function.
2.) Any sibling function (including itself).
3.) Any ancestor function AND the siblings of each ancestor function.

Q: 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?

A: 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 the address of the most recent activation record of the ancestor's parent, and then sets the callee's nesting link to that address.

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

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

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

A: 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 emit code that follows (de references) that many nesting links, starting from the caller's nesting link, to get the address of the most recent activation record of the ancestor function containing the non-local reference. The non-local variable will be at a known offset (that is, known at compile time) within this activation record.

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

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 activation record (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 activation record 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 records, 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 activation record 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.