Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,28 @@ jobs:
run: |
cmake --build build --target lint

docs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup dependencies
uses: ./.github/actions/setup-deps
- name: Install documentation dependencies
run: |
sudo apt-get update
sudo apt-get install -y doxygen graphviz python3-venv
- name: Build documentation
run: |
cmake -B build -S .
cmake --build build --target docs

test:
strategy:
matrix:
build_type: [Release, Debug]
runs-on: ubuntu-latest
needs: [lint, fmt]
needs: [lint, fmt, docs]
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
6 changes: 3 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ set_target_properties(bfc bf_lib
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
)

option(DOCKER_BUILD "Docker build mode - disables tests and scripts" OFF)
option(DOCKER_BUILD "Docker build mode - disables tests and verification" OFF)
if(NOT DOCKER_BUILD)
enable_testing()
add_subdirectory(test)
add_subdirectory(scripts)
add_subdirectory(verification)
endif()

add_custom_target(debug
Expand Down Expand Up @@ -260,7 +260,7 @@ if(AFL_CC)
)

add_custom_command(TARGET fuzz POST_BUILD
COMMAND ${CMAKE_SOURCE_DIR}/scripts/run_afl_parallel.sh ${CMAKE_BINARY_DIR}/bfc_fuzz
COMMAND ${CMAKE_SOURCE_DIR}/verification/run_afl_parallel.sh ${CMAKE_BINARY_DIR}/bfc_fuzz
)
endif()

Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ $ docker compose up

The input validation and parsing is mathematically proven correct for `bf` programs up to eight commands long using the [C Bounded Model Checker](https://github.com/diffblue/cbmc) (CBMC). Details are [here](#model-check-memory-safety).

![alt](doc/screenshot.png)
![alt](docs/screenshot.png)

## Dependencies

Expand All @@ -23,6 +23,13 @@ $ sudo apt-get install cmake llvm-dev check expect clang-format cpplint
$ brew install cmake llvm check expect clang-format cpplint
```

#### Documentation
To build the docs locally, install Doxygen and the Sphinx Python packages:
```bash
$ brew install doxygen
$ python3 -m pip install -r docs/requirements.txt
```

## Building

```bash
Expand Down Expand Up @@ -56,6 +63,14 @@ Hello, World!
$ cmake --build build --target fmt lint
```

### Documentation

```bash
$ cmake --build build --target docs
```

The generated HTML site is written to `build/docs/html/index.html`.

### Tests

```bash
Expand Down
53 changes: 53 additions & 0 deletions docs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
find_package(Doxygen QUIET)
find_package(Python3 COMPONENTS Interpreter QUIET)

set(DOCS_VENV_DIR ${CMAKE_BINARY_DIR}/docs/.venv)
if(WIN32)
set(DOCS_VENV_PYTHON ${DOCS_VENV_DIR}/Scripts/python.exe)
else()
set(DOCS_VENV_PYTHON ${DOCS_VENV_DIR}/bin/python3)
endif()

if(Python3_Interpreter_FOUND)
add_custom_target(docs-setup
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/docs
COMMAND ${Python3_EXECUTABLE} -m venv ${DOCS_VENV_DIR}
COMMAND ${DOCS_VENV_PYTHON} -m pip install --disable-pip-version-check --quiet --upgrade pip
COMMAND ${DOCS_VENV_PYTHON} -m pip install --disable-pip-version-check --quiet -r ${CMAKE_CURRENT_SOURCE_DIR}/requirements.txt
COMMENT "Creating docs virtualenv and installing Sphinx dependencies"
)
else()
add_custom_target(docs-setup
COMMAND ${CMAKE_COMMAND} -E echo "docs-setup requires Python 3 with venv support."
COMMENT "Documentation setup unavailable"
)
endif()

if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
set(DOXYGEN_OUTPUT_DIR ${CMAKE_BINARY_DIR}/docs/doxygen)
set(DOXYGEN_INPUT_DIR ${CMAKE_SOURCE_DIR}/src)

configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in
${CMAKE_CURRENT_BINARY_DIR}/Doxyfile
@ONLY
)

