ThinkAloud
Projects
ThinkAloud
  1. 01Projects21 ready to build and explain
Browse projects
Theme

Projects from Coding Challenges by John Crickett

Projects/Brainfuck Interpreter

Build your own Brainfuck interpreter

An interpreter for Brainfuck, a tiny language of eight one-character commands, that grows into a compiler to bytecode and a virtual machine to run it, then gets many times faster through optimisation. Because the language is so small, nothing hides the machinery every interpreter shares: memory and a program counter, loops resolved before the program runs, and speed won by doing more work per instruction and measured with care.

From Coding Challenges #81 ↗ by John Crickett

  • Challenge #81
  • Intermediate
  • 9 concepts
  • 7 steps + 1 optional
  • Not yet checked
Start Brainfuck Interpreter

No sign-up. You tell the mentor your language: Java, Python or Node.js.

What you'll learn · 9 concepts

The concepts behind Brainfuck Interpreter

In the order the steps use them. Each one links to the best places to learn it, and you explain it in your own words once its steps are built.

  1. 1The read-eval-print loopStep 2

    The first step is a REPL that only echoes, and it becomes the front end that feeds your interpreter, alongside running a whole file.

    Covers: read, evaluate, print, repeat · state kept between entries · input that isn't complete yet · ending cleanly at end of input

    Read-eval-print loop (Wikipedia) ↗ · Bash Reference Manual: Shell Operation ↗

  2. 2The machine: a tape of cells, a data pointer and a program counterStep 3

    Your interpreter is this machine. Every command is a small change to the tape, the data pointer or the program counter.

    Covers: the tape of cells, all starting at zero · the data pointer · the program counter, kept apart from the data · loops as the only control flow · Turing completeness

    Brainfuck (Wikipedia) ↗ · Turing Machines (Sam Rose) ↗

  3. 3Fetch, decode, execute: the loop inside every interpreterStep 3, 5

    The interpreter in step 2 is this loop over the source text, and the virtual machine in step 4 is the same loop over bytecode.

    Covers: the program counter moving one step at a time · deciding what an instruction means · jumps that set the program counter · the fixed cost of every trip round the loop

    Instruction cycle (Wikipedia) ↗ · Adventures in JIT compilation, part 1: an interpreter (Eli Bendersky) ↗

  4. 4Matching brackets and precomputing jump targetsStep 3, 4

    Step 2 needs the program counter to find matching brackets, and step 3 moves that work to compile time by storing each jump's target in the bytecode.

    Covers: nesting and a stack of open brackets · scanning at run time versus a jump table built once · unmatched brackets as errors found before running

    Adventures in JIT compilation, part 1: an interpreter (Eli Bendersky) ↗ · Crafting Interpreters: Jumping Back and Forth ↗

  5. 5What the spec leaves open: cell size, wrap-around, tape ends and end of inputStep 3, 6

    The page settles none of these, and the sample programs only work when your choices match what they expect. You decide them when you build the interpreter and must keep them when you optimise.

    Covers: cell size and wrap-around · moving off either end of the tape · what the comma command does at end of input · implementation-defined behaviour and portable programs

    The Epistle to the Implementors (Daniel B. Cristofani) ↗ · Brainfuck (Esolang wiki) ↗ · Unspecified behavior (Wikipedia) ↗

  6. 6Compiling to bytecode: moving work from run time to compile timeStep 4

    Step 3 asks for exactly this split: a compiler that drops comments and fixes every loop's jump target, so the virtual machine only has to run instructions.

    Covers: translate once, run many times · opcodes and operands · dropping comments and resolving jumps ahead of time · errors found before running · the next step down, native code and just-in-time compilation

    Crafting Interpreters: Chunks of Bytecode ↗ · Bytecode (Wikipedia) ↗ · Adventures in JIT compilation, part 2: an x64 JIT (Eli Bendersky) ↗

  7. 7Virtual machines: an instruction set run by softwareStep 5, 7

    Step 4 builds the virtual machine that runs your bytecode, and step 6 grows its instruction set with instructions for common patterns.

    Covers: designing the instruction set · the instruction set as a contract with the compiler · the machine state, held by the virtual machine · dispatch cost per instruction · growing the instruction set

    Crafting Interpreters: A Virtual Machine ↗ · Berkeley Packet Filter (Wikipedia) ↗

  8. 8Peephole optimisation: run-length and recognised patternsStep 6, 7

    Steps 5 and 6 are this optimiser: first runs of the same command, then the clear loop and the clear-and-move pair, each judged by unchanged output and by time.

    Covers: collapsing a run of the same command into one counted instruction · recognising idioms such as the clear loop · keeping the meaning exactly the same · finding the hot patterns by counting what runs

    Brainfuck optimization strategies (Mats Linander) ↗ · Peephole optimization (Wikipedia) ↗

  9. 9Measuring speed honestlyStep 5

    The page asks you to time the Mandelbrot program after the interpreter, the virtual machine and each optimisation, and to judge every change by how much faster it got.

    Covers: wall-clock time versus processor time · a recorded baseline and the same inputs · noise and repeated runs · keeping output out of the measurement · profiling to find where the time goes · correct output before any speed claim

    time(1), Linux manual page ↗ · Crafting Interpreters: Optimization (see Measuring Performance) ↗

