If building a transformer in MIT Scratch pushes the limits of floating-point arithmetic and memory layouts, building a CPU emulator pushes the limits of discrete logic.
Modern CPU emulators rely heavily on bitwise operations (&, |, ^, <<, >>), unsigned byte overflows, direct pointer arithmetic, and fast instruction jump tables. MIT Scratch provides none of these:
- There are no bitwise operators. No AND, no OR, no XOR, no shifts.
- Numbers are IEEE 754 double-precision floats, not unsigned 8-bit or 16-bit integers.
- Scratch has no function pointers or switch statements, turning opcode dispatch into a non-trivial architectural challenge.
To see how far an educational block environment could be taken, I built cpuFromScratch: a cycle-conforming CPU emulator implemented in MIT Scratch 3.0, generated programmatically from Python, and verified headlessly against industry-standard test suites.
The emulator supports SUBLEQ (a one-instruction computer), SCRATCH-8, full CHIP-8, and a complete MOS 6502 microprocessor core—the chip that powered the Apple II, Commodore 64, and NES.
1. Synthesizing Bitwise Logic Without Bitwise Operators
The first wall in building any CPU in Scratch is the Arithmetic Logic Unit (ALU). How do you execute AND, ORA, EOR, ASL, or LSR on 8-bit registers when your language only has +, -, *, /, and mod?
We evaluated two approaches:
Approach A: Arithmetic Bit Extraction
You can extract bits mathematically by successive division and modulo: While mathematically pure, extracting 8 bits dynamically for every single instruction creates unacceptable interpreter overhead under Scratch's frame budget.
Approach B: LUT & Page Decomposition
Instead of bit-banging per cycle, we synthesized bitwise operations using structured lookups and decomposed power-of-two arithmetic. We verified this with exhaustive automated testing:
- Exhaustive 65,536-pair tests for every combination of
(a, b) ∈ [0, 255] × [0, 255]forAND,OR, andXOR. - Bit shifts (
ASL,LSR,ROL,ROR) implemented with explicit carry-flag extraction and 8-bit wrap masks (val mod 256).
Every bitwise operation was proven bit-identical to native C/Python behavior before a single opcode was wired up.
2. Emulating the MOS 6502 Core
The MOS 6502 is notorious for subtle edge-case behaviors:
- Decimal Mode (
SED/CLD): When the Decimal flag () is set, addition (ADC) and subtraction (SBC) operate in Binary Coded Decimal (BCD). Implementing correct BCD carry and overflow flags under IEEE floats required meticulous nibble-by-nibble masking. - The Overflow Flag (): Two's-complement signed overflow occurs when adding two numbers of the same sign produces a result of the opposite sign:
- Zero-Page and Indexed Indirect Addressing: Handling the 6502's 13 addressing modes (
Indexed Indirect (zp,X),Indirect Indexed (zp),Y,Absolute,X) requires wrapping 8-bit page boundaries cleanly within a 1-indexed 64KB memory arena list.
3. Automated Verification & Conformance
In emulator engineering, writing code is 20% of the effort; passing edge-case test suites is the remaining 80%. We tested the Scratch 6502 implementation headlessly using Node.js scratch-vm against the most grueling community benchmarks:
| Test Suite | Scope | Result |
|---|---|---|
| Klaus Dormann Functional Suite | 30,646,176 instructions executed to $3469 |
PASSED (0 errors) |
| Klaus Dormann Decimal Suite | 14,464,187 instructions tested for BCD parity to $024B |
PASSED (ERROR=0) |
| Tom Harte ProcessorTests | 2,510,059 test cases across all 151 legal NMOS opcodes | PASSED |
nestest (nestest.nes) |
5,003 official opcode instructions verified against golden logs | PASSED (bit-identical) |
| Timendus CHIP-8 Suite | 8 ROM suites (1-chip8-logo to 8-scrolling) |
PASSED (golden frame hashes verified) |
| Full Automated Harness | tests/test_m0.py through tests/test_m11.py |
133 passed, 1 skipped, 0 failed |
4. CHIP-8 Graphics and Live Input
In addition to the 6502 core, the project implements a complete CHIP-8 virtual machine capable of running original 1970s game ROMs (Pong, Space Invaders, Tetris).
- Display: CHIP-8 renders a monochrome framebuffer. In Scratch, the display is rendered through a contiguous VRAM list, drawing pixels using coordinate mapping into Scratch stage dimensions.
- Input: Standard 16-key hex keypads (
0–F) mapped cleanly to keyboard inputs1234 / qwer / asdf / zxcv. - Timers: Emulated 60 Hz delay and sound timers decremented independently of instruction fetch cycles.
5. Architectural Lessons
- Emulation is Specification Compliance: A CPU emulator is either 100% correct or it cannot boot an operating system or game. The difference between an emulator that "sort of works" and one that passes Klaus Dormann is hundreds of subtle flag interactions (like the undocumented behavior of decimal mode on invalid BCD inputs).
- Generators Beat Hand-Coding: Hand-drawing blocks in the Scratch UI for 151 distinct opcodes with 13 addressing modes would take months and introduce endless human transcription bugs. Writing a compiler in Python that emits valid Scratch
.sb3JSON project archives turns an impossible GUI task into a deterministic software build pipeline. - The Substrate Doesn't Limit Rigor: Constraints (like Scratch's lack of bitwise logic) do not excuse sloppy engineering. If a language lacks an abstraction, you synthesize it from first principles and test it until the error rate is zero.
The complete code, generators, and test harness are open source on GitHub:
- Repository: github.com/ayodejiades/cpuFromScratch