Skip to content

Backend API

This is the backend interface, version 1 (BACKEND_API_VERSION).

A backend takes the canonical IR after the shared passes and returns text, bytes, or built artifacts. It:

  • may hang passes at the backend stage
  • must refuse in validate what it cannot take
  • says whether its toolchain is present

An external backend is a package with an entry point in the ppy.backends group. The compiler finds it without importing it, and loads it when it is asked for. Backends explains how the pieces fit and walks through a whole example package.

A backend package imports this module and ppy_compiler.ir.

ppy_compiler.backend.base

The backend interface: what a code generator is to the compiler.

A backend consumes the canonical IR after the shared passes and nothing else -- never the Python AST, never the checker's tables -- and answers with text, bytes, or built artifacts. It may hang passes of its own at the backend stage, must refuse IR it cannot take in validate, and says whether its toolchain is here. Plugins are the other extension: a plugin models a library's semantics and lowering; a backend makes code. Neither does the other's job.

An external backend is a package with an entry point in the ppy.backends group whose factory takes the backend's options from pyproject.toml ([tool.ppy.backends.<name>]) and returns a Backend::

[project.entry-points."ppy.backends"]
toy = "ppy_toy:create_backend"

The interface is versioned by BACKEND_API_VERSION, independently of the compiler's version: a backend written against another one is refused with the reason, never loaded and hoped about.

BackendConfig(name, options=dict()) dataclass

The [tool.ppy.backends.<name>] table, as the backend receives it.

fingerprint()

A digest of the options: part of every artifact key, so a changed setting is a different artifact.

BackendError

Bases: Exception

A backend's refusal; the message is the reason.

BackendUnavailable

Bases: BackendError

The backend's toolchain is not here; what is missing is named.

BackendValidationError(backend, what, *, capability='', location=None)

Bases: BackendError

IR the backend does not take: the backend, what it cannot take, the capability it lacks, and the source location when the IR carries one.

EmitFormat(name, suffix, binary=False, description='', scope='module', requires_toolchain=True) dataclass

One thing a backend can write for ppy emit <name>.

ToolchainStatus(available, detail='') dataclass

Whether the backend can build here, and what it found or missed.

BuildResult(outputs=(), notes=()) dataclass

What build produced.

BackendPassRegistrar(manager, backend)

Where a backend hangs its passes: the backend stage, and nowhere else.

The stages before it belong to the shared pipeline and to the plugins -- a pass of a backend's own running among them would decide for every other backend what the canonical IR is. A backend is handed this rather than the PassManager so that the invalid thing cannot be spelled.

add(factory)

Run factory()'s pass at the backend stage, after every shared pass.

register_stage_pass(stage, factory)

add(factory), for the one stage a backend owns; any other is refused.

BackendContext(root, config, backend_config, opt_level, target, registry, plugin_fingerprints=(), identity=dict(), entry=None, notes=list()) dataclass

Everything a backend is told besides the IR.

The IR is already through the shared passes and the backend's own; nothing here reaches back into the frontend. identity is the cache key the driver computed for the module (compiler, IR, backend, its fingerprint and configuration, target, optimization level, plugins), for a backend that keeps artifacts of its own.

Backend(options=None)

The class an external backend extends. Every method has a default: a backend that emits registers formats and implements emit; one that builds implements build; the rest is optional.

fingerprint()

What identifies this backend's code generation for the cache: its version, its SDK's, anything whose change makes old artifacts wrong. The default is the class and the interface version -- a backend with a toolchain under it should include that toolchain's version.

emit_formats()

The formats ppy emit may ask this backend for.

register_passes(passes)

Hang the backend's own passes: passes.add(MyPass).

They run at the backend stage, after every shared pass and every plugin's, and the module is verified after each of them.

validate(module, context)

Refuse IR the backend cannot take, with a BackendValidationError naming what and why. Called after every pass, before emit or build.

emit(module, format, context)

format (one of emit_formats) for one module: text, or bytes for a binary format.

emit_program(modules, format, context)

A program-scoped format: every module at once, one artifact back.

Only a format that declared scope="program" arrives here; a module-scoped one goes to emit, once per module.

build(modules, output, context)

Build every module into output, which exists, and say what was written.

toolchain_status()

Whether the backend can work here; ppy doctor prints the detail.

The registry

ppy_compiler.backend.registry

Which backends there are, and loading one when it is asked for.

The builtin backends are always here. An external one is a distribution with an entry point in the ppy.backends group; discovery reads the entry points without importing anything, and the package is imported only when its backend is selected -- by --backend, by a format it registers, or by ppy doctor. One broken package does not break the others: its failure is a reported problem, or the error of the command that asked for it. A name registered twice is never settled by luck: it is reported, and asking for it is an error naming both distributions.

BackendLoadError

Bases: BackendError

A backend that cannot be used: unknown, duplicated, incompatible, or failing to load; the message says which and names what is available.

BackendInfo(name, builtin, origin, distribution=('', ''), entry=None) dataclass

One backend as discovery sees it, loaded or not.

BackendCatalog(backends=dict(), problems=list(), duplicates=dict()) dataclass

Every backend by name, and what discovery had to refuse.

FormatOwner(backend, format) dataclass

The backend that emits a format, and the format as it declared it.

discover_external_backends()

Every installed external backend by name, without importing any, and the names that were registered more than once (each is a problem).

available_backends()

The builtin backends and every discovered external one, nothing imported.

load_backend(name, options=None)

The backend called name, constructed with its options: a builtin one directly, an external one through its entry point, imported now.

Refused with a BackendLoadError that says why: no such backend (and which there are), registered twice, a factory that fails or answers something that is not a Backend, or an interface version other than BACKEND_API_VERSION.

discover_format_owners()

Which backend each installed distribution says owns which emit format, from ppy.backend-formats alone: no backend package is imported.

emit_format_owner(kind, options_for=None)

The backend that emits kind.

A builtin format is answered without loading anything. For any other name every external backend is loaded (each with the options options_for(name) gives it) and asked; a backend that fails to load is reported in the error only when the format is not found elsewhere. Two backends claiming one format is an error naming both.