How Humans Learned to Speak to the Machine: Assembly and the Assembler
Planted 02026-08-01
How can a person write a language made for circuitry?
How can a person write a program when inserting one instruction changes the address of every instruction after it?
How can anyone reliably compose a plan in a language where a single misplaced bit may become a different—but perfectly valid—command?
The CPU had solved the machine’s problem.
It could fetch a binary instruction, decode its fields, direct values through the ALU, preserve a result, and continue to another instruction. Given the correct sequence of bit patterns, it could repeat a calculation millions of times without becoming bored or losing its place.
But someone still had to create those patterns.
A processor might be perfectly comfortable receiving:
1110001100001000
A person had to remember what every position meant.
Which bits selected the calculation?
Which bits selected the destination?
Which number identified the memory location?
Which address should a jump use after three new instructions were inserted near the beginning?
The stored-program computer had made its behavior replaceable.
Now people needed a way to replace that behavior without thinking like the instruction decoder every moment.
The next breakthrough would not give the processor a new operation.
It would give humans a more merciful way to describe the operations the processor already knew.
1. The program waiting in a queue
In the early years of EDSAC at the University of Cambridge, programmers did not sit before personal screens and revise a program while it ran.
They prepared instructions on coding sheets. The program was punched into five-hole paper tape. The tape, accompanied by a note, joined a physical job queue: a line of tapes waiting for an operator to feed them into the machine.
When its turn arrived, EDSAC read the holes in the tape, loaded the program, executed it, and printed the result on a teleprinter.
If the program was wrong, the machine did not explain the programmer’s intention back to them.
It obeyed the instructions it had received.
A mistaken code might select the wrong operation. A mistaken address might fetch an unrelated value. A jump might send execution into a region of memory that was never meant to contain instructions.
The programmer could study the output, revise the coding sheet, punch another tape, and return to the queue.
Machine time was scarce. Errors had a social cost as well as an intellectual one. A faulty program consumed not only the programmer’s attention but part of a shared instrument’s day.
The burden began before execution.
A programmer had to translate a mathematical procedure into a sequence of machine operations, assign those operations to memory locations, and make every reference point to the correct numerical address.
Then the program changed.
An instruction was inserted.
Everything after it moved.
A jump that once targeted location 47 might now need to target location 48. A value once stored at location 63 might have moved to 64. The modification could be locally correct and still make distant parts of the program wrong.
The problem was no longer merely:
What should the machine do?
It was:
How can a person preserve the meaning of a program while its numerical geography keeps changing?
2. The language the machine preferred
A machine instruction is already a kind of language.
Its vocabulary is the processor’s available operations.
Its grammar is the instruction format.
Its words are bit patterns.
Consider a Hack instruction from Nand to Tetris:
1110001100001000
The first bits identify it as a computation instruction. Other fields select the ALU operation, destination, and possible jump behavior.
The Hack CPU interprets that pattern as:
M=D
The instruction places the value in the D register into the memory location currently selected by the A register.
The processor does not find the symbolic version clearer.
It never sees M, D, or the equals sign. It receives the binary pattern, and the control circuitry responds.
For a person, however, the two forms demand different kinds of attention.
To write the binary form directly, the programmer must remember:
The prefix for a computation instruction
The bits representing D
The bits selecting memory as the destination
The bits specifying no jump
The position of every field
To write the symbolic form, the programmer can think:
Put D into memory.
The machine language is not defective. It is precisely matched to the hardware.
That is the problem.
A language convenient for a decoder is not automatically a language convenient for a mind. Nand to Tetris uses this contrast directly: Hack assembly provides symbolic forms for instructions that are ultimately translated into the binary codes executed by the Hack hardware.
3. Giving operations names
The first relief was to give machine operations memorable symbols.
EDSAC’s written orders used function letters. An A order meant addition. An S order meant subtraction. A T order transferred the accumulator into memory. The full written order also included an address and a marker indicating the length of the value.
An instruction might resemble:
A 42 F
The notation still exposed the machine.
The programmer still had to know what the accumulator was, what lived at address 42, and whether the operation used a short or long value.
But the letter A was easier to remember than the corresponding binary function code.
David Wheeler, who worked on EDSAC while completing his doctorate at Cambridge, later recalled helping choose the order letters and trying to make them mnemonic where possible. Not every ideal letter was available, and the chosen names reflected how the operations were understood at the time. What a later programmer might call a jump, for example, was treated as a transfer of control.
This is a small but important shift:
Binary control pattern
↓
Memorable operation name
The letter does not change the circuit.
It changes what the programmer must carry in memory.
An operation code such as A is a promise between people and a translator:
Whenever I write this symbol here, produce the bit pattern that makes the machine add.
The computer’s language has acquired a human-facing name.
But a name written on paper tape is not yet an instruction the CPU can execute.
Something must turn the name back into bits.
4. The program that waited before every other program
When EDSAC started, a small program called the Initial Orders was already available to it.
The Initial Orders were held in a form of read-only storage constructed from telephone switching equipment. They read the characters arriving from paper tape, interpreted the symbolic order notation, converted it into EDSAC’s internal instruction representation, placed the resulting instructions into memory, and transferred control to the loaded program.
Before a scientist’s calculation could run, another program prepared it.
Symbols on paper tape
↓
Initial Orders
↓
Machine instructions in memory
↓
The requested calculation
Cambridge’s historical account describes the Initial Orders as a primitive assembler.
The word assembler captures the work.
The program gathers the pieces written by a person—operation symbols, numerical addresses, directives, and constants—and assembles the exact bit patterns expected by the processor.
This introduces a new relationship into the machine:
A program can exist in one representation and be transformed into another program with the same intended behavior.
Until now, software had been something the CPU executed.
Now software was also becoming part of the process that made other software executable.
The machine was beginning to assist in its own programmability.
5. Translation moves the burden
Suppose a processor uses a particular binary field for addition:
00101
A programmer writes:
A
The assembler contains the agreement:
A → 00101
When it encounters A, it emits the corresponding bits in the operation field.
The processor remains unchanged. It still executes the same binary code.
The programmer’s experience changes because the assembler absorbs a repetitive task.
Without the assembler:
Human intention
↓
Human remembers encoding
↓
Human writes exact bits
↓
CPU executes
With the assembler:
Human intention
↓
Human writes mnemonic
↓
Assembler remembers encoding
↓
CPU executes
The knowledge has not disappeared.
It has moved.
Someone must still define the mapping between the symbol and the bits. Someone must implement and test the assembler. But every later programmer can rely on that work instead of reproducing it by hand.
This is one of software’s deepest recurring patterns:
A tool makes an agreement once so that people do not have to reconstruct it every time.
Operation names solve only part of the problem.
The more painful bookkeeping lives in addresses.
6. The number that forgot why it mattered
Consider this jump:
@47
0;JMP
In Hack assembly, @47 places 47 into the A register. The jump instruction then makes the program counter continue from that address.
The processor receives an exact destination.
The programmer receives almost no explanation.
Why 47?
Is it the beginning of a loop?
The end of the program?
An error handler?
A section that draws the next row of pixels?
The number describes where the destination happens to be. It does not describe what the destination means.
Now insert three instructions before it.
The destination moves to 50.
The programmer must find every place that referred to 47 and decide whether it should now refer to 50. Some occurrences of 47 may represent the jump target. Others may be ordinary numerical values. The same digits can carry unrelated intentions.
A numerical address binds meaning to location:
The loop begins at 47.
A symbolic name separates them:
The loop begins at LOOP.
LOOP currently happens to be 47.
That difference becomes visible in assembly:
(LOOP)
// repeated work
@LOOP
0;JMP
LOOP can move.
Its meaning can remain.
The label promises:
Wherever this block ultimately lands, return here.
The programmer no longer has to preserve the numerical address through every edit.
The assembler assumes responsibility for discovering it.
7. A program acquires landmarks
A label is not a machine instruction.
It does not ask the CPU to add, store, or jump.
It marks a position in the source program:
(LOOP)
When the assembler encounters the label, it records the address of the next real machine instruction.
Suppose the source is:
@0
D=M
(LOOP)
@1
D=D+M
@LOOP
0;JMP
The label itself occupies no instruction memory.
The assembled instructions are counted like this:
Address 0 @0
Address 1 D=M
Address 2 @1
Address 3 D=D+M
Address 4 @LOOP
Address 5 0;JMP
Therefore:
LOOP → 2
When the assembler later sees:
@LOOP
it can emit the same binary value it would have emitted for:
@2
The processor still jumps to a number.
Only the programmer gets the name.
This distinction matters:
Symbolic assembly does not make the hardware symbolic.
It creates a human layer whose names are removed before execution.
The machine receives addresses.
The source preserves landmarks.
8. The name that appears before its destination
Labels create a new difficulty.
A program may refer to a destination before declaring it:
@END
D;JEQ
// many instructions
(END)
@END
0;JMP
When the assembler encounters the first @END, it may not yet know where END will be.
A translator that insists on completing each instruction immediately reaches a dead end:
How can I emit an address that has not been discovered?
One possible response is to forbid forward references. Every label would have to appear before any instruction could mention it.
That would simplify the assembler by making programs harder for people to organize.
A better answer is to let translation happen in stages.
First pass: learn the program’s geography
The assembler scans the source without producing the final machine code.
It counts only instructions that will occupy memory.
Whenever it encounters a label, it records the current instruction address:
LOOP → 4
END → 18
At the end of the pass, the assembler possesses a map.
Second pass: translate with the map available
The assembler scans the program again.
This time, when it sees:
@END
it consults the recorded mapping:
END → 18
and emits the binary form of 18.
The forward reference is no longer mysterious because the assembler has already seen the whole source once.
This reveals a broader principle:
Sometimes a translator must first learn the structure of the whole before it can interpret each part.
The machine still works one instruction at a time.
The tool preparing those instructions does not have to.
9. The assembler’s notebook
The assembler needs somewhere to preserve the relationships it discovers:
Name Numerical meaning
----------------------------
LOOP 4
END 18
counter 16
SCREEN 16384
This structure is called a symbol table.
It is the assembler’s notebook of agreements.
In Hack assembly, symbols can come from several places.
Predefined symbols
Some names refer to locations established by the architecture:
R0
R1
...
SCREEN
KBD
The assembler begins with those relationships already known.
Label symbols
A declaration such as:
(LOOP)
associates LOOP with an instruction address.
The first pass discovers these.
Variable symbols
A name such as:
@counter
may refer to a data-memory location even though the programmer never explicitly chose a number.
The assembler can assign one.
The symbol table allows the source program to remain stable while the numerical details are calculated, discovered, or allocated elsewhere.
A person thinks in names.
The machine acts on numbers.
The table holds the bridge.
Symbol handling is the major extension that turns the basic Hack translator into the complete assembler in the Nand to Tetris project.
10. Variables without boxes
A variable is often pictured as a labeled container:
counter
┌───────┐
│ 7 │
└───────┘
That picture is useful, but the hardware does not create a tiny box bearing the word counter.
Consider:
@counter
M=0
The first time the Hack assembler encounters the unknown symbol counter, it assigns the name an available RAM address. In the Hack convention, variable allocation begins after the predefined register locations.
The symbol table may record:
counter → 16
Every later occurrence of counter resolves to the same address.
The assembled program behaves as though the source had contained:
@16
M=0
At execution time, the processor knows nothing of counter.
It knows:
Select memory location 16.
Store zero there.
The name exists at the level where a person composes and revises the program.
The address exists at the level where the machine routes electrical signals.
Both descriptions refer to the same evolving state.
Human intention
↓
counter
↓
RAM address 16
↓
Selected memory circuitry
↓
Stored bit pattern
Abstraction does not remove the physical event.
It gives the event a stable role in a larger idea.
11. The two sentences of Hack assembly
The Hack assembly language is intentionally small.
Its executable instructions come in two main forms.
A-instruction: select a value or address
@value
The A-instruction places a fifteen-bit value into the A register.
For example:
@2
becomes:
0000000000000010
The first bit identifies the instruction form. The remaining bits carry the value.
The value may be written directly:
@21
or supplied through a symbol:
@counter
@LOOP
@SCREEN
By the time the CPU executes the instruction, every symbol has become a number.
C-instruction: compute, store, and possibly jump
A computation instruction has the symbolic shape:
destination = computation ; jump
Not every part must appear.
Examples include:
D=A
M=D
D=D+M
0;JMP
D;JEQ
The assembler separates each line into fields.
For:
D=D+M
it asks:
What computation? D+M
What destination? D
What jump? none
It then looks up the binary code assigned to each field and joins them into one sixteen-bit instruction.
For:
M=D
the result is:
1110001100001000
The symbolic instruction and the binary instruction do not describe two different actions.
They are two representations of the same architectural agreement.
The assembler’s obligation is exactness.
One wrong output bit may direct the ALU toward another operation, preserve the result in another destination, or alter the program’s path.
Translation is an act of trust.
12. The assembler reads structure
To translate a source file, the assembler must recognize what each line is trying to express.
Consider:
// Increase the counter
@counter
M=M+1
(LOOP)
@LOOP
0;JMP
Before translating, it may:
Remove comments.
Ignore blank lines.
Trim irrelevant whitespace.
Classify each remaining line.
Separate each instruction into fields.
Resolve names.
Emit binary.
The possible source forms are few:
@value A-instruction
dest=comp C-instruction
comp;jump C-instruction
(LOOP) Label declaration
This recognition is a small form of parsing.
The assembler is not merely replacing every visible word with another word. It is identifying structure.
In:
D=M+1;JGT
D has one role.
M+1 has another.
JGT has another.
The punctuation helps mark the relationships:
destination = computation ; jump
The assembler converts each part according to the instruction format.
Later software tools will parse languages with variables, expressions, functions, classes, and nested statements.
The assembler’s grammar is tiny by comparison.
But the underlying act has appeared:
Written symbols can be treated as structured data and transformed systematically.
13. Translation is not execution
Suppose a symbolic program adds two and three:
@2
D=A
@3
D=D+A
@0
M=D
The assembler does not calculate five.
It does not simulate the values in the registers.
It does not place the result into RAM.
It produces a binary program whose execution will cause the CPU to do those things.
Assembly source
↓
Assembler
↓
Machine-code output
↓
CPU execution
↓
Result
This distinction is crucial.
The assembler operates on the representation of a procedure.
The CPU performs the procedure.
The source file is data to the assembler.
The output file is also data while it is being written or stored.
Later, when loaded into instruction memory and selected by the program counter, the same output becomes a sequence of commands.
A program can therefore occupy more than one role:
Text being read by a tool
Binary being written to a file
Instructions being executed by a processor
The bits do not change their intrinsic nature between those roles.
The surrounding machinery changes what is done with them.
This is the stored-program idea turning inward.
A computer can manipulate descriptions of computation using computation.
14. The program that made programs portable
The first version of EDSAC’s Initial Orders translated its written instruction notation and placed a program into memory.
A later version, developed under Maurice Wilkes’s direction and refined by David Wheeler, did more.
It supported forms of relative addressing and relocation. A routine could be written using addresses relative to its own beginning rather than rewritten for one permanent location. When the routine was loaded elsewhere, the Initial Orders adjusted the relevant addresses.
In one EDSAC notation, a relative address marked with @ told the loading system to add the routine’s starting location.
An order written conceptually as:
Use the value 11 places from the beginning of this routine
could continue to mean that even if the routine moved.
This solved a larger version of the label problem.
It allowed code to become less dependent on one exact geography of memory.
Wheeler also developed the closed subroutine, and the Cambridge laboratory built a shared library of routines. Users did not have to recreate every common mathematical operation from the machine’s primitive orders. They could incorporate tested work prepared by others.
The human stakes had expanded.
The question was no longer only:
How can I make this one program run?
It was also:
How can a program be moved, reused, and shared without forcing every new user to reconstruct its numerical anatomy?
The assembler and loader began turning individual ingenuity into infrastructure.
A name could survive movement.
A routine could survive reuse.
A translation tool could preserve agreements across many programs.
15. The first rung of the software hierarchy
Nand to Tetris describes its assembler project as the first rung of the software hierarchy.
Until this point, the construction has moved mostly downward into hardware:
NAND
↓
Logic gates
↓
ALU
↓
Memory
↓
CPU
The assembler begins another direction:
Binary machine language
↑
Symbolic assembly
↑
Future software layers
In Project 6, the task is to write a program—using Python, Java, or another general-purpose implementation language—that translates Hack assembly files into binary programs executable by the Hack computer.
The recommended construction mirrors the conceptual difficulty.
First, build a basic translator for programs containing explicit numerical addresses.
Then add symbol handling.
The completed assembler must preserve several agreements:
Mnemonic computations → ALU control bits
Destinations → destination bits
Jump conditions → jump bits
Numerical values → binary values
Labels → instruction addresses
Variables → data-memory addresses
When the assembler succeeds, the output is indistinguishable from binary written perfectly by hand.
The processor cannot tell how its instructions were produced.
It does not know whether a person flipped switches, punched raw bits, used assembly language, or invoked a compiler through several intermediate layers.
It knows only the final pattern.
That indifference makes abstraction possible.
16. What an assembler really is
An assembler is often defined as a program that converts assembly language into machine code.
That is correct.
But it leaves out the human transformation.
An assembler is a mechanism for moving bookkeeping out of the programmer’s mind.
It remembers encodings.
It calculates addresses.
It resolves names.
It distinguishes labels from instructions.
It assigns storage to variables.
It produces the exact numerical form demanded by the hardware.
Its recurring promise is:
Write the machine’s operations using these human-facing symbols.
I will recover the bits.
The symbols do not make the CPU more intelligent.
LOOP does not exist inside the program counter.
counter is not etched onto a memory cell.
D=M+1 is not understood by the ALU.
Those names exist for the person composing the plan.
The assembler removes them while preserving what they mean operationally.
This is not merely a convenience.
It is a new division of labor.
The programmer preserves intention.
The assembler preserves encoding.
The CPU preserves execution.
Each layer depends on the promises below it.
The programmer trusts the assembler to translate D=M+1 correctly.
The assembler trusts the architecture’s instruction specification.
The CPU trusts its gates, registers, and clock.
At the bottom, transistors still alter electrical pathways.
At the top, a person can think about a counter increasing.
The human hand has moved another level away from the wires.
But not very far.
Assembly still exposes the machine’s anatomy.
The programmer must think in registers, memory locations, jumps, and individual operations. Multiplication may require a loop of additions. A temporary value must be placed somewhere explicitly. Calling a reusable routine requires agreements about where arguments, results, and return addresses will live.
The assembler gives names to the processor’s language.
It does not give the programmer a new machine.
That possibility creates the next question.
What if software could present a simpler computer than the physical one underneath?
What if every program could target the same imaginary stack machine, while another translator absorbed the differences below?
The next layer would not merely rename the machine’s instructions.
It would invent a machine made from software.