Fil-C on Nix

Filnix wiki

mbrock/filnix/nanofil

filc_abi.pl

Fil-C ABI lowering, before AMD64 placement and instruction selection.

lower_function/4 reads two synchronized views: checked operations (proof/access pairs already fused) and ordinary operations for liveness. It never inserts an instruction into either view. Each abi(Actions) occupies its original closed instruction interval, including all its helper calls. Memory operations pass through unchanged.

The product is a function with a frame, entry actions, lowered blocks, exception exit, public object and origins. Frames name payload slots, ordered roots and aliases, not native offsets. Fast words explicitly split pointers into address/lower; generic cells carry separate data and aux sources, including zero aux for scalars. registers/buffer(N) chooses the return protocol; AMD64 assigns registers and buffer bytes.

framePlain, Roots, Aliases includes localName, Size in Plain for nonescaping objects, separate from temporary slots. The special stack_auxName roots form a suffix before the setjmp slots; originWhich, Catch, NRoots, NSetjmps, NLocals describes that order to the collector. initialize_localName zeroes payload and aux and publishes the stack-aux root without a safepoint. It does not produce a heap lower or an address value.

returns_twice invokes native setjmp in the current body and restores payloads without a safepoint before writing the full native int result. Setjmp disables sharing even for unreachable sites. Its snapshots contain every logical temporary/phi payload, never frame bookkeeping or jmpbuf payloads. Snapshot roots are stable from entry; jmpbuf roots are last and persist across nonlocal returns. Variadic entry promotes the incoming packet eagerly and passes an invocation-lived hidden pointer to the private body. promote_incoming uses the original packet size, then roots that pointer in the body before any further safepoint. See docs/setjmp.md and abi-findings.md.

useslists
usesassoc
usespairs
usesordsets
usesir
useslower_rootslowers_mode/1, derived_lowers/3
useslowers_checkcheck_lowers/4
lower_function+Symbols, +FusedFunction, +LiveBlocks, -Productdet

Product is the ABI plan, abi_function/10, of the checked function FusedFunction, whose checks are fused with their accesses (filc_amd64's fused_blocks/5), LiveBlocks being the same blocks as liveness reads them: its frame(Plain, Roots, Aliases), entry actions, lowered blocks, exception exit, public object and origins (this module's header). Symbols is the module's ir:module_symbols/2 (or the module itself). Calls and getters look their targets up in it. The frame's pools are shared; lower_function/5 chooses.

55lower_functionModule, Func, LiveBlocks, Product ←
lower_functionshared, Module, Func, LiveBlocks, Product
lower_function+Pools, +Symbols, +FusedFunction, +LiveBlocks, -Productdet

As lower_function/4, with the temporaries' slot pools shared or own (frame_slots:temp_slots/9).

62lower_functionPools, Module, Func, LiveBlocks, abi_functionName, Linkage, Ret, Sig, Frame, Entry, Blocks, Exit, Object, Origins ←
Func = funcName, Linkage, Ret, Params, Blocks0
param_kindsParams, Ks
signatureRet, Ks, Sig
return_protocolRet, Return
body_paramsFunc, Params, BodyParams
framePools, BodyParams, LiveBlocks, Return, Frame, Snapshots
Shared lowers (docs/lowers.md) are validated, not trusted.
→
check_lowersName, BodyParams, LiveBlocks, Frame
else
true
Frame = framePlain, Roots, _
lengthRoots, NRoots
findallN, memberlocalN, _, Plain, LocalNames
lengthLocalNames, NLocals
fast_paramsBodyParams, Incoming
Snapshots = snapshotsSites, Payload
sortSites, InitSites
Entry = native_frameReturn, stack_check, save_thread, save_returnReturn, zero_rootsNRoots, push_framec, receiveReturn, Incoming, snapshotsInitSites, Payload
Exit = pop_frame, exceptionReturn
maplistlower_blockModule, Ret, Return, Snapshots, Blocks0, Blocks
public_objectFunc, Return, Object
Snapshots = snapshotsSites, _
lengthSites, NSites
Origins = originc, catches, NRoots, NSites, NLocals, originn, no_catch, NRoots, NSites, NLocals
signature+Ret, +ParamKs, -Sigdet

