Computing Foundations › Inside the Machine › Day 2
Hands-on lab — Day 2: The CPU: Fetch, Decode, Execute
- ← Back to the Day 2 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-002-the-cpu-fetch-decode-execute/
Commands
Setup
cd labs/sections/computing-foundations/day-002-the-cpu-fetch-decode-execute Run
bash examples/toy_cpu.sh examples/programs/add-two-numbers.txt
bash examples/toy_cpu.sh examples/programs/trace-01.txt
bash examples/toy_cpu.sh examples/programs/trace-02.txt
bash examples/toy_cpu.sh examples/programs/trace-03.txt
bash examples/toy_cpu.sh starter/my_program.txt Test
bash tests/run_tests.sh File tree
examples/programs/add-two-numbers.txt examples/programs/trace-01.txt examples/programs/trace-02.txt examples/programs/trace-03.txt examples/toy_cpu.sh expected-output/add-two-numbers-run.txt expected-output/my-program-starter-run.txt expected-output/trace-01-run.txt expected-output/trace-02-run.txt expected-output/trace-03-run.txt metadata.yml README.md requirements/README.md security.md starter/my_program.txt starter/trace-worksheet.md tests/run_tests.sh troubleshooting.md
Lab README
Day 002 lab — Trace the Machine: a Toy CPU in Your Terminal
Lesson
- Lesson title: The CPU: Fetch, Decode, Execute
- Day number: 2 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-002-the-cpu-fetch-decode-execute
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-002-the-cpu-fetch-decode-executewhen the site is running.
Purpose
Day 2's lesson dissects the CPU: registers, the ALU, the control unit, the program counter, and the fetch-decode-execute cycle they perform together. This lab makes the cycle visible. You run a working toy CPU — a bash script that interprets a four-instruction assembly language — and watch it fetch, decode, and execute one instruction at a time, printing the PC and every register after each step. Then you become the CPU yourself: you hand-trace three programs on a worksheet before running them, and finally you write your own program for the machine.
Learning objectives
- Read a short assembly-style program and predict exactly what a CPU will do with it, step by step.
- Follow the program counter through a run: where it points, when it advances, and how
HALTstops the loop. - Trace register state by hand through
LOAD,ADD, andPRINTinstructions, including the tricky case where a register is both source and destination. - Explain the difference between the fetch, decode, and execute steps by pointing at the corresponding lines of the simulator's output.
- Write and run your own program in a four-instruction instruction set, and verify it with an automated test suite.
Prerequisites
- The Day 2 lesson (it introduces every part of the machine this lab simulates) and the Day 1 lab (basic comfort running commands in a terminal).
- A terminal with
bash: Terminal.app (macOS), any terminal (Linux), or WSL (Windows). - No programming experience needed — the whole instruction set is four opcodes.
Supported operating systems
- macOS — fully supported; the scripts run on the preinstalled bash 3.2.
- Linux — fully supported on any distribution with bash.
- Windows — run everything unmodified inside WSL; native PowerShell is not supported for this lab because the simulator is a bash script.
Hardware requirements
Any computer that can open a terminal. The simulator is a few kilobytes of shell script; it needs no minimum RAM, disk, or GPU.
Required software
bash(3.2 or newer — preinstalled on macOS and Linux).- Standard utilities used by the scripts:
tr,grep,sed,mktemp— all part of the base system.
Free and open-source options
Everything in this lab is free: bash and every utility used ship with your operating system. No account, API key, download, or purchase is needed.
Installation
None. From the repository root:
cd labs/sections/computing-foundations/day-002-the-cpu-fetch-decode-execute
File structure
day-002-the-cpu-fetch-decode-execute/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── trace-worksheet.md ← YOUR worksheet: predict before you run
│ └── my_program.txt ← YOUR program (ships working; extend it)
├── examples/
│ ├── toy_cpu.sh ← the toy CPU simulator (read it!)
│ └── programs/
│ ├── add-two-numbers.txt ← the demo program from the lesson
│ ├── trace-01.txt ← worksheet program 1
│ ├── trace-02.txt ← worksheet program 2
│ └── trace-03.txt ← worksheet program 3
├── tests/
│ └── run_tests.sh ← automated checks (27 checks)
├── expected-output/
│ ├── add-two-numbers-run.txt ← real captured run
│ ├── trace-01-run.txt ← real captured run
│ ├── trace-02-run.txt ← real captured run
│ ├── trace-03-run.txt ← real captured run
│ └── my-program-starter-run.txt ← real captured run of the unmodified starter
├── requirements/
│ └── README.md ← dependency statement (none beyond bash)
├── troubleshooting.md
└── security.md
How to run
From this directory, in this order:
## 1. Watch the machine run the lesson's demo program
bash examples/toy_cpu.sh examples/programs/add-two-numbers.txt
## 2. STOP. Open starter/trace-worksheet.md and hand-trace all three
## programs — fill in every cell BEFORE running them.
## 3. Check your predictions against the real machine
bash examples/toy_cpu.sh examples/programs/trace-01.txt
bash examples/toy_cpu.sh examples/programs/trace-02.txt
bash examples/toy_cpu.sh examples/programs/trace-03.txt
## 4. Run, then extend, your own program
bash examples/toy_cpu.sh starter/my_program.txt
## 5. Check everything
bash tests/run_tests.sh
What the commands do
bash examples/toy_cpu.sh <program-file>— starts the simulator: it loads the program file into an array (its "memory", one instruction per cell), sets registers R1–R4 to 0 and the PC to 0, then loops. Each iteration prints aFETCHline (the PC and the raw instruction), advances the PC, prints aDECODEline (the opcode and operands it recognized), anEXECUTEline (the effect), and aREGSline (all four registers).PRINTemits anOUTPUT:line;HALTstops the loop and prints the instruction count and final registers. Bad opcodes, bad register names, and programs withoutHALTend the run with a non-zero exit code.- The instruction set the simulator accepts (uppercase opcodes, one instruction per line,
#starts a comment):LOAD Rn,value,ADD Ra,Rb->Rc,PRINT Rn,HALT. bash tests/run_tests.sh— runs all four example programs and checks their exact outputs, step counts, and final register states; runs yourstarter/my_program.txtand checks it reachesHALTand prints at least oneOUTPUT:line; and feeds the simulator deliberately broken programs to confirm they are rejected.
Expected output
See expected-output/add-two-numbers-run.txt — a real captured run:
=== Toy CPU ===
Program: examples/programs/add-two-numbers.txt (5 instructions in memory)
Registers start at: R1=0 R2=0 R3=0 R4=0
PC=0 FETCH LOAD R1,5
DECODE opcode=LOAD dest=R1 value=5
EXECUTE R1 <- 5
REGS R1=5 R2=0 R3=0 R4=0
PC=1 FETCH LOAD R2,3
DECODE opcode=LOAD dest=R2 value=3
EXECUTE R2 <- 3
REGS R1=5 R2=3 R3=0 R4=0
PC=2 FETCH ADD R1,R2->R3
DECODE opcode=ADD src1=R1 src2=R2 dest=R3
EXECUTE R3 <- R1 + R2 = 5 + 3 = 8
REGS R1=5 R2=3 R3=8 R4=0
PC=3 FETCH PRINT R3
DECODE opcode=PRINT reg=R3
EXECUTE send R3 to the output
OUTPUT: 8
REGS R1=5 R2=3 R3=8 R4=0
PC=4 FETCH HALT
DECODE opcode=HALT
EXECUTE stop the clock
HALT reached after 5 instructions.
Final registers: R1=5 R2=3 R3=8 R4=0
Captured runs of all three worksheet programs and of the unmodified starter program are in expected-output/ — but do not read the worksheet ones until your predictions are on paper.
Validation steps
- Run the demo program (step 1 above) and match it line-for-line against the expected output block here.
- Complete
starter/trace-worksheet.mdfully — every cell, every predictedOUTPUT:line, every final-register prediction — then run the three trace programs and mark each prediction right or wrong. - For every wrong cell, write the one-sentence "why" the worksheet asks for.
- Extend
starter/my_program.txtso it uses at least oneADDand prints at least two values, then run it — it must reachHALT reached after N instructions.with no errors. - Run the tests (next section) — all checks must pass.
Tests
bash tests/run_tests.sh
Expected final line: 27 checks, 0 failure(s). The suite checks exact OUTPUT: values, fetched-instruction counts, and final register states for all four example programs, structural success for your program, and non-zero exits for four kinds of broken program. The command exits 0 on success and non-zero on any failure, so it can run in CI. (The checks on my_program.txt only require it to run, print, and halt — your extensions cannot break the suite as long as the program stays valid.)
Cleanup
Nothing to clean up: the simulator writes no files (the tests create theirs in a temporary directory that is removed on exit). To reset your work: git checkout -- starter/my_program.txt starter/trace-worksheet.md.
Troubleshooting
See troubleshooting.md for the full list (unknown opcode errors, register-name errors, the missing-HALT message, Windows notes).
Security notes
See security.md. Short version: a small, readable shell script that interprets text files you control — no network, no privileges, no files written. Read it before running it; that habit is the real security lesson.
Extension exercises
- Compute 5 × 6 using only the four instructions (hint: multiplication is repeated addition — chain
ADDs through a register). Predict the instruction count first. - The simulator's PC advances immediately after fetch, before execute. Find the two lines in
examples/toy_cpu.shwhere this happens, and write one sentence on what aJUMPinstruction would have to change for loops to become possible. - Add a
SUB Ra,Rb->Rcinstruction to a copy oftoy_cpu.sh(model it on theADDcase), write a program that uses it, and extend a copy of the test suite to cover it.
Navigation
- Previous day: Day 1 — How a Computer Works: From Transistors to Programs
- Next day: Day 3 — Memory Hierarchy: Registers, RAM, and Storage (
../day-003-memory-hierarchy-registers-ram-and-storage/, to be written).
Expected output
add-two-numbers-run.txt
=== Toy CPU ===
Program: examples/programs/add-two-numbers.txt (5 instructions in memory)
Registers start at: R1=0 R2=0 R3=0 R4=0
PC=0 FETCH LOAD R1,5
DECODE opcode=LOAD dest=R1 value=5
EXECUTE R1 <- 5
REGS R1=5 R2=0 R3=0 R4=0
PC=1 FETCH LOAD R2,3
DECODE opcode=LOAD dest=R2 value=3
EXECUTE R2 <- 3
REGS R1=5 R2=3 R3=0 R4=0
PC=2 FETCH ADD R1,R2->R3
DECODE opcode=ADD src1=R1 src2=R2 dest=R3
EXECUTE R3 <- R1 + R2 = 5 + 3 = 8
REGS R1=5 R2=3 R3=8 R4=0
PC=3 FETCH PRINT R3
DECODE opcode=PRINT reg=R3
EXECUTE send R3 to the output
OUTPUT: 8
REGS R1=5 R2=3 R3=8 R4=0
PC=4 FETCH HALT
DECODE opcode=HALT
EXECUTE stop the clock
HALT reached after 5 instructions.
Final registers: R1=5 R2=3 R3=8 R4=0
my-program-starter-run.txt
=== Toy CPU ===
Program: starter/my_program.txt (3 instructions in memory)
Registers start at: R1=0 R2=0 R3=0 R4=0
PC=0 FETCH LOAD R1,2
DECODE opcode=LOAD dest=R1 value=2
EXECUTE R1 <- 2
REGS R1=2 R2=0 R3=0 R4=0
PC=1 FETCH PRINT R1
DECODE opcode=PRINT reg=R1
EXECUTE send R1 to the output
OUTPUT: 2
REGS R1=2 R2=0 R3=0 R4=0
PC=2 FETCH HALT
DECODE opcode=HALT
EXECUTE stop the clock
HALT reached after 3 instructions.
Final registers: R1=2 R2=0 R3=0 R4=0
trace-01-run.txt
=== Toy CPU ===
Program: examples/programs/trace-01.txt (5 instructions in memory)
Registers start at: R1=0 R2=0 R3=0 R4=0
PC=0 FETCH LOAD R1,4
DECODE opcode=LOAD dest=R1 value=4
EXECUTE R1 <- 4
REGS R1=4 R2=0 R3=0 R4=0
PC=1 FETCH LOAD R2,7
DECODE opcode=LOAD dest=R2 value=7
EXECUTE R2 <- 7
REGS R1=4 R2=7 R3=0 R4=0
PC=2 FETCH ADD R1,R2->R3
DECODE opcode=ADD src1=R1 src2=R2 dest=R3
EXECUTE R3 <- R1 + R2 = 4 + 7 = 11
REGS R1=4 R2=7 R3=11 R4=0
PC=3 FETCH PRINT R3
DECODE opcode=PRINT reg=R3
EXECUTE send R3 to the output
OUTPUT: 11
REGS R1=4 R2=7 R3=11 R4=0
PC=4 FETCH HALT
DECODE opcode=HALT
EXECUTE stop the clock
HALT reached after 5 instructions.
Final registers: R1=4 R2=7 R3=11 R4=0
trace-02-run.txt
=== Toy CPU ===
Program: examples/programs/trace-02.txt (5 instructions in memory)
Registers start at: R1=0 R2=0 R3=0 R4=0
PC=0 FETCH LOAD R1,10
DECODE opcode=LOAD dest=R1 value=10
EXECUTE R1 <- 10
REGS R1=10 R2=0 R3=0 R4=0
PC=1 FETCH ADD R1,R1->R2
DECODE opcode=ADD src1=R1 src2=R1 dest=R2
EXECUTE R2 <- R1 + R1 = 10 + 10 = 20
REGS R1=10 R2=20 R3=0 R4=0
PC=2 FETCH ADD R2,R2->R2
DECODE opcode=ADD src1=R2 src2=R2 dest=R2
EXECUTE R2 <- R2 + R2 = 20 + 20 = 40
REGS R1=10 R2=40 R3=0 R4=0
PC=3 FETCH PRINT R2
DECODE opcode=PRINT reg=R2
EXECUTE send R2 to the output
OUTPUT: 40
REGS R1=10 R2=40 R3=0 R4=0
PC=4 FETCH HALT
DECODE opcode=HALT
EXECUTE stop the clock
HALT reached after 5 instructions.
Final registers: R1=10 R2=40 R3=0 R4=0
trace-03-run.txt
=== Toy CPU ===
Program: examples/programs/trace-03.txt (7 instructions in memory)
Registers start at: R1=0 R2=0 R3=0 R4=0
PC=0 FETCH LOAD R1,6
DECODE opcode=LOAD dest=R1 value=6
EXECUTE R1 <- 6
REGS R1=6 R2=0 R3=0 R4=0
PC=1 FETCH LOAD R2,2
DECODE opcode=LOAD dest=R2 value=2
EXECUTE R2 <- 2
REGS R1=6 R2=2 R3=0 R4=0
PC=2 FETCH ADD R1,R2->R1
DECODE opcode=ADD src1=R1 src2=R2 dest=R1
EXECUTE R1 <- R1 + R2 = 6 + 2 = 8
REGS R1=8 R2=2 R3=0 R4=0
PC=3 FETCH PRINT R1
DECODE opcode=PRINT reg=R1
EXECUTE send R1 to the output
OUTPUT: 8
REGS R1=8 R2=2 R3=0 R4=0
PC=4 FETCH ADD R1,R2->R1
DECODE opcode=ADD src1=R1 src2=R2 dest=R1
EXECUTE R1 <- R1 + R2 = 8 + 2 = 10
REGS R1=10 R2=2 R3=0 R4=0
PC=5 FETCH PRINT R1
DECODE opcode=PRINT reg=R1
EXECUTE send R1 to the output
OUTPUT: 10
REGS R1=10 R2=2 R3=0 R4=0
PC=6 FETCH HALT
DECODE opcode=HALT
EXECUTE stop the clock
HALT reached after 7 instructions.
Final registers: R1=10 R2=2 R3=0 R4=0
Source files
examples/programs/add-two-numbers.txt (197 bytes)
# add-two-numbers.txt — the demonstration program from the Day 2 lesson.
# Loads 5 and 3 into registers, adds them into R3, prints the sum, stops.
LOAD R1,5
LOAD R2,3
ADD R1,R2->R3
PRINT R3
HALT
examples/programs/trace-01.txt (123 bytes)
# trace-01.txt — worksheet program 1. Predict the output before running.
LOAD R1,4
LOAD R2,7
ADD R1,R2->R3
PRINT R3
HALT
examples/programs/trace-02.txt (202 bytes)
# trace-02.txt — worksheet program 2. A register can be a source and the
# destination of the same instruction. Predict the output before running.
LOAD R1,10
ADD R1,R1->R2
ADD R2,R2->R2
PRINT R2
HALT
examples/programs/trace-03.txt (219 bytes)
# trace-03.txt — worksheet program 3. Two PRINTs: the machine outputs the
# register's value at that moment. Predict both outputs before running.
LOAD R1,6
LOAD R2,2
ADD R1,R2->R1
PRINT R1
ADD R1,R2->R1
PRINT R1
HALT
examples/toy_cpu.sh (5297 bytes)
#!/usr/bin/env bash
# toy_cpu.sh — a four-instruction toy CPU simulator for the Day 2 lab.
#
# It reads a program file (one instruction per line, '#' starts a comment)
# and executes it exactly the way the lesson describes: fetch the
# instruction at the program counter (PC), advance the PC, decode the
# instruction into an opcode and operands, execute it, and show the
# register state after every step.
#
# Instruction set (opcodes must be uppercase):
# LOAD Rn,value put an integer into register Rn
# ADD Ra,Rb->Rc add registers Ra and Rb, store the sum in Rc
# PRINT Rn send the value of register Rn to the output
# HALT stop the machine
#
# Registers: R1 R2 R3 R4, all starting at 0.
#
# Usage: bash toy_cpu.sh <program-file>
#
# Exit codes: 0 = program ran to HALT; 1 = bad instruction or no HALT;
# 2 = usage error (missing or unreadable program file).
set -u
if [ $# -ne 1 ]; then
echo "usage: bash $0 <program-file>" >&2
exit 2
fi
program_file="$1"
if [ ! -f "${program_file}" ]; then
echo "error: program file not found: ${program_file}" >&2
exit 2
fi
# --- machine state -----------------------------------------------------
R1=0; R2=0; R3=0; R4=0
PC=0
executed=0
halted=no
die() {
# PC has already advanced past the fetched instruction, so report PC-1.
echo "ERROR at PC=$((PC - 1)): $1" >&2
exit 1
}
is_reg() {
case "$1" in
R1 | R2 | R3 | R4) return 0 ;;
*) return 1 ;;
esac
}
reg_value() {
# Caller must have validated the name with is_reg first.
eval "printf '%s' \"\$$1\""
}
set_reg() {
eval "$1=\$2"
}
is_int() {
case "$1" in
'' | -) return 1 ;;
-*) case "${1#-}" in *[!0-9]* | '') return 1 ;; *) return 0 ;; esac ;;
*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
# --- load the program into "memory" (a bash array) ---------------------
program=()
while IFS= read -r raw || [ -n "${raw}" ]; do
line="${raw%%#*}" # strip comments
line="${line#"${line%%[![:space:]]*}"}" # trim leading whitespace
line="${line%"${line##*[![:space:]]}"}" # trim trailing whitespace
[ -z "${line}" ] && continue
program[${#program[@]}]="${line}"
done < "${program_file}"
if [ "${#program[@]}" -eq 0 ]; then
echo "error: ${program_file} contains no instructions" >&2
exit 2
fi
echo "=== Toy CPU ==="
echo "Program: ${program_file} (${#program[@]} instructions in memory)"
echo "Registers start at: R1=0 R2=0 R3=0 R4=0"
echo
# --- the fetch-decode-execute loop --------------------------------------
while [ "${PC}" -lt "${#program[@]}" ]; do
instr="${program[${PC}]}"
printf 'PC=%s FETCH %s\n' "${PC}" "${instr}"
PC=$((PC + 1)) # PC advances right after fetch
case "${instr}" in
*' '*) op="${instr%% *}"; args="${instr#* }" ;;
*) op="${instr}"; args="" ;;
esac
args="$(printf '%s' "${args}" | tr -d '[:space:]')"
case "${op}" in
LOAD)
dest="${args%%,*}"
value="${args#*,}"
[ "${dest}" = "${args}" ] && die "LOAD needs the form: LOAD Rn,value"
is_reg "${dest}" || die "unknown register '${dest}' (valid: R1 R2 R3 R4)"
is_int "${value}" || die "'${value}' is not an integer"
printf ' DECODE opcode=LOAD dest=%s value=%s\n' "${dest}" "${value}"
set_reg "${dest}" "${value}"
printf ' EXECUTE %s <- %s\n' "${dest}" "${value}"
;;
ADD)
case "${args}" in
*,*'->'*) ;;
*) die "ADD needs the form: ADD Ra,Rb->Rc" ;;
esac
src1="${args%%,*}"
rest="${args#*,}"
src2="${rest%%->*}"
dest="${rest#*->}"
is_reg "${src1}" || die "unknown register '${src1}' (valid: R1 R2 R3 R4)"
is_reg "${src2}" || die "unknown register '${src2}' (valid: R1 R2 R3 R4)"
is_reg "${dest}" || die "unknown register '${dest}' (valid: R1 R2 R3 R4)"
v1="$(reg_value "${src1}")"
v2="$(reg_value "${src2}")"
sum=$((v1 + v2))
printf ' DECODE opcode=ADD src1=%s src2=%s dest=%s\n' "${src1}" "${src2}" "${dest}"
set_reg "${dest}" "${sum}"
printf ' EXECUTE %s <- %s + %s = %s + %s = %s\n' "${dest}" "${src1}" "${src2}" "${v1}" "${v2}" "${sum}"
;;
PRINT)
reg="${args}"
[ -n "${reg}" ] || die "PRINT needs the form: PRINT Rn"
is_reg "${reg}" || die "unknown register '${reg}' (valid: R1 R2 R3 R4)"
printf ' DECODE opcode=PRINT reg=%s\n' "${reg}"
printf ' EXECUTE send %s to the output\n' "${reg}"
printf 'OUTPUT: %s\n' "$(reg_value "${reg}")"
;;
HALT)
printf ' DECODE opcode=HALT\n'
printf ' EXECUTE stop the clock\n'
executed=$((executed + 1))
halted=yes
break
;;
*)
die "unknown opcode '${op}' (valid: LOAD ADD PRINT HALT)"
;;
esac
executed=$((executed + 1))
printf ' REGS R1=%s R2=%s R3=%s R4=%s\n\n' "${R1}" "${R2}" "${R3}" "${R4}"
done
echo
if [ "${halted}" = yes ]; then
echo "HALT reached after ${executed} instructions."
echo "Final registers: R1=${R1} R2=${R2} R3=${R3} R4=${R4}"
else
echo "ERROR: the program ended without HALT — a real CPU would keep fetching whatever bits sit in the next memory cells and try to execute them." >&2
exit 1
fi
metadata.yml (826 bytes)
lesson_id: D002
day: 2
kind: process-simulation
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-002-the-cpu-fetch-decode-execute
run_commands:
- bash examples/toy_cpu.sh examples/programs/add-two-numbers.txt
- bash examples/toy_cpu.sh examples/programs/trace-01.txt
- bash examples/toy_cpu.sh examples/programs/trace-02.txt
- bash examples/toy_cpu.sh examples/programs/trace-03.txt
- bash examples/toy_cpu.sh starter/my_program.txt
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/my_program.txt starter/trace-worksheet.md # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), bash tests/run_tests.sh → 27 checks, 0 failure(s).'
requirements/README.md (614 bytes)
# Dependencies — Day 002 lab
**None beyond a POSIX shell.** This lab intentionally has zero installable
dependencies:
- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux
distribution; on Windows, use WSL).
- Standard utilities the scripts call: `tr`, `grep`, `sed`, `mktemp`,
`dirname` — all part of the base system on macOS and Linux.
There is deliberately no `requirements.txt`/`package.json` here: the toy
CPU is a single readable shell script, because the point of the lab is
that you can see every moving part. Later labs declare their Python or
Node dependencies in this directory.
starter/my_program.txt (604 bytes)
# my_program.txt — your own program for the toy CPU.
#
# It already works. From the lab directory, run:
#
# bash examples/toy_cpu.sh starter/my_program.txt
#
# Then extend it. Rules of the machine:
# - four registers: R1 R2 R3 R4, all starting at 0
# - LOAD Rn,value / ADD Ra,Rb->Rc / PRINT Rn / HALT
# - opcodes in uppercase, one instruction per line, '#' starts a comment
#
# Ideas: double a number three times, sum three different values into R4,
# or print a register before and after changing it. Keep HALT last —
# remove it and watch what the simulator says.
LOAD R1,2
PRINT R1
HALT
starter/trace-worksheet.md (3446 bytes)
# Trace worksheet — be the CPU before you run the CPU
Work through all three programs **by hand, on this sheet, before running
anything**. That order matters: predicting first and checking second is how
you find out whether your mental model of fetch-decode-execute is right.
The programs live in `../examples/programs/`.
Rules of the machine (same as the lesson):
- Four registers — R1, R2, R3, R4 — all start at 0.
- The PC starts at 0 and moves to the next line after each fetch.
- `LOAD Rn,value` puts a number in a register. `ADD Ra,Rb->Rc` adds two
registers into a third (which may be one of the sources). `PRINT Rn`
emits `OUTPUT: <value>`. `HALT` stops the machine.
Fill every empty cell. The first row of program 1 is done for you.
## Program 1 — `trace-01.txt`
```text
LOAD R1,4
LOAD R2,7
ADD R1,R2->R3
PRINT R3
HALT
```
| PC | Instruction | What the execute step does | R1 | R2 | R3 | R4 |
| --- | --- | --- | --- | --- | --- | --- |
| 0 | `LOAD R1,4` | R1 gets the value 4 | 4 | 0 | 0 | 0 |
| 1 | `LOAD R2,7` | | | | | |
| 2 | `ADD R1,R2->R3` | | | | | |
| 3 | `PRINT R3` | | | | | |
| 4 | `HALT` | | | | | |
- Predicted `OUTPUT:` line(s): `OUTPUT: ____`
- Predicted final line: `Final registers: R1=__ R2=__ R3=__ R4=__`
- Predicted instruction count in `HALT reached after __ instructions.`
## Program 2 — `trace-02.txt`
```text
LOAD R1,10
ADD R1,R1->R2
ADD R2,R2->R2
PRINT R2
HALT
```
Watch the third line closely: R2 is both a source and the destination. The
ALU reads the old value, computes, and only then does the write-back
replace it.
| PC | Instruction | What the execute step does | R1 | R2 | R3 | R4 |
| --- | --- | --- | --- | --- | --- | --- |
| 0 | `LOAD R1,10` | | | | | |
| 1 | `ADD R1,R1->R2` | | | | | |
| 2 | `ADD R2,R2->R2` | | | | | |
| 3 | `PRINT R2` | | | | | |
| 4 | `HALT` | | | | | |
- Predicted `OUTPUT:` line(s): `OUTPUT: ____`
- Predicted final line: `Final registers: R1=__ R2=__ R3=__ R4=__`
- Predicted instruction count in `HALT reached after __ instructions.`
## Program 3 — `trace-03.txt`
```text
LOAD R1,6
LOAD R2,2
ADD R1,R2->R1
PRINT R1
ADD R1,R2->R1
PRINT R1
HALT
```
Two `PRINT`s: each one emits the register's value *at that moment*, so the
two output lines will differ.
| PC | Instruction | What the execute step does | R1 | R2 | R3 | R4 |
| --- | --- | --- | --- | --- | --- | --- |
| 0 | `LOAD R1,6` | | | | | |
| 1 | `LOAD R2,2` | | | | | |
| 2 | `ADD R1,R2->R1` | | | | | |
| 3 | `PRINT R1` | | | | | |
| 4 | `ADD R1,R2->R1` | | | | | |
| 5 | `PRINT R1` | | | | | |
| 6 | `HALT` | | | | | |
- Predicted `OUTPUT:` line(s): `OUTPUT: ____` and `OUTPUT: ____`
- Predicted final line: `Final registers: R1=__ R2=__ R3=__ R4=__`
- Predicted instruction count in `HALT reached after __ instructions.`
## Check yourself
Only now, run each program and compare every prediction:
```bash
bash ../examples/toy_cpu.sh ../examples/programs/trace-01.txt
bash ../examples/toy_cpu.sh ../examples/programs/trace-02.txt
bash ../examples/toy_cpu.sh ../examples/programs/trace-03.txt
```
For any cell you got wrong, write one sentence below it about *why* — the
mistake is more valuable than the correction. The most common ones: using
a register's new value one step too early (program 2), and forgetting that
`PRINT` reports the value at that instant, not the final value (program 3).
tests/run_tests.sh (4763 bytes)
#!/usr/bin/env bash
# Tests for the Day 002 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Verifies that the toy CPU simulator produces exactly the outputs, step
# counts, and final register states the lesson and worksheet promise, that
# the learner's program in starter/my_program.txt runs to HALT, and that
# the simulator rejects bad programs with a non-zero exit code.
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cpu="${lab_dir}/examples/toy_cpu.sh"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
contains() {
# contains <haystack> <needle-regex-free-fixed-string>
case "$1" in *"$2"*) return 0 ;; *) return 1 ;; esac
}
run_program_checks() {
# run_program_checks <program> <label> <steps> <final-regs> <output-values...>
local program="$1" label="$2" steps="$3" regs="$4"
shift 4
local output
echo "Testing ${label} ..."
if ! output="$(bash "${cpu}" "${program}" 2>&1)"; then
check "${label}: simulator exits 0" "no"
echo "${output}" | sed 's/^/ /'
return
fi
check "${label}: simulator exits 0" "yes"
local fetches
fetches="$(printf '%s\n' "${output}" | grep -c '^PC=[0-9]* FETCH')"
if [ "${fetches}" = "${steps}" ]; then
check "${label}: ${steps} instructions fetched" "yes"
else
check "${label}: ${steps} instructions fetched (got ${fetches})" "no"
fi
if contains "${output}" "HALT reached after ${steps} instructions."; then
check "${label}: halts after ${steps} instructions" "yes"
else
check "${label}: halts after ${steps} instructions" "no"
fi
if contains "${output}" "Final registers: ${regs}"; then
check "${label}: final registers are '${regs}'" "yes"
else
check "${label}: final registers are '${regs}'" "no"
fi
local expected_outputs actual_outputs
expected_outputs=""
for v in "$@"; do
expected_outputs="${expected_outputs}OUTPUT: ${v}
"
done
actual_outputs="$(printf '%s\n' "${output}" | grep '^OUTPUT: ' || true)
"
if [ "${actual_outputs}" = "${expected_outputs}" ]; then
check "${label}: output lines are exactly [$*]" "yes"
else
check "${label}: output lines are exactly [$*]" "no"
fi
}
# --- 1. the reference programs, against the values on the worksheet -----
run_program_checks "${lab_dir}/examples/programs/add-two-numbers.txt" \
"add-two-numbers" 5 "R1=5 R2=3 R3=8 R4=0" 8
run_program_checks "${lab_dir}/examples/programs/trace-01.txt" \
"trace-01" 5 "R1=4 R2=7 R3=11 R4=0" 11
run_program_checks "${lab_dir}/examples/programs/trace-02.txt" \
"trace-02" 5 "R1=10 R2=40 R3=0 R4=0" 40
run_program_checks "${lab_dir}/examples/programs/trace-03.txt" \
"trace-03" 7 "R1=10 R2=2 R3=0 R4=0" 8 10
# --- 2. the learner's own program ---------------------------------------
echo "Testing starter/my_program.txt ..."
if my_out="$(bash "${cpu}" "${lab_dir}/starter/my_program.txt" 2>&1)"; then
check "my_program: runs to completion (exit 0)" "yes"
else
check "my_program: runs to completion (exit 0)" "no"
echo "${my_out}" | sed 's/^/ /'
my_out=""
fi
if printf '%s\n' "${my_out}" | grep -q '^OUTPUT: '; then
check "my_program: produces at least one OUTPUT line" "yes"
else
check "my_program: produces at least one OUTPUT line" "no"
fi
if contains "${my_out}" "HALT reached after"; then
check "my_program: ends with HALT" "yes"
else
check "my_program: ends with HALT" "no"
fi
# --- 3. the simulator must reject broken programs ------------------------
echo "Testing error handling ..."
tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT
printf 'FLY R1,3\nHALT\n' > "${tmp_dir}/bad-opcode.txt"
if bash "${cpu}" "${tmp_dir}/bad-opcode.txt" >/dev/null 2>&1; then
check "unknown opcode is rejected (non-zero exit)" "no"
else
check "unknown opcode is rejected (non-zero exit)" "yes"
fi
printf 'LOAD R9,1\nHALT\n' > "${tmp_dir}/bad-register.txt"
if bash "${cpu}" "${tmp_dir}/bad-register.txt" >/dev/null 2>&1; then
check "unknown register is rejected (non-zero exit)" "no"
else
check "unknown register is rejected (non-zero exit)" "yes"
fi
printf 'LOAD R1,1\nPRINT R1\n' > "${tmp_dir}/no-halt.txt"
if bash "${cpu}" "${tmp_dir}/no-halt.txt" >/dev/null 2>&1; then
check "program without HALT is rejected (non-zero exit)" "no"
else
check "program without HALT is rejected (non-zero exit)" "yes"
fi
if bash "${cpu}" "${tmp_dir}/does-not-exist.txt" >/dev/null 2>&1; then
check "missing program file is rejected (non-zero exit)" "no"
else
check "missing program file is rejected (non-zero exit)" "yes"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 002 lab
ERROR at PC=N: unknown opcode 'load'
Opcodes must be uppercase: LOAD, ADD, PRINT, HALT. The simulator is
deliberately strict, because real CPUs are stricter still — a single wrong
bit in an opcode is a different instruction or an illegal one. Fix the
case and rerun.
ERROR at PC=N: unknown register 'R5' (or R0)
The machine has exactly four registers: R1, R2, R3, R4. There is
no R0 and no R5 — just as a real CPU has a fixed register set baked into
its silicon. Use one of the four valid names.
ERROR at PC=N: ADD needs the form: ADD Ra,Rb->Rc
Check the punctuation: a comma between the two sources and an ASCII arrow
-> before the destination, e.g. ADD R1,R2->R3. Spaces around the comma
and arrow are fine; a Unicode arrow (→) pasted from a document is not.
ERROR: the program ended without HALT …
Every program must end by telling the machine to stop. The message is the
lesson: a real CPU never decides it is finished — it fetches whatever the
PC points at next, forever, even if that memory is garbage. Add HALT as
the last instruction.
error: program file not found: …
Run the commands from the lab directory (the one containing README.md),
so relative paths like examples/programs/trace-01.txt resolve. If you
are inside starter/, the paths start with ../examples/ instead — the
worksheet uses that form.
Permission denied when running the simulator
You do not need to make anything executable — invoke it through bash
explicitly: bash examples/toy_cpu.sh <program>. If you prefer
./examples/toy_cpu.sh, first run chmod +x examples/toy_cpu.sh.
The tests fail on my_program: …
The suite requires your starter/my_program.txt to (a) run without
errors, (b) print at least one OUTPUT: line, and (c) end with HALT.
Run it directly — bash examples/toy_cpu.sh starter/my_program.txt — and
the simulator's own error message will point at the offending line.
My edits to my_program.txt vanished after cleanup
git checkout -- starter/my_program.txt restores the shipped version —
that is what it is for. Copy your program elsewhere first if you want to
keep it.
Windows: bash is not recognized
Install WSL (wsl --install, then open Ubuntu) and run the lab there
unchanged. The simulator is a bash script, so PowerShell alone cannot run
it; Git Bash generally works too, but WSL matches the environment the
whole course assumes.
Security notes
Security notes — Day 002 lab
- What the scripts do:
examples/toy_cpu.shreads a text file you name on the command line, simulates four arithmetic-and-print instructions, and writes only to your terminal.tests/run_tests.shruns the simulator on the bundled programs plus a few deliberately broken ones it creates in a temporary directory (removed on exit). Neither script makes any network connections, writes files into the repository, or changes settings. - Privileges: everything runs as your normal user. Nothing here needs
sudo, and nothing ever will for this lab. - Interpreting untrusted programs: the simulator validates every instruction against a four-opcode whitelist and rejects anything else with a non-zero exit code — it never passes program text to the shell for execution. Still, treat program files from other people the way you treat any file from the internet: read them first. They are at most seven lines long.
- Reading before running: the simulator is about 180 commented lines — short enough to read end to end, and the lab expects you to. Running unread shell scripts is one of the most common ways developers get compromised; every lab in this course keeps its scripts small enough to audit before executing, and this one doubles as the lesson itself.