Fil-C on Nix

Filnix wiki

mbrock/filnix/nanofil

pizlonate.pl

Pizlonation: from the unchecked IR to the checked IR (ir.pl), the form the Fil-C backend compiles, named after Fil-C's LLVM pass.

The checks Fil-C requires are implicit in the unchecked stage, where a load or store at a p address means "check, then access". This pass states them: each such access becomes a check, whose class a result is the proven address, and the raw access that reads it.

UncheckedChecked
lettmpD, K, loadT, AlettmpQ, a, check0, S, read, Al, A, then lettmpD, K, raw_loadT, 0, tmpQ
storeW, V, AlettmpQ, a, check0, S, write, Al, A, then raw_storeW, 0, V, tmpQ
accessK, A, S, AllettmpQ, a, check0, S, K, Al, A, then touchtmpQ
pointer load/storethe same check, then raw_load_pointer or raw_store_pointer with its explicit pointer_memory_effects/2 sequence
atomic_loadp, atomic_storep, V, Athe same check, then the raw pointer access with the atomic effect list ([atomic_read], [atomic_write])
letD, K, casT, E, N, Aa write check of T's width at that alignment, then letD, K, raw_casT, 0, tmpQ, E, N
callRes, tmpP, ArgslettmpQ, f, check_function, tmpP, then raw_callRes, tmpQ, Args
letD, p, add, P, X (also sub)flightD, add, addressP, X, lowerP
letD, p, copy, P (not a symbol)flightD, copy, addressP, lowerP
letD, p, itop, XflightD, copy, X, none

S and Al are the access's size and alignment (ir:access_size_align/3), and Q is a fresh name.

Every edge that does not go forward in block order, to a block at or before its source, gets a pollcheck, pollL, so every cycle polls. In a plain module, whose addresses are not Fil-C pointers, only the polls are added.

