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 a interpreter can implement static scope 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.) 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.) At run time, search the chain of environmentss 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 a interpreter can implement static scope when there are nested functions.

Note: If you do not have nested functions, then, when using static sccope, all non-local references are to "global" variables, as in C/C++ and Java, and the interpreter 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.


           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 environment a "nesting link" that points to the most recent environment 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 the environment 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 makes, how does the interpreter use the nesting links to access the non-local reference?


Q1: 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.


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

A: The three cases are
1.) When calling a child function, the nesting link in the environment of the callee points to the environment of the caller.
2.) When calling a sibling function (or when calling itself), the nesting link in the environment of the callee is equal to the nesting link from the environment of the caller.
3.) When calling an ancestor function, or the sibling of an ancestor function, the interpreter 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 interpreter will follows (de references) that many nesting links, starting from the caller's nesting link, to get to the most recent environment 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?

A: 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 makes, how does the interpreter 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 interpreter has to count how many levels up the tree it is from the nested function to the given ancestor function. Then the interpreter will need to follows (de references) that many nesting links, starting from the caller's nesting link, to get to the most recent environment 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 interpreter to optimize the nesting link in an environment for f9 by making the nesting link point to the most recent environment 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 interpreter 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.

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 environment 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 environment. This means that the compiler does not know how large the environment 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 environment 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 environment. We need to realize that "environment" is not synonymous with "stack frame". That is, there are other ways of creating and storing environments 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 environment 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 environments, 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 environment, the compiler could issue code that calls a library function that dynamically allocates, from a heap, enough memory to store the environment. The compiler would then issue code to link this new "environment node object" into a linked list of active environments (if you want, you can think of the environment as a "heap frame" instead of a "stack frame"). And instead of a stack pointer register that points to the current environment, the compiler has to set up a runtime variable that keeps track of the head node of the linked list of environments. 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 environments. (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 environments are in a dynamically allocated (and automatically garbage collected) linked list of "heap frames". In every other way, the environments 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 environments to another node in the chain of environments 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 environments, 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 environments and nesting links. When f1 is called, an environment is allocated at the front of the list of environments. The nesting link in this environment is null, since f1 is a top level function. When f1 calls f2, another environment is allocated at the front of the list. The nesting link points to the environment for f1 (so the environment at the front of the linked list contains two pointers to the environment 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 environment for f2. When f2 returns, its environment is unlinked from the front of the linked list of environments. But the environment for f1 will store a reference to the function object that is the return value from f2. So inside f1's environment is a pointer to a function object that contains a pointer to f2's previous environment. So this previous environment in not garbage and cannot be de allocated by the garbage collector. (DRAW A PICTURE OF THE ENVIRONMENTS AND THEIR INTERCONNECTING LINKS!). Now f1 calls f6 by using the function object. This creates a new environment at the front of the linked list and the nesting link points to (...what?, think hard, don't peek ahead). The environment uses the function object to initialize its nesting link, and as we mentioned, the function object points to the old environment from the call to f2 and this old environment is still around because it can't be garbage collected. (DRAW ANOTHER PICTURE OF THE ENVIRONMENTS AND THEIR INTERCONNECTING LINKS!) When f6 executes it now has access to the non-local variables that it needs inside the old environment from the (long gone) call to f2. When f6 returns, its environment 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 environment 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 environments, 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.