Topics covered: Register Machines
Instructors: Hal Abelson and Gerald Jay Sussman
Subtitles for this course are provided through the generous assistance of Henry Baker, Hoofar Pourzand, Heather Wood, Aleksejs Truhans, Steven Edwards, George Menhorn, and Mahendra Kumar.
[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?
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]