pizlonate itself stays naive: one check right before each access (a vector's access/4 becomes a full-width check and a touch before its chunks), and every backward edge polls. Under --checks (renew is the -O default), check_reuse.pl's reuse, renew and guard run after it (docs/m7.md); each result must pass its *_refines identity check, and the validator (ir.pl, checked_insts/5) then checks the evidence independently.

The jmp_buf store of setjmp and the cursor of va_arg stay opaque: the backend checks them inside the instruction, as the runtime helpers they call do.

modulepizlonate
useslists
usesassoc
usesformat
usesir
usescfginst_def/2
pizlonate_module+M0, -Mdet

M is the unchecked module M0 with every check explicit and a poll on every edge to a block at or before its source in block order. In a plain module, whose addresses are not Fil-C pointers, only the polls are added.

58pizlonate_moduleM0, moduleItems ←
module_modeM0, Mode
legacy_contextM0, moduleItems0
code_itemsItems0, Code
context_indexItems0, Contexts
maplistpizlonated_item_onceMode, Contexts, Code, Groups
appendGroups, Items

Each item has one pizlonation; committing to it frees the choicepoints that would otherwise keep every function's intermediate terms alive.

66pizlonated_item_onceMode, All, Item, Items ←
oncepizlonated_itemMode, All, Item, Items
68pizlonated_itemMode, All, funcName, Linkage, Ret, Params, Blocks0, Items ←
func_temp_namesParams, Blocks0, Names
findallL−I, nth0I, Blocks0, blockL, _, _, _, Pairs
list_to_assocPairs, Index
context_recordsAll, Name, Rs0
pizlonated_blocksBlocks0, pMode, Names, Index, 0, 0, Blocks, Map, Generated
remap_sitesRs0, Map, Rs1
checked_originsRs0, Rs1, Generated, Rs
with_contextfuncName, Linkage, Ret, Params, Blocks, Rs, Items
77pizlonated_item_, _, dataN, L, A, Is, dataN, L, A, Is
78pizlonated_item_, _, aliasN, L, T, aliasN, L, T
80checked_origins[], Rs, _, Rs ←
!
81checked_originsBefore, Rs0, Generated, Rs ←
site_indexBefore, Sites
origin_idsBefore, Ids0
findallQ, memberproofQ, _, Generated, Temps
retire_valuesRs0, Temps, Ids0, Ids1, Retired
findallL−true, memberpollL, Generated, Pairs
list_to_assocPairs, Polls
excludepolled_sitePolls, Retired, Kept
generated_recordsGenerated, Sites, Ids1, Extra
appendKept, Extra, Rs

Each proof temp and polled label is generated once, so the bases pizlonate(Q) and poll(L) are distinct and Ids need not grow.

91generated_records[], _, _, []
92generated_recordsproofQ, Site|Gs, Sites, Ids, originId, none, generatedpizlonate, check, Os, valueQ, none, Id|Rs ←
site_originsSites, Site, Os
fresh_originpizlonateQ, Ids, Id
generated_recordsGs, Sites, Ids, Rs
97generated_recordspollL|Gs, Sites, Ids, originId, none, generatedpizlonate, poll, Os, sitejumpL, Id|Rs ←
site_originsSites, jumpL, Os
fresh_originpollL, Ids, Id
generated_recordsGs, Sites, Ids, Rs
102polled_sitePolls, sitejumpL, _ ←
get_assocL, Polls, _
func_temp_names+Params, +Blocks, -Namesdet

Names is the assoc of every temp name the function defines, so a fresh name avoids them. A valid function reads only temps it defines.

108func_temp_namesParams, Blocks, Names ←
findallN−t,
memberparam_, tmpN, Params
or
memberblock_, Phis, Insts, _, Blocks
memberphitmpN, _, _, Phis
or
memberInst, Insts
inst_defInst, N
,
Pairs0
sortPairs0, Pairs
list_to_assocPairs, Names
pizlonated_blocks+Blocks0, +P, +I, +N, -Blocks, -Map, -Generateddet

Blocks are Blocks0, the first at index I in block order, checked: each block's instructions by checked_insts/11 and its jump by polled_jump/4. P is p(Mode, Names, Index), Index each label's position in block order, and N counts fresh names. Map is the old-to-new site map for provenance's remap_sites/3, and Generated the proof(Q, Site) and poll(L) facts the context gains. Plain recursion: Trealla copies a closure's arguments at every call.

129pizlonated_blocks[], _, _, _, [], [], []
130pizlonated_blocksblockL, Phis, Insts0, Jump0|Bs0, P, I, N0, blockL, Phis, Insts, Jump|Bs, Map, Generated ←
P = pMode, Names, Index
checked_instsInsts0, Mode, Names, N0, N, Insts, L, 0, 0, Map0, G0
polled_jumpJump0, I, Index, Jump
I1 is I + 1
pizlonated_blocksBs0, P, I1, N, Bs, Map1, G1
Jump0 ≡ Jump
→
G2 = G1
else
G2 = pollL|G1
appendMap0, Map1, Map
appendG0, G2, Generated
checked_insts+Insts0, +Mode, +Names, +N0, -N, -Insts, +L, +Old, +New, -Map, -Generateddet

Insts are Insts0 of block L with Fil-C's checks explicit (filc mode only): each access becomes its check and raw access (checked_access/4), each pure pointer operation a flight (flight_inst/2), and everything else stays. Fresh proof names chkN count from N0 to N, avoiding Names. Old and New number the instructions before and after, for the site Map; Generated records each proof made.

149checked_insts[], _, _, N, N, [], _, _, _, [], []
150checked_instsInst0|Insts0, Mode, Names, N0, N, Insts, L, Old, New, Map, Generated ←
Mode ≡ filc
checked_accessInst0, Q, Out, []
→
fresh_nameNames, N0, N1, Q
Generated = proofQ, instL, Old|Gs
Mode ≡ filc
flight_instInst0, Inst
→
Out = Inst
N1 = N0
Generated = Gs
else
Out = Inst0
N1 = N0
Generated = Gs
appendOut, Insts1, Insts
emission_sitesL, Old, New, Out, Map, Maps, Next
Old1 is Old + 1
checked_instsInsts0, Mode, Names, N1, N, Insts1, L, Old1, Next, Maps, Gs
flight_inst+Inst0, -Instsemidet

Inst is the flight of the pure pointer operation Inst0: add or sub of an offset, a copy of a temporary or constant, or itop, which has no lower. The address calculation and lower are chosen independently; no new temporary or safepoint separates them. A symbol copy still calls its getter; it is not a pure projection, and stays.

flight_inst
168letD, p, add, P, XflightD, add, addressP, X, lowerP
169letD, p, sub, P, XflightD, sub, addressP, X, lowerP
170letD, p, copy, tmpNflightD, copy, addresstmpN, lowertmpN
171letD, p, copy, constNflightD, copy, addressconstN, lowerconstN
172letD, p, itop, XflightD, copy, X, none
checked_access+Access, ?Q, -Insts, ?Insts1semidet

Insts less Insts1 are Access's check, defining tmp(Q), and its raw access: the table in this module's header, one clause per row. Fails for an instruction that is not an access.

179checked_accessletD, p, loadp, A, Q, lettmpQ, a, check0, Size, read, Align, A, letD, p, raw_load_pointer0, Effects, tmpQ|Is, Is ←
!
access_size_alignp, Size, Align
184checked_accessstorep, V, A, Q, lettmpQ, a, check0, Size, write, Align, A, raw_store_pointer0, V, tmpQ, Effects|Is, Is ←
!
access_size_alignp, Size, Align

An atomic pointer access has the same inline check; Fil-C's helper does the rest (ir.pl, pointer_memory_effects/2).

191checked_accessletD, p, atomic_loadp, A, Q, lettmpQ, a, check0, Size, read, Align, A, letD, p, raw_load_pointer0, Effects, tmpQ|Is, Is ←
!
access_size_alignp, Size, Align
pointer_memory_effectsatomic_load, Effects
196checked_accessatomic_storep, V, A, Q, lettmpQ, a, check0, Size, write, Align, A, raw_store_pointer0, V, tmpQ, Effects|Is, Is ←
!
access_size_alignp, Size, Align
pointer_memory_effectsatomic_store, Effects

A cas, integer or pointer, checks write rights at its width's alignment (ir.pl); for p Fil-C's helper does the rest.

203checked_accessletD, K, casT, E, N, A, Q, lettmpQ, a, check0, Size, write, Align, A, letD, K, raw_casT, 0, tmpQ, E, N|Is, Is ←
!
access_size_aligncasT, Size, Align
207checked_accessletD, K, loadT, A, Q, lettmpQ, a, check0, Size, read, Align, A, letD, K, raw_loadT, 0, tmpQ|Is, Is ←
access_size_alignT, Size, Align
!
211checked_accessstoreW, V, A, Q, lettmpQ, a, check0, Size, write, Align, A, raw_storeW, 0, V, tmpQ|Is, Is ←
access_size_alignW, Size, Align
!

A vector's full-width check (docs/vector-access.md): the check, then touch, which uses its proof and accesses nothing.

217checked_accessaccessK, A, Size, Align, Q, lettmpQ, a, check0, Size, K, Align, A, touchtmpQ|Is, Is ←
!
219checked_accesscallRes, tmpP, Args, Q, lettmpQ, f, check_function, tmpP, raw_callRes, tmpQ, Args|Is, Is
222checked_accesslocal_loadD, K, T, Name, Off, Q, lettmpQ, a, check_localName, Off, Size, read, Align, [], Access|Is, Is ←
access_size_alignT, Size, Align
T ≡ p
→
Access = letD, p, raw_load_pointer0, Es, tmpQ
else
Access = letD, K, raw_loadT, 0, tmpQ
229checked_accesslocal_storeW, V, Name, Off, Q, lettmpQ, a, check_localName, Off, Size, write, Align, [], Access|Is, Is ←
access_size_alignW, Size, Align
W ≡ p
→
pointer_memory_effectslocal_store, Es
Access = raw_store_pointer0, V, tmpQ, Es
else
Access = raw_storeW, 0, V, tmpQ
fresh_name+Names, +N0, -N, -Qdet

Q is chkI for the least I >= N0 not among Names, and N is I + 1.

240fresh_nameNames, N0, N, Q ←
formatatomQ0, "chk~d", N0
N1 is N0 + 1
get_assocQ0, Names, _
→
fresh_nameNames, N1, N, Q
else
Q = Q0
N = N1
polled_jump+Jump0, +From, +Index, -Jumpdet

Jump is Jump0, ending the block at index From, with poll(L) for each target L at or before it in block order (Index), so every cycle of the CFG polls.

253polled_jumpgotoL, From, Index, gotoT ←
polled_targetL, From, Index, T
255polled_jumpbranchV, L1, L2, From, Index, branchV, T1, T2 ←
polled_targetL1, From, Index, T1
polled_targetL2, From, Index, T2
258polled_jumpretV, _, _, retV
259polled_jumpretsVs, _, _, retsVs
260polled_jumpret, _, _, ret
261polled_jumpunreachable, _, _, unreachable

Checked IR (a poll target, and no load or store) passes unchanged.

264polled_targetpollL, _, _, pollL ←
!
265polled_targetL, From, Index, T ←
get_assocL, Index, To
To ≤ From
→
T = pollL
else
T = L