add_custom_target(docs
COMMAND ${CMAKE_COMMAND} -E make_directory ${DOXYGEN_OUTPUT_DIR}
COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile
COMMAND ${CMAKE_COMMAND} -E env SPHINX_DOXYGEN_XML_DIR=${DOXYGEN_OUTPUT_DIR}/xml ${DOCS_VENV_PYTHON} -m sphinx -W --keep-going -b html ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/docs/html
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Building Doxygen XML and Sphinx HTML documentation"
)
add_dependencies(docs docs-setup)
else()
message(WARNING "Docs target disabled: install Doxygen, Python 3, and the Sphinx/Breathe packages to build documentation.")
add_custom_target(docs
COMMAND ${CMAKE_COMMAND} -E echo "Docs require Doxygen, Python 3, and the Sphinx/Breathe Python packages."
COMMENT "Documentation tooling not available"
)
if(Python3_Interpreter_FOUND)
add_dependencies(docs docs-setup)
endif()
endif()
17 changes: 17 additions & 0 deletions docs/Doxyfile.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
PROJECT_NAME = "bf"
PROJECT_BRIEF = "Brainf*ck to LLVM IR compiler frontend"
OUTPUT_DIRECTORY = @DOXYGEN_OUTPUT_DIR@
INPUT = @DOXYGEN_INPUT_DIR@
FILE_PATTERNS = *.h *.c
RECURSIVE = YES
GENERATE_HTML = NO
GENERATE_XML = YES
GENERATE_LATEX = NO
EXTRACT_ALL = YES
EXTRACT_STATIC = NO
OPTIMIZE_OUTPUT_FOR_C = YES
QUIET = YES
WARN_IF_UNDOCUMENTED = NO
WARN_AS_ERROR = YES
HAVE_DOT = YES
STRIP_FROM_PATH = @CMAKE_SOURCE_DIR@
Empty file added docs/_static/.gitkeep
Empty file.
11 changes: 11 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
API Reference
=============

.. toctree::
:maxdepth: 1

api_common
api_ir
api_read
api_interp
api_llvm
4 changes: 4 additions & 0 deletions docs/api_common.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
common.h
========

.. doxygenfile:: common.h
4 changes: 4 additions & 0 deletions docs/api_interp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interp.h
========

.. doxygenfile:: interp.h
4 changes: 4 additions & 0 deletions docs/api_ir.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ir.h
====

.. doxygenfile:: ir.h
4 changes: 4 additions & 0 deletions docs/api_llvm.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
llvm.h
======

.. doxygenfile:: llvm.h
4 changes: 4 additions & 0 deletions docs/api_read.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
read.h
======

.. doxygenfile:: read.h
21 changes: 21 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pathlib import Path
import os

project = "bf"
author = "Ben M. Andrew"
copyright = f"2026, {author}"
language = "en_GB"
# Sphinx HTML search uses the English stemmer code `en` for all English variants.
html_search_language = "en"
extensions = ["breathe"]
templates_path = ["_templates"]
exclude_patterns = []
html_theme = "furo"
html_static_path = ["_static"]
html_title = "bf Documentation"

default_xml_dir = Path(__file__).resolve().parent.parent / "build" / "docs" / "doxygen" / "xml"
breathe_projects = {
"bf": os.environ.get("SPHINX_DOXYGEN_XML_DIR", str(default_xml_dir))
}
breathe_default_project = "bf"
14 changes: 14 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
``bf`` Documentation
====================

This documentation covers usage and API reference for the ``bf`` project.

- Repository: https://github.com/benmandrew/bf.
- Live instance: https://benmandrew.com/articles/compiler-frontend.

.. toctree::
:maxdepth: 2
:caption: Contents

