Toolchain & C ABI¶
The postc CLI¶
postc check FILE... structural subset checking
postc build FILE|DIR [options] compile to native artifacts
Build options:
| Option | Effect |
|---|---|
--output PATH |
artifact path (default: next to the source) |
| (directory target) | builds the package: __post__.py entry if present, else the __init__.py manifest |
--ext-module |
build an importable CPython extension registering NumPy ufuncs |
--module-name NAME |
artifact / importable name (defaults to the file stem, or the package directory for __init__.py) |
--emit-header |
write the C ABI header next to the output |
--emit-manifest |
write the JSON export manifest next to the output |
--prefix PREFIX |
install the libpp<name> package-manager layout |
--search-path DIR |
additional POST module source root (repeatable) |
--backend {c,qbe,llvm} |
code-gen backend (default c); qbe lowers the scalar/array kernel subset via QBE, llvm via llvmlite at -O2 |
--cc, --cflags, --keep-c |
toolchain control |
The qbe backend is an alternative code generator over the same IR. It lowers the full scalar + array kernel subset the C backend supports — int/float/bool arithmetic (including floor-div, mod, power, abs, casts, and select), array load/store/dim/stride/alloc, mutable locals and output stores, cross-module / libm calls, and the NumPy generalized-ufunc inner loop — and is runtime-verified bit-for-bit against the C backend across the example corpus. Complex numbers are not lowered and raise error: dtype Complex128 not supported by the QBE backend; there is no automatic fallback to C, and choosing the C backend is the user's call. Float16 and Str/Bytes are rejected by the frontend with PP900, on every backend including C — there is no C path for them either. The CPython extension-module glue (--ext-module) is C regardless. The pp_<name> export wrappers are compiled as C either way and link against the QBE-assembled objects unchanged (same platform C ABI).
The same pipeline is available as a Python API: postc.build.build_file / build_source.
What a build does¶
- Check — the structural checker rejects non-subset constructs (
PP0xx). - Compile the program — the entry file and every POST module it imports, dependencies first, one translation unit each (spec §9.1). Imports from the standard library or site-packages are CPython-boundary imports and are never compiled implicitly.
- Emit C99, compile, link — one object file per translation unit, linked into a single shared library (or extension module). Private (
_-prefixed) functions get internal linkage; public names must be unique across the program (PP501).
The stable C ABI (spec §9.1.1)¶
Every artifact defines pp_<name> wrapper symbols for its export set — the entry unit's public functions, its POST imports under their local names, and module-level aliases like gammaln = lgamma. Kernel symbol names underneath are implementation detail (libm-colliding names such as j0 are mangled to __pp_j0); pp_j0 is the contract.
The header (--emit-header) is self-contained C99: the __pp_array view struct (spec §9.2), one declaration per export with provenance and docstring comments, and the NumPy ufunc loop symbols (spec §8.5).
The manifest (--emit-manifest) is versioned JSON:
{
"post_abi": 1,
"artifact": "ppspecial",
"exports": [
{
"name": "gammaln",
"c_symbol": "pp_gammaln",
"kernel_symbol": "__pp_lgamma",
"module": "_gamma",
"kind": "alias",
"alias_of": "lgamma",
"params": [{"name": "x", "dtype": "Float64", "is_array": false,
"is_core_dim": false}],
"return_dtype": "Float64",
"ufunc": {"loop_symbol": "lgamma_ufunc_loop", "signature": "()->()"},
"doc": "..."
}
]
}
Packaging recipes and foreign-language bindings should consume the manifest rather than guessing symbol names.
Array ABI¶
Arrays cross the C boundary as views with explicit metadata (spec §9.2):
typedef struct __pp_array {
void *data;
int64_t ndim;
int64_t const *shape;
int64_t const *strides; /* byte strides, NumPy-compatible */
int64_t offset_bytes;
} __pp_array;
Vectorized functions additionally export <name>_ufunc_loop with the NumPy generalized-ufunc calling convention — outer broadcast steps followed by per-argument core-dimension strides — so loops registered via PyUFunc_FromFuncAndData handle non-contiguous inputs correctly.