Computing FoundationsInside the Machine › Day 2

Hands-on lab — Day 2: 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

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 HALT stops the loop.
  • Trace register state by hand through LOAD, ADD, and PRINT instructions, 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 a FETCH line (the PC and the raw instruction), advances the PC, prints a DECODE line (the opcode and operands it recognized), an EXECUTE line (the effect), and a REGS line (all four registers). PRINT emits an OUTPUT: line; HALT stops the loop and prints the instruction count and final registers. Bad opcodes, bad register names, and programs without HALT end 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 your starter/my_program.txt and checks it reaches HALT and prints at least one OUTPUT: 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

  1. Run the demo program (step 1 above) and match it line-for-line against the expected output block here.
  2. Complete starter/trace-worksheet.md fully — every cell, every predicted OUTPUT: line, every final-register prediction — then run the three trace programs and mark each prediction right or wrong.
  3. For every wrong cell, write the one-sentence "why" the worksheet asks for.
  4. Extend starter/my_program.txt so it uses at least one ADD and prints at least two values, then run it — it must reach HALT reached after N instructions. with no errors.
  5. 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

  1. Compute 5 × 6 using only the four instructions (hint: multiplication is repeated addition — chain ADDs through a register). Predict the instruction count first.
  2. The simulator's PC advances immediately after fetch, before execute. Find the two lines in examples/toy_cpu.sh where this happens, and write one sentence on what a JUMP instruction would have to change for loops to become possible.
  3. Add a SUB Ra,Rb->Rc instruction to a copy of toy_cpu.sh (model it on the ADD case), write a program that uses it, and extend a copy of the test suite to cover it.

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.sh reads a text file you name on the command line, simulates four arithmetic-and-print instructions, and writes only to your terminal. tests/run_tests.sh runs 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.