What you'll build · 7 steps

Build Brainfuck Interpreter step by step

Steps of 30 to 60 minutes, in your own editor and language. Each one has a check, so you know when it works.

  1. 01

    Set up

    Choose your language and editor, and save the challenge's sample programs as files: the short Hello! program, the test script and the Mandelbrot program from the linked blog post. Before reading the page's answer, try writing a program that prints Hello! yourself.

    Works whenThe three sample programs are saved and ready to run, and you can explain how the Hello! program builds each letter in its cells.

  2. 02

    REPL and echo

    Start an interactive prompt that reads what you type and prints it straight back. Then, when a file name is given, print that file's contents instead.

    Works whenA line typed at the prompt comes back unchanged, and running the program on the Hello! file prints the file's text exactly.

    Concepts: the read-eval-print loop

  3. 03

    Interpreter

    Run a program one command at a time on a tape of 30,000 cells set to zero, or a size you can choose. Ignore every character that isn't one of the eight commands, and make the program counter jump to the matching bracket when a loop is skipped or repeated.

    Works whenThe Hello! program prints Hello!, the test script prints a readable message, and the Mandelbrot program draws a recognisable Mandelbrot set in text. Timed with its output thrown away, the Mandelbrot run takes tens of seconds at this stage, and that time is your baseline.

    Concepts: the machine · fetch, decode, execute · matching brackets and precomputing jump targets · what the spec leaves open

  4. 04

    Bytecode compiler

    Compile the source into bytecode with one instruction for each command. Leave the comments out, and give every loop instruction the position of its partner, worked out at compile time.

    Works whenThe bytecode for each sample program has no comments, one instruction per command, and loop instructions that point at each other, and the programs still behave exactly as before.

    Concepts: compiling to bytecode · matching brackets and precomputing jump targets

  5. 05

    Virtual machine

    Run the bytecode in a virtual machine with its own program counter and data pointer. It reads each opcode and, for loops, its target, then updates the program counter or the current cell.

    Works whenEvery earlier program gives the same output as with the step 2 interpreter, and the Mandelbrot program is timed again under the same conditions and compared with the baseline.

    Concepts: virtual machines · fetch, decode, execute · measuring speed honestly

  6. 06

    Collapse repeated commands

    Make the compiler turn a run of the same command into one instruction with a count, such as a single instruction that adds ten in place of ten plus commands, and make the virtual machine carry it out.

    Works whenThe Hello! program compiles to about a third as many instructions as it has commands (the page gets 26 from 70), its output is unchanged, and the Mandelbrot program runs much faster than the baseline.

    Concepts: peephole optimisation · what the spec leaves open

  7. 07

    Replace common patterns

    Recognise the loop that subtracts one until its cell is zero, and that loop followed by a move right, and compile each into a single new instruction that the virtual machine supports.

    Works whenEvery sample program still gives the same output, and timing shows a clear further speed-up over step 5.

    Concepts: peephole optimisation · virtual machines

  8. +

    Going furtherOptional

    Count how often each loop runs and turn the most common loop shapes into new instructions. Beyond that, replace the virtual machine with a just-in-time compiler, or emit assembly or a compiler back end's intermediate form instead of bytecode.

    Works whenThe Mandelbrot program draws the same picture in a small fraction of the original time, with the page's target under 15 percent of it.

    Concepts: measuring speed honestly · peephole optimisation · compiling to bytecode

Ready to start Brainfuck Interpreter?

Start Brainfuck Interpreter

From Coding Challenges #81 ↗ by John Crickett. Notes written in our own words.

Projects from Coding Challenges by John Crickett