The Backend Protocol¶
Every postc backend consumes the same typed SSA IR and turns it into a native object file. A program therefore compiles identically through any backend — the differential test suite enforces that they agree. This page documents the Backend protocol, its supporting types, and the registry that keys backends by name and selects one at runtime.
Everything here lives in postc.compiler.backends. To implement a backend, see Writing a backend.
The Backend protocol¶
Backend is a @runtime_checkable typing.Protocol: any object with the right attributes and methods qualifies — no base class to inherit. A backend carries three attributes and four methods.
| Attribute | Type | Role |
|---|---|---|
name |
str |
Registry key, e.g. "c", "qbe", "llvm" |
source_ext |
str |
Extension of emitted source, e.g. ".c", ".ssa" |
capabilities |
Capabilities |
What the backend can lower (see below) |
def emit(self, module: Module) -> str:
"""Lower one IR module to backend source text."""
def compile_object(self, source: Path, obj: Path, tc: Toolchain) -> None:
"""Turn an emitted source file into an object file."""
def runtime_sources(self, modules: list[Module]) -> list[RuntimeUnit]:
"""Extra C runtime units this backend needs for *modules* (may be empty)."""
@classmethod
def is_available(cls) -> bool:
"""True when this backend's external tools are present on this machine."""
emitdoes the code generation: one IRModulein, backend source text out. The driver writes it to a file named withsource_ext.compile_objectassembles that source into a.o. It receives aToolchainso it can invokecc,qbe, and friends without hard-coding tool names or flags.runtime_sourcesdeclares any extra C support code the emitted module needs linked in (returned asRuntimeUnits). It may return an empty list.is_availableis aclassmethodreporting whether the backend's external tools exist on this machine — used byavailable_backends()and thepostc backend-infocommand.
Capabilities¶
A frozen dataclass describing what a backend can lower. The driver checks these before building so an unsupported program fails early with a clear message rather than deep inside code generation.
@dataclass(frozen=True)
class Capabilities:
complex: bool = False # Complex64/128 arithmetic
ext_module: bool = False # can back a CPython extension (needs C-API glue)
float16: bool = False # native float16 arithmetic
The built-in backends advertise:
| Backend | complex |
ext_module |
float16 |
|---|---|---|---|
c |
✅ | ✅ | ❌ |
qbe |
❌ | ❌ | ❌ |
llvm |
❌ | ❌ | ❌ |
Toolchain¶
A frozen dataclass bundling the external tools and flags used to turn backend source into an object, plus two helpers.
@dataclass(frozen=True)
class Toolchain:
cc: str = "cc"
qbe: str = "qbe"
cflags: tuple[str, ...] = ("-O2", "-fPIC")
def run(self, cmd: list[str]) -> None: ...
def compile_c(self, source: Path, obj: Path) -> None: ...
cflagsis the full flag list placed before-c— optimisation, PIC, user flags, NumPy includes — composed by the driver.runexecutes a command, capturing output, and raisesToolchainErroron a non-zero exit.compile_ccompiles one C translation unit to an object viacc+cflags. Every backend'sruntime_sourcesare compiled as C, so a backend'scompile_objecttypically callsrunfor its own tool and lets the driver usecompile_cfor the runtime units.
RuntimeUnit¶
A C source unit a backend needs linked in — always compiled as C, whatever the backend's own source_ext is. The QBE backend, for example, ships an integer-power helper and export wrappers this way.
@dataclass(frozen=True)
class RuntimeUnit:
stem: str # base filename for the emitted .c
source: str # the C source text
The registry¶
The package __init__ holds a private name→backend map and exposes:
| Function | Returns / raises | Purpose |
|---|---|---|
register(backend) |
— | Register backend under its name (last wins) |
get_backend(name) |
Backend / UnknownBackendError |
Look up by name |
all_backends() |
list[str] |
Every registered name, sorted |
available_backends() |
list[str] |
Names whose is_available() is true |
UnknownBackendError subclasses LookupError and carries .requested and .available, so callers can report the valid choices.
Registration and plugin discovery¶
On import the package registers the three built-ins — CBackend, QBEBackend, and LLVMBackend — then discovers third-party backends via the postc.backends entry-point group:
# a plugin package's pyproject.toml
[project.entry-points."postc.backends"]
spirv = "postc_spirv:SPIRVBackend"
An entry point may resolve to a Backend class, a factory callable, or a ready-made instance; a callable result is invoked, otherwise the object is registered as-is.
Broken plugins are skipped
A plugin that fails to load must never break importing postc. Each is
loaded in a try/except; on failure it is skipped with a warnings.warn
(emitted even under -W error) naming the entry point and the exception.
Runtime backend selection¶
The driver and CLI resolve which backend to build with in a fixed precedence:
- Explicit argument —
postc build --backend qbe, orbackend=in the API. POSTC_BACKENDenvironment variable.- Default
"c".
An unknown name (including one coming from POSTC_BACKEND) is rejected against all_backends() with a message listing the valid choices.
postc backends # list registered backends + availability
postc backend-info qbe # capabilities and tool status for one backend
POSTC_BACKEND=qbe postc build kernel.py
postc build kernel.py --backend c # explicit arg wins over the env var
See Writing a backend to implement your own, and the toolchain reference for how the external tools are wired.