Fil-C on Nix

Filnix wiki

mbrock/filnix/nanofil

schedule.pl

Scheduled checks (docs/schedule.md): a check of a struct pointer covers, early, the bytes that every path ahead of it will need, so that the later checks of the same object become renewals.

FilPizlonator's scheduleChecks moves bounds requirements backward to where every path will need them, and merges them into one range check. This pass does the part of that which pays in nanofil, in two steps that keep every check where it is:

1. **Widen.** A heap check of P, a pointer at an exact offset K from

a single-definition base B (ir.pl's guard_chain/3), whose access
is followed on every path, before anything but the closed list
crossable/1, a poll edge or an edge back, by accesses through B
that reach B + H, becomes `check_ahead(Lo, H - K, Rights, Align,
Hi)`. Only the upper end moves. Its trap comes earlier than the
reference's only on executions on which the reference traps later.

2. **Renew at a distance.** Every check that a dominating check or

check_ahead of the same base covers, shifted by their distance,
becomes a renew (check_reuse's renew_checks/3 with `any`), a flag
test.

Facts

FactMeans
B-H in needevery path from here, before a blocker, accesses bytes through base tmpB reaching B + H

A block's exit facts are the meet of what its edges carry (a plain edge to a later block: that block's entry facts; any other edge: none), each base keeping its least H. Walking backward, an instruction that is not crossable/1 ends every fact, a definition of B ends B's fact, and an access raises the needs of its pointer's exact bases. The validator (ir.pl's ahead_justified/3) recomputes the same facts by its own code and accepts each check_ahead only if they justify it; schedule_refines/2 checks that nothing else changed.

The pass is selected with --checks=schedule, after reuse and renewal; it is opt-in, since it changes when a trap happens.

useslists
usesassoc
usesirsingle_definitions/3, guard_chain/3, access_size_align/3, pointer_memory_effects/2
usescheck_reuserenew_checks/3
schedule_checks+M0, -Mdet

M is the checked module M0 with each heap check widened to a check_ahead over the bytes that every path ahead needs from its base, then each check that a dominating check or check_ahead covers at a distance renewed. Functions with a setjmp are unchanged.

55schedule_checksM0, M ←
M0 = moduleItems0
maplistwidened_item, Items0, Items1
renew_checksany, moduleItems1, moduleItems2
maplistrepointed_item, Items1, Items2, Items
M = moduleItems

repointed_item(+Item1, +Item2, -Item): a renew that renewal at distance 0 made reads a check's proof Q; when this pass renewed that check too, from Q1, the renew reads Q1 instead (its root, following such renewals), which covers it by transitivity: Q1's check covers Q's range shifted by their distance, and Q's covered the renew's.

67repointed_itemfunc_, _, _, _, Bs1, funcN, L, R, Ps, Bs2, funcN, L, R, Ps, Bs ←
!
findallQ−Q1,
nth0I, Bs1, block_, _, Is1, _
nth0I, Bs2, block_, _, Is2, _
nth0J, Is1, lettmpQ, a, Op, _
Op ≠ renew_, _, _, _
nth0J, Is2, lettmpQ, a, renew_, _, _, _, tmpQ1, _
,
Pairs0
Pairs0 ≡ []
→
Bs = Bs2
else
sortPairs0, Pairs
list_to_assocPairs, Moved
maplistrepointed_blockMoved, Bs2, Bs
76repointed_item_, Item, Item
78repointed_blockMoved, blockL, Ph, Is0, J, blockL, Ph, Is, J ←
maplistrepointed_instMoved, Is0, Is
81repointed_instMoved, letQ, a, renewLo, Hi, Rs, Al, tmpS0, P, letQ, a, renewLo, Hi, Rs, Al, tmpS, P ←
!
root_sourceMoved, S0, S
84repointed_inst_, I, I
86root_sourceMoved, S0, S ←
get_assocS0, Moved, S1
→
root_sourceMoved, S1, S
else
S = S0
89widened_itemfuncN, L, R, Ps, Bs0, funcN, L, R, Ps, Bs ←
¬
memberblock_, _, Is, _, Bs0
memberlet_, _, setjmp_, _, Is
memberblock_, _, Is0, _, Bs0
memberlet_, a, check_, _, _, _, _, Is0
!
single_definitionsPs, Bs0, Once
findallQ−P,
memberblock_, _, Is, _, Bs0
memberlettmpQ, a, Op, Args, Is
proven_pointerOp, Args, P
get_assocQ, Once, _
,
QPs0
sortQPs0, QPs
list_to_assocQPs, Proven
findallLb−Ix, nth1Ix, Bs0, blockLb, _, _, _, Pos0
list_to_assocPos0, Pos
reverseBs0, Rev
empty_assocE
foldlwidened_blockOnce−Proven, Pos, Rev, E−[], _−RevBs
Bs = RevBs
104widened_itemItem, Item
proven_pointer
106check_, _, _, _PP
107renew_, _, _, __, PP
108renew_if_, _, _, _P, _P
109check_limit_, _, _, _P, _P
widened_block+Env, +Pos, +Block, +Entries0−Done0, −Entries−Done

Entries maps each walked block to its entry needs; Done collects the widened blocks, front first, as the walk goes from the last block.

114widened_blockEnv, Pos, blockL, Phis, Is0, J, Entries0−Done0, Entries−blockL, Phis, Is, J|Done0 ←
get_assocL, Pos, Here
findallT, jump_target_edgeJ, T, Ts
maplistcarriedPos, Here, Entries0, Ts, Carried
meetCarried, Exit
reverseIs0, Rev0
need_walkRev0, Env, Exit, none, Entry0, [], Is
foldlphi_kill, Phis, Entry0, Entry
put_assocL, Entries0, Entry, Entries
jump_target_edge
124gotoTT
125branch_, T, _T
126branch_, _, TT
128carried_, _, _, poll_, E ←
!
empty_assocE
129carriedPos, Here, Entries, T, Need ←
get_assocT, Pos, There
There > Here
get_assocT, Entries, Need0
→
Need = Need0
else
empty_assocNeed
135meet[], E ←
empty_assocE
136meetN|Ns, M ←
assoc_to_listN, Ps
includein_allNs, Ps, Ks
maplistleastNs, Ks, Ms
list_to_assocMs, M
142in_allNs, B−_ ←
forallmemberN, Ns, get_assocB, N, _
143leastNs, B−H0, B−H ←
foldlN, A0, A >> get_assocB, N, A1, A is minA0, A1, Ns, H0, H
need_walk+RevInsts, +Env, +After, +AfterNext, -Before, +Acc, -Insts

Insts are the block's instructions (Acc holds those after them), each widened check rewritten; After are the needs after the first of RevInsts, AfterNext those after the instruction that follows it.

150need_walk[], _, Need, _, Need, Is, Is
151need_walkI0|Rev, Env, After, Next, Before, Acc, Is ←
widenedI0, Env, Next, Acc, I
need_beforeI0, Env, After, Need
need_walkRev, Env, Need, After, Before, I|Acc, Is
156widenedlettmpQ, a, checkLo, Hi, Rights, Align, P, Once−_, Next, Access|_, lettmpQ, a, check_aheadLo, Hi1, Rights, Align, Hi, P ←
Next ≢ none
Its own access runs after the early trap point, so it must be
crossable too (not a pointer store or an atomic).
crossableAccess
guard_chainOnce, P, Chain
A base counts only where the check's start, B + K + Lo, is near
too, as ir.pl's ahead_insts/8 asks.
findallH1,
membertmpB−iK, K, Chain
get_assocB, Once, _
KLo is K + Lo
nearKLo
get_assocB, Next, H
H1 is H−K
,
H1s
max_listH1s, Hi1
Hi1 > Hi
Hi1−Lo ≤ 2147483647
!
170widenedI, _, _, _, I
172need_beforeI, Once−Proven, After, Need ←
→
del_assocD, After, _, N1
→
true
else
N1 = After
heap_accessI, Q, X, Off
get_assocQ, Proven, P
→
guard_chainOnce, P, Chain
foldlneedsOnce, Off, Size, Chain, N1, Need
else
Need = N1
else
empty_assocNeed
184needsOnce, Off, Size, tmpB−iK, K1, N0, N ←
K ≡ K1
get_assocB, Once, _
H is K + Off + Size
!
get_assocB, N0, H0
H0 ≥ H
→
N = N0
else
put_assocB, N0, H, N
188needs_, _, _, _, N, N
near+K

offsets are exact only modulo 2^64; count only those well inside the signed range (ir.pl's near_offset/1, by its own code).

192nearK ←
K ≥ −1 << 62
K ≤ 1 << 62
194phi_killphitmpD, _, _, N0, N ←
del_assocD, N0, _, N
→
true
else
N = N0
196defineslettmpD, _, _, _, D
197definesflighttmpD, _, _, _, D
heap_access
199let_, _, raw_loadT, Off, tmpQQTOff
200raw_storeW, Off, _, tmpQQWOff
201let_, p, raw_load_pointerOff, _, tmpQQpOff

crossable(+Inst): a check may be widened past Inst toward a later access: Inst cannot exit to the runtime, call, free, run a signal handler, fault otherwise than a Fil-C trap or loop. Pointer stores (ensure-aux may exit), calls, allocation, getters, atomics, setjmp, va_arg and division are not.

208crossableloc_, _
209crossabletouch_
210crossableflight_, _, _, _
211crossableraw_storeW, _, _, _ ←
W ≢ p
212crossablelet_, p, raw_load_pointer_, Es, _ ←
213crossablelet_, _, Op, Args ←
crossable_opOp, Args
215crossable_opcheck_, _, _, _, _
216crossable_opcheck_ahead_, _, _, _, _, _
217crossable_oprenew_, _, _, _, _
218crossable_opraw_loadT, _, _ ←
T ≢ p
219crossable_opcmp_, _, _
220crossable_opext_, _
221crossable_opcopy, Args ←
¬memberchkglobal_, Args
222crossable_opOp, _ ←
memberchkOp, add, sub, mul, and, or, xor, sar, shr, shl, neg, ptoi, cast
schedule_refines+M0, +Msemidet

M is M0 except for heap checks, each at its own index with its own pointer, rights, alignment and lower end, that became a check_ahead of a wider range whose access's own range is the check's, or a renew of at least the check's range. This is identity, not safety or timing: the validator (ir.pl) accepts each check_ahead only where its early bytes are needed ahead, and each renew only where an earlier check covers it.

234schedule_refinesmoduleItems0, moduleItems ←
maplistitem_refines, Items0, Items
237item_refinesfuncN, L, R, Ps, Bs0, funcN, L, R, Ps, Bs ←
!
maplistblock_refines, Bs0, Bs
239item_refinesItem, Item
241block_refinesblockL, Ph, Is0, J, blockL, Ph, Is, J ←
maplistinst_refines, Is0, Is
244inst_refinesI, I ←
!
245inst_refinesletQ, a, checkLo, Hi, Rs, Al, P, letQ, a, check_aheadLo, Hi1, Rs, Al, Hi, P ←
!
Hi1 > Hi
248inst_refinesletQ, a, checkLo, Hi, Rs, Al, P, letQ, a, renewLo, Hi1, Rs, Al, tmp_, P1 ←
P1 ≡ P
Hi1 ≥ Hi
251inst_refinesletQ, a, renewLo, Hi, Rs, Al, tmp_, P, letQ, a, renewLo, Hi, Rs, Al, tmp_, P1 ←
P1 ≡ P