rocq/theories/Runtime.v
The runtime as axioms: Campaign 2's module type Runtime, and the interpreter's instance of it (formal.md, runtime-axioms.md, which gives each axiom's source in the runtime).
The model of the checked IR is about one thread's program. What the Fil-C runtime does around that program, where it puts a new object, what free does to an object's header, what the collector may do at a safepoint, is not the model's to decide. Runtime states those facts as one page of axioms, read against the staging Fil-C the toolchain pins (mbrock/fil-c at 18b6663a2785c8eba6cc2a544a7711e19fc448c7, the coreRev of lib/filc-staging.json). Each axiom about Fil-C has a native test in tests/c/prog/axiom-*.c that fails if it is stated in the obvious stronger way that the runtime does not promise.
One thread's view
The model is one thread's. At a safepoint the collector acts, and what the program sees change there, the safepoint relation, is the collector's rewriting of stored capabilities to freed objects and nothing else: no region changes (collector_live). Fil-C's runtime never frees an object by itself. Only code the program brings in does: a signal handler it installed, which Fil-C runs only at pollchecks (tests/c/prog/axiom-safepoint.c), or a thread it started. Both are the program's code, not the runtime's, and a module that installs a handler or starts a thread calls an external with no contract, which ends its run in the model; a handler is to be verified as program code of its own. The model takes a run to be the program's whole execution after the trusted entry, with no handler installed and no other program thread when it starts: a constructor, a preloaded library or a native caller that installed one is outside it. A pointer store's aux exit is a native safepoint inside a check and its access, which the validator keeps facts across, so a handler a program installs could free a checked object there; a verified handler must not free an object whose check is in flight. Another thread is outside the model: its writes are (non_moving lets no byte change at a safepoint), and so are its frees, which natively show at any check, not only at a safepoint (tests/c/prog/axiom-threads.c). For a threaded program, native safety rests on Fil-C's own argument, not on the theorems proved over this module type: every check traps on a freed object, and an access whose check passed before the free reads retained memory (retention).
What the axioms speak of
The capability c is that of an object freed in the regions rs.
Every nonempty access through the capability c, in the regions rs, traps out of bounds, as every access to a freed object does. The collector's free singleton is such a capability.
Every object of the regions rs lies between 0 and 2^48, native Fil-C's address space (PAS_ADDRESS_BITS); a native address is unsigned, so no object starts below 0. An address inside one never wraps (raw_address/3), and two addresses of one object are less than 2^63 apart, so a near offset between them does not wrap either (hull_covered/7). A lower half check compares its address, read signed, with the object's base (half_bound/6), so an address that wrapped below 0 fails it.
Every freed object of the regions rs is empty. Free and the end of an invocation end an object's life and empty its bounds together (lifetime_step, region_step).
The address a is inside the current bounds of an object of the memory m.
How the program may change objects, from memory m to m': every identity of m' was one of m, unchanged, or live and now ended, its bounds empty: a free, or the end of an invocation's locals. Unlike lifetime_step, an object of any kind may end.
What the program itself may do to memory between allocations, from m to m': end objects (region_step), and write bytes and stored capabilities only inside objects' bounds.
What a free that succeeds does: on the null pointer, nothing; on the base pointer of a live heap object at an address other than 0, it empties and frees that object's region and changes no other region, no byte and no stored capability. The axiom free_succeeds says the runtime's free does so.
The module type
The runtime's own state, which the program never reads: the interpreter's Next.
The runtime's state when a run starts.
The base of a new object for the request q, and the state after it; None when the runtime cannot hold the object, which ends the run with trap(out_of_memory).
The allocator gives each new object an address no other object uses. Where the runtime in state s is ready in memory m, a request q with a positive alignment and an extent of at least 0 that alloc grants gets a fresh base: a new identity, aligned, overlapping no object, ending at or below 2^64, reading as zero with no stored capability; and the runtime is ready in the memory with the new object. The model needs it so that a new object cannot alias an old one, and so the step relation's choice of any fresh base covers every runtime.
Natively a freed object's address can be a new object's once nothing names the old one; the model never reuses an identity, because a capability never dies in it. The two agree on whether each access through a capability the program still holds traps, not on its address bits after a collection. Native test: axiom − fresh.c.
Every object lies in native Fil-C's address space: where the runtime in state s is ready in memory m, every object of m lies between 0 and 2^48 (objects_fit). The model needs it so that no address inside an object wraps and a wrapped address fails a lower half check; an allocation the runtime cannot hold is refused, a trap. Native test: axiom − address.c.
No freed object of a memory m the runtime is ready in has bounds (freed_empty). Every free and the end of every invocation empty them (lifetime_step, region_step), so it constrains only the memory a run starts in. It is about the runtime's own bookkeeping, not Fil-C, so it has no native test.
The program's own changes keep the runtime ready: if it is ready in m, and m' is m after the program wrote inside objects and ended some (kept), it is ready in m'. With fresh_allocation and safepoint_ready this makes ready hold in every state of a run (Machine.v). No native test: it is about the runtime's own bookkeeping.
Free changes an object's bounds only by emptying a live heap object's, for every alias at once, and makes no new identity (lifetime_step). The model needs it so that a capability the program holds keeps its meaning across a free except for the freed object, whose every access then traps. Native test: axiom − lifetime.c.
Free changes no byte and no stored capability: a freed object's memory is retained while anything can name it. Machine.v uses it to show a free keeps the program's other memory; natively it is also what lets an access whose check passed before another thread's free read the same bytes. Native test: axiom − retention.c.
Free reclaims nothing: every identity before it is one after it, the freed object's too. Only the collector reclaims, at a safepoint (rooting), and natively only of objects nothing can name, whose regions the model keeps. With fresh_allocation this keeps a later allocation from taking a freed object's base. Native test: axiom − identity.c.
Free succeeds where a program frees what it owns: on the null pointer it does nothing, and on the base pointer of a live heap object it frees that object as frees_heap says. free_lifetime, retention and free_keeps_ids bound what any free may change; this says that such a free returns, so that a program logic can prove a call of free goes on (Logic.v's wp_free). The pointer carries the object's Obj capability, not a read-only one. The model's heap objects are what the IR's alloc makes (malloc-like calls, and the address-taken locals, VLAs and allocas nanofil leaves on the heap), what the malloc and strdup contracts make, and the startup argv and environ objects; natively each is an ordinary collected object, which free returns on. Free of a global, an mmap'd object, an interior pointer or a freed object traps. Native test: axiom − free − succeeds.c.
The runtime may act at the instruction i: the collector's handshake, a pollcheck callback, or a signal handler the program installed. Another thread's effects are outside the model (the header's One thread's view).
The runtime acts only at instructions that are not evidence_preserving/1: calls, polls, alloc and the rest. The model needs it because the validator keeps evidence across every other instruction, which would be unsound if the runtime could free or rewrite memory there. Native test: axiom − safepoint.c.
What the program may find changed when it reaches a safepoint with the runtime in state s and memory m: the runtime's state s' and memory m' after the collector acted.
At a safepoint no region changes: the collector frees no object, live or freed, and no identity appears. Natively the collector reclaims objects nothing can name; the model keeps their regions, as fresh_allocation explains. Only the program frees, by free or by code it brought in (a signal handler, a thread), and that is not the runtime's action. The model needs it so that an object live before a poll is live after it with the same bounds. Native test: axiom − collector − live.c.
At a safepoint, as at a free, an object's bounds change only by a live heap object's being freed, and no identity appears (lifetime_step); at a safepoint none changes at all (collector_live).
Explore proof 3 tactics
Initial goal
intros s m s′ m′ H b reg′ E
rewrite collector_lives m s′ m′ H in E
eauto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
At a safepoint no byte moves or changes: the collector writes no object's contents and no address word. The model needs it so that the bytes a check passed stay readable, unchanged, until the program next looks at the header. Native test: axiom − nonmoving.c.
At a safepoint each stored capability (in the aux) stays, or, if its object was freed, becomes one that traps as a freed object does: the collector's free singleton. The model needs it so that a pointer loaded back after a collection behaves as the one stored, reading or trapping alike. Native test: axiom − aux.c.
At a safepoint the collector reclaims no identity the program can name: every object of m is still one of m'. The model needs it because a capability never dies in the model, so the object it names must stay. It follows from collector_live. Native test: axiom − rooting.c.
Explore proof 3 tactics
Initial goal
intros s m s′ m′ H b
rewrite collector_lives m s′ m′ H
auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
The runtime's actions at a safepoint keep it ready. With fresh_allocation and ready_kept this makes ready hold in every state of a run (Machine.v). No native test: it is about the runtime's own bookkeeping.
Facts about Memory.v
Removing the object at base from the regions rs leaves every other base b's object as it was.
Explore proof 9 tactics
Initial goal
intros b base rs Hne
induction rs as | r rs IH ; simpl ; auto
destruct Z.eqbr_baser base eqn: E
2 openfocused goal
–apply Z.eqb_eq in E
2 opendeferred goal
destruct Z.eqbr_baser b eqn: E2 ; auto
2 opendeferred goal
apply Z.eqb_eq in E2
2 opendeferred goal
lia
deferred goal
–simpl
destruct Z.eqbr_baser b ; auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
If replacing the object at base by the region r, whose base is base, gives rs', then the object at base in rs' is r and every other object is as in rs.
Explore proof 14 tactics
Initial goal
unfold replace_region
intros base r rs rs′ b H Hr
destruct object_regionbase rs ; inversion H ; subst ; clear H
simpl
destruct Z.eqbb r_baser eqn: E
2 openfocused goal
–apply Z.eqb_eq in E
2 opendeferred goal
subst
2 opendeferred goal
rewrite Z.eqb_refl
2 opendeferred goal
reflexivity
deferred goal
–assert b ≠ r_baser by apply Z.eqb_neq ; auto
destruct Z.eqbr_baser b eqn: E2
2 openfocused goal
+apply Z.eqb_eq in E2
2 opendeferred goal
lia
deferred goal
+apply object_region_remove ; auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
What Memory.v's free_res (free_res/4) may do: change nothing, or free one live heap object, emptying its bounds, and change nothing else.
Explore proof 5 tactics
Initial goal
intros x m r m′ H
destruct x as a | p | c size ; simpl in H ; try discriminate
2 openfocused goal
–inversion H ; auto
deferred goal
–unfold cap_free_res in H
destruct ptr_capp as | base | base | f | n ; try inversion H ; auto ; fail ; try discriminate ; unfold object_free_res in H ; destruct Z.eqbptr_addrp 0 ; try inversion H ; auto ; fail ; destruct object_regionbase m_regionsm as reg | eqn: Ereg ; try discriminate ; destruct kind_eqbr_kindreg Heap eqn: Ek ; simpl in H ; try inversion H ; auto ; fail ; destruct Z.eqbptr_addrp base ; simpl in H ; try inversion H ; auto ; fail ; destruct r_lifereg eqn: El ; try inversion H ; auto ; fail ; destruct replace_regionbase mkRegionbase base Heap Freed m_regionsm as rs | eqn: Er ; try discriminate ; inversion H ; subst ; right ; exists base, reg ; split ; auto | ; split ; destruct r_kindreg ; simpl in Ek ; congruence | ; simpl ; auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
free_res changes objects only as lifetime_step allows: the interpreter's proof of free_lifetime.
Explore proof 15 tactics
Initial goal
intros x m r m′ H
destruct free_res_freesx m r m′ H as → | base & reg & Hreg & Hk & Hl & Hr & _
2 open–intros b reg′ Hb
2 openexists reg′
2 openauto
–intros b reg′ Hb
rewrite object_region_replace_ _ _ _ b Hr eq_refl in Hb
destruct Z.eqbb base eqn: E
2 openfocused goal
+apply Z.eqb_eq in E
2 opendeferred goal
subst
2 opendeferred goal
inversion Hb ; subst
2 opendeferred goal
exists reg
2 opendeferred goal
auto
deferred goal
+exists reg′
auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
Every identity before a free_res is one after it: the interpreter's proof of free_keeps_ids.
Explore proof 4 tactics
Initial goal
intros x m r m′ H b Hb
destruct free_res_freesx m r m′ H as → | base & reg & _ & _ & _ & Hr & _ ; exact Hb |
rewrite object_region_replace_ _ _ _ b Hr eq_refl
destruct Z.eqbb base ; discriminate | exact Hb
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
free_res (free_res/4) answers ok or a trap, never unchecked: the interpreter's proof of free_checked.
Explore proof 4 tactics
Initial goal
intros x m w m′ H
unfold free_res , cap_free_res , object_free_res in H
repeat match type of H withcontext [match with _ ⇒ _ end]⇒destruct y ; try discriminate Hend
No remaining goals.
all: injection H as H ; discriminate H
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
Explore proof 2 tactics
Initial goal
intros x m r m′ H
destruct free_res_freesx m r m′ H as → | _ & _ & _ & _ & _ & _ & Hb & Ha ; auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
free_res frees null and a live heap object's base as frees_heap says: the interpreter's proof of free_succeeds.
Explore proof 14 tactics
Initial goal
split ; reflexivity |
intros m b reg Ereg Hlive Hk Hb
unfold replace_region
rewrite Ereg
eexists
2 opensplit ; reflexivity |
cbn
unfold object_free_res
rewrite proj2Z.eqb_neqb 0 Hb
rewrite Ereg , Hk , Z.eqb_refl , Hlive
cbn
unfold replace_region
rewrite Ereg
reflexivity
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
The interpreter's runtime
a, rounded up to a multiple of a positive align, is at least a.
Explore proof 5 tactics
Initial goal
intros a align H
unfold align_up
pose proof Z.div_moda + align − 1 align ltac:lia
pose proof Z.mod_pos_bounda + align − 1 align H
nia
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
a, rounded up to a multiple of a positive align, is a multiple of align.
Explore proof 4 tactics
Initial goal
intros a align H
unfold align_up
apply Z.mod_mul
lia
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
The interpreter's runtime: interp.pl's allocator (alloc_base/6), free as Memory.v states it (free_res), and no other thread or collector. It proves every axiom, so the module type is consistent; the extracted model is DriverInterpRuntime (Transport.v). It is left unsealed, so that what it does can be read off it; Transport.v's DriverInterpRuntime and Persistence.v's PersistenceInterpRuntime check it against Runtime.
interp.pl's Next.
As stack_base/1: allocations start at 2^32, above the data.
As address_space/2: Next stays at or below 2^48, libpas's address space (PAS_ADDRESS_BITS).
The interpreter, its Next being next, is ready in m: every object lies between 0 and Next, empty if freed, Next is within the address space, and nothing at or above Next has been written.
As alloc_base/6: the base is Next rounded up to the request's alignment; Next then moves past the object, rounded up to 16, and a 16-byte gap. Where that would pass the address space's end, the runtime is out of memory.
The interpreter's allocator gives a fresh base and stays ready: everything at or above Next is unwritten and owned by no object, and the new object lies there.
Proof 42 steps Qed
Objects lie between 0 and Next, which is within 2^48.
Explore proof 4 tactics
Initial goal
intros next m Hpos & Hobj & _ b reg E
destruct Hobj_ _ E as ? & ? & _
unfold address_end in *
lia
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
A freed object of a memory the interpreter is ready in is empty, as ready says.
Explore proof 2 tactics
Initial goal
intros next m _ & Hobj & _ b reg E
exact proj2proj2Hobj_ _ E
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
Objects stay below Next as they end, and the program writes only inside them.
Explore proof 19 tactics
Initial goal
intros next m m′ Hpos & Hobj & Hbytes Hreg & Hout
split ; lia |
split
2 openfocused goal
–intros b reg′ E
2 opendeferred goal
destruct Hreg_ _ E as reg & E0 & Hr
2 opendeferred goal
destruct Hobj_ _ E0 as ? & ? & Hf
2 opendeferred goal
destruct Hr as → | _ & → ; simpl
3 openfocused goal
deferred goal
+split ; lia |
3 opendeferred goal
deferred goal
split ; lia | exact Hf
2 opendeferred goal
deferred goal
+split ; lia |
2 opendeferred goal
split ; lia | reflexivity
deferred goal
–intros a Ha
assert Hn : ¬in_objectm a
2 openfocused goal
{intros b & reg & E & Hr
2 opendeferred goal
destruct Hobj_ _ E
2 opendeferred goal
lia
deferred goal
destruct Houta Hn as → →
apply Hbytes
auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
Memory.v's free, the pinned runtime's as the interpreter has it.
Free empties only a live heap object's bounds: free_res_lifetime.
Free changes no byte and no stored capability: free_res_retention.
Free reclaims no identity: free_res_keeps_ids.
Free never answers unchecked: free_res_checked.
Free succeeds on null and a live heap base: free_res_succeeds.
The interpreter's runtime may act wherever the compiler does not promise that it cannot.
A safepoint of the interpreter's runtime is not evidence preserving, by its definition.
Explore proof 3 tactics
Initial goal
unfold at_safepoint
intros i H
destruct evidence_preservingi ; auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
No other thread and no collector: nothing changes at a safepoint.
Nothing changes at a safepoint, so no region does.
Explore proof 2 tactics
Nothing changes at a safepoint, so no object does.
Explore proof 3 tactics
Initial goal
intros s m s′ m′ _ → b reg′ H
exists reg′
auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
Nothing changes at a safepoint, so no byte does.
Explore proof 2 tactics
Nothing changes at a safepoint, so every stored capability stays.
Explore proof 2 tactics
Initial goal
intros s m s′ m′ _ → a
auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
Nothing changes at a safepoint, so no identity is reclaimed.
Explore proof 2 tactics
Initial goal
intros s m s′ m′ _ →
auto
No remaining goals.
Qed
✓ Proof checked and closed by Rocq.
Source script
Nothing changes at a safepoint, so the runtime stays ready.