[MUSIC PLAYING - "JESU, JOY OF MAN'S DESIRING" BY JOHANN
SEBASTIAN BACH]
PROFESSOR: Well, up 'til now, I suppose, we've been learning
about a lot of techniques for organizing big programs,
symbolic manipulation a bit, some of the technology that
you use for establishing languages, one in terms of
another, which is used for organizing very large
programs. In fact, the nicest programs I know look more like
a pile of languages than like a decomposition of a problem
into parts.
Well, I suppose at this point, there are still, however, a
few mysteries about how this sort of stuff works.
And so what we'd like to do now is diverge from the plan
of telling you how to organize big programs, and rather tell
you something about the mechanisms by which these
things can be made to work.
The main reason for this is demystification, if you will,
that we have a lot of mysteries left, like exactly
how it is the case that a program is controlled, how a
computer knows what the next thing to do is, or
something like that.
And what I'd like to do now is make that clear to you, that
even if you've never played with a physical computer
before, the mechanism is really very simple, and that
you can understand it completely with no trouble.
So I'd like to start by imagining that we--
well, the way we're going to do this, by the way, is we're
going to take some very simple Lisp programs, very simple
Lisp programs, and transform them into hardware.
I'm not going to worry about some intermediate step of
going through some existing computer machine language and
then showing you how that computer works, because that's
not as illuminating.
So what I'm really going to show you is how a piece of
machinery can be built to do a job that you have written down
as a program.
That program is, in fact, a description of a machine.
We're going to start with a very simple program, proceed
to show you some simple mechanisms, proceed to a few
more complicated programs, and then later show you a not very
complicated program, how the evaluator transforms into a
piece of hardware.
And of course at that point, you have made the universal
transition and can execute any program imaginable with a
piece of well-defined hardware.
Well, let's start up now, give you a real concrete feeling
for this sort of thing.
Let's start with a very simple program.
Here's Euclid's algorithm.
It's actually a little bit more modern
than Euclid's algorithm.
Euclid's algorithm for computing the greatest common
divisor of two numbers was invented 350 BC, I think.
It's the oldest known algorithm.
But here we're going to talk about GCD of A and B, the
Greatest Common Divisor or two numbers, A and B. And the
algorithm is extremely simple.
If B is 0, then the result is going to be A. Otherwise, the
result is the GCD of B and the remainder when A is divided by
B.
So this we have here is a very simple iterative process.
This a simple recursive procedure, recursively defined
procedure, recursive definition, which yields an
iterative process.
And the way it works is that every step, it determines
whether B was zero.
And if B is 0, we got the answer in A. Otherwise, we
make another step where A is the old B, and B is the
remainder of the old A divided by the old B. Very simple.
Now this, I've already told you some of the mechanism by
just saying it that way.
I set it in time.
I said there are certain steps, and that, in fact, one
of the things you can see here is that one of the reasons why
this is iterative is nothing is needed of the last step to
get the answer.
All of the information that's needed to run this algorithm
is in A and B. It has two well-defined state variables.
So I'm going to define a machine for you that can
compute you GCDs.
Now let's see.
Every computer that's ever been made that's a
single-process computer, as opposed to a multiprocessor of
some sort, is made according to the same plan.
The plan is the computer has two parts, a part called the
datapaths, and a part called the controller.
The datapaths correspond to a calculator that you might
have. It contains certain registers that remember
things, and you've all used calculators.
It has some buttons on it and some lights.
And so by pushing the various buttons, you can cause
operations to happen inside there among the registers, and
some of the results to be displayed.
That's completely mechanical.
You could imagine that box has no intelligence in it.
Now it might be very impressive that it can produce
the sine of a number, but that at least is apparently
possibly mechanical.
At least, I could open that up in the same way I'm
about to open GCD.
So this may have a whole computer inside of it, but
that's not interesting.
Addition is certainly simple.
That can be done without any further mechanism.
Now also, if we were to look at the other half, the
controller, that's a part that's dumb, too.
It pushes the buttons.
It pushes them according to the sequence, which is written
down on a piece of paper, and observes the lights.
And every so often, it comes to a place in a sequence that
says, if light A is on, do this sequence.
Otherwise, do that sequence.
And thereby, there's no complexity there either.
Well, let's just draw that and see what we feel about that.
So for computing GCDs, what I want you to think about is
that there are these registers.
A register is a place where I store a number, in this case.
And this one's called a.
And then there's another one for storing b.
Now we have to see what things we can do with these
registers, and they're not entirely obvious what you can
do with them.
Well, we have to see what things we
need to do with them.
We're looking at the problem we're trying to solve.
One of the important things for designing a computer,
which I think most designers don't do, is you study the
problem you want to solve and then use what you learn from
studying the problem you want to solve to put in the
mechanisms needed to solve it in the computer you're
building, no more no less.
Now it may be that the problem you're trying to solve is
everybody's problem, in which case you have to build in a
universal interpreter of some language.
But you shouldn't put any more in than required to build the
universal interpreter of some language.
We'll worry about that in a second.
OK, going back to here, let's see.
What do we have to be able to do?
Well, somehow, we have to be able to get B into A. We have
to be able to get the old value of B into the value of
A. So we have to have some path by which stuff can flow,
whatever this information is, from b to a.
I'm going to draw that with by an arrow saying that it is
possible to move the contents of b into a, replacing the
value of a.
And there's a little button here which you push which
allows that to happen.
That's what the little x is here.
Now it's also the case that I have to be able to compute the
remainder of a and b.
Now that may be a complicated mess.
On the other hand, I'm going to make it a small box.
If we have to, we may open up that box and look inside and
see what it is.
So here, I'm going to have a little box, which I'm going to
draw this way, which we'll call the remainder.
And it's going to take in a.
That's going to take in b.
And it's going to put out something, the remainder of a
divided by b.
Another thing we have to see here is that we have to be
able to test whether b is equal to 0.
Well, that means somebody's got to be looking at--
a thing that's looking at the value of b.
I have a light bulb here which lights up if b equals 0.
That's its job.
And finally, I suppose, because of the fact that we
want the new value of a to be the old value of b, and
simultaneously the new value of b to be something I've done
with a, and if I plan to make my machine such that
everything happens one at a time, one motion at a time,
and I can't put two numbers in a register, then I have to
have another place to put one while I'm interchanging.
OK?
I can't interchange the two things in my hands, unless I
either put two in one hand and then pull it back the other
way, or unless I put one down, pick it up, and put the other
one, like that, unless I'm a juggler, which I'm not, as you
can see, in which case I have a
possibility of timing errors.
In fact, much of the type of computer design people do
involves timing errors, of some potential timing errors,
which I don't much like.
So for that reason, I have to have a place to put the second
one of them down.
So I have a place called t, which is a register just for
temporary, t, with a button on it.
And then I'll take the result of that, since I have to take
that and put into b, over here, we'll take the result of
that and go like this, and a button here.
So that's the datapaths of a GCD machine.
Now what's the controller?
Controller's a very simple thing, too.
The machine has a state.
The way I like to visualize that is that I've got a maze.
And the maze has a bunch of places
connected by directed arrows.
And what I have is a marble, which represents the state of
the controller.
The marble rolls around in the maze.
Of course, this analogy breaks down for energy reasons.
I sometimes have to pump the marble up to the top, because
it's going to otherwise be a perpetual motion machine.
But not worrying about that, this is
not a physical analogy.
This marble rolls around.
And every time it rolls around certain bumpers, like in a
pinball machine, it pushes one of these buttons.
And every so often, it comes to a place, which is a
division, where it has to make a choice.
And there's a flap, which is controlled by this.
So that's a really mechanical way of thinking about it.
Of course, controllers these days, are not built that way
in real computers.
They're built with a little bit of
ROM and a state register.
But there was a time, like the DEC PDP-6, where that's how
you built the controller of a machine.
There was a bit that ran around the delay line, and it
triggered things as it went by.
And it would come back to the beginning and
get fed round again.
And of course, there were all sorts of great bugs you could
have like two bits going around, two marbles.
And then the machine has lost its marbles.
That happens, too.
Oh, well.
So anyway, for this machine, what I have
to do is the following.
I'm going to start my maze here.
And the first thing I've got to do, in a notation which
many of you are familiar with, is b equal to zero, a test.
And there's a possibility, either yes, in
which case I'm done.
Otherwise, if no, then I'm going have to
roll over some bumpers.
I'm going to do it in the following order.
I want to do this interchange game.
Now first, since I need both a and b, but then the first--
and this is not necessary--
I want to collect this.
This is the thing that's going to go into b.
So I'm going to say, take this, which depends upon both
a and b, and put the remainder into here.
So I'm going to push this button first. Then, I'm going
to transfer b to a, push that button, and then I transfer
the temporary into b, push that button.
So a very sequential machine, it's very inefficient.
But that's fine right now.
We're going to name the buttons, t gets remainder.
a gets b.
And b gets t.
And then I'm going to go around here and it's to go
back to start.
And if you look, what are we seeing here?
We're seeing the various--
what I really have is some sort of mechanical connection,
where t gets r controls this thing.
And I have here that a gets b controls this fellow over
here, and this fellow over here.
Boy, that's absolutely pessimal,
the inverse of optimal.
Every line heads across every other line the way I drew it.
I suppose this goes here, b gets t.
Now I'd like to run this machine.
But before I run the machine, I want to write down a
description of this controller, just so you can
see that these things, of course, as usual, can be
written down in some nice language, so that we don't
have to always draw these diagrams. One of the problems
with diagrams is that they take up a lot of space.
And for a machine this small, it takes two blackboards.
For a machine that's the evaluator machine, I have
trouble putting it into this room, even though
it isn't very big.
So I'm going to make a little language for this that's just
a description of that, saying define a
machine we'll call GCD.
Of course, once we have something like this, we have a
simulator for it.
And the reason why we want to build a language in this form,
is because all of a sudden we can manipulate these
expressions that I'm writing down.
And then of course I can write things that can algebraically
manipulate these things, simulate them, all that sort
of things that I might want to do, perhaps transform them as
a layout, who knows.
Once I have a nice representation of registers,
it has certain registers, which we can call A, B, and T.
And there's a controller.
Actually, a better language, which would be more explicit,
would be one which named every button also and
said what it did.
Like, this button causes the contents of T to go to the
contents of B. Well I don't want to do that, because it's
actually harder to read to do that, and it
takes up more space.
So I'm going to have that in the instructions written in
the controller.
It's going to be implicit what the operations are.
They can be deduced by reading these and collecting together
all the different things that can be done.
Well, let's just look at what these things are.
There's a little loop that we go around which says branch,
this is the representation of the little flap that decides
which way you go here, if 0 fetch of B, the contents of B,
and if the contents of B is 0, then go to a
place called done.
Now, one thing you're seeing here, this looks very much
like a traditional computer language.
And what you're seeing here is things like labels that
represent places in a sequence written down as a sequence.
The reason why they're needed is because over here, I've
written something with loops.
But if I'm writing English text, or something like that,
it's hard to refer to a place.
I don't have arrows.
Arrows are represented by giving names to the places
where the arrows terminate, and then referring to them by
those names.
Now this is just an encoding.
There's nothing magical about things like that.
Next thing we're going to do is we're going to say, how do
we do T gets R?
Oh, that's easy enough, assign.
We assign to T the remainder.
Assign is the name of the button.
That's the button-pusher.
Assign to T the remainder, and here's the representation of
the operation, when we divide the fetch of A by the fetch of
B.
And we're also going to assign to A the fetch of B, assign to
B the result of getting the contents of T. And now I have
to refer to the beginning here.
I see, why don't I call that loop like I have here?
So that's that reference to that arrow.
And when we're done, we're done.
We go to here, which is the end of the thing.
So here's just a written representation of this
fragment of machinery that we've drawn here.
Now the next thing I'd like to do is run this.
I want us to feel it running.
Never done this before, you got to do it once.
So let's take a particular problem.
Suppose we want to compute the GCD of a equals 30
and b equals 42.
I have no idea what that is right now.
But a 30 and b is 42.
So that's how I start this thing up.
Well, what's the first thing I do?
I say is B equal to 0, no.
Then assign to T the remainder of the fetch of A and the
fetch of B. Well the remainder of 30 when divided by
42 is itself 30.
Push that button.
Now the marble has rolled to here.
A gets B. That pushes this button.
So 42 moves into here.
B gets C. Push that button.
The 30 goes here.
Let met just interchange them.
Now let's see, go back to the beginning.
B 0, no.
T gets the remainder.
I suppose the remainder when dividing 42 by 30 is 12.
I push that one.
Next thing I do is allow the 30 to go to here, push this
one, allow the 12 to go to here.
Go around this thing.
Is that done?
No.
How about--
so now I have to find out the remainder of 30 divided by 12.
And I believe that's 6.
So 6 goes here on this button push.
Then the next thing I push is this one, which the
12 goes into here.
Then I push this button.
The 6 gets into here.
Is 6 equal to 0?
No.
OK.
So then at that point, the next thing to do is divide it.
Ooh, this has got a remainder of 0.
Looks like we're almost done.
Move the 6 over here next.
0 over here.
Is the answer 0?
Yes.
B is 0, therefore the answer is in A.
The answer is 6.
And indeed that's right, because if we look at the
original problem, what we have is 30 is 2 times 3 times 5,
and 42 is 2 times 3 times 7.
So the greatest common divisor is 2 times 3, which is 6.
Now normally, we write one other little line here, just
to make it a little bit clearer, which is that we
leave in a connection saying that this light is the guy
that that flap looks at.
Of course, any real machine has a lot more complicated
things in it than what I've just shown you.
Let's look for a second at the first still store.
Wow.
Well you see, for example, one thing we might want to do is
worry about the operations that are of IO form.
And we may have to collect something from the outside.
So a state machine that we might have, the controller may
have to, for example, get a value from something and put
register a to load it up.
I have to master load up register b with another value.
And then later, when I'm done, I might want to print the
answer out.
And of course, that might be either simple or complicated.
I'm writing, assuming print is very simple, and
read is very simple.
But in fact, in the real world, those are very
complicated operations, usually much, much larger and
more complicated than the thing you're doing as your
problem you're trying to solve.
On the other hand, I can remember a time when, I
remember using IBM 7090 computer of sorts, where
things like read and write of a single object, a single
number, a number, is a primitive operation of the IO
controller.
OK?
And so we have that kind of thing in there.
And in such a machine, well, what are we really doing?
We're just saying that there's a source over here called
"read," which is an operation which always has a value.
We have to think about this as always having a value which
can be gated into either register a or b.
And print is some sort of thing which when you gate it
appropriately, when you push the button on it, will cause a
print of the value that's currently in register a.
Nothing very exciting.
So that's one sort of thing you might want to have. But
these are also other things that are
a little bit worrisome.
Like I've used here some complicated mechanisms.
What you see here is remainder.
What is that?
That may not be so obvious how to compute.
It may be something which when you open it up, you get a
whole machine.
OK?
In fact, that's true.
For example, if I write down the program for remainder, the
simplest program for it is by repeated subtraction.
Because of course, division can be done by repeated
subtraction of numbers, of integers.
So the remainder of N divided by D is nothing more than if N
is less than D, then the result is N. Otherwise, it's
the remainder when we subtract D from N with respect to D,
when divided by D. Gee, this looks just
like the GCD program.
Of course, it's not a very nice way to do remainders.
You'd really want to use something like binary notation
and shift and things like that in a practical computer.
But the point of that is that if I open this thing up, I
might find inside of it a computer.
Oh, we know how to do that.
We just made one.
And it could be another thing just like this.
On the other hand, we might want to make a more efficient
or better-structured machine, or maybe make use of some of
the registers more than once, or some horrible mess like
that that hardware designers like to do, and
for very good reasons.
So for example, here's a machine that you see, which
you're not supposed to be able to read.
It's a little bit complicated.
But what it is is the integration of the remainder
into the GCD machine.
And it takes, in fact, no more registers.
There are three registers in the datapaths.
OK?
But now there's a subtractor.
There are two things that are tested.
Is b equal to 0, or is t less than b?
And then the controller, which you see over here, is not much
more complicated.
But it has two loops in it, one of which is the main one
for doing the GCD, and one of which is the subtraction loop
for doing the remainder sub-operation.
And there are ways, of course, of, if you think about it,
taking the remainder program.
If I take remainder, as you see over there, as a lambda
expression, substitute it in for remainder over here in the
GCD program, then do some simplification by substituting
a and b for remainder in there, then I
can unwind this loop.
And I can get this piece of machinery by basically, a
little bit of algebraic simplification on the lambda
expressions.
So I suppose you've seen your first very
simple machines now.
Are there any questions?
Good.
This looks easy, doesn't it?
Thank you.
I suppose, take a break.
[MUSIC PLAYING - "JESU, JOY OF MAN'S DESIRING" BY JOHANN
SEBASTIAN BACH]
PROFESSOR: Well, let's see.
Now you know how to make an iterative procedure, or a
procedure that yields an iterative
process, turn into a machine.
I suppose the next thing we want to do is worry about
things that reveal recursive processes.
So let's play with a simple factorial procedure.
We define factorial of N to be if n is 1, the result is 1,
using 1 right now to decrease the amount of work I have to
do to simulate it, else it's times N factorial N minus 1.
And what's different with this program, as you know, is that
after I've computed factorial of N minus 1 here, I have to
do something to the result.
I have to multiply it by N.
So the only way I can visualize what this machine is
doing, because of the fact--
think of it this way, that I have a machine out here which
somehow needs a factorial machine in order to compute
its answer.
But this machine, the outer machine, has to exist before
and after the factorial machine, which is inside.
Whereas in the iterative case, the outer machine doesn't need
to exist after the inner machine is running, because
you never need to go back to the outer
machine to do anything.
So here we have a problem where we have a machine which
has the same machine inside of it, an
infinitely large machine.
And it's got other things inside of it, like a
multiplier, which takes some inputs, and there's a minus 1
box, and things like that.
You can imagine that's what it looks like.
But the important thing is that here I have something
that happens before and after, in the outer machine, the
execution of the inner machine.
So this machine has to have a life.
It has to exist on both times sides of this machine.
So somehow, I have to have a place to store the things that
this thing needs to run.
Infinite objects don't exist in the real world.
What we have to do is arrange an illusion that we have an
infinite object, we have an infinite
amount of hardware somewhere.
Now of course, illusion's all that really matters.
If we can arrange that every time you look at some infinite
object, the part of it that you look at is there, then
it's as infinite as you need it to be.
And of course, one of the things we might want to do,
just look at this thing over here, is the organization that
we've had so far involves having a part of the machine,
which is the controller, which sits right over here, which is
perfectly finite and very simple.
We have some datapaths, which consist of
registers and operators.
And what I propose to do here is decompose the machine into
two parts, such that there is a part which is fundamentally
finite, and some part where a certain amount of infinite
stuff can be kept.
On the other hand this is very simple and really isn't
infinite, but it's just very large.
But it's so simple that it could be cheaply reproduced in
such large amounts, we call it memory, that we can make a
structure called a stack out of it which will allow us to,
in fact, simulate the existence of an infinite
machine which is made out of a recursive
nest of many machines.
And the way it's going to work is that we're going to store
in this place called the stack the information required after
the inner machine runs to resume the operation of the
outer machine.
So it will remember the important things about the
life of the outer machine that will be needed for this
computation.
Since, of course, these machines are nested in a
recursive manner, then in fact the stack will only be
accessed in a manner which is the last thing that goes in is
the first thing that comes out.
So we'll only need to access some little part
of this stack memory.
OK, well, let's do it.
I'm going to build you a datapath now, and I'm going to
write the controller.
And then we're going to execute this to
see how you do it.
So the factorial machine isn't so bad.
It's going to have a register called the value, where the
answer is going to be stored, and a registered called N,
which is where the number I'm taking factorial will be
stored, factorial of.
And it will be necessary in some instances to connect VAL
to N.
In fact, one nice case of this is if I just said over here,
N, because that would be right for N equal 1N.
And I could just move the answer over
there if that's important.
I'm not worried about that right now.
And there are things I have to be able to do.
Like I have to be able to, as we see here, multiply N by
something in VAL, because VAL is the result
of computing factorial.
And I have to put the result back into VAL.
So here we can see that the result of computing a
factorial is N times the result
of computing a factorial.
VAL will be the representation of the answer
of the inner factorial.
And so I'm going to have to have a multiplier here, which
is going to sample the value of N and the value of VAL and
put the result back into VAL like that.
I'm also going to have to be able to see if N is 1.
So I need a light bulb.
And I suppose the other thing I'm going to need to have is a
way of decrementing N. So I'm going to have a decrementer,
which takes N and is going to put back the result into N.
That's pretty much what I need in my machine.
Now, there's a little bit else I need.
It's a little bit more complicated, because I'm also
going to need a way to store, to save away, the things that
are going to be needed for resuming the computation of a
factorial after I've done a sub-factorial.
What's that?
One thing I need is N.
So I'm going to build here a thing called a stack.
The stack is a bunch of stuff that I'm going to write in
sequentially.
I don't how long it is.
The longer it is, the better my illusion of infinity.
And I'm going to have to have a way of getting stuff out of
N and into the stack and vice versa.
So I'm going to need a connection like this, which is
two-way, whereby I can save the value of N and then
restore it some other time through that connection.
This is the stack.
I also need a way of remembering where I was in the
computation of factorial in the outer program.
Now in the case of this machine, it
isn't very much a problem.
Factorial always returns, has to go back to the place where
we multiply by N, except for the last time, when it has to
return to whatever needs the factorial or
go to done or stop.
However, in general, I'm going to have to remember where I
have been, because I might have computed factorial from
somewhere else.
I have to go back to that place and continue there.
So I'm going to have to have some way of taking the place
where the marble is in the finite state controller, the
state of the controller, and storing that in
the stack as well.
And I'm going to have to have ways of restoring that back to
the state of the-- the marble.
So I have to have something that moves the marble to the
right place.
Well, we're going to have a place which is the marble now.
And it's called the continue register, called continue,
which is the place to put the marble next
time I go to continue.
That's what that's for.
And so there's got to be some path from that into the
controller.
I also have to have some way of saving that on the stack.
And I have to have some way of setting that up to have
various constants, a certain fixed number of constants.
And that's very easy to arrange.
So let's have some constants here.
We'll call this one after-fact.
And that's a constant which we'll get into the continue
register, and also another one called fact-done.
So this is the machine I want to build.
That's its datapaths, at least. And it mixes a little
with the controller here, because of the fact that I
have to remember where I was and restore
myself to that place.
But let's write the program now which represents the
controller.
I'm not going to write the define machine thing and the
register list, because that's not very interesting.
I'm just going to write down the sequence of instructions
that constitute the controller.
So we have assign, to set up, continue to done.
We have a loop which says branch if equal 1 fetch N, if
N is 1, then go to the base step of the induction, the
simple case.
Otherwise, I have to remember the things that are necessary
to perform a sub-factorial.
I'm going to go over here, and I have to perform a
sub-factorial.
So I have to remember what's needed after I will
be done with that.
See, I'm about to do something terrible.
I'm about to change the value of N. But this guy has to know
the old value of N. But in order to make the
sub-factorial work, I have to change the value of N. So I
have to remember the old value.
And I also have to remember where I've been.
So I save up continue.
And this is an instruction that says, put
something in the stack.
Save the contents of the continuation register, which
in this case is done, because later I'm going to change
that, too, because I need to go back to
after-fact, as well.
We'll see that.
We save N, because I'm going to need that for later.
Assign to N the decrement of fetch N. Assign continue,
we're going to look at this now, to after, we'll call it.
That's a good name for this, a little bit easier and shorter,
and fits in here.
Now look what I'm doing here.
I'm saying, if the answer is 1, I'm done.
I'm going to have to just get the answer.
Otherwise, I'm going to save the continuation, save N, make
N one less than N, remember I'm going to come back to
someplace else, and go back and start
doing another factorial.
However, I've got a different machine [? in me ?] now.
N is 1, and continue is something else.
N is N minus 1.
Now after I'm done with that, I can go there.
I will restore the old value of N, which is the opposite of
this save over here.
I will restore the continuation.
I will then go to here.
I will assign to the VAL register the product
of N and fetch VAL.
VAL fetch product assign.
And then I will be done.
I will have my answer to the sub-factorial in VAL.
At that point, I'm going to return by going to the place
where the continuation is pointing.
That says, go to fetch continue.
And then I have finally a base step, which is
the immediate answer.
Assign to VAL fetch N, and go to fetch continue.
And then I'm done.
Now let's see how this executes on a very simple
case, because then we'll see the use of this stack to do
the job we need.
This is statically what it's doing, but we have look
dynamically at this.
So let's see.
First thing we do is continue gets done.
The way that happened is I pushed this.
Let's call that done the way I have it.
I push that button.
Done goes into there.
Now I also have to set this thing up to
have an initial value.
Let's consider a factorial of three, a simple case.
And we're going to start out with our stack
growing over here.
Stacks have their own little internal state saying where
they are, where the next place I'm going to write is.
So now we say, is N 1?
The answer is no.
So now I'm going to save continue, bang.
Now that done goes in here.
And this moves to here, the next place I'm going to write.
Save N 3.
OK?
Assign to N the decrement of N. That means
I've pushed this button.
This becomes 2.
Assign to continue aft.
So I've pushed that button.
Aft goes in here.
OK, now go to loop, bang, so up to here.
Is N 1?
No.
So I have to save continue.
What's continue?
Continue is aft.
Push this button.
So this moves to here.
I have to save N. N is over here.
I got to 2.
Push that button.
So a 2 gets written there.
And then this thing moves down here.
OK, save N. Assign N to the decrement of N.
This becomes a 1.
Assign continue to aft.
A-F-T gets written there again.
Go to loop.
Is N equal to 1?
Oh, yes, the answer is 1.
OK, go to base step.
Assign to VAL fetch of N. Bang, 1 gets put in there.
Go to fetch continue.
So we look in continue.
Basically, I'm pushing a button over here that goes to
the controller.
The continue becomes aft, and all of a sudden, the program's
running here.
I now have to restore the outer version of factorial.
So we go here.
We say, restore N. So restore N means take the contents
that's here.
Push this button, and it goes into here, 2, and the
pointer moves up.
Restore continue, pretty easy.
Go push this button.
And then aft gets written in here again.
That means this thing moves up.
I've gotten rid of something else on my stack.
Right, then I go to here, which says, assign to VAL the
product of N an VAL.
So I push this button over here, bang.
2 times 1 gives me a 2, get written there.
Go to fetch continue.
Continue is aft.
I go to aft.
Aft says restore N. Do your restore N, means I take the
value over here, which is 3, push this up to here, and move
it into here, N. Now it's pushing that button.
The next thing I do is restore continue.
Continue is now going to become done.
So this moves up here when I push this button.
Done may or may be there anymore, I'm not interested,
but it certainly is here.
Next thing I do is assign to VAL the product of the fetch
of N and the fetch of VAL.
That's pushing this button over here, bang.
2 times 3 is 6.
So I get a 6 over here.
And go to fetch continue, whoops, I go to
done, and I'm done.
And my answer is 6, as you can see in the VAL register.
And in fact, the stack is in the state it
originally was in.
Now there's a bit of discipline in using these
things like stacks that we have to be careful of.
And we'll see that in the next segment.
But first I want to ask if there are any
questions for this.
Are there any questions?
Yes, Ron.
AUDIENCE: What happens when you roll off the end of the
stack with--
PROFESSOR: What do you mean, roll off of?
AUDIENCE: Well, the largest number--
a larger starting point of N requires more memory, correct?
PROFESSOR: Oh, yes.
Well, I need to have a long enough stack.
You say, what if I violate my illusion?
AUDIENCE: Yes.
PROFESSOR: Well, then the magic doesn't work.
The truth of the matter is that every machine is finite.
And for a procedure like this, there's a limit to the number
of sub-factorials I could have.
Remember when we were doing the y-operator a while ago, we
pointed out that there was a sequence of exponentiation
procedures, each of which was a little better than the
previous one.
Well, we're now seeing how we implement that
mathematical idea.
The limiting process is only so good as as far as
you take the limit.
If you think about it, what am I using here?
I'm using about two pieces of memory for every recursion of
this process.
If we try to compute factorial of 10,000, that's
not a lot of memory.
On the other hand, it's an awful big number.
So the question is, is that a valuable thing in this case.
But it really turns out not to be a terrible limit, because
memory is el cheapo, and people are pretty expensive.
OK, thank you, let's take a break.
[MUSIC PLAYING - "JESU, JOY OF MAN'S DESIRING" BY JOHANN
SEBASTIAN BACH]
PROFESSOR: Well, let's see.
What I've shown you now is how to do a simple iterative
process and a simple recursive process.
I just want to summarize the design of simple machines for
specific applications by showing you a little bit more
complicated design, that of a thing that does doubly
recursive Fibonacci, because it will indicate to us, and
we'll understand, a bit about the conventions required for
making stacks operate correctly.
So let's see.
I'm just going to write down, first of all, the program I'm
going to translate.
I need a Fibonacci procedure, it's very simple, which says,
if N is less than 2, the result is N, otherwise it's
the sum of Fib of N minus 1 and Fib of N minus 2.
That's the plan I have here.
And we're just going to write down the
controller for such a machine.
We're going to assume that there are registers, N, which
holds the number we're taking Fibonacci of, VAL, which is
where the answer is going to get put, and continue, which
is the thing that's linked to the controller, like before.
But I'm not going to draw another physical datapath,
because it's pretty much the same as the
last one you've seen.
And of course, one of the most amazing things about
computation is that after a while, you build up a little
more features and a few more features, and all of the
sudden, you've got everything you need.
So it's remarkable that it just gets there so fast. I
don't need much more to make a universal computer.
But in any case, let's look at the controller for the
Fibonacci thing.
First thing I want to do is start the thing up by assign
to continue a place called done, called Fib-done here.
So that means that somewhere over here, I'm going to have a
label, Fib-done, which is the place where I go when I want
the machine to stop.
That's what that is.
And I'm going to make up a loop.
It's a place I'm going to go to in order to start up
computing a Fib.
Whatever is in N at this point, Fibonacci will be
computed of, and we will return to the place specified
by continue.
So what you're going to see here at this place, what I
want here is the contract that says, I'm going to write this
with a comment syntax, the contract is N contains arg,
the argument.
Continue is the recipient.
And that's where it is.
At this point, if I ever go to this place, I'm expecting this
to be true, the argument for computing the Fibonacci.
Now the next thing I want to do is to branch.
And if N is less than 2--
by the way, I'm using what looks like Lisp syntax.
This is not Lisp.
This does not run.
What I'm writing here does not run as a simple Lisp program.
This is a representation of another language.
The reason I'm using the syntax of parentheses and so
on is because I tend to use a Lisp system to write an
interpreter for this which allows me to simulate the
machine I'm trying to build.
I don't want to confuse this to think that
this is Lisp code.
It's just I'm using a lot of the pieces of Lisp.
I'm embedding a language in Lisp, using Lisp as pieces to
make my process of making my simulator easy.
So I'm inheriting from Lisp all of its properties.
Fetch of N 2, I want to go to a place
called immediate answer.
It's the base step.
Now, that's somewhere over here, just above done.
And we'll see it later.
Now, in the general case, which is the part I'm going to
write down now, let's just do it.
Well, first of all, I'm going to have to
call Fibonacci twice.
In each case--
well, in one case at least, I'm going to have to know what
to do to come back and do the next one.
I have to remember, have I done the first Fib, or have I
done the second one?
Do I have to come back to the place where I do the second
Fib, or do I have to come back to the place
where I do the add?
In the first case, over the first Fibonacci, I'm going to
need the value of N for computing for the second one.
So I have to store some of these things up.
So first I'm going to save continue.
That's who needs the answer.
And the reason I'm doing that is because I'm about to assign
continue to the place which is the place I
want to go to after.
Let's call it Fib-N-minus-1, big long name,
classic Lisp name.
Because I'm going to compute the first Fib of N minus 1,
and then after that, I want to come back and
do something else.
That's the place I want to go to after I've done the first
Fibonacci calculation.
And I want to do a save of N, because I'm going to need it
later, after that.
Now I'm going to, at this point, get ready to do the
Fibonacci of N minus 1.
So assign to N the difference of the fetch of N and 1.
Now I'm ready to go back to doing the Fib loop.
Have I satisfied my contract?
And the answer is yes.
N contains N minus 1, which is what I need.
Continue contains a place I want to go to when I'm done
with calculating N minus 1.
So I've satisfied the contract.
And therefore, I can write down here a label,
after-Fib-N-minus-1.
Now what am I going to do here?
Here's a place where I now have to get ready to do
Fib of N minus 2.
But in order to do a Fib of N minus 2, look, I don't know.
I've clobbered my N over here.
And presumably my N is counted down all the way to 1 or 0 or
something at this point.
So I don't know what the value of N in the N register is.
I want the value of N that was on the stack that I saved over
here so that could restore it over here.
I saved up the value of N, which is this value of N at
this point, so that I could restore it after computing Fib
of N minus 1, so that I could count that down to N minus 2
and then compute Fib of N minus 2.
So let's restore that.
Restore of N.
Now I'm about to do something which is superstitious, and we
will remove it shortly.
I am about to finish the sequence of doing the
subroutine call, if you will.
I'm going to say, well, I also saved up the continuation,
since I'm going to restore it now.
But actually, I don't have to, because I'm not
going to need it.
We'll fix that in a second.
So we'll do a restore of continue, which is what I
would in general need to do.
And we're just going to see what you would call in the
compiler world a peephole optimization, which says,
whoops, you didn't have to do that.
OK, so the next thing I see here is that I have to get
ready now to do Fibonacci of N minus 2.
But I don't have to save N anymore.
The reason why I don't have to save N anymore is because I
don't need N after I've done Fib of N minus 2, because the
next thing I do is add.
So I'm just going to set up my N that way.
Assign N minus difference of fetch N and 2.
Now I have to finish the setup for calling
Fibonacci of N minus 2.
Well, I have to save up continue and assign continue,
continue, to the place which is after Fib N 2, that place
over here somewhere.
However, I've got to be very careful.
The old value, the value of Fib of N minus 1, I'm going to
need later.
The value of Fibonacci of N minus 1, I'm going to need.
And I can't clobber it, because I'm going to have to
add it to the value of Fib of N minus 2.
That's in the value register, so I'm going to save it.
So I have to save this right now, save up VAL.
And now I can go off to my subroutine, go to Fib loop.
Now before I go any further and finish this program, I
just want to look at this segment so far and see, oh
yes, there's a sequence of instructions here, if you
will, that I can do something about.
Here I have a restore of continue, a save of continue,
and then an assign of continue, with no other
references to continue in between.
The restore followed by the save
leaves the stack unchanged.
The only difference is that I set the continue register to a
value, which is the value that was on the stack.
Since I now clobber that value, as in it was never
referenced, these instructions are unnecessary.
So we will remove these.
But I couldn't have seen that unless I had
written them down.
Was that really true?
Well, I don't know.
OK, so we've now gone off to compute
Fibonacci of N minus 2.
So after that, what are we going to do?
Well, I suppose the first thing we have to do--
we've got two things.
We've got a thing in the value register
which is now valuable.
We also have a thing on the stack that can be restored
into the value register.
And what I have to be careful with now is I want to shuffle
this right so I can do the multiply.
Now there are various conventions I might use, but
I'm going to be very picky and say, I'm only going to restore
into a register I've saved from.
If that's the case, I have to do a shuffle here.
It's the same problem with how many hands I have. So I'm
going to assign to N, because I'm not going to need N
anymore, N is useless, the current value of VAL, which
was the value of Fib of N minus 2.
And I'm going to restore the value register now.
This restore matches this save. And if you're very
careful and examine very carefully what goes on,
restores and saves are always matched.
Now there's an outstanding save, of course, that we have
to get rid of soon.
And so I restored the value register.
Now I restore the continue one, which matches this one,
dot, dot, dot, dot, dot, dot, dot, down to here, restoring
that continuation.
That continuation is a continuation of Fib of N,
which is the problem I was trying to solve, a major
problem I'm trying to solve.
So that's the guy I have to go back to who wants Fib of N. I
saved them all the way up here when I realized N was
not less than 2.
And so I had to do a complicated operation.
Now I've got everything I need to do it.
So I'm going to restore that, assign to VAL the sum of fetch
VAL and fetch of N, and go to continue.
So now I've returned from computing Fibonacci of N, the
general case.
Now what's left is we have to fix up a few details, like
there's the base case of this induction, immediate answer,
which is nothing more than assign to VAL fetch of N,
because N was less than 2, and therefore, the answer is N in
our original program, and return continue--
bobble, bobble almost--
and finally Fib done.
So that's a fairly complicated program.
And the reason I wanted you see to that is because I want
you to see the particular flavors of stack discipline
that I was obeying.
It was first of all, I don't want to take anything that I'm
not going to need later.
I was being very careful.
And it's very important.
And there are all sorts of other disciplines people make
with frames and things like that of some sort, where you
save all sorts of junk you're not going to need later and
restore it because, in some sense, it's easier to do that.
That's going to lead to various disasters, which we'll
see a little later.
It's crucial to say exactly what you're
going to need later.
It's an important idea.
And the responsibility of that is whoever saves something is
the guy who restores it, because he needs it.
And in such discipline, you can see what things are
unnecessary, operations that are unimportant.
Now, one other thing I want to tell you about that's very
simple is that, of course, the picture you see is not the
whole picture.
Supposing I had systems that had things like other
operations, CAR, CDR, cons, building a vector and
referencing the nth element of it, or things like that.
Well, at this level of detail, whatever it is, we can
conceptualize those as primitive
operations in the datapath.
In other words, we could say that some machine that, for
example, has the append machine, which has to do cons
of the CAR of x with the append of the CDR of x and y,
well, gee, that's exactly the same as
the factorial structure.
Well, it's got about the same structure.
And what do we have?
We have some sort of things in it which may be registers, x
and y, and then x has to somehow move to y sometimes, x
has to get the value of y.
And then we may have to be able to do
something which is a cons.
I don't remember if I need to like this is in this system,
but cons is sort of like subtract or add or something.
It combines two things, producing a thing which is the
cons, which we may then think goes into there.
And then maybe a thing called the CAR, which will produce--
I can get the CAR or something.
And maybe I can get the CDR of something, and so on.
But we shouldn't be too afraid of saying things this way,
because the worst that could happen is if we open up cons,
what we're going to find is some machine.
And cons may in fact overlap with CAR and CDR, and it
always does, in the same way that plus and minus overlap,
and really the same business.
Cons, CAR, and CDR are going to overlap, and we're going to
find a little controller, a little datapath, which may
have some registers in it, some stuff like that.
And maybe inside it, there may also be an infinite part, a
part that's semi-infinite or something, which is a lot of
very uniform stuff, which we'll call memory.
And I wouldn't be so horrified if that were the way it works.
In fact, it does, and we'll talk about that later.
So are there any questions?
Gee, what an unquestioning audience.
Suppose I tell you a horrible pile of lies.
OK.
Well, thank you.
Let's take our break.
[MUSIC PLAYING - "JESU, JOY OF MAN'S DESIRING" BY JOHANN
SEBASTIAN BACH]