elouann.dev
FR — version française

ProjectsLow-level & embedded

Brainfxcker

Brainfuck to x86-64 compilerongoing

Period
Apr 2026 – present
Context
personal
Team
solo
  • C
  • Brainfuck
  • Assembly
  • NASM x86-64 Intel
  • Compiler
  • Low-level
  • Imperative programming

View source

A real little compiler: it reads a Brainfuck program, passes it through a lexer, a parser and an intermediate representation, then writes x86-64 assembly that NASM and ld turn into an ELF binary.

main.bfsourcelexertokensparsertreeIRADD, MOVE…optim.mergecodegen.asmnasmelf64 .oldELFBrainfuckoptimised IRx86-64 (NASM)+++ADD 3add byte [rbx], 3>>MOVE 2add rbx, 2[LOOP_BEGcmp byte [rbx], 0 ; je .L0_end<MOVE -1add rbx, -1]LOOP_ENDcmp byte [rbx], 0 ; jne .L0_beg.OUTPUTmov rax, 1 … syscallrepeated symbols are merged into a single instruction
DiagramThe compilation pipeline, and what a small piece of program becomes at each stage.

Why Brainfuck

Brainfuck has only eight instructions, acting on a tape of memory cells and a pointer. The language is trivial: all the interest is in the translation. It was the perfect excuse to understand assembly and the stages of a compiler, without getting lost in the grammar of a complex language.

The stages

The lexer only keeps the eight meaningful characters. The parser builds a tree in which each [ ] loop holds its own block. That tree is then flattened into intermediate instructions: ADD, MOVE, SET, OUTPUT, INPUT, LOOP_BEG, LOOP_END.

An optimisation pass merges repeated instructions (+++ becomes ADD 3, >> becomes MOVE 2) and spots reset loops such as [-]. The code generator finally writes NASM code: the 30,000-byte tape is reserved in .bss, the pointer lives in rbx, and input / output go straight through the read, write and exit system calls.

The details

Instructions are stored in a hand-written “paged vector”: an array of 64-element pages that grows without ever moving the elements already stored. Since I work on a Mac, the compiler runs in a linux/amd64 Docker container to produce and run real x86-64 binaries.

What I took away

  • A compiler is mostly a series of representations, each one closer to the machine.
  • The Linux system call convention (rax, rdi, rsi, rdx, then syscall).
  • What the assembler and the linker actually do, and what is inside an ELF file.