Tobechukwu Ikenwe

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.

Hardware DesignComputer ArchitecturePythonAssemblyCompilersOperating Systems

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.py

The 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
add

conceptually 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 2

hides 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 0

The 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.