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 all the 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. Me, missing a lot of more sophisticated features that you'd find in many computers, and then you'll have this context for the, for the HACK machine language. So, the first important thing to remember is that the machine language is a most important interface probably in the whole world of computer science. It is the interface between hardware are software, it's ex, ex,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? What does, that, what, where does it get the data that it operates on, what is the control of the operations, and so on. In principle, and usually, these, this kind of interface is done in a almost one to one correspondence with the actual hardware implementation. The idea is that the hardware is built in a way that directly corresponds to the 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 to, just so the compiler writers will be happy about the codes they need to emit. And the hardware will be eh, another layer removed from it. But we're not going to talk about it. And in first, 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 a cost-performance tradeoff. The more sophisticated operations that we want to give our machine language, the more large data types, or sophisticated data types it 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 tradeoff. So let's talk about the type of operations that our hardware can perform. Each machine language defines a set of operations and these fall into several categories. For example, the 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 the operations that control the flow control. That tell the, the hardware when to jump inside the program. So these are the type of instruction that we usually will have in any machine language. And different machine languages define different sets, sets of such operations which may defer from each other in terms of their richness. For example, some machines may allow, 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 is the question, what data types do, 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 our hardware that performs in one operation. In addition of 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 a, do also provide richer data types. For example, you may have a, 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 this kind of flow, 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 and 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 a 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 work to operate on, requires a large amount of just as many bits to specify it. Because you need to give 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 oh, add the last two numbers, I'm, can't, 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, a large amount of time. Com, compared to the state of 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 a, what, what type of data are we working on. Without requiring all these costs of specifying the large address and getting the information from a far away place if you wish. In terms of time, the basic solution was whats called a memory hierarchy. And this was already figured out by 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 have to specify large address space because there 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 is going to be slightly larger memories, usually called cache, and even larger memories, sometimes called the big, the main memory. And maybe even, even larger memories that are going to sit on disk. At each time we get farther away from the arithmetic unit itself, our memory be, gets bigger. Accessing it becomes harder borth, both in terms of giving a larger, a wider address. And in terms of the time we need to wait until we get the value. But we have more information there. The ways 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, the smallest memory that usually resides really inside the CPU, and how we handle that. So eh, almost every CPU has a few, very small amount of memory registers that are located really inside the CPU. Their type and functionality's really part of the machine language. And the main point is that since there are so few of them, everything then requires very few bits, and getting the information of them is extremely quickly. They are built from the fastest technology available and it's, they are already inside the CPU, so there is no delay in getting in, any information from there. So, what types of registers do we have? The first kind of things 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 contents of register 2, if that is the meaning of this operation in our machine language. So, once we have a vi, 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, with 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 at which part of the re of the bigger memory we want to access for operations if we want to access. For example, if we have a operation like store register 1 into memory address that is specified by that register called A. Then, what will happen is, once we, once we actually perform the hardware, perform 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 us, an operation, let's say a simple add operation, what is it supposed to operate upon? And there are a bunch of different possibilities. Here are base, here are four possibilities. These are sometimes called addressing modes, and there, 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, it's 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. This is a example we had previously for using the A register where the at memory address that we access is not specified as part of 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, 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 computers which values, which data to work on in each instruction. While were talking about how to where to take the data from, and where to put it. We might also add something that is usually piggybacked at upon it, which is how do we deal with input and output in most machine languages? So as we all know, there are an enormous amount of input and output devices. Printers and screens and sens, 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, that last movement is written into some kind of a register. And that register is accessible by the computer in a pre, in a certain address as part of the memory. This gives us access to input and output as though we are accessing the memory itself and of course the software that actually deals with this. Software that are 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 I 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 a time to jump to another part of the program. So, this will be what sometime is called an unconditional jump. And one of the main uses for it will be actually be to do a loop. Suppose we want to actually eh, start and do something for values one, two, three, four, five, six 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 to let's say, if that is the register 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 oh, after a, after you do a, instruction number 156 in our example. Don't continue to 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 for, 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 actually addresses, 101, 102 and 156 are not really that important. What is really important was that when we jumped to address 102, we need it to be the address that we're actually meaning. So, we could have, do this in a symbolic manner in the same, in the following way. Just give a name to important locations. For example, location 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, eh which we need to handle flow control, where we need to do what is sometimes called unconditional jump. In some cases, we want to jump to another location. While in other cases, depending on let's say the last 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 and 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, 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 the number is negative, I want to first turn it into to 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 is greater than 0, I want you to jump to the label cont. This is just a number, just a name that I made up. Otherwise, just continue. This means that if we have a positive number, then we are not going to do the next instruction, the subtract instruction. But if we have a negative number, we're just going to continue directly to do the subtract instruction. Which really does eh, takes R1 and negates its value, makes it positive. Now, in both cases, we are continuing with the same sequence of instructions. And now, we already have an R1 anyway of positive value. So this is another, is an example why we sometimes will need the conditional jump. And machine languages always have some kind of a, some kind of a practice, some kind of a way to actually instruct the hardware to do these kind of conditional jump. At this point, we've finished very, very quick, very high level, very basic overview of the type of instructions that machine languages provide. And now we're ready to actually talk about our computer, the HACK machine language.