In the previous unit we talked about the general idea of a machine language and how they control computers. What we want to do in this unit is discuss basic elements that appear in all machine languages. The main reason is to give you some context for what you will learn about the Hack machine language in the next unit. Because most of the elements that you find in the Hack machine language are of course elements that appear in some way, in some form, in other machine languages. So, we first start with the general description of the kind of elements you find in machine languages. A very elementary description, that is, missing a lot of more sophisticated features that you'd find in many computers. And then you'll have this context for the Hack machine language. So, the first important thing to remember is that the machine language is the most important interface probably in the whole world of computer science. It is the interface between hardware and software, it is exactly the way that software can control the hardware. This kind of machine language needs to specify what are the operations that our hardware performs, where does it get the data that it operates on, what is the control of the operations, and so on. In principle and usually, this kind of interface is done in almost a one-to-one correspondence with the actual hardware implementation. The idea is that the hardware is built in a way that directly corresponds to type of functionality that it provides to the software layer above it. This need not happen always. Sometimes you can want to provide nice functionality even at this level, even just so the compiler writers will be happy about the code they need to omit, and the hardware will be another layer removed from it. But we're not going to talk about it. And then, from first principles, basically the machine language specifies exactly the kind of things that the hardware can do for us. Of course, when we actually go to design a machine language, the basic element is the cost-performance trade-off. The more sophisticated operation that we want to give our machine language, the more large data types or sophisticated data types that operates on, the more costly it will be to actually build this. Costly in terms of area of silicon, costly in terms of time that the hardware actually needs to operate, and so on. Of course, in our computer, we're always taking this kind of trade-off to the simplest. We're trying to present the simplest kind of thing and not really worrying about real performance. But in any real machine, the whole thing that drives the design of the machine language is a cost-performance trade-off. So let's talk about the type of operations that our hardware can perform. Each machine language defines the set of operations, and these fall into several categories. For example, there are arithmetic operations, for example, addition of two numbers, subtraction, maybe also multiplication or division. There are logical operations, for example, taking the AND of two bits, or maybe the bitwise AND of two words. And then there are also operations that control the flow control, that tell the hardware when to jump inside the program. So these are the type of instructions that we usually will have in any machine language. And different machine languages define different sets of such operations, which may differ from each other in terms of their richness. For example, some machines may provide division as a basic operation, while other machines will decide not to do that because it's too expensive in terms of silicon. But rather the software will, of course, have to provide that functionality. Probably even more important in the question, what data types can our hardware access primitively? So there's a big difference of course, between adding 8-bit numbers and adding 64-bit numbers. If your software program really needs arithmetic on 64-bit values, then of course, hardware that performs in one operation, addition, of such 64-bit values will be at least eight times faster than hardware that needs to actually implement addition of 64-bit values by a sequence of additions of 8-bit values. Similarly, some computers can also provide richer data types. For example, you may have your hardware support directly floating point operations, numbers that are not integer, but rather, real numbers. And deal with them, provide addition, multiplication, division of them, as a basic operation. If you want to do scientific computation which works with these kind of floating point numbers or real numbers rather than just integers, of course such machines will be much faster than machines that can only handle integers basically. While the difference in set of operations that we've seen previously is quite obvious, the next issue is probably even more important, although slightly more subtle. And that is the question, how do we decide what data to work on? How does the hardware allow us to specify what type of data values are we going to work on? The basic problem that we have here is that what we're going to work on resides in memory, and accessing memory is an expensive operation. It's expensive in at least two related points of view. First of all, if you have a large memory, specifying what part of the memory do you want to operate on, requires a large amount of just as many bits to specify it, because you need to give in an address in a very large memory. And that's going to be wasteful in terms of the instruction. If I just want to say, add the last two numbers, I won't be able to just do that, because I will have to specify two very large addresses in order to tell the hardware what to operate on. The second element, which is closely related, is the fact that just accessing a value from a large memory takes relatively large amount of time compared to the speed of the CPU itself, of the arithmetic operations themselves. So the way to handle these two things, the way to give us good control over what type of data are we working on without requiring all these costs of specifying a large address. And getting the information from a far-away place, if you wish, in terms of the time, the basic solution was what's called the memory hierarchy. And this was already figured out by von Neumann when he built the first computer. The basic idea is, instead of having just one large block of memory, we're going to have a whole sequence of memories that are getting bigger and bigger. The smallest memories are going to be very easy to access, first of all, because we don't want to have to specify large address space, because they are only going to be a very few of them. Second of all, because there are only very few of them, we can actually get information from them very quickly. And then there are going to be slightly larger memories, usually called cache. And even larger memories, sometimes called the main memory, and maybe even even larger memories that are going to sit on disk. And each time we get farther away from the arithmetic unit itself, our memory gets bigger. Accessing it becomes harder, both in terms of giving a larger, a wider address and in terms of the time we need to wait until we get a value. But we have more information there. The way that the different levels of the memory hierarchy are handled differs according to the different levels. But what we're going to discuss now is the way that registers, the smallest memory that usually resides really inside the CPU, how we handle that. So almost every CPU has a few very small amount of memory registers that are located, really, inside the CPU. Their type and functionality is really part of the machine language. And the main point is that since there are so few of them, everything requires very few bits, and getting the information of them is extremely quickly. They are built from the fastest technology available, and they are already inside the CPU, so there is no delay in getting any information from there. So what types of registers do we have? The first kind of thing that we will do with these memory location registers that are inside our CPU is just use them for data. For example, we can put numbers in them and have operations saying something like add the Register 1 to Register 2. In this situation, basically what will happen is, the contents of Register 1 will be added to the content of Register 2, if that is the meaning of this operation in our machine language. So once we have a small number of registers inside the CPU, we can do lots of operations on a small amount of memory very, very quickly. The second kind of things we do within these registers is use them as addresses. We can also sometimes put into one of these registers an address into main memory, which will allow us to specify which part of the bigger memory we want to access for operations that we want to access. For example, if we have an operation like store Register 1 into memory address that is specified by that register called A. Then what will happen is once the hardware performs this operation, that number 77 will be written into the main memory. This can be an operation that takes a larger amount of time than internal operation to the CPU, but the important point is that the address into which we write this information was actually given by the A register. This is another type of usage we have for registers that are inside our main memory. Once we have these registers, now we can think about, go back to the original question. How do we decide which data to work upon? How do we tell the computer for, it's an operation, let's say it's a simple add operation. What is it supposed to operate upon? And there are a bunch of different possibilities, here are four possibilities. These are sometimes called addressing modes, and some computers have other possibilities as well. Sometimes we just want to work on these registers, so we, for example, we can say add Register 1 to Register 2, and this means, of course, that the addition operation is on two registers. Sometimes we have direct access to memory. We can have an operation saying, add Register 1 to Memory Location 200. In which case we're telling the computer to directly address not just the Register 1, but also a memory address that we just specified inside the command. Yet another possibility is what's called indirect addressing, and this is the example we had previously for using the A register. Where the @ memory address that we access is not specified as part of the instruction, but rather is already written inside the address register that already was previously loaded inside the CPU with some correct value. And yet another possibility is that we actually have a value inside the instruction itself. For example, we can say add 73 to Register 1 and 73 is a constant 73 is part of the instruction. So all these are different ways we can actually tell the computer which values, which data, to work on in each instruction. While we're talking about where to take the data from and where to put it, we might also add something that usually piggybacks upon it. Which is how do we deal with input and output in most machine languages. So, as we all know, there are enormous amount of input and output devices, printers and screens and various sensors and keyboards and mice and mouses and so on. So, one way to actually access these input or output devices is to actually connect them, connect the registers which control them, these output devices, as part of your memory. For example, we may have a mouse that is connected in a way that whenever the user moves the mouse, the last movement is written into some kind of a register. And that register is accessible by the computer in a certain address, as part of the memory. This gives us access to input and output as though we're accessing the memory itself. And of course, the software that actually deals with this, software that is usually part of the drivers in an operating system. Must know exactly not only what are the addresses to which this input or output device is connected, but also how to speak with it. What does the values in that locations, what do they really mean? The final element that we wish to discuss is what's called flow control. How can we tell the hardware what instruction to execute next? So usually it's very simple. Usually if I now was in instruction 73, the next instruction will be 74, that's a normal situation. But there are situations where, of course, we need to change the flow of control, and not just continue doing instructions one after another. The first reason is, sometimes we just need to go back to a previous, to another location. For example, doing a loop, or maybe jumping to another part of the software. Just because now is the time to jump to another part of the program. So this would be what sometimes is called an unconditional jump, and one of the main uses for it will actually be to do a loop. Suppose we want to actually start and do something for values 1, 2, 3, 4, 5, 6, and so on. The way we do it will have some kind of register holding these values. Each time we want to add 1 to R1, and let's say if that is the register that we chose to actually have this value. And then we do whatever we need to do with this new value. Now we next want to do the same thing with the next value of R1. The way we do it, we have to tell the computer, after you do instruction number 156 in our example, don't continue doing instruction 157. But go back to instruction 102, which basically adds 1 to R1, giving us the next value of R1, and then continue doing whatever you did with the new value. So this allows you to do a loop. And we should, and machine languages always have some kind of capability, that the software tells the hardware to do something again, or to return back, or to jump to a different direction. Notice that the actual addresses 101, 102, and 156 are not really that important. What is really important was that when we jump to address 102, we need it to be the address that we are actually meaning. So we could do this in a symbolic manner in the following way. Just give a name to important locations. For example, location at 102, I give it the name loop, and then I say jump to loop. This doesn't really change anything, this is exactly the same thing when we actually write it in bits in our machine language. But it's more convenient for humans to look at, so we'll just do that as part of the way of describing programs. Then there are other cases, which we need to handle flow control, where we need to do what is sometimes called a conditional jump. In some cases, we want to jump to another location, while in other cases, depending on, let's say, the last instruction that we performed. Or according to the value of some register, or according to some other condition, in some cases, we want to jump to another location. In other cases, we just want to continue for the next instruction, and this is called a conditional jump. For example, suppose I want to do something on the absolute value of a number. So if the number is positive, I just want to do some operation on the number, but if our number is negative, I want to first turn it into be positive, and then work on the positive version of it. The way we can do that, we can have a conditional jump. Jump greater than, which means if R1 > 0, I want you to jump to the label cont. It's just a name that I made up, otherwise, just continue. This means that if we have a positive number, then we're not going to do the next instruction, the subtract instruction. But if we have a negative number, we'll just going to continue directly to do the subtract instruction, which really takes R1 and negates its value, makes it positive. Now, in both cases we're continuing with the same sequence of instructions. And now we already have an R1 anyway, a positive value. So this is another example why we sometimes would need the conditional jump. And machine languages always have some kind of apparatus, some kind of way, to actually instruct the hardware to do this kind of conditional jump. At this point we've finished a very, very quick, very high-level, very basic overview of the type of instructions that machine languages provide. And now we are ready to actually talk about our computer, the Hack machine language.