PROFESSOR: Well, I hope you appreciate that we have
inducted you into some real magic, the magic of building
languages, really building new languages.
What have we looked at?
We've looked at an Escher picture language: this
language invented by Peter Henderson.
We looked at digital logic language.
Let's see.
We've looked at the query language.
And the thing you should realize is, even though these
were toy examples, they really are the kernels of really
useful things.
So, for instance, the Escher picture language was taken by
Henry Wu, who's a student at MIT, and developed into a real
language for laying out PC boards based just on extending
those structures.
And the digital logic language, Jerry mentioned when
he showed it to you, was really extended to be used as
the basis for a simulator that was used to
design a real computer.
And the query language, of course, is kind
of the germ of prologue.
So we built all of these languages, they're
all based on LISP.
A lot of people ask what particular problems is LISP
good for solving for?
The answer is LISP is not good for solving any particular
problems. What LISP is good for is constructing within it
the right language to solve the problems you want to
solve, and that's how you should think about it.
So all of these languages were based on LISP.
Now, what's LISP based on?
Where's that come from?
Well, we looked at that too.
We looked at the meta-circular evaluator and said well, LISP
is based on LISP.
And when we start looking at that, we've got to do some
real magic, right?
So what does that mean, right?
Why operators, and fixed points, and the idea that what
this means is that LISP is somehow the fixed-point
equation for this funny set of things which are defined in
terms of themselves.
Now, it's real magic.
Well, today, for a final piece of magic, we're going to make
all the magic go away.
We already know how to do that.
The idea is, we're going to take the register machine
architecture and show how to implement
LISP on terms of that.
And, remember, the idea of the register machine is that
there's a fixed and finite part of the machine.
There's a finite-state controller, which does some
particular thing with a particular amount of hardware.
There are particular data paths: the operation the
machine does.
And then, in order to implement recursion and
sustain the illusion of infinity, there's some large
amount of memory, which is the stack.
So, if we implement LISP in terms of a register machine,
then everything ought to become, at this point,
completely concrete.
All the magic should go away.
And, by the end of this talk, I want you get the feeling
that, as opposed to this very mysterious meta-circular
evaluator, that a LISP evaluator really is something
that's concrete enough that you can hold in the
palm of your hand.
You should be able to imagine holding a
LISP interpreter there.
All right, how are we going to do this?
We already have all the ingredients.
See, what you learned last time from Jerry is how to take
any particular couple of LISP procedures and hand-translate
them into something that runs on a register machine.
So, to implement all of LISP on a register machine, all we
have to do is take the particular procedures that are
the meta-circular evaluator and hand-translate them for a
register machine.
And that does all of LISP, right?
So, in principle, we already know how to do this.
And, indeed, it's going to be no different, in kind, from
translating, say, recursive factorial
or recursive Fibonacci.
It's just bigger and there's more of it.
So it'd just be more details, but nothing really
conceptually new.
All right, also, when we've done that, and the thing is
completely explicit, and we see how to implement LISP in
terms of the actual sequential register operations, that's
going to be our final most explicit model of
LISP in this course.
And, remember, that's a progression
through this course.
We started out with substitution, which is sort of
like algebra.
And then we went to the environment model, which
talked about the actual frames and how
they got linked together.
And then we made that more concrete in the
meta-circular evaluator.
There are things the meta-circular evaluator
doesn't tell us.
You should realize that.
For instance, it left unanswered the question of how
a procedure, like recursive factorial here , somehow takes
space that grows.
On the other hand, a procedure which also looks syntactically
recursive, called fact-iter, somehow doesn't take space.
We justify that it doesn't need to take space by showing
the substitution model.
But we didn't really say how it happens that the machine
manages to do that, that that has to do with the details of
how arguments are passed to procedures.
And that's the thing we didn't see in the meta-circular
evaluator precisely because the way arguments got passed
to procedures in this LISP depended on the way arguments
got passed to procedures in this LISP.
But, now, that's going to become extremely explicit.
OK.
Well, before going on to the evaluator, let me just give
you a sense of what a whole LISP system looks like so you
can see the parts we're going to talk about and the parts
we're not going to talk about.
Let's see, over here is a happy LISP user, and the LISP
user is talking to something called the reader.
The reader's job in life is to take characters from the user
and turn them into data structures in something called
a list structure memory.
All right, so the reader is going to take symbols,
parentheses, and A's and B's, and ones and threes that you
type in, and turn these into actual list structure: pairs,
and pointers, and things.
And so, by the time evaluator is going, there are no
characters in the world.
And, of course, in more modern list systems, there's sort of
a big morass here that might sit between the user and the
reader: Windows systems, and top levels, and mice, and all
kinds of things.
But conceptually, characters are coming in.
All right, the reader transforms these into pointers
to stuff in this memory, and that's what the
evaluator sees, OK?
The evaluator has a bunch of helpers.
It has all possible primitive operators you might want.
So there's a completely separate box, a floating point
unit, or all sorts of things, which do
the primitive operators.
And, if you want more special primitives, you build more
primitive operators, but they're
separate from the evaluator.
The evaluator finally gets an answer and communicates that
to the printer.
And now, the printer's job in life is to take this list
structure coming from the evaluator, and turn it back
into characters, and communicate them to the user
through whatever interface there is.
OK.
Well, today, what we're going to talk
about is this evaluator.
The primitive operators have nothing particular to do with
LISP, they're however you like to implement primitive
operations.
The reader and printer are actually complicated, but
we're not going to talk about them.
They sort of have to do with details of how you might build
up list structure from characters.
So that is a long story, but we're not going
to talk about it.
The list structure memory, we'll talk about next time.
So, pretty much, except for the details of reading and
printing, the only mystery that's going to be left after
you see the evaluator is how you build list structure on
conventional memories.
But we'll worry about that next time too.
OK.
Well, let's start talking about the evaluator.
The one that we're going to show you, of course, is not, I
think, nothing special about it.
It's just a particular register
machine that runs LISP.
And it has seven registers, and here
are the seven registers.
There's a register, called EXP, and its job is to hold
the expression to be evaluated.
And by that, I mean it's going to hold a pointer to someplace
in list structure memory that holds the
expression to be evaluated.
There's a register, called ENV, which holds the
environment in which this expression is to be evaluated.
And, again, I made a pointer.
The environment is some data structure.
There's a register, called FUN, which will hold the
procedure to be applied when you go to apply a procedure.
A register, called ARGL, which wants the list
of evaluated arguments.
What you can start seeing here is the basic
structure of the evaluator.
Remember how evaluators work.
There's a piece that takes expressions and environments,
and there's a piece that takes functions, or
procedures and arguments.
And going back and forth around here is
the eval/apply loop.
So those are the basic pieces of the eval and apply.
Then there's some other things, there's continue.
You just saw before how the continue register is used to
implement recursion and stack discipline.
There's a register that's going to hold the result of
some evaluation.
And then, besides that, there's one temporary
register, called UNEV, which typically, in the evaluator,
is going to be used to hold temporary pieces of the
expression you're working on, which you haven't gotten
around to evaluate yet, right?
So there's my machine: a seven-register machine.
And, of course, you might want to make a machine with a lot
more registers to get better performance, but this is just
a tiny, minimal one.
Well, how about the data paths?
This machine has a lot of special operations for LISP.
So, here are some typical data paths.
A typical one might be, oh, assign to the VAL register the
contents of the EXP register.
In terms of those diagrams you saw, that's a little button on
some arrow.
Here's a more complicated one.
It says branch, if the thing in the expression register is
a conditional to some label here, called the
ev-conditional.
And you can imagine this implemented in a lot of
different ways.
You might imagine this conditional test as a special
purpose sub-routine, and conditional might be
represented as some data abstraction that you don't
care about at this level of detail.
So that might be done as a sub-routine.
This might be a machine with hardware-types, and
conditional might be testing some bits for
a particular code.
There are all sorts of ways that's beneath the level of
abstraction we're looking at.
Another kind of operation, and there are a lot of different
operations assigned to EXP, the first clause
of what's in EXP.
This might be part of processing a conditional.
And, again, first clause is some selector whose details we
don't care about.
And you can, again, imagine that as a sub-routine which'll
do some list operations, or you can imagine that as
something that's built directly into hardware.
The reason I keep saying you can imagine it built directly
into hardware is even though there are a lot of operations,
there are still a fixed number of them.
I forget how many, maybe 150.
So, it's plausible to think of building these
directly into hardware.
Here's a more complicated one.
You can see this has to do with looking up
the values of variables.
It says assign to the VAL register the result of looking
up the variable value of some particular expression, which,
in this case, is supposed to be a variable in some
environment.
And this'll be some operation that searches through the
environment structure, however it is represented, and goes
and looks up that variable.
And, again, that's below the level of detail that we're
thinking about.
This has to do with the details of the data structures
for representing environments.
But, anyway, there is this fixed and finite number of
operations in the register machine.
Well, what's its overall structure?
Those are some typical operations.
Remember what we have to do, we have to take the
meta-circular evaluator--
and here's a piece of the meta-circular evaluator.
This is the one using abstract syntax that's in the book.
It's a little bit different from the one
that Jerry shows you.
And the main thing to remember about the evaluator is that
it's doing some sort of case analysis on the kinds of
expressions: so if it's either self-evaluated, or quoted, or
whatever else.
And then, in the general case where the expression it's
looking at is an application, there's some tricky
recursions going on.
First of all, eval has to call itself both to evaluate the
operator and to evaluate all the operands.
So there's this sort of red recursion of values walking
down the tree that's really the easy recursion.
That's just a val walking down this tree of expressions.
Then, in the evaluator, there's a hard recursion.
There's the red to green.
Eval calls apply.
That's the case where evaluating a procedure or
argument reduces to applying the procedure
to the list of arguments.
And then, apply comes over here.
Apply takes a procedure and arguments and, in the general
case where there's a compound procedure, apply goes around
and green calls red.
Apply comes around and calls eval again.
Eval's the body of the procedure in the result of
extending the environment with the parameters of the
procedure by binding the arguments.
Except in the primitive case, where it just calls something
else primitive-apply, which is not really the
business of the evaluator.
So this sort of red to green, to red to green, that's the
eval/apply loop, and that's the thing that we're going to
want to see in the evaluator.
All right.
Well, it won't surprise you at all that the two big pieces of
this evaluator correspond to eval and apply.
There's a piece called eval-dispatch, and a piece
called apply-dispatch.
And, before we get into the details of the code, the way
to understand this is to think, again, in terms of
these pieces of the evaluator having contracts with the rest
of the world.
What do they do from the outside before getting into
the grungy details?
Well, the contract for eval-dispatch--
remember, it corresponds to eval.
It's got to evaluate an expression in an environment.
So, in particular, what this one is going to do,
eval-dispatch will assume that, when you call it, that
the expression you want to evaluate
is in the EXP register.
The environment in which you want the evaluation to take
place is in the ENV register.
And continue tells you the place where the machine should
go next when the evaluation is done.
Eval-dispatch's contract is that it'll actually perform
that evaluation, and, at the end of which, it'll end up at
the place specified by continue.
The result of the evaluation will be in the VAL register.
And it just warns you, it makes no promises about what
happens to the registers.
All other registers might be destroyed.
So, there's one piece, OK?
Together, the pieces, apply-dispatch that
corresponds to apply, it's got to apply a procedure to some
arguments, so it assumes that this register, ARGL, contains
a list of the evaluated arguments.
FUN contains the procedure.
Those correspond to the arguments to the apply
procedure in the meta-circular evaluator.
And apply, in this particular evaluator, we're going to use
a discipline which says the place the machine should go to
next when apply is done is, at the moment apply-dispatch is
called at the top of the stack, that's just discipline
for the way this particular machine's organized.
And now apply's contract is given all that.
It'll perform the application.
The result of that application will end up in VAL.
The stack will be popped.
And, again, the contents of all the other registers may be
destroyed, all right?
So that's the basic organization of this machine.
Let's break for a little bit and see if there are any
questions, and then we'll do a real example.
Well, let's take the register machine now, and actually step
through, and really, in real detail, so you see completely
concrete how some expressions are evaluated, all right?
So, let's start with a very simple expression.
Let's evaluate the expression 1.
And we need an environment, so let's imagine that somewhere
there's an environment, we'll call it E,0.
And just, since we'll use these later, we obviously
don't really need anything to evaluate 1.
But, just for reference later, let's assume that E,0 has in
it an X that's bound to 3 and a Y that's bound to 4, OK?
And now what we're going to do is we're going to evaluate 1
in this environment, and so the ENV register has a pointer
to this environment, E,0, all right?
So let's watch that thing go.
What I'm going to do is step through the code.
And, let's see, I'll be the controller.
And now what I need, since this gets rather complicated,
is a very little execution unit.
So here's the execution unit, OK?
OK.
All right, now we're going to start.
We're going to start the machine at
eval-dispatch, right?
That's the beginning of this.
Eval-dispatch is going to look at the expression in dispatch,
just like eval where we look at the very first thing.
We branch on whether or not this expression is
self-evaluating.
Self-evaluating is some abstraction we
put into the machine--
it's going to be true for numbers--
to a place called ev-self-eval, right?
So me, being the controller, looks at ev-self-eval, so
we'll go over to there.
Ev-self-eval says fine, assign to val whatever is in the
expression unit, OK?
And I have a bug because what I didn't do when I initialized
this machine is also say what's supposed to happen when
it's done, so I should have started out the machine with
done being in the continue register, OK?
So we assign to VAL.
And now go to fetch of continue, and
[? the value changed. ?]
OK.
OK, let's try something harder.
Let's reset the machine here, and we'll put in the
expression register, X, OK?
Start again at eval-dispatch.
Check, is it self-evaluating?
No.
Is it a variable?
Yes.
We go off to ev-variable.
It says assign to VAL, look up the variable value in the
expression register, OK?
Go to fetch of continue.
PROFESSOR: Done.
PROFESSOR: OK.
All right.
Well, that's the basic idea.
That's a simple operation of the machine.
Now, let's actually do something a little bit more
interesting.
Let's look at the expression the sum of x and y.
OK.
And now we'll see how you start unrolling these
expression trees, OK?
Well, start again at eval-dispatch, all right?
Self-evaluating?
No.
Variable?
No.
All the other special forms which I didn't write down,
like quote, and lambda, and set, and whatever,
it's none of those.
It turns out to be an application, so we go off to
ev-application, OK?
Ev-application, remember what it's going to do overall.
It is going to evaluate the operator.
It's going to evaluate the arguments, and then it's going
to go apply them.
So, before we start, since we're being very literal, we'd
better remember that, somewhere in this environment,
it's linked to another environment in which plus is
bound to the primitive procedure plus before we get
an unknown variable in our machine.
OK, so we're at ev-application.
OK, assign to UNEV the operands of what's in the
expression register, OK?
Those are the operands.
UNEV's a temporary register where we're
going to save them.
PROFESSOR: I'm assigning.
PROFESSOR: Assign to x the operator.
Now, notice we've destroyed that expression in x, but the
piece that we need is now in UNEV. OK.
Now, we're going to get set up to recursively
evaluate the operator.
Save the continue register on the stack.
Save the environment.
Save UNEV. OK, assign to continue a
label called eval-args.
Now, what have we done?
We've set up for a recursive call.
We're about to go to eval-dispatch.
We've set up for a recursive call to eval-dispatch.
What did we do?
We took the things we're going to need later, those operands
that were in UNEV; the environment in which we're
going to eventually have to, maybe, evaluate those
operands; the place we eventually want to go to,
which, in this case, was done; we've saved them on the stack.
The reason we saved them on the stack is because
eval-dispatch makes no promises about what registers
it may destroy.
So all that stuff is saved on the stack.
Now, we've set up eval-dispatch's contract.
There's a new expression, which is the operator plus; a
new environment, although, in this case, it's the same one;
and a new place to go to when you're
done, which is eval-args.
So that's set up.
Now, we're going to go off to eval-dispatch.
Here we are back at eval-dispatch.
It's not self-evaluating.
Oh, it's a variable, so we'd better go off to
ev-variable, right?
Ev-variable is assigned to VAL.
Look up the variable value of the expression, OK?
So VAL is the primitive procedure plus, OK?
And go to fetch of continue.
PROFESSOR: Eval-args.
PROFESSOR: Right, which is now eval-args not done.
So we come back here at eval-args, and what do we do?
We're going to restore the stuff that we saved, so we
restore UNEV. And notice, there, it wasn't necessary,
although, in general, it would be.
It might be some arbitrary evaluation that happened.
We restore ENV. OK, we assign to FUN fetch of VAL.
OK, now, we're going to go off and start
evaluating some arguments.
Well, first thing we'd better do is save FUN because some
arbitrary stuff might happen in that evaluation.
We initialize the argument list. Assign to argl an empty
argument list, and go to eval-arg-loop, OK?
At eval-arg-loop, the idea of this is we're going to
evaluate the pieces of the expressions that are in UNEV,
one by one, and move them from unevaluated in UNEV to
evaluated in the arg list, OK?
So we save argl.
We assign to x the first operand of the stuff in UNEV.
Now, we check and see if that was the last operand.
In this case, it is not, all right?
So we save the environment.
We save UNEV because those are all things
we might need later.
We're going to need the environment to do some more
evaluations.
We're going to need UNEV to look at what the rest of those
arguments were.
We're going to assign continue a place called
accumulate-args, or accumulate-arg.
OK, now, we've set up for another call to
eval-dispatch, OK?
All right, now, let me short-circuit this so we don't
go through the details of eval-dispatch.
Eval-dispatch's contract says I'm going to end up, the world
will end up, with the value of evaluating this expression in
this environment in the VAL register,
and I'll end up there.
So we short-circuit all of this, and a 3 ends up in VAL.
And, when we return from eval-dispatch, we're going to
return to accumulate-arg.
PROFESSOR: Accumulate-arg.
PROFESSOR: With 3 in the VAL register, OK?
So that short-circuited that evaluation.
Now, what do we do?
We're going to go back and look at the rest of the
arguments, so we restore UNEV. We restore
ENV. We restore argl.
One thing.
PROFESSOR: Oops!
Parity error.
[LAUGHTER]
PROFESSOR: Restore argl.
PROFESSOR: OK.
OK, we assign to argl consing on fetch of the value register
to what's in argl.
OK, we assign to UNEV the rest of the operands in fetch of
UNEV, and we go back to eval-arg-loop.
PROFESSOR: Eval-arg-loop.
PROFESSOR: OK.
Now, we're about to do the next argument, so the first
thing we do is save argl.
OK, we assign to x the first operand of fetch of UNEV. OK,
we test and see if that's the last operand.
In this case, it is, so we're going to go to a special place
that says evaluate the last argument because, notice,
after evaluating the argument, we don't need the
environment any more.
That's going to be the difference.
So here, at eval-last-arg, which is assigned to
accumulate-last-arg, now, we're set up again for
eval-dispatch.
We've got a place to go to when we're done.
We've got an expression.
We've got an environment.
OK, so we'll short-circuit the call to eval-dispatch.
And what'll happen is there's a y there, it's 4 in that
environment, so VAL will end up with 4 in it.
And, then, we're going to end up at accumulate-last-arg, OK?
So, at accumulate-last-arg, we restore argl.
We assign to argl cons of fetch of the new value onto
it, so we cons a 4 onto that.
We restore what was saved in the function register.
And notice, in this case, it had not been destroyed, but,
in general, it will be.
And now, we're ready to go off to apply-dispatch, all right?
So we've just gone through the eval.
We evaluated the argument, the operator, and the arguments,
and now, we're about to apply them.
So we come off to apply-dispatch here, OK?
We come off to apply-dispatch, and we're going to check
whether it's a primitive or a compound procedure.
PROFESSOR: Yes.
PROFESSOR: All right.
So, in this case, it's a primitive procedure, and we go
off to primitive-apply.
So we go off to primitive-apply, and it says
assign to VAL the result of applying primitive procedure
of the function to the argument list.
PROFESSOR: I don't know how to add.
I'm just an execution unit.
PROFESSOR: Well, I don't know how to add either.
I'm just the evaluator, so we need a primitive operator.
Let's see, so the primitive operator, what's the
sum of 3 and 4?
AUDIENCE: 7.
PROFESSOR: OK, 7.
PROFESSOR: Thank you.
PROFESSOR: Now, we restore continue, and we go to fetch
of continue.
PROFESSOR: Done.
PROFESSOR: OK.
Well, that was in as much detail as you will ever see.
We'll never do it in as much detail again.
One very important thing to notice is that we just
executed a recursive procedure, right?
This whole thing, we used a stack and the
evaluator was recursive.
A lot of people think the reason that you need a stack
and recursion in an evaluator is because you might be
evaluating recursive procedures like
factorial or Fibonacci.
It's not true.
So you notice we did recursion here, and all we evaluated was
plus X, Y, all right?
The reason that you need recursion in the evaluator is
because the evaluation process, itself, is
recursive, all right?
It's not because the procedure that you might be evaluating
in LISP is a recursive procedure.
So that's an important thing that people get
confused about a lot.
The other thing to notice is that, when we're done here,
we're really done.
Not only are we at done, but there's no accumulated stuff
on the stack, right?
The machine is back to its initial state, all right?
So that's part of what it means to be done.
Another way to say that is the evaluation process has reduced
the expression, plus X, Y, to the value here, 7.
And by reduced, I mean a very particular thing.
It means that there's nothing left on the stack.
The machine is now in the same state, except there's
something in the value register.
It's not part of a sub-problem of anything.
There's nothing to go back to.
OK.
Let's break.
Question?
AUDIENCE: The question here, in the stack, is because the
data may be recursive.
You may have embedded expressions, for instance.
PROFESSOR: Yes, because you might have embedded
expressions.
But, again, don't confuse that with what people sometimes
mean by the data may be recursive, which is to say you
have these list-structured, recursive data list
operations.
That has nothing to do with it.
It's simply that the expressions contain
sub-expressions.
Yeah?
AUDIENCE: Why is it that the order of the arguments in the
arg list got reversed?
PROFESSOR: Ah!
Yes, I should've mentioned that.
Here, the reason the order is reversed--
it's a question of what you mean by reversed.
I believe it was Newton.
In the very early part of optics, people realized that,
when you look through the lens of your eye, the image was
up-side down.
And there was a lot of argument about why that didn't
mean you saw things up-side down.
So it's sort of the same issue.
Reversed from what?
So we just need some convention.
The reason that they're coming at 4, 3 is because we're
taking UNEV and consing the result onto argl.
So you have to realize you've made that convention.
The place that you have to realize that--
well, there's actually two places.
One is in apply-primitive-operator,
which has to realize that the arguments to primitives go in,
in the opposite order from the way you're writing them down.
And the other one is, we'll see later when you actually go
to bind a function's parameters, you should realize
the arguments are going to come in from the opposite
order of the variables to which you're binding them.
So, if you just keep track of that, there's no problem.
Also, this is completely arbitrary because, if we'd
done, say, an iteration through a vector assigning
them, they might come out in the other order, OK?
So it's just a convention of the way this particular
evaluator works.
All right, let's take a break.
We just saw evaluating an expression and, of course,
that was very simple one.
But, in essence, it would be no different if it was some
big nested expression, so there would just be deeper
recursion on the stack.
But what I want to do now is show you the last piece.
I want to walk you around this eval and apply loop, right?
That's the thing we haven't seen, really.
We haven't seen any compound procedures where applying a
procedure reduces to evaluating the body of the
procedure, so let's just suppose we had this.
Suppose we were looking at the procedure define F of A and B
to be the sum of A and B. So, as we typed in that procedure
previously, and now we're going to evaluate F of X and
Y, again, in this environment, E,0, where X is bound to 3 and
Y is bound to 4.
When the defined is executed, remember, there's a lambda
here, and lambdas create procedures.
And, basically, what will happen is, in E,0, we'll end
up with a binding for F, which will say F is a procedure, and
its args are A and B, and its body is plus a,b.
So that's what the environment would have looked like had we
made that definition.
Then, when we go to evaluate F of X and Y, we'll go through
exactly the same process that we did before.
It's even the same expression.
The only difference is that F, instead of having primitive
plus in it, will have this thing.
And so we'll go through exactly the same process,
except this time, when we end up at apply-dispatch, the
function register, instead of having primitive plus, will
have a thing that will represent it saying procedure,
where the args are A and B, and the body is plus A, B.
And, again, what I mean, by its ENV, I mean there's a
pointer to it, so don't worry that I'm writing a lot of
stuff there.
There's a pointer to this procedure data structure.
OK, so, we're in exactly the same situation.
We get to apply-dispatch, so, here, we come to
apply-dispatch.
Last time, we branched off to a primitive procedure.
Here, it says oh, we now have a compound procedure, so we're
going to go off to compound-apply.
Now, what's compound-apply?
Well, remember what the meta-circular evaluator did?
Compound-apply said we're going to evaluate the body of
the procedure in some new environment.
Where does that new environment come from?
We take the environment that was packaged with the
procedure, we bind the parameters of the procedure to
the arguments that we're passing in, and use that as a
new frame to extend the procedure environment.
And that's the environment in which we evaluate the
procedure body, right?
That's going around the apply/eval loop.
That's apply coming back to call eval, all right?
OK.
So, now, that's all we have to do in compound-apply.
What are we going to do?
We're going to manufacture a new environment.
And we're going to manufacture a new environment, let's see,
that we'll call E,1.
E,1 is going to be some environment where the
parameters of the procedure, where A is bound to 3 and B is
bound to 4, and it's linked to E,0 because
that's where f is defined.
And, in this environment, we're going to evaluate the
body of the procedure.
So let's look at that, all right?
All right, here we are at compound-apply, which says
assign to the expression register the body of the
procedure that's in the function register.
So I assign to the expression register the
procedure body, OK?
That's going to be evaluated in an environment which is
formed by making some bindings using information determined
by the procedure--
that's what's in FUN--
and the argument list.
And let's not worry about exactly what that does, but
you can see the information's there.
So make bindings will say oh, the procedure, itself, had an
environment attached to it.
I didn't write that quite here.
I should've said in environment because every
procedure gets built with an environment.
So, from that environment, it knows what the procedure's
definition environment is.
It knows what the arguments are.
It looks at argl, and then you see a
reversal convention here.
It just has to know that argl is reversed, and it builds
this frame, E,1.
All right, so, let's assume that that's what make bindings
returns, so it assigns to ENV this thing, E,1.
All right, the next thing it says is restore continue.
Remember what continue was here?
It got put up in the last segment.
Continue got stored.
That was the original done, which said what are you going
to do after you're done with this particular application?
It was one of the very first things that happened when we
evaluated the application.
And now, finally, we're going to restore continue.
Remember apply-dispatch's contract.
It assumes that where it should go to next was on the
stack, and there it was on the stack.
Continue has done, and now we're going to go back to
eval-dispatch.
We're set up again.
We have an expression, an environment, and
a place to go to.
We're not going to go through that because it's sort of the
same expression.
OK, but the thing, again, to notice is, at this point, we
have reduced the original expression, F,X,Y, right?
We've reduced evaluating F,X,Y in environment E,0 to evaluate
plus A, B in E,1.
And notice, nothing's on the stack, right?
It's a reduction.
At this point, the machine does not contain, as part of
its state, the fact that it's in the middle of evaluating
some procedure called f, that's gone, right?
There's no accumulated state, OK?
Again, that's a very important idea.
That's the meaning of, when we used to write in the
substitution model, this expression reduces to that
expression.
And you don't have to remember anything.
And here, you see the meaning of reduction.
At this point, there is nothing on the stack.
See, that has very important consequences.
Let's go back and look at iterative
factorial, all right?
Remember, this was some sort of loop and doing iter.
And we kept saying that's an iterative procedure, right?
And what we wrote, remember, are things like, we said,
fact-iter of 5.
We wrote things like reduces to iter of 1, and 1, and 5,
which reduces to iter of 1, and 2, and 5, and so on, and
so on, and so on.
And we kept saying well, look, you don't have to build up any
storage to do that.
And we waved our hands, and said in principle, there's no
storage needed.
Now, you see no storage needed.
Each of these is a real reduction, right?
As you walk through these expressions, what you'll see
are these expressions on the stack in some particular
environment, and then these expressions in the EXP
register in some particular environment.
And, at each point, there'll be no accumulated stuff on the
stack because each one's a real reduction, OK?
All right, so, for example, just to go through it in a
little bit more care, if I start out with an expression
that says something like, oh, say, fact-iter of 5 in some
environment that will, at some point, create an environment
in which n is down to 5.
Let's call that--
And, at some point, the machine will reduce this whole
thing to a thing that says that's really iter of 1, and
1, and n, evaluated in this environment, E,1 with nothing
on the stack.
See, at this moment, the machine is not remembering
that evaluating this expression, iter--
which is the loop-- is part of this thing
called iterative factorial.
It's not remembering that.
It's just reducing the expression to that, right?
If we look again at the body of iterative factorial, this
expression has reduced to that expression.
Oh, I shouldn't have the n there.
It's a slightly different convention from the slide to
the program, OK?
And, then, what's the body of iter?
Well, iter's going to be an it, and I won't go through the
details of if.
It'll evaluate the predicate.
In this case, it'll be false.
And this iter will now reduce to the expression iter of
whatever it says, star, counter product, and--
what does it say--
plus counter 1 in some other environment, by this time,
E,2, where E,2 will be set up having bindings for product
and counter, right?
And it'll reduce to that, right?
It won't be remembering that it's part of something that it
has to return to.
And when iter calls iter again, it'll reduce to another
thing that looks like this in some environment, E,3, which
has new bindings for product and counter.
So, if you're wondering, see, if you've always been queasy
about how it is we've been saying those procedures, that
look syntactically recursive, are, in fact, iterative, run
in constant space, well, I don't know if this makes you
less queasy, but at least it shows you what's happening.
There really isn't any buildup there.
Now, you might ask well, is there buildup in principle in
these environment frames?
And the answer is yeah, you have to make these new
environment frames, but you don't have to hang onto them
when you're done.
They can be garbage collected, or the space can be reused
automatically.
But you see the control structure of the evaluator is
really using this idea that you actually have a reduction,
so these procedures really are iterative procedures.
All right, let's stop for questions.
All right, let's break.
Let me contrast the iterative procedure just so you'll see
where space does build up with a recursive procedure, so you
can see the difference.
Let's look at the evaluation of recursive
factorial, all right?
So, here's fact-recursive, or standard factorial definition.
We said this one is still a recursive procedure, but this
is actually a recursive process.
And then, just to link it back to the way we started, we said
oh, you can see that it's going to be recursive process
by the substitution model because, if I say recursive
factorial of 5, that turns into 5 times--
what is it, fact-rec, or record fact--
5 times recursive factorial of 4, which turns into 5 times 4
times fact-rec of 3, which returns into 5 times 4 times 3
times, and so on, right?
The idea is there was this chain of stuff building up,
which justified, in the substitution model, the fact
that it's recursive.
And now, let's actually see that chain of stuff build up
and where it is in the machine, OK?
All right, well, let's imagine we're going
to start out again.
We'll tell it to evaluate recursive factorial of 5 in
some environment, again, E,0 where recursive factorial is
defined, OK?
Well, now we know what's eventually going to happen.
This is going to come along, it'll evaluate those things,
figure out it's a procedure, build somewhere over here an
environment, E,1, which has n bound to 5, which hangs off of
E,0, which would be, presumably, the definition
environment of recursive factorial, OK?
And, in this environment, it's going to go off and
evaluate the body.
So, again, the evaluation here will reduce to evaluating the
body in E,1.
That's going to look at an if, and I won't go through the
details of if.
It'll look at the predicate.
It'll decide it eventually has to evaluate the alternative.
So this whole thing, again, will reduce to the alternative
of recursive factorial, the alternative clause, which says
that this whole thing reduces to times n of recursive
factorial of n minus 1 in the environment E,1, OK?
So the original expression, now, is going to reduce to
evaluating that expression, all right?
Now we have an application.
We did an application before.
Remember what happens in an application?
The first thing you do is you go off and you save the value
of the continue register on the stack.
So the stack here is going to have done in it.
And then you're going to set up to evaluate
the sub-parts, OK?
So here we go off to evaluate the sub-parts.
First thing we're going to do is evaluate the operator.
What happens when we evaluate an operator?
Well, we arrange things so that the operator ends up in
the expression register.
The environments in the ENV register continue someplace
where we're going to go evaluate the arguments.
And, on the stack, we've saved the original continue, which
is where we wanted to be when we're all done.
And then the things we needed when we're going to get done
evaluating the operator, the things we'll need to evaluate
the arguments, namely, the environment and those
arguments, those unevaluated arguments, so there they are
sitting on the stack.
And we're about to go off to evaluate the operator.
Well, when we return from this particular call--
so we're about to call eval-dispatch here--
when we return from this call, the value of that operator,
which, in this case, is going to be the primitive multiplier
procedure, will end up in the FUN register, all right?
We're going to evaluate some arguments.
They will evaluate in here.
That'll give us 5, in this case.
We're going to put that in the argl register, and then we'll
go off to evaluate the second operand.
So, at the point where we go off to evaluate the second
operand-- and I'll skip details like computing, and
minus 1, and all of that-- but, when we go off to
evaluate the second operand, that will eventually reduce to
another call to fact-recursive.
And, what we've got on the stack here is the operator
from that combination that we're going to use it in and
the other argument, OK?
So, now, we're set up for another call
to recursive factorial.
And, when we're done with this one, we're going to go to
accumulate the last arg.
And remember what that'll do?
That'll say oh, whatever the result of this has to get
combined with that, and we're going to multiply them.
But, notice now, we're at another recursive factorial.
We're about to call eval-dispatch again, except we
haven't really reduced it because there's stuff
on the stack now.
The stuff on the stack says oh, when you get back, you'd
better multiply it by the 5 you had hanging there.
So, when we go off to make another call, we
evaluate the n minus 1.
That gives us another environment in which the new
n's going to be down to 4.
And we're about to call eval-dispatch again, right?
We get another call.
That 4 is going to end up in the same situation.
We'll end up with another call to fact-recursive n.
And sitting on the stack will be the stuff from the original
one and, now, the subsidiary one we're doing.
And both of them are waiting for the same thing.
They're going to go to accumulate a last argument.
And then, of course, when we go to the fourth call, the
same thing happens, right?
And this goes on, and on, and on.
And what you see here on the stack, exactly what's sitting
here on the stack, the thing that says times and 5.
And what you're going to do with that is accumulate that
into a last argument.
That's exactly this, right?
This is exactly where that stuff is hanging.
Effectively, the operator you're going to apply, the
other argument that it's got to be multiplied by when you
get back and the parentheses, which says yeah, what you
wanted to do was accumulate them.
So, you see, the substitution model is not such a lie.
That really is, in some sense, what's sitting
right on the stack.
OK.
All right, so that, in some sense, should explain for you,
or at least convince you, that, somehow, this evaluator
is managing to take these procedures and execute some of
them iteratively and some of them recursively, even though,
as syntactically, they look like recursive procedures.
How's it managing to do that?
Well, the basic reason it's managing to do that is the
evaluator is set up to save only what it needs later.
So, for example, at the point where you've reduced
evaluating an expression and an environment to applying a
procedure to some arguments, it doesn't need that original
environment anymore because any environment stuff will be
packaged inside the procedures where the
application's going to happen.
All right, similarly, when you're going along evaluating
an argument list, when you've finished evaluating the list,
when you're finished evaluating the last argument,
you don't need that argument list any more, right?
And you don't need the environment where those
arguments would be evaluated, OK?
So the basic reason that this interpreter is being so smart
is that it's not being smart at all, it's being stupid.
It's just saying I'm only going to save
what I really need.
Well, let me show you here.
Here's the actual thing that's making a tail recursive.
Remember, it's the restore of continue.
It's saying when I go off to evaluate the procedure body, I
should tell eval to come back to the place where that
original evaluation was supposed to come back to.
So, in some sense, you want to say what's the actual line
that makes a tail recursive?
It's that one.
If I wanted to build a non-tail recursive evaluator,
for some strange reason, all I would need to do is, instead
of restoring continue at this point, I'd set up a label down
here called, "Where to come back after you've finished
applying the procedure." Instead, I'd
set continue to that.
I'd go to eval-dispatch, and then eval-dispatch
would come back here.
At that point, I would restore continue and go to the
original one.
So here, the only consequence of that would be to make it
non-tail recursive.
It would give you exactly the same answers, except, if you
did that iterative factorial and all those iterative
procedures, it would execute recursively.
Well, I lied to you a little bit, but just a little bit,
because I showed you a slightly over-simplified
evaluator where it assumes that each procedure body has
only one expression.
Remember, in general, a procedure has a sequence of
expressions in it.
So there's nothing really conceptually new.
Let me just show you the actual evaluator that handles
sequences of expressions.
This is compound-apply now, and the only difference from
the old one is that, instead of going off to eval directly,
it takes the whole body of the procedure, which, in this
case, is a sequence of expressions, and goes off to
eval-sequence.
And eval-sequence is a little loop that, basically, does
these evaluations one at a time.
So it does an evaluation.
Says oh, when I come back, I'd better come back here to do
the next one.
And, when I'm all done, when I want to get the last
expression, I just restore my continue and go off to
eval-dispatch.
And, again, if you wanted for some reason to break tail
recursion in this evaluator, all you need to do is not
handle the last expression, especially.
Just say, after you've done the last expression, come back
to some other place after which you restore continue.
And, for some reason, a lot of LISP evaluators tended
to work that way.
And the only consequence of that is that iterative
procedures built up stack.
And it's not clear why that happened.
All right.
Well, let me just sort of summarize, since this is a lot
of details in a big program.
But the main point is that it's no different,
conceptually, from translating any other program.
And the main idea is that we have this universal evaluator
program, the meta-circular evaluator.
If we translate that into LISP, then
we have all of LISP.
And that's all we did, OK?
The second point is that the magic's gone away.
There should be no more magic in this whole system, right?
In principle, it should all be very clear except, maybe, for
how list structured memory works, and
we'll see that later.
But that's not very hard.
The third point is that all this tail recursion came from
the discipline of eval being very careful to save only what
it needs next time.
It's not some arbitrary thing where we're saying well,
whenever we call a sub-routine, we'll save all
the registers in the world and come back, right?
See, sometimes it pays to really worry about efficiency.
And, when you're down in the guts of your evaluator
machine, it really pays to think about things like that
because it makes big consequences.
Well, I hope what this has done is really made the
evaluator seem concrete, right?
I hope you really believe that somebody could hold a LISP
evaluator in the palm of their hand.
Maybe to help you believe that, here's a LISP evaluator
that I'm holding the palm of my hand, right?
And this is a chip which is actually quite a bit more
complicated than the evaluator I showed you.
Maybe, here's a better picture of it.
What there is, is you can see the same overall structure.
This is a register array.
These are the data paths.
Here's a finite state controller.
And again, finite state, that's all there is.
And somewhere there's external memory
that'll worry about things.
And this particular one is very complicated because it's
trying to run LISP fast. And it has some very, very fast
parallel operations in there like, if you want to index
into an array, simultaneously check that the index is an
integer, check that it doesn't exceed the array bands, and go
off and do the memory access, and do all those things
simultaneously.
And then, later, if they're all OK, actually
get the value there.
So there are a lot of complicated operations in
these data paths for making LISP run in parallel.
It's a completely non-risk philosophy of evaluating LISP.
And then, this microcode is pretty complicated.
Let's see, there's what?
There's about 389 instructions of 220-bit microcode sitting
here because these are very complicated data paths.
And the whole thing has about 89,000 transistors, OK?
OK.
Well, I hope that that takes away a lot of the mystery.
Maybe somebody wants to look at this.
Yeah.
OK.
Let's stop.
Questions?
AUDIENCE: OK, now, it sounds like what you're saying is
that, with the restore continue put in the proper
place, that procedures that would invoke a recursive
process now invoke an integer process just by the way that
the eval signature is?
PROFESSOR: I think the way I'd prefer to put it is that, with
restore continue put in the wrong place, you can cause any
syntactically-looking recursive procedure, in fact,
to build up stack as it runs.
But there's no reason for that, so you might want to
play around with it.
You can just switch around two or three instructions in the
way compound-apply comes back, and you'll get something which
isn't tail recursive.
But the thing I wanted to emphasize is there's no magic.
It's not as if there's some very clever pre-processing
program that's looking at this procedure, factorial iter, and
say oh, gee, I really notice that I don't have to push
stack in order to do this.
Some people think that that's what's going on.
It's something much, much more dumb than that, it's this one
place you're putting the restore instruction.
It's just automatic.
AUDIENCE: OK.
AUDIENCE: But that's not affecting the time
complexity is it?
PROFESSOR: No.
AUDIENCE: It's just that it's handling it recursively
instead of iteratively.
But, in terms of the order of time it takes to finish the
operation, it's the same one way or the other, right?
PROFESSOR: Yes.
Tail recursion is not going to change the time complexity of
anything because, in some sense, it's the same algorithm
that's going on.
What it's doing is really making this thing run as an
iteration, right?
Not going to run out of memory counting up to a giant number
simply because the stack would get pushed.
See, the thing you really have to believe is
that, when we write--
see, we've been writing all these things called
iterations, infinite loops, define loop to be called loop.
That's is as much an iteration as if we wrote do forever
loop, right?
It's just syntactic sugar as the difference.
These things are real, honest to god, iterations, right?
They don't change the time complexity, but they turn them
into real iterations.
All right, thank you.