- C 48.1%
- CMake 13.8%
- JavaScript 12%
- CSS 7.4%
- Shell 6.8%
- Other 11.9%
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. |
||
|---|---|---|
| .github/workflows | ||
| cmake | ||
| docker | ||
| docs | ||
| scripts | ||
| src | ||
| test | ||
| verification | ||
| web | ||
| .clang-format | ||
| .dockerignore | ||
| .envrc | ||
| .gitignore | ||
| CLAUDE.md | ||
| CMakeLists.txt | ||
| CPPLINT.cfg | ||
| flake.lock | ||
| flake.nix | ||
| LICENCE | ||
| MODELCHECKING.md | ||
| README.md | ||
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.
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.
Useful Links for Learning the LLVM Intermediate Representation
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:
