16Programs
Last chapter the machine finally started running itself. A ROM read the situation and raised the control lines, fetching and executing with no hand on the panel. And I left you standing on a small cliff. The control unit decodes bytes it pulls from RAM. But the numbers those instructions add and store are also just bytes in RAM. Same wires, same cells, same 0s and 1s. So which ones are the program, and which ones are the data? How does the machine tell them apart? Stop there and answer it before you read the next line. You wired every path in this thing yourself, so name the wire that checks. Take the guess seriously. It costs you nothing, and it is about to be worth a lot. Ready? Here is the answer, and it's the vertiginous idea this whole chapter turns on: it can't, because there is no difference. A byte has no type stamped on it. "Instruction" and "number" are not properties the bits carry. They're only ever how something chose to read them. The machine treats a cell as an instruction for exactly one reason: the program counter happened to point at it during a fetch. Aim a load at that same cell and the identical byte is now a number to add. If that reads like a word game to you, hold on to the suspicion. Section 01 is where you try to catch me out, with a real byte in your hands, and fail. This is the stored-program idea — von Neumann's — and it's the reason the gibberish you couldn't read twelve chapters ago can run Fibonacci today. The bits never changed. You built the interpreter. In this chapter we turn an idea into a real program. We hand-assemble it down to bytes, deposit it into memory with our own fingers, and watch it run. And by the end the machine you've been building will finally get its proper name: SAP-1.
01The bytes never changed
Let's stare straight at the thing that should feel impossible. Here is a single RAM cell — eight bits, one byte, nothing more. It holds 00010101. Before you read on, commit to an answer: is that a number or an instruction? Pick one. Now watch both answers be right. Read it one way and it's a plain number: add up the place-values the way you have since Chapter 4, and you get 21. Read it the other way and it's an instruction. Split it into a 4-bit opcode and a 4-bit operand — 0001 and 0101 — and it becomes a command like "load from address 5." Same bits. Both readings are complete, and neither one is the real one. So the question I just asked has no answer. The machine cannot ask it either. Nothing in this hardware ever inspects a byte to find out what kind it is. Flip the reading lens below and watch one byte wear both faces at once.
00010101 — with no label saying what they mean. Read them as place values and they sum to the number 21; split them 4 + 4 and the top nibble is an opcode (0001 = LDA, “load”) and the bottom nibble its operand (0101 = 5), the command LDA 5. Flip the lens and click the bits: nothing in the byte changes, only how you choose to read it. That is von Neumann's idea made visible — in one memory, code and data are the very same thing.Sit with that. The byte did not change when you flipped the lens. You did. There is nothing inside those eight bits that says "I am code" or "I am data." The type isn't in the cell. It's in the act of reading. So the honest question isn't "which cells are instructions?" It's "who's reading, and how?"
And the reader is the machine, with exactly one thing that decides a byte's fate: the program counter. When the PC points at a cell and the fetch step pulls it into the instruction register, that byte becomes code. It gets split into opcode and operand and executed. But when a load instruction names that same cell as its operand, the byte rides the bus into the accumulator and lands there as a plain number. Notice where it does not go: the ALU sits downstream of A and B, adding what's already in them, and a load never reaches it. Two roads out of one cell, and the byte has no say in which one it takes. Point the PC at the cell below, then point a load at it, and watch the identical byte take two completely different roads.
So the program counter is the whole distinction. "Code" is just "the bytes the PC happened to walk through." Nothing more mystical than that. This is also why a wild jump into the middle of your data will happily start executing your numbers as instructions. And it's why a bug that runs off the end of the program is so dangerous. The machine never hesitates, because it can't tell it's left the code behind.
There's one more layer to peel, and it kills the last trace of "bytes have meaning." Even as an instruction, a byte only means something relative to this particular machine's opcode table. Feed the exact same eight bits to a CPU with a different architecture, and they decode to a completely different command. That opcode table has a name. The agreement about which nibble means which command is the whole of what engineers call a machine's instruction set architecture (ISA). No deep principle, no committee: different machines simply shipped different tables, and each one's table is its ISA. So meaning isn't in the bits. It's in the decoder you read them against. Send one byte through two different opcode maps and watch it say two different things.
1011 0101 (0xB5) never changes — opcode nibble 0xB, operand nibble 0x5. Plug that opcode into ISA α's table and 0xB reads SUB: subtract register 5 from the accumulator. Swap in ISA β's table — same wire, same voltage, same 0xB — and it reads OUT: write the accumulator to port 5. Nothing about the bits carried an instruction; the table you decoded them against supplied all of it. That's why a compiled binary for one machine is gibberish on another — machine code isn't universal, because meaning was never in the bits. It's in the opcode table you read them against.That's the full weight of the stored-program principle. It's worth naming loudly, because most people never hear it said plainly: code and data live in the same memory, made of the same bytes, and meaning comes only from how they're read. John von Neumann's name is on it. Everything else in this chapter is us using that freedom. We write bytes that are numbers and bytes that are commands into one shared RAM, and let the program counter sort them out.
02From an idea to bytes
Before we can write a program, we need something worth writing: an algorithm. That's a finite recipe that turns an input into an output in a definite number of steps. But here's the part textbooks state and then walk past. A recipe that's only a straight list — do this, then this, then this — can add two numbers. But it can never loop and never choose. What lifts a list into a real algorithm is one extra ingredient hiding in the formal definition: a conditional diversion, a point where the path forks on a test. And you have already built that fork. It's the JZ / JC branch from Chapter 14. Watch the two kinds of step side by side.
That's the cross-domain lock worth carrying out of here. The "if the condition holds, jump elsewhere" in a mathematician's definition of an algorithm, and the conditional jump in your CPU, are the same idea in two languages. And you have both in silicon. But be exact about what that buys. A straight list can only recite. One conditional jump lets it decide and repeat. That is the whole shape of computation. Add unbounded memory to sequence plus branch and you have a universal machine. Yours has sixteen cells. So you have the shape, and not the reach.
Now, we don't want to think in raw opcodes while we design. So we write the plan in pseudocode first — hardware-independent, human-readable — then translate down. Each mnemonic like LDA, ADD, STA, JMP maps to exactly one 4-bit opcode, and its argument becomes the 4-bit operand. Exactly one, and the name is nailed down just as hard: LDA is opcode 1 in this machine's table, always, and it answers to no other name. That translation is mechanical enough that a program can do it — a program called the assembler. Step through one line at a time and watch pseudocode collapse into mnemonics, then into the actual bytes.
LOAD is always 1, ADD is always 2, because a pen (a program) is just checking a row and copying a number. That's the whole trick behind writing in mnemonics instead of raw bytes — the assembler is a lookup table with a pen, one line in, one byte out.Notice what the assembler is — and, more importantly, what it isn't. It's not intelligent. It's a dumb, deterministic hand-map: look up the mnemonic, glue its opcode to its operand, emit one byte. One line in, one byte out. There's no optimization and no understanding. It's just a lookup table with a pen. One honest asterisk on that, because you'll hit it the moment you write a jump: a name can point forward. Write JMP top and the pen hasn't reached top yet, so it can't know the address. So it reads the listing through once, noting where every name landed, and only then goes back and writes the bytes. Two passes, still no thinking. (That's the seed of every compiler you'll ever meet: layers of exactly this, stacked.)
One word we've been leaning on without defining, and I owe you a clean definition. A variable is not a mysterious container. It is simply a named address in RAM that we've agreed will hold a value that changes over time. When the pseudocode says a, the assembler just picks a free cell — say address 13 — and from then on "a" means "cell 13." Rename it, move it, and the program is unchanged. And a label is the very same trick, aimed at code instead of data. top is a name pinned to the address an instruction happens to sit at, and JMP top just means "put that address in the PC." The assembler binds both kinds of name the identical way, because it cannot tell them apart either. Only the program counter decides which one turns out to be code. Bind a few variables to cells below.
Now look hard at the words "a free cell," because this machine has fewer of them than you think. The operand is four bits wide. Four bits count 0000 through 1111, so every address this machine can name runs from 0 to 15. Sixteen cells. That is not sixteen cells of variables — that is the entire world, and your program lives in it too. So when the assembler drops a at address 13, it isn't picking a spot in an ocean. It is claiming 1 of 16 slots that the instructions are also competing for, and every line you write eats one. Let the code grow long enough and it lands on top of a. Nothing will warn you, because nothing in there knows which cells you meant as code. That's the stored-program idea leaving philosophy and starting to charge you rent.
So the whole descent is clear now. An idea becomes pseudocode. Pseudocode becomes mnemonics with named variables. And the assembler turns those into bytes. Some of those bytes are instructions, some are the numbers those instructions touch, and all of them live together in one RAM. Time to build a real one.
03Fibonacci, for real
Let's write something that actually loops — the Fibonacci sequence. The rule is famous and tiny. Start with two seeds, and every number after is the sum of the two before it. 1, 1, 2, 3, 5, 8, 13, 21… Generate a few terms and watch each one being born from its two predecessors.
Here's the first thing that makes it programmable on a machine with almost no memory. To compute the next term you never need the whole history. You only need the last two numbers. So we keep just two cells, call them a and b, and compute c = a + b. Then we slide the window forward: the old b becomes the new a, and the fresh c becomes the new b. Two cells, reused forever. Watch the window slide.
That slide — a←b, b←c — looks trivial, but it hides the single most important truth about how a machine computes. And it's worth being wrong about first. Each store is a side effect: it overwrites a cell, and the old value is gone. So the order of the two moves isn't cosmetic. Get it backwards and you clobber a number you still needed. Predict what happens if you copy c into b before saving b into a, then reveal it.
That's the lesson under all imperative programming: instructions mutate state, so sequence is the computation. The same three instructions in a different order compute a different thing, or ruin it. Machines don't run "the set of your instructions." They run them one at a time, in order, and each one leaves a scar.
Now the loop. We want the machine to compute term after term without ever stopping. So after the slide, we simply JMP straight back to the top. There's no exit test at all. The jump is unconditionally taken every time, which is another way of saying the loop condition is always true. That's a legitimate program shape: an infinite loop, fetching and executing forever, the PC snapping back to the start each pass. Watch it circle.
But an 8-bit machine climbing Fibonacci is heading for a wall, and it's a wall you met back in Chapter 14. A byte holds only 0 to 255. The terms run 89, 144, 233… and the next one, 233 + 144 = 377, simply does not fit. So it wraps. The adder computes mod 256, so 377 − 256 = 121 is what actually lands in the cell. Meanwhile the bit that fell off the top sets the carry flag. Push the sum past 255 and watch the odometer roll and the carry light.
So the machine doesn't error. It lies quietly, handing you 121 and setting a flag it's up to you to check. That honesty-with-a-catch is exactly why the carry flag exists. It's the machine's only way to whisper "there was a bit you didn't have room for." Now let's stop hand-waving and actually run the whole program — Fibonacci and a countdown — in a faithful interpreter, and diff its output against what we predicted.
There it is: a real program, running on a faithful model of the machine you built from transistors, producing exactly the numbers — wrap and all — that the hardware would. No magic anywhere in the stack. Now let's put it into memory the way you'd have to on the real thing: by hand.
04Loading it by hand
Quick recap of the one thing the machine does to every byte it fetches, because we're about to build those bytes ourselves. It splits them. The top 4 bits are the opcode (which instruction), and the bottom 4 bits are the operand (which address or value). That split is done by wiring, not thinking. The instruction register just routes the two nibbles to two different places. Toggle a byte and watch it fall into its two halves.
Now, how does a program physically get into RAM before there's any program to load it? On the earliest machines, and on the one you built, you enter it through a front panel: a bank of address switches, a bank of data switches, and a deposit button. Set the address, set the byte, press deposit, and the byte is written to that cell. Repeat for every line of the program. Hand-load a few bytes below.
Feel what just happened, because it's the whole chapter in your fingers: loading code is nothing but writing data. Those deposit switches don't know they're storing a "program." They're storing bytes, exactly the way you'd store the number 121. The instructions become instructions only later, when the program counter reaches them. Set the PC to the start, release the machine, and watch it walk the very bytes you just deposited — fetch, execute, advance — as a live program.
That is a computer, top to bottom. You wrote bytes into cells with switches, and a ROM-driven datapath walked those cells and obeyed them. Nothing was added between "data you typed" and "program that ran" except the program counter arriving. Which means it's time to admit what this machine actually is.
05You built SAP-1
The machine you've assembled — a bus, an ALU, an accumulator, RAM, a program counter, an instruction register, and a ROM control unit fetching and executing a small instruction set — is not a one-off toy I invented. It's a real, named design: the SAP-1, the "Simple As Possible" computer from Malvino's classic textbook. It's the canonical first CPU that generations of engineers cut their teeth on. Line your blocks up against the SAP-1 block diagram and see the same machine.
I want to be scrupulously honest about one seam, because glossing it would be the kind of hand-wave this course refuses. Our toy runs on a five-state counter with a two-cycle fetch. Back in Chapter 15 we merged the PC-increment into the IR-load to save a beat. The textbook SAP-1 uses a six T-state ring counter with a three-cycle fetch, spending a whole separate T-state just to bump the PC. Same blocks, same shape — with two honest differences: the fetch is sliced differently, and our instruction set adds the store and the jumps (STA, JMP, JZ) that Malvino leaves to SAP-1's successor, SAP-2. That second one is what lets our loop loop. Align the two timelines and watch exactly where they diverge.
With that admitted, look at the whole toolchain you now command, end to end, because this is the payoff of the entire volume. An idea becomes pseudocode. The assembler lowers it to bytes. You deposit those bytes into RAM. The program counter walks them. The control ROM decodes each one into control lines, and the datapath obeys. Every arrow in that chain is something you built from switches. Trace it once, whole.
And here's where all of it sits on the map you've been climbing since the first transistor: one clean ladder from a single voltage-controlled switch up to a running program. You are here.
That's Chapter 16 — and, really, the top of the mountain we started climbing at the transistor. The big idea was one sentence: code and data are the same bytes, and meaning lives only in how they're read. That's von Neumann's stored program, named at last. You turned an algorithm into pseudocode, hand-assembled it into bytes, deposited those bytes into RAM with your own fingers, and watched the program counter sort code from data by nothing but where it pointed. Fibonacci looped, overflowed to 121, set the carry, and kept going. And the machine underneath it turned out to have a name — SAP-1 — a real CPU, honestly reconciled, timing seam and all. So here's the claim that should feel both impossible and, now, obviously true: the processor in your phone is not different in kind. Same fetch, same decode, same execute — just more of everything, running billions of times a second, with a couple of tricks the toy never needed. One of them, the stack, is what finally lets code call code and come back. Let's climb the last stretch of the ladder and meet a chip you could actually buy.