ir.pl
The nanofil IR: modules of functions and data as ground Prolog terms, the contract between the frontend, the middle end, the interpreter and the backends. This module states its grammar and checks that a module is well formed.
A function is a list of blocks; a block is phis, then instructions, then one jump. Temps are ground names; logic variables never appear in IR terms.
func(Name, Linkage, Ret, Params, Blocks)
┌───────────────────────────────┐
│ entry block: first, no phis │
└───────────────┬───────────────┘
▼
┌───────────────────────────────┐
│ phis phi(tmp(D), K, Pairs) │
│ insts let, store, call, loc │
│ jump goto, branch, ret ... │
└───────────────────────────────┘Classes
Every value has a class, one letter:
| Class | Value |
|---|---|
w | 32-bit integer (a word) |
l | 64-bit integer (a long) |
s | 32-bit float (single) |
d | 64-bit float (double) |
p | pointer: an address with its Fil-C capability (filc mode only) |
a | proven address: a pointer or named-local access that a check has cleared (checked stage only) |
f | proven function: a p that a function check has cleared (checked stage only) |
A store's width may also be b (8 bits) or h (16 bits). A load and ext name the narrow types sb ub sh uh sw uw: signed or unsigned byte, halfword and word.
Items
| Term | Means |
|---|---|
moduleItems | Items are func/5, data/4 and optional context/2 terms |
funcName, Linkage, Ret, Params, Blocks | a function, fields below |
dataName, Linkage, Align, Items | a data object, fields under Data |
contextName, Records | explanatory binding, typed-origin, value, alias and site records; see provenance.pl |
| Field of func | Is |
|---|---|
| Name | an atom, the symbol |
| Linkage | export or local |
| Ret | w, l, p, s, d, void, or tupleK1, K2 |
| Params | a list of paramK, tmpN, K one of w l p s d |
| Blocks | a list of blockLabel, Phis, Insts, Jump, the entry block first |
A tupleK1, K2 return is two words, each Ki l or p: a struct of 9 to 16 bytes (filc mode only).
Phis and instructions
| Term | Means |
|---|---|
phitmpD, K, Pred−Value | D is Value when control came from block Pred |
lettmpD, K, Op, Args | D, of class K, is Op of Args (Ops below) |
storeWidth, Value, Addr | store Value at Addr; Width one of b h w l p s d |
local_allocName, Size | create a zeroed, exact-sized invocation-local object, without an address value |
local_loadtmpD, K, T, Name, Off | checked load semantics at the fixed byte offset Off in Name |
local_storeWidth, Value, Name, Off | checked store semantics at the fixed byte offset Off in Name |
callRes, Callee, argK, Value | call Callee: globalF, or (filc mode) a p temp |
locFile, Line | does nothing: the instructions after it, up to the next loc, come from line Line of File |
raw_storeWidth, Off, Value, Addr | checked stage: a non-pointer store at Addr+Off that does not check (Stages, below) |
raw_store_pointerOff, Value, Addr, Effects | checked stage: a pointer store whose ordered aux/barrier effects are explicit |
raw_callRes, Callee, Args | checked stage: call the proven function Callee (class f); argument, result and exception handling are as for call |
flighttmpD, Op, Args, Lower | checked stage: construct a p from a 64-bit address calculation and an independently selected capability |
A call's Res is one of resulttmpD, K, void, or tupleresulttmpD1, K1, resulttmpD2, K2.
Named locals are Fil-C-only. Their ground names have a separate namespace from temporaries. Each is declared once in the entry block, before its uses, and lives until this invocation exits. Functions with setjmp cannot contain them. Off is a signed 64-bit integer; an access outside [0, Size) traps, including every access to a zero-sized object. Pointer contents have ordinary capabilities, but a local's own native address is never a p value or heap lower.
flight computes its address with copy, add or sub, using unsigned 64-bit arithmetic. Each argument is an l value or addressP, the address bits of a p value. Lower is none or lowerP, exactly P's capability identity, not an integer bound. The donors may differ: constructing a pair proves nothing about access. No getter, check, memory access or safepoint occurs inside flight; both components become one rooted p result. Projections occur only here and cannot read globals or proof classes.
Jumps
| Jump | Means |
|---|---|
gotoL | go to L |
branchV, L1, L2 | go to L1 if V is nonzero, else to L2 |
retV | return V |
retsV1, V2 | return a tuple |
ret | return from a void function |
unreachable | stop the program (a trap instruction) |
A jump's target may be pollL: a pollcheck on the edge, after the phis' copies, then L. A pollcheck is a safepoint: the thread may run signal handlers and answer the collector.
Ops
The Op of a lettmpD, K, Op, Args; op_kind/2 and valid_op/1 state these sets once, op_result_ok/3 and op_arg_classes/4 the classes.
| Op | Result K | Args | Means |
|---|---|---|---|
add sub | any | [K, K]; at p, a p and an l offset | |
mul div | w l s d | [K, K] | |
udiv rem urem and or xor | w l | [K, K] | |
sar shr shl | w l | [K, w] | the count is read at w |
neg | w l s d | [K] | |
copy | any | [K] | |
cmpCond, K1 | w l; w when K1 is a float | [K1, K1] | Cond for K1 w or l: eq ne sle slt sge sgt ule ult uge ugt; for s or d: eq ne le lt ge gt o uo |
extT | l for sw uw; w or l for sh uh sb ub | [w] | extend the low bytes |
exts | d | [s] | single to double |
truncd | s | [d] | double to single |
stosi stoui / dtosi dtoui | w l | [s] / [d] | float to signed or unsigned integer |
swtof uwtof / sltof ultof | s d | [w] / [l] | signed or unsigned integer to float |
cast | w s l d | [s] [w] [d] [l] | bitwise: w and s, l and d |
loadT | as T: l, p, s, d; w or l for a narrow T | [Addr] | T one of l sw uw sh uh sb ub p s d |
allocAlign | the address class | [l], the size | Align one of 4, 8, 16 |
ptoi | l | [p] | filc only |
itop | p | [l] | filc only |
va_args | p | constN | filc only: a cursor into the entry argument snapshot; N, non-negative, is the byte count of the fixed prefix |
va_arg | any | [p] | filc only: a load through a va_list address, advancing its cursor (reads and writes memory) |
setjmpKind | w | [p, w]: Buf, SaveMask | filc only, below |
checkLo, Hi, Rights, Align | a | [p] | checked stage, below |
check_localName, Off, Bytes, Rights, Align | a | [] | checked stage: prove exact bounds and alignment for the named object, preserving its identity and offset |
raw_loadT, Off | as loadT, except p | [a] | checked stage: a non-pointer load at A+Off that does not check |
raw_load_pointerOff, Effects | p | [a] | checked stage: read aux, resolve the lower, then read the primary address |
check_function | f | [p] | checked stage: trap unless the pointer designates a function object at its exact callable address |
setjmpKind: Kind is setjmp, '_setjmp' or sigsetjmp, and SaveMask must be const0 except for sigsetjmp. It returns twice natively; the snapshot and restore contract is in docs/setjmp.md.
checkLo, Hi, Rights, Align reads [p P]. It traps unless P's capability permits Rights (read or write) on [P+Lo, P+Hi) and P+Lo is Align-aligned (Align one of 1, 2, 4, 8, 16); otherwise its result is P, proven accessible.
check_local proves [Off, Off+Bytes) within the named object's exact size and 16-aligned base. Its adjacent raw access has offset zero. Pointer load/store effects are respectively [aux_read, address_read] and [lower_write, address_write]: explicit stack aux has neither a heap header nor a lower box. Validation selects this protocol by proof provenance, not list shape.
Values
| Value | Means |
|---|---|
tmpName | a temporary, defined once (SSA) |
constInteger | a number; at s or d an unsigned IEEE bit pattern (docs/float.md) |
globalName | the address of a function or data object, by its symbol |
Data
In dataName, Linkage, Align, Items, Linkage is export, local, roexport or rolocal, ro for read-only data (in .rodata), and Align is 1, 2, 4, 8 or 16. Items is a list of:
| Item | Means |
|---|---|
itemW, constN | N at width W |
itemb, strChars | the bytes themselves, one character per byte (codes 0..255); a backend escapes them when it prints assembly |
iteml, symName, Off | the address of Name plus Off (plain mode) |
itemp, symName, Off | a pointer with the capability of Name (filc mode only) |
zeroN | N zero bytes |
Modes
A module is in one of two modes, told apart by what it uses (module_mode/2):
| Mode | Means |
|---|---|
plain | classes w, l, s and d only. Addresses are l values, and the plain backend (amd64.pl) compiles it to ordinary x86-64. |
filc | anything of class p, a tuple return, read-only data, a pointer-only op, or a call to a function the module does not define. Addresses are p values (flight pointers with a capability), alloc yields p, loads and stores take a p address, p arithmetic is add or sub of an l offset, and ptoi and itop convert. |
In filc mode calls may name functions outside the module, or go through a p value. A symbol globalS is read only by lettmpD, p, copy, globalS, which yields a pointer to the data or function object; S may be defined outside the module. See docs/m3.md and docs/m4.md.
Stages
A module is in one of two stages, told apart by its vocabulary:
| Stage | Means |
|---|---|
unchecked | as the frontend and the middle end write it. In filc mode, loadT and store at a p address mean "Fil-C's access check, then the access". |
checked | after pizlonate.pl, what the Fil-C backend compiles: every check is explicit, and every cycle of the CFG has a poll edge. |
In the checked stage a check yields a temporary of class a, a proven address, which only raw accesses read: raw_loadT, Off and raw_storeW, Off, V, A. The check that covers an access is its address operand's definition. A raw access its check does not cover is a bug of the pass, which the interpreter reports as the outcome uncheckedWhy.
Pure pointer copies, add/sub and integer-to-pointer conversion use flight in checked IR; ordinary p copy remains only for getters. The middle end runs before pizlonation. Its entry point leaves a directly supplied checked module unchanged, even with -O.
A check_function similarly yields class f, consumed only as the callee of the immediately following raw_call. It proves the target's identity, not a signature match: raw_call still selects fast or generic argument/result handling. A bad raw target reports uncheckednot_a_function; traps from that handling or from the callee remain traps. Direct calls keep call and their ABI thunks.
Pointer accesses carry the complete ordered list described by pointer_memory_effects/2. The list is executed, not an annotation. Each access is one instruction for liveness and layout, not an atomic transaction: aux_ensure may exit/re-enter or fail without returning. rooted_reload reloads the original p pair, not the ephemeral proof. The stored value also remains rooted through the whole instruction.
Neither proof class crosses an intervening safepoint (a call, a pollcheck, alloc, setjmp, va_arg or getter), since a free may make it stale. Each check is followed immediately by its one use. The pointer store's internal ensure-aux/reload sequence is the existing Fil-C exception, not permission to move another effect between the pair.
Variable names
In the predicates below: M a module, Fn a function name, K a class, V an IR value, L a label.
Bits is the width in bits of the numeric class K (w, l, s or d).
Op takes N arguments; it fails for anything that is not an Op of the grammar.
Op is an instruction of the IR: one of op_kind/2's, with operands from the sets below where it has them.
the closed protocol for an ordinary non-atomic pointer access. aux_read reads the encoded lower (or none); lower_resolve unboxes it without taking the box's address. A store ensures aux (may exit), barriers the value's lower (may call, cannot exit), reloads its destination p pair from rooted storage, then writes the ordinary lower before the primary address. No effect list may omit or reorder these obligations.
| 382 | load | aux_read, lower_resolve, address_read |
| 383 | store | aux_ensure, lower_barrier, rooted_reload, lower_write, address_write |
Proven nonescaping memory has an explicit stack aux, never a heap header or atomic box. These protocols are selected by check_local's provenance, not merely by a shorter effect list supplied by the caller. | ||
| 388 | local_load | aux_read, address_read |
| 389 | local_store | lower_write, address_write |
A load of type X or a store of width X accesses Size bytes, and Fil-C requires its address to be Align-aligned: 8 for a pointer, whose capability lives in the aux word of its 8-byte slot, else 1.
Kind is the category of Op that its consumers dispatch on, so that each can case-split on Kind in clause heads. Deterministic in mode (+, -): one fact per op, indexed on the first argument.
additive add sub: integer, or (filc) a p plus or minus an l offset alu mul div udiv rem urem and or xor: binary, integer only shift sar shr shl: the count is read at w neg copy ptoi itop each its own kind; copy may be of class p cmp ext load alloc compounds check ld the checked stage: a check and a raw load
Func is the func/5 term that defines Name in the module M.
Entry snapshot metadata is retained even if its cursor is unused.
Nonlocal returns restore compiler slots, not mutable local memory. Transformations must retain this boundary even in unreachable blocks.
Module mode
Mode is filc when anything in M has class p, uses a pointer-only op, or calls a function M does not define (which only the Fil-C ABI can reach), and plain otherwise.
The mode is inferred rather than stated: M uses a feature only filc mode has, or it does not. Every rule above is such a feature, the external call as much as class p. Stating the mode would mean a module(Mode, Items) term or a Mode argument for valid_module/1, run/5, promote and the backends, and a mode clause in each IR file, for no case the inference gets wrong: a C module is filc unless it uses none of these features (no memory, no pointers, no external calls), and then validation, promotion and the interpreter treat it alike in both modes; nanofil cc picks the Fil-C backend itself.
Item has class p somewhere, or is read-only data, which only filc mode models. The cuts commit to the first witness.
Inst produces, stores, passes or calls through a p value, or converts one (ptoi). Several clauses may hold for one Inst.
Validation
M is a well-formed module. When it is not, throws nanofil_error(Message) describing the first problem found, so callers get a diagnostic rather than a silent failure. A few terms outside the grammar (an item, instruction or jump of no known shape) make it fail instead.
Functions are checked against a context
ctx(Module, Fn, Ret, LabelSet, TempClasses, PredMap, Mode)
whose three assocs map each label to t, each temporary to its class, and each label to its sorted predecessors.
Stage is checked when M uses any word of the checked stage (a check, a raw access, flight or a poll edge), else unchecked.
Item, an item of M, is well formed.
C is a character that stands for one byte.
declarations, in instruction order. Names belong to a separate namespace: they are never ordinary values.
Name-Class for every definition of a temporary, in parameter and then block order.
Defs is sorted, so two classes for one temporary are adjacent.
each label's sorted predecessors.
Block's phis cover exactly its predecessors, and its instructions and jump are well formed.
Jump can branch to the label L, with or without a pollcheck.
a temporary may have a value class or a proof class. Proofs live in no memory and are not values of C.
only a check defines a proof temporary, and nothing passes one as a call argument. Phis, parameters and call results would carry a proof across an edge or a safepoint. (A check's own class is op_result_ok/3's.)
definitions_dominate(+Fn, +Params, +Blocks): in every reachable block, each use of a temporary is dominated by one of its definitions: a parameter, an earlier instruction or phi of the same block, or one in a block that strictly dominates it. A phi reads its value at the end of the predecessor, if that is reachable. A temporary may be defined more than once (tests/fixtures/memory.ir); one dominating definition is enough. Unreachable blocks are not checked.
N-at(L, I) for each definition of tmp(N), at index I of block L, a phi's at -1.
Pairs N-Site, sorted by N, grouped as N-Sites.
every use in the reachable blocks of Blocks (those with a dominator-tree interval) is dominated: an instruction's at its index, the jump's at the end of the block, and a phi's operand at the end of its predecessor, if that is reachable. D is dom(Fn, Sites, Intervals, Ends), Ends the number of instructions of each block. Plain recursion, not maplist/3 over closures, which Trealla copies with D at every call.
some definition of tmp(N) dominates position I of block L.
the definition Def comes before position I of block L on every path from the entry.
Fn's blocks are well formed for the module's Stage. A checked function accesses memory only by raw accesses, each covered by the check right before it, and polls on every cycle.
each check in Insts is followed by the one access it covers, which is its only use, so no safepoint can come between them; no other instruction accesses memory unchecked.
a pure pointer operation whose address/lower policy must be made explicit before either validation or emission of checked IR. Getter copies are effects, not members of this set.
Access is a raw access at tmp(Q) within [Q+Lo, Q+Hi), as aligned as it must be, and Rights allow it (write allows reading too).
Every cycle of the control-flow graph of Blocks, unreachable blocks included, has a poll edge: without them the graph is acyclic. A call does not count, since the callee need not poll. Depth-first search, with each label gray while on the path and done after.
no cycle of Graph is reachable from Ls; a gray successor closes one. Plain recursion, since Trealla copies a closure's arguments, Graph included, at every call.
pairs_keys_values/3, without depending on library(pairs).
Inst is well formed and reads each operand at a class its definition allows. In filc mode a symbol is an operand only of copy, which yields its pointer; the cut commits to that case before the general clause for ins/4.
the call site reads the result the way the callee returns it.
a call site may read at class K the result of Callee, which returns Ret.
as to_result_ok/4, for a result of class Ret. A long may be read as a word, nothing else changes class.
Jump names existing labels and returns what the function's return class requires.
the jump target T, a label or poll(Label), names a block.
V may be read at class K. Arguments are in the order maplist needs; valid_use_/3 indexes on the value.
A p temporary is used only at p and vice versa; ptoi and itop are the conversions. An integer constant at p is a pointer with no capability. Only a check makes a proof (class a or f): no constant is one.
the symbol global(G) may be read at class K in a module of Mode: in plain mode as an address of data, in filc mode never (copy, which yields its pointer, is checked apart).
Operand and result classes
Op may produce a result of class K in a module of Mode. With Op partial (cmp(_, _), ext(_), load(_)) it enumerates the operands.
as op_result_ok/3, for Op of Kind.
Classes is the list of classes at which the arguments of Op are read when its result has class K in a module of Mode. Fails for an Op outside the grammar.
as op_arg_classes/4, for Op of Kind.
throw nanofil_error with the message Fmt/Args, attributed to the function Fn.
Erase explanatory metadata at execution/code-generation boundaries. Never use erasure to relax checked-IR validation's adjacency rule.