bf to LLVM IR compiler frontend
  • C 48.1%
  • CMake 13.8%
  • JavaScript 12%
  • CSS 7.4%
  • Shell 6.8%
  • Other 11.9%
Find a file
benmandrew bc56fafda9
docs(readme): tighten the CFG section and fix stale build steps
The control flow graph section carried three paragraphs on what the
optimiser folds away, making it the longest section in the README.
Move them to docs/OPTIMISATION.md under "What the graph shows", leaving
the commands, the -U pairing, and a link.

Also: prefix the wasm build step with `nix develop -c`, since the flake
supplies emcc and the step fails outside the shell; give the screenshot
real alt text; and drop the backticks around "bf" in prose, where they
now read as a command name that does not exist. CLAUDE.md notes why its
commands carry the `build/` prefix the README omits.
2026-09-01 14:33:58 +01:00
.github/workflows build: provide emscripten via the nix devShell 2026-07-17 01:37:20 +01:00
cmake refactor: return compiler output as strings behind a shared API 2026-07-16 22:54:52 +01:00
docker build(docker): pin the aflplusplus base and drop root in the fuzz image 2026-07-26 22:25:13 +01:00
docs docs(readme): tighten the CFG section and fix stale build steps 2026-09-01 14:33:58 +01:00
scripts docs(naming): use lowercase "bf" for the language name everywhere 2026-09-01 13:21:31 +01:00
src docs(naming): use lowercase "bf" for the language name everywhere 2026-09-01 13:21:31 +01:00
test docs(naming): use lowercase "bf" for the language name everywhere 2026-09-01 13:21:31 +01:00
verification build: extend fmt and lint to cover scripts 2026-07-14 18:20:20 +01:00
web docs(naming): use lowercase "bf" for the language name everywhere 2026-09-01 13:21:31 +01:00
.clang-format Remove language specifier due to clang-format bug 2025-11-15 11:24:16 +00:00
.dockerignore feat: render control-flow graphs from the backend runtime image 2026-07-14 23:20:41 +01:00
.envrc build: add .envrc for direnv-loaded devShell 2026-07-14 16:31:41 +01:00
.gitignore build: cross-compile bfc to WebAssembly with a parity-checked CI job 2026-07-16 22:55:28 +01:00
CLAUDE.md docs(readme): tighten the CFG section and fix stale build steps 2026-09-01 14:33:58 +01:00
CMakeLists.txt build: extend fmt and lint to cover scripts 2026-07-14 18:20:20 +01:00
CPPLINT.cfg Switch linter from clang-tidy to cpplint 2025-11-15 11:14:06 +00:00
flake.lock build: bump the toolchain to LLVM 22 to unbreak sanitizers on macOS 26 2026-07-14 23:00:16 +01:00
flake.nix build: provide emscripten via the nix devShell 2026-07-17 01:37:20 +01:00
LICENCE chore: Add LICENCE 2026-04-06 16:15:05 +01:00
MODELCHECKING.md doc: Document model checking details 2026-04-27 01:25:46 +01:00
README.md docs(readme): tighten the CFG section and fix stale build steps 2026-09-01 14:33:58 +01:00

bf to LLVM IR Compiler Frontend

A compiler frontend for the bf language that outputs code in LLVM Intermediate Representation (IR), which can then be compiled to any desired target architecture with clang.

You can interact with it online here, or self-host the web interface — which shows the bf source, the compiled LLVM IR, and its control flow graph side by side. The demo runs entirely in the browser: bfc is built to WebAssembly and compiles client-side, with no backend. Build the static bundle and serve it locally:

$ nix develop -c scripts/build-wasm.sh                # -> web/wasm/bfc.{mjs,wasm}
$ nix develop -c cmake -B build
$ nix develop -c cmake --build build --target site    # -> build/site/
$ cd build/site && nix develop -c python3 -m http.server 8080

Then open http://localhost:8080. The control flow graph is themed and syntax-highlighted in the browser (web/highlight.js), then laid out by a vendored Graphviz compiled to WebAssembly. See web/README.md for the full pipeline and the MIME types a static host must set.

The input validation and parsing functionality is formally verified to be memory safe for inputs up to thirteen commands long. Details are in MODELCHECKING.md.

