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
postypdtype (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
Valuename. It is not strict SSA — there are no phi nodes; cross-block data flow that needs mutation usesAssignValueinstead. - Backend-agnostic. Nothing in the IR names a target language. Byte-offset array addressing and explicit
Allockeep 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 UInt8…UInt64, 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.