Sig is Fil-C's fast-call signature number for a function returning Ret (a class, void or tuple([K1, K2])) on parameters of classes ParamKs: 1 + R + 133 * A, R and A the bijective base-11 encodings of the return and argument word digits, independent of native locations. More than 16 argument words have no fast signature, and get never_signature/1. It names private bodies, pizlonatedFIP<Sig>_F.

94signatureRet, ParamKs, Sig ←
ret_kindsRet, RetKs
maplistclass_digit, RetKs, RetDigits
maplistclass_digit, ParamKs, ArgDigits
lengthArgDigits, NArgs
NArgs ≤ 16
→
bijective_valueRetDigits, R
bijective_valueArgDigits, A
Sig is 1 + R + 133 × A
never_signature-Sigdet

More than 16 words have no fast signature (clang's is generic, 0). A call site gets one no function object has, the largest 64-bit value (fast signatures stay below 2^63), so it always goes generic; the frontend makes such a definition generic-only, and its private body is named with this one.

112never_signature18446744073709551615
class_digit
114w0
115l0
116p7
117s1
118d2
119bijective_value[], 0
120bijective_valueD|Ds, V ←
V is D + 1 + 11 × V1
123param_kindsPs, Ks ←
findallK, memberparamK, _, Ps, Ks
ret_kinds
124void[]
125tupleKsKs
126ww
127ll
128pp
129ss
130dd

A buffer has one exception byte followed by N full payload/lower words.

133return_protocolRet, Protocol ←
ret_kindsRet, Ks
foldlreturn_words, Ks, 0, N
N > 2
→
Protocol = bufferN
else
Protocol = registers
136return_wordsp, N0, N ←
N is N0 + 2
137return_wordsw, N0, N ←
N is N0 + 1
138return_wordsl, N0, N ←
N is N0 + 1

Float words return in %xmm0 and %xmm1, not in integer registers.

140return_wordss, N, N
141return_wordsd, N, N
143body_paramsFunc, Ps, Body ←
→
appendPs, paramp, tmphiddenva_snapshot, Body
else
Body = Ps
148framePools, Params, Blocks, Return, framePlain, Roots, Aliases, snapshotsSites, Payload ←
findallN−K,
memberparamK, tmpN, Params
or
memberblock_, Phis, Is, _, Blocks
memberphitmpN, K, _, Phis
or
memberI, Is
inst_destI, N, K
,
Temps0
sortTemps0, Temps
findallN−K,
memberblock_, Phis, _, _, Blocks
memberphitmpN, K, _, Phis
,
Shadows0
sortShadows0, Shadows
findallshadowN, memberN−_, Shadows, PayloadShadows
findallD,
memberblock_, _, Is, _, Blocks
memberlettmpD, w, setjmp_, _, Is
,
Sites
temp_storageSites, Pools, Params, Blocks, Temps, Keys, Lowers, Aliases0
shadow_storageSites, Pools, Blocks, Temps0, Shadows, ShadowKeys, ShadowRoots, ShadowAliases
appendAliases0, ShadowAliases, Aliases
return_storageReturn, Extra
findalltmphiddensetjmp_bufD, memberD, Sites, JPlain
local_objectsfuncframe, local, void, Params, Blocks, Locals
findalllocalN, Size, memberN−Size, Locals, LocalStorage
findallstack_auxN, memberN−_, Locals, LocalRoots
appendKeys, ShadowKeys, thread, aux, Extra, JPlain, LocalStorage, Plain
findalllowersetjmp_snapshotD, memberD, Sites, SnapRoots
findalllowertmphiddensetjmp_bufD, memberD, Sites, JRoots
Explicit stack auxes are the final root group before jmpbufs,
as filc_function_origin_stack_aux_begin/end_index specifies.
appendLowers, ShadowRoots, SnapRoots, LocalRoots, JRoots, Roots
Preserve the old assoc's canonical payload traversal order.
findalltmpN, memberN−_, Temps, TempPayload
appendTempPayload, PayloadShadows, Payload0
sortPayload0, Payload
shadow_storage+Sites, +Pools, +Blocks, +Defs, +Shadows, -Keys, -Roots, -Aliases

the phi shadows' plain keys, root keys and aliases (docs/perf.md item 9). A shadow is stored and loaded only inside one edge's parallel copy (filc_amd64.pl's edge_moves//4, which checks that no safepoint is inside it), so in the allocating modes (pools not shared) of a function without setjmp sites, the i-th phi of class C in its block uses shadow_slot(C, i), C being int (w, l), p or float (s, d), and a pointer phi's lower the root lower(shadow_slot(p, i)). A temporary defined more than once (Defs, one N-K per definition) keeps a shadow of its own, as every phi does in the reference layout.

190shadow_storageSites, Pools, Blocks, Defs, Shadows, Keys, Roots, Aliases ←
Sites ≡ []
Pools ≢ shared
→
findallN−x, memberN−_, Defs, Ds0
keysortDs0, Ds
group_pairs_by_keyDs, Counted
findallN, memberN−_, Counted, Once0
sortOnce0, Once
findallPs,
memberblock_, Phis, _, _, Blocks
block_shadowsPhis, Once, Ps
,
PerBlock
appendPerBlock, Positions
findallN, memberN−_−_, Positions, Placed0
sortPlaced0, Placed
findallN−K,
memberN−K, Shadows
¬ord_memberchkN, Placed
,
Own
foldlclass_countPerBlock, int, p, float, Counts, []
findallshadowN, memberN−_, Own, OwnKeys
findallshadow_slotC, I,
memberC−M, Counts
Last is M−1
between0, Last, I
,
SlotKeys
appendOwnKeys, SlotKeys, Keys
findalllowershadowN, memberN−p, Own, OwnRoots
memberchkp−MP, Counts
→
true
else
MP = 0
LastP is MP−1
findalllowershadow_slotp, I, between0, LastP, I, SlotRoots
appendOwnRoots, SlotRoots, Roots
findallA,
memberN−C−I, Positions
A = shadowN−shadow_slotC, I
or
C ≡ p
A = lowershadowN−lowershadow_slotp, I
,
Aliases
else
findallshadowN, memberN−_, Shadows, Keys
findalllowershadowN, memberN−p, Shadows, Roots
Aliases = []
block_shadows+Phis, +Once, -Positions

N-Class-Index for each phi of a block whose destination is defined once, numbered within its class.

221block_shadowsPhis, Once, Positions ←
foldlphi_positionOnce, Phis, int−0, p−0, float−0−Positions, _−[]
224phi_positionOnce, phitmpN, K, _, Next0−Ps0, Next−Ps ←
ord_memberchkN, Once
→
nth0J, Next0, C−I, Rest
nth0J, Next, C−I1, Rest
I1 is I + 1
Ps0 = N−C−I|Ps
else
Next = Next0
Ps0 = Ps
shadow_class
231wint
232lint
233pp
234sfloat
235dfloat
class_count+PerBlock, +Class, -Counts0, -Counts

Class-M with M the largest number of Class phis a block shares, when M > 0.

239class_countPerBlock, C, C−M|Cs, Cs ←
findallL,
memberPs, PerBlock
findallx, member_−C−_, Ps, Xs
lengthXs, L
,
Ls
max_list0|Ls, M
M > 0
!
243class_count_, _, Cs, Cs

Under --lowers=shared a derived flight's address has a slot of its own and its lower is its root's lowers[] entry (lower_roots.pl).

247temp_storage[], Pools, Ps, Bs, Temps, Keys, Lowers, Aliases ←
→
derived_lowersPs, Bs, Derived
else
empty_assocDerived
temp_slotsPools, Derived, Ps, Bs, Temps, Map, NS, NP, NW, NF, NA
storage_keysscalar, NS, Scalars
storage_keysfloat, NF, Floats
storage_keyspointer, NP, Pointers
storage_keyswide, NW, Wides
storage_keysaddress, NA, Addresses
appendScalars, Floats, Pointers, Addresses, Wides, Keys
mapliststorage_lower, Pointers, Lowers
assoc_to_listMap, Pairs
foldlstorage_aliasMap, Derived, Pairs, Aliases, []
256temp_storage_|_, _, _, _, Temps, _, _, _ ←
memberchk_−x, Temps
!
thrownanofil_error'long double in a function that calls setjmp is not supported'
259temp_storage_|_, _, _, _, Temps, Keys, Lowers, [] ←
findalltmpN, memberN−_, Temps, Keys
findalllowertmpN, memberN−p, Temps, Lowers
262storage_keysPool, N, Keys ←
Last is N−1
findallstoragePool, I, between0, Last, I, Keys
264storage_lowerKey, lowerKey
265storage_alias_, _, N−storagepointer, I, tmpN−storagepointer, I, lowertmpN−lowerstoragepointer, I|As, As ←
!
267storage_aliasMap, Derived, N−storageaddress, I, tmpN−storageaddress, I, lowertmpN−lowerstoragepointer, J|As, As ←
!
get_assocN, Derived, R
get_assocR, Map, storagepointer, J
270storage_alias_, _, N−storagescalar, I, tmpN−storagescalar, I|As, As ←
!
271storage_alias_, _, N−storagefloat, I, tmpN−storagefloat, I|As, As ←
!
272storage_alias_, _, N−storagewide, I, tmpN−storagewide, I|As, As
273return_storageregisters, []
274return_storagebuffer_, retbuf
275inst_destlettmpN, K, _, _, N, K
276inst_destflighttmpN, _, _, _, N, p
277inst_destlocal_loadtmpN, K, _, _, _, N, K
278inst_destcallresulttmpN, K, _, _, N, K
279inst_destcalltupleTos, _, _, N, K ←
memberresulttmpN, K, Tos
280inst_destusesI, _, N, K ←
inst_destI, N, K
281inst_destasm_fixed_, _, Outs, N, w ←
member_−tmpN, Outs

No physical locations: these are ordered words and their sources.

284fast_paramsPs, Ts ←
foldlfast_param, Ps, Ts, []

The cuts keep these per-word steps deterministic: a choicepoint per temporary or argument (9,000 in SQLite's sqlite3VdbeExec) would pin the trail and heap of the rest of the function's emission.

288fast_paramparamp, T, wfl, T, wfl, lowerT|Ts, Ts ←
!
289fast_paramparamK, T, wfK, T|Ts, Ts ←
K ≢ p
290fast_argsAs, Ts ←
foldlfast_arg, As, Ts, []
291fast_argargp, V, wfl, addrV, wfl, lowerV|Ts, Ts ←
!
292fast_argargK, V, wfK, valK, V|Ts, Ts ←
K ≢ p

Generic cells are one eightbyte per source value, not per fast word. Source names for adapters are abstract too: data(I), aux(I), snapshot.

296generic_argsArgs, Cells ←
findallcellI, B, V, Lower,
nth0I, Args, argK, V
value_lowerK, V, Lower
,
Cells
299payload_classp, l
300payload_classK, K ←
K ≢ p
301value_lowerp, V, lowerV
302value_lowerK, _, none ←
K ≢ p
303generic_paramsKs, Ts ←
findallwfB, Source,
nth0I, Ks, K
packet_wordK, I, B, Source
,
Ts
305packet_wordp, I, l, dataI
306packet_wordp, I, l, auxI
307packet_wordK, I, K, dataI ←
K ≢ p
result_tos
309void[]
310resultT, KresultT, K
311tupleTosTos
312result_retvoid, void
313result_retresult_, K, K
314result_rettupleTos, tupleKs ←
findallK, memberresult_, K, Tos, Ks
316result_slotsTos, Slots ←
foldlresult_slot, Tos, Slots, []
317result_slotresultT, p, T, lowerT|Ss, Ss ←
!
318result_slotresultT, K, T|Ss, Ss ←
K ≢ p

Results name their channels explicitly. Scalar floating results are distinct from integer slots; the target must test the exception first. A tuple with a float word names each slot's class: typed(K-Slot).

323fast_resultRes, resultReturn, Channels ←
result_retRes, Ret
return_protocolRet, Return
Res = resultT, K
memberchkK, s, d
→
Channels = floatK, T
Res = tupleTos
→
foldltyped_slot, Tos, Typed, []
Channels = typedTyped
else
result_tosRes, Tos
result_slotsTos, Slots
Channels = slotsSlots
329float_tupleTos ←
memberresult_, K, Tos
memberchkK, s, d
!
330typed_slotresultT, p, l−T, l−lowerT|Ss, Ss ←
!
331typed_slotresultT, K, K−T|Ss, Ss
332generic_resultRes, receive_genericNeed, Cells ←
result_tosRes, Tos
lengthTos, N
Need is 8 × N
findallcellI, K, T, Lower,
nth0I, Tos, resultT, K
value_lowerK, T, Lower
,
Cells
337lower_blockModule, Ret, Return, Snapshots, blockL, Ps, Is0, J0, blockL, Ps, Is, J ←
maplistlower_instModule, Snapshots, Is0, Is
lower_jumpRet, Return, J0, J
341lower_instModule, _, callRes, globalF, Args, abiActions ←
!
call_signatureRes, Args, Sig0
call_symbol_signatureRes, Args, Sig0, Sig
direct_targetModule, F, Sig, Target
fast_resultRes, Result
Result = resultReturn, _
fast_argsArgs, Words
Call = originc, reserve_resultReturn, invokeTarget, Return, Words, Result, release_resultReturn
small_copyModule, F, Res, Args, Filter
→
appendFilter|Call, copy_join, Actions
else
Actions = Call
351lower_inst_, _, fusedcheck_function, callRes, P, Args, abiActions ←
!
call_signatureRes, Args, Sig
fast_resultRes, Result
Result = resultReturn, _
fast_argsArgs, Words
generic_argsArgs, Cells
lengthArgs, N
Size is 8 × N
generic_resultRes, GenericResult
Actions = originc, reserve_resultReturn, dispatchP, Sig, Return, Words, Result, Size, Cells, GenericResult, release_resultReturn
359lower_instModule, _, lettmpD, p, copy, globalS, abioriginc, getterTarget, c, D ←
!
getter_targetModule, S, Target
362lower_inst_, snapshots_, Keys, lettmpD, w, setjmpKind, Buf, Mask, abioriginc, returns_twiceD, KindNo, Buf, Mask, Keys ←
!
setjmp_kindKind, KindNo
365lower_inst_, _, lettmpD, p, va_args, constOff, abisnapshot_cursortmphiddenva_snapshot, Off, D ←
!
367lower_inst_, _, lettmpD, K, va_arg, P, abioriginc, consume_vaK, P, Size, D ←
!
A long double's cell is 16 bytes at 16 (docs/float.md).
K ≡ x
→
Size = 16
else
371lower_inst_, _, local_allocName, _, abiinitialize_localName ←
!
372lower_inst_, _, I, I

Returns are actions too: materialize before popping roots, then signal success in the appropriate exception channel. No safepoint intervenes.

376lower_jumpK, Return, retV, abi_returnreturn_valueK, V, pop_frame, successReturn ←
!
377lower_jumptupleKs, bufferN, retsVs, abi_returnreturn_bufferWords, success_buffer, pop_frame, native_return ←
!
maplistreturn_arg, Ks, Vs, Args
fast_argsArgs, Words
lengthWords, N
380lower_jumptupleKs, registers, retsVs, abi_returnreturn_typedKs, Vs, pop_frame, successregisters ←
memberK, Ks
memberchkK, s, d
!
383lower_jump_, registers, retsVs, abi_returnreturn_registersVs, pop_frame, successregisters ←
!
385lower_jump_, Return, ret, abi_returnpop_frame, successReturn ←
!
386lower_jump_, _, J, J
387return_argK, V, argK, V
setjmp_kind
389setjmp0
390'_setjmp'1
391sigsetjmp2
393call_signatureRes, Args, Sig ←
result_retRes, Ret
findallK, memberargK, _, Args, Ks
signatureRet, Ks, Sig
call_symbol_signature+Res, +Args, +Sig0, -Sig

Sig names the weak thunk a direct call goes through. Every call of more than 16 words has the never signature, so its thunk is named never(R-A) by the whole word list instead: two such calls with different words (a variadic callee's, say) must not share one thunk and its packet size.

401call_symbol_signatureRes, Args, Sig0, Sig ←
→
result_retRes, Ret
findallK, memberargK, _, Args, Ks
word_codesRet, Ks, Code
Sig = neverCode
else
Sig = Sig0
407word_codesRet, Ks, R−A ←
ret_kindsRet, RetKs
maplistclass_digit, RetKs, RDs
maplistclass_digit, Ks, ADs
small_copy+Module, +F, +Res, +Args, -Filter

the call is a memcpy or memmove of a constant 1 to 16 bytes that the module does not define, so the emitter may put the inline filter of docs/perf.md item 7a in front of the unchanged call; Filter is copy_filter(Dst, Src, N, Res).

415small_copyModule, F, Res, argp, D, argp, S, argl, constN, copy_filterD, S, N, Res ←
memberchkF, memcpy, memmove
integerN
N ≥ 1
N ≤ 16
Res ≡ void
→
true
else
Res = result_, p
¬func_namedModule, F, _
¬data_namedModule, F

A weak definition may be replaced by another object's, so the module's own calls and addresses of it go through its PLT getter, as an external symbol's do; a weak declaration's getter may be null.

425direct_targetModule, F, _, implementationF, Sig ←
func_namedModule, F, Func
¬variadic_namedModule, F
¬weak_definedModule, F
!
Func = func_, _, Ret, Ps, _
signatureRet, Ks, Sig
429direct_target_, F, Sig, entryF, Sig
430getter_targetSymbols, S, Target ←
func_namedSymbols, S, _
or
data_namedSymbols, S
¬weak_definedSymbols, S
→
Target = local_getterS
weak_declaredSymbols, S
→
Target = weak_getterS
else
Target = external_getterS

Adapter plans are consumed by public generic entries and weak thunks. The void generic result is one zero word, despite having no fast value.

441adapter_returnRet, adapter_resultReturn, Channel, Size, Packet ←
return_protocolRet, Return
ret_kindsRet, Ks
lengthKs, N
Size is 8 × max1, N
Ks = []
→
Channel = void
Ks = K
memberchkK, s, d
→
Channel = floatK
memberK, Ks
memberchkK, s, d
→
Channel = typedKs
else
Channel = words
findallS, memberwf_, S, Words, Copies
Ks = []
→
Zeros = data0, aux0
else
findallauxI,
nth0I, Ks, K0
K0 ≢ p
,
Zeros
Packet = packetCopies, Zeros
452public_objectFunc, _, Object ←
Func = func_, _, Ret, Ps, _
lengthPs, N
Need is 8 × N
adapter_returnRet, Result
→
generic_paramsKs, Prefix
appendPrefix, wfl, snapshot, wfl, snapshot, Words
Object = generic_onlypromote_incomingNeed, Words, Result
else
signatureRet, Ks, Sig
Object = fast_and_genericSig, adaptNeed, Words, Result
lower_thunks+Module, -Thunksdet

Thunks are the plans of the weak external thunks Module needs: one per symbol and signature for each direct call to a function the module does not define, as clang emits one per external callee (docs/abi-findings.md, Weak thunks are shared by signature).

468lower_thunksModule, Thunks ←
Module = moduleItems
module_symbolsModule, Symbols
findalltF, Sig, Ret, Ks,
memberfunc_, _, _, _, Bs, Items
memberblock_, _, Is, _, Bs
membercallRes, globalF, Args, Is
direct_targetSymbols, F, unused, Target
Target = entryF, unused
result_retRes, Ret
findallK, memberargK, _, Args, Ks
signatureRet, Ks, Sig0
call_symbol_signatureRes, Args, Sig0, Sig
,
Ts0
sortTs0, Ts1
maplistlower_thunkSymbols, Ts, Thunks
478one_per_symbol[], []
479one_per_symbolT|Ts0, T|Ts ←
T = tF, Sig, _, _
excludesame_symbolF, Sig, Ts0, Ts1
481same_symbolF, Sig, tF, Sig, _, _

Ref is weak if the callee is declared weak: its getter may be null; local if this module defines it with local linkage, so that the thunk is local too and never merges with another object's thunk for its own local function of the same name, as clang keeps such thunks local. Local data called directly is the same: its thunk calls this module's getter, whose function check traps, and another object's function d must not replace it.

489lower_thunkSymbols, tF, Sig, Ret, Ks, thunkF, Ref, Sig, Words, Cells, ArgsSize, Result, Need ←
weak_declaredSymbols, F
→
Ref = weak
func_namedSymbols, F, func_, local, _, _, _
→
Ref = local
ir:data_itemSymbols, F, data_, L, _, _
memberchkL, local, rolocal, tlslocal
→
Ref = local
else
Ref = strong
findallparamK, tmpI, nth0I, Ks, K, Ps
fast_paramsPs, Words
thunk_cellsKs, 0, 0, Cells
lengthKs, N
ArgsSize is 8 × N
adapter_returnRet, Result
ret_kindsRet, RKs
lengthRKs, NR
Need is 8 × NR

Native saves are indexed by flattened word, generic cells by value. These saves are not roots: until the callee's entry records them, an incoming capability is rooted only by its caller (abi-findings.md).

505thunk_cells[], _, _, []
506thunk_cellsK|Ks, I, W, cellI, B, savedW, Lower|Cells ←
K ≡ p
→
L is W + 1
Lower = savedL
Next is W + 2
else
Lower = none
Next is W + 1
I1 is I + 1
thunk_cellsKs, I1, Next, Cells