Reading

Section 3.3 of Programming Language Pragmatics

Homework

NO HW.

Single Global Scope (review)

We discussed this in detail in our previous class. This is what we might expect from, for example, a simple calculator program. From an implementation side, this means that we keep a single global symbol table --- i.e. a structure that maps names to values.

Dynamic Scope - Central Reference Table (review)

We discussed this in detail in our previous class and you are implemented (a slightly simplified version) of this for lab. From the semantics side, dynamic scope means that a non-local reference of a name 'x' in function 'f' refers to the binding of 'x' that was in scope when the function 'f' was called. From the implementation side, this means that we keep track of bindings in a single global structure that maps names to stacks of values. This data strucure is called a central reference table. This is actually a very simple mechanism, from an implentation perspective, which is nice. The only complexifying issue is that we need to track when variables come to the end of their scope so that we know to pop them off the stack. This is the thing I did not ask you to do in lab. It requires a step in between constructing the AST and actually eval/exec-ing the AST. This "analysis" stage traverses the AST finding "new" statments with a parent block node, and adding a new "endscope" node to the end of the block that reminds the block which names to pop. I decided this was a bit more work than I wanted for the lab, but maybe it's too bad we didn't do it. First off, then we'd really have a full dynamic scope implementation ... which is cool. Second of all, the idea of analysing code by traversing the AST is really important!

Lexical Scope - (review)

The semantics of lexical scope are that non-local references resolve to the binding that was in scope at the point at which the function was defined (as opposed to when the function was called). Note that the languages you're used to all use lexical (aka static) scoping. So how about implementation? Well ... that's tricky. We'll look at several different scenarios

Lexical Scope - Locals and Globals only

Recall that Standard C allows only local scope, i.e. the local variables in a function, and global scope, i.e. one global symbol table. In other words, the only non-local references that are allowed are global variables. Pragmatically, this means that you cannot define functions within other functions (this is often referred to as "nested functions"). By limiting functions to being defined only in the global scope, C ensures that non-local references can only refer to global variables under the lexical scoping rules. So presto - local and global scope only! But how to implement this?

Easy ... this is the good old call stack you know and love! Each function call results in a new record being pushed to the call stack. Each record on the stack is its own scope - basically its own symbol table. Give a name, either that name is a local binding, i.e. you get the value from the mini-symbol table that is the function call stack record, or it is global. Those are the only options. We can see this in action with the following program:

int y = 10;

int f(int x) { return x + y; }

int g(int z) {
  int x = 5;
  return f(x+1);
}

int main() {
  g(2);
}

Lexical Scope - nested functions

The good old call stack as per the previous section is not quite enough when we allow nested functions, i.e. functions defined within functions. Pascal, for example, allows this, as does gcc's default (and non-standard!) C complier. With nested functions we may have non-local references that are not global. Concretely, consider the following modification of the program from the previous section, where we've nested the defintion of f within the definition of g, and we've changed f to return x + y + z.
int y = 10;

int g(int z) {
  int x = 5;
  int f(int x) { return x + y + z; }
  return f(x+1);
}

int main() {
  g(2);
}
The interesting part to this is that the "z" referred to in f's return statement is not a local binding, nor is it a global binding. The semantics of lexical scope are that we must use the binding that is in scope when f is defined, and f is defined inside g. This means the z binding we are referring to is the parameter to the function g. So our good old call stack doesn't work, since we won't find the binding for z in f's call stack record nor in the global symbol table. We need a little more.

The way lexical scoping is usually handled when nested function definitions are allowed is that the call stack record for a function with a defintion that is nested within another function (like f is nested in g in the above example) is augmented with a pointer to the stack record for the function call in which it was defined. So at the point at which x + z + z is evaluated the picture looks like this:

So now when we evaluate x + y + z, the x is local (i.e. we find it in the call stack record for f), the z is not local, but we follow the parent pointer down and find it in the call stack record for the parent (i.e. g), and y is not local, nor do we find it in the parent, so it must be global --- and we find it in the global symbol table.

So life is good! We can implement lexical scope with nested scopes - which are really functions that get created at runtime, which is cool! However, true first class functions would mean also being able to return the function f from g. Note, not returning the result of calling f, rather actually returning the function f. And what we'll see next class is that this causes serious trouble! So we're not done with lexical scope yet!


Christopher W Brown