usage
api
3 changes: 3 additions & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
sphinx>=7.0
breathe>=4.35
furo>=2024.8.6
File renamed without changes
61 changes: 61 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Usage
=====

Live Instance
-------------

You can try the project online at:

https://benmandrew.com/articles/compiler-frontend

Run Locally with Docker Compose
--------------------------------

To run the web interface locally:

.. code-block:: bash

$ docker compose up

Then open:

http://localhost:8080

Build the CLI Tools
-------------------

Build the project with CMake:

.. code-block:: bash

$ cmake -B build
$ cmake --build build

After building, both executables are available in the ``build`` directory:

- ``bfc``: Brainfuck frontend that emits LLVM IR
- ``bfi``: Brainfuck interpreter

Use ``bfc`` (compile to LLVM IR)
--------------------------------

Generate LLVM IR from a Brainfuck program and compile it with ``clang``:

.. code-block:: bash

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

Use ``bfi`` (interpret directly)
--------------------------------

Run a Brainfuck program directly with the interpreter:

.. code-block:: bash

$ ./build/bfi test/res/helloworld.b
Hello, World!
1 change: 1 addition & 0 deletions src/common.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef COMMON_H
#define COMMON_H

/// Number of cells in the Brainfuck data tape.
#define DATA_SIZE (65536)

#endif
22 changes: 11 additions & 11 deletions src/interp.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ size_t abstract_to_concrete_pc(size_t pc, struct program *p) {
case CMD_SIMPLE_LEFT:
case CMD_SIMPLE_OUTPUT:
case CMD_SIMPLE_INPUT:
concrete_pc += p->cmds[i].simple_count;
concrete_pc += p->cmds[i].value.simple_count;
break;
case CMD_JUMP_FORWARD:
case CMD_JUMP_BACK:
Expand Down Expand Up @@ -129,40 +129,40 @@ int interp(struct context_t *ctx, int out_fd, int in_fd, bool byte_output) {
struct cmd c = ctx->p.cmds[ctx->pc];
switch (c.type) {
case CMD_SIMPLE_INC:
ctx->data[ctx->dp] += c.simple_count;
ctx->data[ctx->dp] += c.value.simple_count;
break;
case CMD_SIMPLE_DEC:
ctx->data[ctx->dp] -= c.simple_count;
ctx->data[ctx->dp] -= c.value.simple_count;
break;
case CMD_SIMPLE_RIGHT:
assert(ctx->dp < DATA_SIZE - c.simple_count);
ctx->dp += c.simple_count;
assert(ctx->dp < DATA_SIZE - c.value.simple_count);
ctx->dp += c.value.simple_count;
if (ctx->dp > ctx->max_dp) {
ctx->max_dp = ctx->dp;
}
break;
case CMD_SIMPLE_LEFT:
assert(ctx->dp > c.simple_count - 1);
ctx->dp -= c.simple_count;
assert(ctx->dp > c.value.simple_count - 1);
ctx->dp -= c.value.simple_count;
break;
case CMD_SIMPLE_OUTPUT:
for (size_t i = 0; i < c.simple_count; i++) {
for (size_t i = 0; i < c.value.simple_count; i++) {
interp_dot(ctx, out_fd, byte_output);
}
break;
case CMD_SIMPLE_INPUT:
for (size_t i = 0; i < c.simple_count; i++) {
for (size_t i = 0; i < c.value.simple_count; i++) {
interp_comma(ctx, in_fd);
}
break;
case CMD_JUMP_FORWARD:
if (ctx->data[ctx->dp] == 0) {
ctx->pc = c.jump_index;
ctx->pc = c.value.jump_index;
}
break;
case CMD_JUMP_BACK:
if (ctx->data[ctx->dp] > 0) {
ctx->pc = c.jump_index;
ctx->pc = c.value.jump_index;
}
break;
default:
Expand Down
Loading
Loading