|
BFC-Compiler
A C23 Brainfuck compiler
|
Project README · Architecture · CLI reference · API documentation
A backend converts generic Brainfuck IR into assembly for one exact architecture and operating-system combination.
This guide explains the complete work required to add a backend: defining the target, implementing the ABI and instruction lowering, registering the backend, and validating the generated program.
A backend is not architecture-only. Two targets using the same CPU architecture may still require different backends because their ABI, object format, symbol naming, relocation syntax, section directives, unwind rules, and external-call conventions differ.
A backend corresponds to one bfc_target_t pair:
Examples:
The target triple is parsed separately. A recognized target triple becomes usable only when:
The generic compilation pipeline owns parsing, IR construction, optimization, and traversal:
The generic code generator visits each IR instruction and delegates target-specific emission to the selected backend.
The backend does not parse Brainfuck, build IR, manage loop recursion, allocate assembly buffers, or write the output file.
Each backend provides one immutable bfc_backend_t object:
Every callback must be initialized. Generic codegen assumes the backend object is complete.
The backend object should have external linkage and remain immutable:
Individual emitter functions should remain static inside the backend source file.
Generic codegen calls backend functions in this order:
The intended responsibilities are:
| Callback | Required result |
|---|---|
| emit_header | Select assembly dialect and emit file-level directives or external declarations |
| emit_data_section | Reserve the zero-initialized Brainfuck tape |
| emit_symbol | Emit the entry symbol, function prologue, and initial tape-pointer setup |
| emit_op_add | Apply a wrapping 8-bit change to the current cell |
| emit_op_move | Move the tape pointer by a signed byte offset |
| emit_op_get | Read one byte, convert EOF according to BFC semantics, and store it |
| emit_op_put | Pass the current byte to the platform output function |
| emit_op_set | Store a normalized byte value directly |
| emit_loop_test_z | Branch to the supplied label when the current cell is zero |
| emit_loop_test_nz | Branch to the supplied label when the current cell is nonzero |
| emit_end | Return success from main, restore preserved state, and emit final directives |
Loop labels themselves are emitted by generic codegen. Backend loop callbacks emit only the test and conditional branch.
Do not place target-specific instructions or directives in generic codegen.
Do not duplicate bfc_codegen_emit_text() or bfc_codegen_emitf() inside backend modules.
Complete this table before writing the backend. It prevents ABI and object-format details from being discovered piecemeal while coding.
| Property | Target decision |
|---|---|
| Target triple | Exact triple accepted by the CLI |
| Architecture enum | BFC_ARCH_* value |
| Operating-system enum | BFC_OS_* value |
| Object format | Mach-O, ELF, or PE/COFF |
| Assembly dialect | Apple/GNU, Intel, AT&T, MASM-compatible, or another required dialect |
| C symbol prefix | For example _main on Mach-O or main on ELF |
| Entry symbol | Exact emitted symbol |
| Input symbol | Exact getchar spelling |
| Output symbol | Exact putchar spelling |
| Tape symbol | Exact backend-private tape symbol |
| Tape section | Exact zero-initialized-data section and directives |
| Tape-pointer register | Must survive external calls or be restored |
| Scratch register | Must be legal for the selected ABI |
| First integer argument register | Required by putchar |
| Return-value register | Used by getchar and main |
| Stack alignment | Required alignment at each external call |
| Preserved registers | Registers the backend must save and restore |
| Small immediate range | Directly encodable movement or arithmetic range |
| Large immediate strategy | Scratch-register materialization sequence |
| Address materialization | Relocation sequence used for the tape symbol |
| Function return sequence | ABI-correct epilogue and return |
| Native test environment | Physical host, VM, container, or emulator |
This worksheet is part of backend design, not generated code. Keep the decisions reflected in the target section of this document.
Confirm that the architecture and operating-system enums exist in bfc_target.h.
Confirm that the target triple is recognized by bfc_target_parse() in bfc_target.c.
Target recognition and backend support are separate. It is valid for a triple to be recognized before its backend is implemented, but code generation must then return an unsupported-target error.
Do not add a second spelling for the same target unless the CLI intentionally supports aliases.
Create one backend source file named for both the operating system and architecture:
Examples:
Include:
The file should contain:
Do not expose emitter functions in a public header.
Implement these callbacks first:
At the end of this phase, the backend should be able to emit an empty program that:
Validate an empty program before implementing Brainfuck operations. This isolates object-format, symbol, prologue, relocation, and stack-alignment problems.
Reserve exactly BFC_TAPE_SIZE bytes in a zero-initialized section.
The backend must:
The tape pointer is a byte pointer. IR_MOVE 1 advances exactly one Brainfuck cell.
Implement and validate operations in this order.
Normalize the immediate to one byte and store it directly.
Required cases:
Load or update the current byte so arithmetic wraps modulo 256.
The implementation must behave correctly for:
Do not allow a wider temporary result to change the required byte-wrapping semantics.
Move the tape pointer by a signed byte offset.
Support:
The backend does not perform tape-bounds checking unless the language semantics are changed globally.
Load the current cell, zero-extend it to the ABI-required argument width, and call putchar().
Passing a sign-extended byte is incorrect for cell values from 128 through 255.
Call getchar() and store the low byte of the result.
BFC currently maps EOF to zero. The backend must test the full return value before truncating it to one byte.
emit_loop_test_z() must branch to the supplied end label when the current byte is zero.
emit_loop_test_nz() must branch to the supplied start label when the current byte is nonzero.
Generic codegen owns:
Validate:
Declare the backend object in bfc_codegen_internal.h if backend objects are declared individually.
Add the target-to-backend mapping in the generic backend lookup used by bfc_codegen().
The mapping must compare both:
Do not select a backend by architecture alone.
Ensure the new source file is included by the Makefile. If source files are discovered automatically, verify that the new object appears in the build. Otherwise, add it to the source list explicitly.
After registration:
must select the new backend instead of returning an unsupported-target error.
Use a minimal progression so failures identify one subsystem at a time:
For every stage:
Assembly generation alone is not sufficient validation.
A backend is complete only when its generated programs execute successfully on the target.
Tests should cover:
| Area | Required coverage |
|---|---|
| Program structure | Empty program links and returns success |
| Cell arithmetic | Increment, decrement, overflow, and underflow |
| Pointer movement | Positive, negative, small, and large offsets |
| Output | Values below and above 127 |
| Input | Normal byte input and EOF-to-zero behavior |
| Loops | Zero-entry, repeated execution, and nesting |
| Optimized IR | Coalesced operations and IR_SET |
| ABI | Multiple input/output calls without tape-pointer corruption |
| Diagnostics | Unsupported targets still fail cleanly |
| Toolchain | Generated assembly assembles and links with the intended tools |
Cross-assembly on another host is useful, but it does not replace target-native execution.
Tape section:
Tape-address materialization:
| Register | Role |
|---|---|
| x19 | Tape pointer; callee-saved and preserved |
| x16 | Large-immediate scratch |
| w16 | Temporary cell value |
| w0 | Integer argument and return value |
| x29 | Frame pointer |
| x30 | Link register |
| IR operation | Strategy |
|---|---|
| IR_ADD | Load byte, add normalized immediate, store byte |
| IR_MOVE | Direct immediate up to the encodable limit; otherwise materialize in x16 |
| IR_GET | Call _getchar, map EOF to zero, store low byte |
| IR_PUT | Load byte into w0, call _putchar |
| IR_SET | Store wzr for zero or an immediate byte |
| Loop tests | cbz and cbnz |
Dialect directive:
| Register | Role |
|---|---|
| rbx | Tape pointer; callee-saved and preserved |
| r11 | Large-immediate scratch |
| eax | Return value and input temporary |
| edi | First integer argument |
| rbp | Frame pointer |
| IR operation | Strategy |
|---|---|
| IR_ADD | add byte ptr [rbx], imm |
| IR_MOVE | Direct immediate when possible; otherwise use movabs r11 |
| IR_GET | Call _getchar, map EOF to zero, store al |
| IR_PUT | Zero-extend the byte into edi, call _putchar |
| IR_SET | Store an immediate byte |
| Loop tests | Compare the byte with zero, then use je or jne |
Do not create a new backend by copying an existing backend and changing only symbol names.
Use this checklist only after completing the implementation phases above.
An x86-64 macOS backend is not a valid x86-64 Linux or Windows backend. Always match the complete target.
getchar() returns an int. Test for EOF before storing the low byte.
Brainfuck cells are unsigned bytes. Zero-extend the cell before passing it to putchar().
External calls may overwrite caller-saved registers. Use a preserved register or explicitly save and restore the pointer.
A program may assemble and still crash when calling the C runtime if stack alignment or platform-specific call-frame rules are wrong.
Every architecture limits directly encoded immediates. Test the boundary and provide a large-immediate fallback.
Generic codegen owns abstract loop labels. Backend callbacks should only emit target-specific tests and branches to the supplied label.
Readable assembly is not proof of correctness. Assemble, link, execute, and compare behavior on the target.