Hardware · Computer Architecture · Systems
Nand2Tetris — Building a Computer From First Principles
A bottom-up journey from NAND gates to a functioning computer, compiler, operating system, and Tetris application.
1. Why I Started
I wanted to understand what happens underneath programs. Normally, I can write something as simple as x = 10 without thinking about what the computer actually has to do to execute it.
I wanted to follow that process all the way down: from a high-level program, through the compiler and machine language, into CPU instructions, and eventually into the hardware that executes those instructions.
That became the central idea behind Nand2Tetris: progressively remove abstractions until the computer itself was no longer a black box.
Logic Gates → CPU + Memory → Machine Language → Assembler → Virtual Machine → Compiler → Operating System → Application → Tetris
2. Building the Hardware
The project started at the lowest level: NAND.
From that primitive building block, I progressively implemented the components required to construct a computer.
NAND
↓
NOT / AND / OR / XOR
↓
Multiplexers
↓
Adders
↓
ALU
↓
Registers
↓
RAM
↓
CPU
↓
Complete Computer
In total, I implemented more than 30 hardware components. Each layer depended on the correctness of the layers below it.
The ALU was especially important because it connected the basic logic operations to actual computation. I had to reason about bitwise operations, two's complement representation, control bits, zeroing and negating inputs, arithmetic operations, and output flags.
By the end of this stage, I had built a 16-bit computer architecture rather than simply using an existing CPU as a black box.
3. Project 6 — Building an Assembler
Once the CPU existed, I needed a practical way to communicate with it.
Humans do not want to write raw binary, so the next layer was Hack assembly.
Hack Assembly
↓
Parser
↓
Symbols / Labels
↓
Binary Encoding
↓
.hack Machine Code
I implemented the assembler in Python. One of the important problems was handling labels.
A label does not directly represent a machine instruction. It represents the address of an instruction, so I used a two-pass process.
Pass 1
Scan the program and record each label and its corresponding instruction address.
Pass 2
Translate instructions and replace symbolic references with their resolved addresses.
For example, if LOOP represents instruction address 12, then an instruction such as@LOOP ultimately becomes a reference to address 12 before being encoded into binary.
4. Testing the Assembler
I treated generated artifacts as part of the debugging process. Instead of only asking whether the program ran, I inspected intermediate output to determine whether each transformation was correct.
I used Linux tools and Python checks such as:
python3 -m py_compile Assembler.pyThe general workflow became:
Implement → Run → Inspect Output → Find Mismatch → Understand Why → Fix → Test Again
5. Projects 7–8 — The Virtual Machine
Assembly was already a useful abstraction, but writing programs directly in assembly still exposed too many hardware details.
The next layer was a stack-based virtual machine.
VM Code
↓
VM Parser
↓
VM Translator
↓
Hack Assembly
↓
Hack Binary
A command such as:
push constant 10
push constant 20
addconceptually transforms the stack from containing 10 and 20 to containing their sum, 30.
The translator implements this abstraction by manipulating RAM and the stack pointer.
6. Functions and the Runtime Model
Function calls made the VM translator substantially more interesting.
A command such as:
call Foo.bar 2hides a significant amount of runtime work.
The translator has to establish the runtime state required for the called function while preserving the caller's state.
This required understanding the roles of:
- • SP — stack pointer
- • LCL — local segment
- • ARG — argument segment
- • THIS — object/array context
- • THAT — object/array context
7. The Challenge: Understanding the Implementation
One of the more important challenges was realizing that having a working-looking implementation was not the same thing as fully understanding it.
I had to separate two goals: getting through a project and being able to explain exactly what the implementation was doing. That distinction changed how I approached the later stages.
Instead of memorizing transformations, I wanted to understand what happened to the stack, memory, and runtime state when a function call or other VM command executed.
8. Project 9 — Tetris
This was where the layers started to feel like one actual computer system.
Tetris was implemented using Jack and consisted of components such as:
- • Main
- • Board
- • Piece
- • TetrisGame
The application relied on operating-system services for drawing, keyboard input, memory allocation, and output.
Tetris
↓
Jack OS
↓
VM
↓
Hack Assembly
↓
Hack Machine Code
↓
CPU
↓
Hardware
9. Understanding the Project Structure
The Tetris directory initially looked confusing because it contained both application code and operating-system components, along with generated artifacts.
Files included application classes such as Main.jack, Board.jack, and TetrisGame.jack, alongside operating-system classes such as Memory, Math, Screen, Keyboard, Output, String, Array, and Sys.
The key realization was that a directory's physical organization does not necessarily represent the conceptual architecture. Application code, OS code, and generated artifacts can exist together while serving completely different roles.
10. Projects 10–11 — Building a Compiler
The next problem was moving from VM programming to a high-level language.
Jack Source
↓
Tokenizer
↓
Parser / Compilation Engine
↓
Program Structure
↓
Symbol Information
↓
VM Code
I implemented the compiler in Python using components including the tokenizer, compilation engine, and compiler driver.
11. Tokenization
The tokenizer converts raw characters into meaningful tokens.
For example:
let x = 10;becomes a sequence such as:
let · x · = · 10 · ;
12. Parsing the Program
Tokenization tells the compiler what the pieces are. Parsing determines how those pieces relate to one another.
LET STATEMENT
├── variable: x
└── expression
└── integer constant: 10
The compiler could now reason about the structure of the program rather than simply processing a stream of characters.
13. Symbol Tables
The compiler also needed to understand where variables live at runtime.
function void foo(int x) {
var int y;
let y = x;
}The compiler can associate:
- • x → argument 0
- • y → local 0
This information allows the compiler to generate VM commands that access the correct runtime locations.
14. Code Generation
A statement such as:
let y = x;can become conceptually:
push argument 0
pop local 0The complete transformation is therefore:
Source → Tokens → Syntax → Symbol Resolution → VM
15. Project 12 — Building the Operating System
The final major software layer was the Jack operating system.
It provided services including:
- • Memory
- • Array
- • Math
- • String
- • Screen
- • Keyboard
- • Output
- • Sys
Tetris could now rely on these services instead of implementing every low-level operation itself.
Importantly, the operating system itself went through the same compilation pipeline as application code.
Memory.jack → Jack Compiler → Memory.vm → VM Translator → Memory.asm → Assembler → Memory.hack
16. The Tetris Moment
The goal was no longer just to implement individual components. It was to take:
TetrisGame.jack
↓
.vm
↓
.asm
↓
.hack
↓
Execute on the computer architecture I built
I built the stack underneath Tetris.
17. Debugging Across Layers
One of the most valuable lessons was learning that the layer where a failure appears is not necessarily the layer where the problem originated.
When something went wrong, I learned to trace the transformation backward:
Jack Source
↓
Tokenizer — Is the token wrong?
↓
Compiler — Is the syntax or VM wrong?
↓
VM Translator — Is the assembly wrong?
↓
Assembler — Is the binary wrong?
↓
CPU — Is the hardware behaving incorrectly?
This turned debugging from guessing into localization: identify the first layer where the expected transformation differs from the actual transformation.
18. Development and Debugging Workflow
The project was developed primarily with Python for the software components and Linux tools for testing, inspection, and debugging.
Implement
↓
Syntax Check
↓
Run Tests
↓
Inspect Generated Artifacts
↓
Trace Transformation
↓
Identify Layer
↓
Fix
↓
Retest
This also taught me to distinguish between a broken implementation and a broken environment or toolchain rather than immediately changing working code.
19. What I Actually Built
- Hardware: NAND, logic gates, ALU, registers, memory, CPU, and computer.
- Machine-level software: Hack assembler.
- Intermediate layer: Virtual machine and VM translator.
- High-level software: Jack language tooling and compiler.
- System software: Jack operating system.
- Application: Tetris.
20. The Complete Transformation
The entire project can be understood through a single high-level statement:
let x = 10;That statement can be progressively transformed:
Characters
↓
Tokens
↓
Structured Syntax
↓
Symbol Resolution
↓
VM Commands
↓
Hack Assembly
↓
Machine Code
↓
CPU Operations
↓
Memory and Hardware State
The point was not simply knowing each individual layer. It was understanding how the layers connect and why each abstraction exists.
21. Engineering Lessons
- Decompose complex systems. A computer becomes much easier to understand when each abstraction has a clearly defined responsibility.
- Debug transformations, not just symptoms. When a high-level program fails, trace the intermediate representations until the first incorrect transformation is found.
- Test intermediate outputs. VM code, assembly, and machine code are not merely generated artifacts; they are evidence about what the system is doing.
- Work from specifications. Each layer has rules that constrain how it can interact with the layers above and below it.
- Understand before optimizing. Building the system from first principles forced me to understand the mechanisms before trying to abstract them away.
22. Final Architecture
TETRIS
↓
Jack OS
↓
Jack Compiler
↓
VM Translator
↓
Hack Assembly
↓
Assembler
↓
16-bit Computer
↓
CPU · ALU · RAM · Registers
↓
NAND / Digital Logic
23. Why Tetris Matters
The most meaningful part of the project is not simply that I implemented Tetris.
It is that Tetris became a demonstration that all of the underlying layers worked together.
The application depended on an operating system that depended on a compiler that generated VM code that was translated into assembly that was converted into machine code executed by a CPU built from the hardware level upward.
I didn't just build an application. I built the environment capable of running the application.
Final Takeaway
Nand2Tetris changed how I think about computer systems. Instead of seeing hardware, compilers, operating systems, and applications as completely separate areas, I can now trace the abstractions connecting them.
The project started with a NAND gate and ended with a program running on a computer architecture built from that same foundation.
The real outcome was not simply completing a collection of projects. It was removing the black box between software and the machine executing it.
