Skip to content

The IR

postc lowers POST Python to a single typed, SSA-inspired, three-address intermediate representation before any backend runs. Every backend consumes this IR unchanged — the C, QBE, LLVM, and WASM backends all walk the same Module object — which is the whole reason postc can add backends without touching the frontend. The IR lives in src/postc/compiler/ir.py and is built entirely from @dataclass nodes.

Three properties define it:

  • Typed. Every value carries a postyp dtype (and, for arrays, a shape and layout). There are no untyped temporaries; the type checker has already run by the time the IR exists.
  • SSA-inspired. Each instruction produces at most one result, and every instruction result is a fresh, single-assignment Value name. It is not strict SSA — there are no phi nodes; cross-block data flow that needs mutation uses AssignValue instead.
  • Backend-agnostic. Nothing in the IR names a target language. Byte-offset array addressing and explicit Alloc keep it lower than Python but higher than any one backend's ABI.

Values

A Value is a named, typed SSA value — the currency every instruction reads and writes.

Field Type Meaning
name str SSA name (unique within a function; results are always fresh)
dtype type[DType] element/scalar type from postyp (see below)
shape Shape dimension info for arrays; defaults to AnyShape (fully dynamic)
layout ArrayLayout memory layout; defaults to COrder (C-contiguous)
is_array bool True if this value is an array view rather than a scalar
is_output bool True if this is a caller-provided output (out-parameter) array

Value.__repr__ prints IR-style: %name: &Float64[]Shape[3, 3] — the & marks an output, [] marks an array, and the trailing shape/layout are shown only when they differ from the defaults.

The dtype is one of the postyp DType subclasses: Bool, the signed ints Int8/Int16/Int32/Int64, the unsigned UInt8UInt64, the floats Float16/Float32/Float64, Complex64/Complex128, and Str/Bytes. Shape dimensions may be concrete (10), dynamic (None), or fully unknown (AnyShape); ArrayLayout is COrder, FOrder, or an explicit Strides descriptor (byte strides, NumPy convention).

Operators

BinOp and UnaryOp are enums whose values are the source-level spelling.

BinOp Op BinOp Op UnaryOp Op
ADD + EQ == NEG -
SUB - NE != NOT not
MUL * LT < ABS abs
DIV / LE <=
FDIV // GT >
MOD % GE >=
POW ** AND and
OR or

Instructions

An Instruction produces at most one Value. The Instruction type alias is the union of all node types below; a BasicBlock holds them in order.

Node Fields Semantics
Const result, value result = literal (int/float/bool/complex/str/bytes)
BinOpInstr result, op, left, right result = left op right
UnaryOpInstr result, op, operand result = op operand
ArrayLoad result, array, index result = array[index]; index is a byte offset from the view origin
ArrayStore array, index, value array[index] = value; index is a byte offset from the view origin
ArrayDim result, array, axis result = array.shape[axis]
ArrayStride result, array, axis result = array.strides[axis], in bytes
Call result, func, args result = func(args…); func is a name resolved at link time, result is None for void calls
Cast result, operand result = (result.dtype) operand — numeric cast to the result's dtype
AssignValue target, value, declare target = value; declare=True also declares target first (the non-SSA escape hatch for mutable locals)
Select result, cond, if_true, if_false result = cond ? if_true : if_false
Alloc result, length result = alloc(result.dtype, length) — allocate a length-element array on the POST Python heap
GetField result, aggregate, field_name result = aggregate.field_name
SetField aggregate, field_name, value aggregate.field_name = value

Byte-offset addressing

ArrayLoad/ArrayStore indices are byte offsets, not element indices. Frontend lowering multiplies logical indices by strides (via ArrayStride) so the backends never need to know an element's size.

Terminators

Every BasicBlock ends in exactly one Terminator (the union of the three below).

Node Fields Semantics
Return value return value; value = None for a void return
Branch target unconditional jump to the block labelled target
CondBranch cond, true_target, false_target if cond goto true_target else false_target

Basic blocks

A BasicBlock is a straight-line sequence of instructions ending in a terminator.

Field / method Meaning
label block name (branch targets reference it)
instructions ordered list[Instruction]
terminator the closing Terminator; None until set
append(instr) add an instruction
terminate(term) set the terminator (asserts the block is not already terminated)

Functions and ufuncs

A Function is a typed definition with a list of blocks.

Field / member Meaning
name function name
params list[Param] (same shape as Value: name, dtype, shape, layout, is_array, is_output)
return_dtype scalar/element return type; None → void
return_shape return Shape; defaults to AnyShape
core_dim_params extra params carrying ufunc core-dimension sizes
blocks list[BasicBlock]
doc source docstring, if any
entry property: blocks[0]
new_block(label) append and return a fresh block

A UFunc is a Function subclass carrying a broadcast layout signature in ufunc_sig: UFuncSignature | None. UFuncSignature parses the layout string (e.g. (m,k),(k,n)->(m,n)) into inputs and outputs — lists of core-dimension name lists. Its core_dims property returns every unique named core dimension in declaration order, and str() round-trips back to the signature text.

Modules

A Module is one POST Python translation unit (one .py file) and the top-level object every backend is handed.

Field / member Meaning
name translation-unit name (usually the source filename stem)
functions list[Function] (includes UFuncs)
post_imports names imported from other POST units, keyed by local name → ImportedName
intrinsic_imports names from postc.math, keyed by local name → libm-compatible symbol
boundary_imports names from non-compilable CPython-boundary modules (calls are diagnosed, not lowered)
dependencies dotted names of imported POST modules
dep_modules resolved Module objects for those dependencies (set by compile_program)
constants module-level constants: name → (dtype, folded value)
function_aliases alias name → aliased name (e.g. gammaln = lgamma)
export_all contents of a top-level __all__, if present; narrows exports and ufunc registration
add_function(fn) append a function
get_function(name) look up a function by name, or None
ufuncs property: the UFuncs among functions

ImportedName (used by post_imports and boundary_imports) is a frozen record of local_name, module_name, and source_name — the local binding, the dotted source module, and the name as defined there.

For how this IR is produced and consumed, see the architecture overview; for what a backend must implement against it, see the backend protocol.