The web demo: bf source, compiled LLVM IR, and control flow graph side by side

Dependencies

The project ships a Nix flake with a devShell providing every tool the build needs — cmake, LLVM/clang, check, expect, clang-format, cpplint, Doxygen, Graphviz, Python, ruff, shfmt, and shellcheck — pinned via flake.lock for reproducibility. This is what CI uses, and is the recommended way to build locally:

$ nix develop

Every command in this README can be run unmodified inside that shell.

A .envrc is checked in, so with direnv the shell loads on entering the directory — direnv allow once, and nix develop becomes unnecessary. Installing nix-direnv alongside it is worthwhile: it caches the shell so re-entry is instant and stops the garbage collector from reclaiming the dependencies.

Manual install (alternative to Nix)

Ubuntu/Debian

$ sudo apt-get install cmake llvm-dev check expect clang-format cpplint doxygen graphviz

MacOS (Homebrew)

$ brew install cmake llvm check expect clang-format cpplint doxygen graphviz

Building

$ cmake -B build
$ cmake --build build

After building, the bfc (compiler) and bfi (interpreter) executables will be in the build directory.

To compile a bf program to a binary executable:

# Generate LLVM IR
$ bfc test/res/helloworld.b > main.ll
# Compile IR to binary
$ clang main.ll -o main
$ ./main
Hello, World!

To execute a bf program with the interpreter:

$ bfi test/res/helloworld.b
Hello, World!

Visualising the Control Flow Graph

Basic blocks are named after the loop that creates them (loop6.body, loop6.end). Passing --label-blocks additionally appends the span of bf source each block covers, which is enough to read a control flow graph (CFG) back against the original program:

$ scripts/cfg.sh test/res/fib.b                 # writes cfg.png
$ scripts/cfg.sh -o fib_cfg.svg test/res/fib.b  # extension picks the format
$ scripts/cfg.sh -i test/res/fib.b              # with each block's IR, highlighted
$ scripts/cfg.sh -U test/res/fib.b              # unoptimised, with source-span labels

bfc --emit-cfg-dot emits the graph as Graphviz dot directly — -i adds --cfg-instructions for the instruction-level view — so the script only chains bfc and dot, theming the graph and syntax-highlighting the IR on the way through with highlight.py. scripts/cfg.sh -h lists the remaining flags, and both scripts document how the theming works and why the highlighting follows Prism's LLVM grammar. Because bfc produces the dot with the same LLVM it links against, there is no separate opt and no version skew to guard against.

-U turns optimisation off and switches --label-blocks on, and the two belong together: simplifycfg merges and renames blocks, so source spans only mean anything on an unoptimised graph. That graph is much bigger, since fib.b draws 18 bounds-check blocks rather than 5 — see docs/OPTIMISATION.md for what the optimiser folds away.

Formatting and Linting

$ cmake --build build --target fmt lint

The C sources go through clang-format and cpplint; the shell and Python under scripts/ and verification/ go through shfmt, shellcheck and ruff. cmake/scripts.cmake attaches the latter three to the same targets.

Documentation

Code docs can be accessed online at benmandrew.com/docs/bf/, or built locally with

$ cmake --build build --target docs

Doxygen comes from the devShell, but Sphinx is pip-installed into a virtualenv under build/docs at build time, so this target needs network access on a cold build. The generated HTML site is written to build/docs/html/index.html.

Tests

$ cmake --build build --target tests

Fuzzing

You can fuzz test with AFL:

$ docker run -ti -v .:/src benmandrew/bf:fuzz

If there are crashes, the offending inputs will be located in build-fuzz/fuzz_output/default/crashes.

FAQ

Why does ASan fail with malloc: nano zone abandoned due to inability to reserve vm space.?

On MacOS, every ASan-built binary prints this error. This is not an issue with the program, and can be fixed by setting the environment variable MallocNanoZone=0. See https://github.com/google/sanitizers/issues/1666.

Compiling to the LLVM IR is a niche topic, and it is hard to find resources for learning. Here are a few useful ones I found:

  • Mapping High Level Constructs to LLVM IR (link)
  • A Complete Guide to LLVM for Programming Language Creators (link)
  • My First Language Frontend with LLVM Tutorial (link)