pub struct Heap { /* private fields */ }Expand description
A precise, non-moving GC heap (§12.1, ADR-011).
#[repr(C)] so the RuntimeContext.heap pointer offset is stable
(Appendix B). Every mutable field is a Cell: the collector runs through a
&Heap that the descriptor trace callbacks reborrow, so a RefCell would
only buy a double-borrow panic that a scalar and a raw pointer cannot need —
and would charge a borrow-flag round trip on the hottest path in the runtime.
Implementations§
Source§impl Heap
impl Heap
Sourcepub const BYTES_SINCE_COLLECT_OFFSET: usize
pub const BYTES_SINCE_COLLECT_OFFSET: usize
Where Heap::bytes_since_collect and Heap::collect_threshold sit
within a Heap, for the one caller outside this crate that needs
them: the Cranelift backend, which loads both and compares them inline
(ADR-113).
Exported from here, with offset_of!, for
GcHeader::DESCRIPTOR_OFFSET’s
reason — the fields are private and this struct is their one layout
authority, so the alternative is a number written out in the backend that
nothing keeps true. the_pacing_predicate_is_one_unsigned_compare_of_the_two_exported_words
reads a live Heap through exactly these two displacements and asserts
the words it finds are the ones Heap::collection_is_due compares, so
the offsets and the predicate cannot drift apart.
This pair is the whole export surface, and its narrowness is
deliberate. A pacer whose predicate needed a third term would have
nothing to hand the backend, which is the point at which whoever writes
it has to read Heap::collection_is_due’s doc.
Sourcepub const COLLECT_THRESHOLD_OFFSET: usize
pub const COLLECT_THRESHOLD_OFFSET: usize
Sourcepub fn new() -> Self
pub fn new() -> Self
A fresh, empty heap, paced by Pacer::from_env.
No page is created here: the first allocation of a class creates that class’s first page. A heap that never allocates costs nothing, which matters because the debugger mints a second one (ADR-032).
Sourcepub fn with_pacer(pacer: Pacer) -> Self
pub fn with_pacer(pacer: Pacer) -> Self
A fresh, empty heap paced by an explicit Pacer.
The door every pacing test goes through, so no test’s result depends on
the ambient environment — including the debug-profile pass ADR-112’s
Consequences requires, which runs the whole suite with
PRAXIS_GC_PACER set.
Sourcepub fn committed_bytes(&self) -> usize
pub fn committed_bytes(&self) -> usize
Bytes of address space this heap’s pages occupy.
The number RT-01 is about: a program that allocates and collects a bounded working set in a loop must not grow it.
Sourcepub fn page_count(&self) -> usize
pub fn page_count(&self) -> usize
How many pages this heap holds, live or pooled.
Sourcepub fn owns(&self, value: GcRef) -> bool
pub fn owns(&self, value: GcRef) -> bool
Whether value was allocated by this heap and has not been swept.
O(1): it reads the owning id out of the header, which is the same test the collector applies to every root, and the same one that licenses masking an address to find its page.
Sourcepub fn charge_owned_growth(&self, bytes: usize)
pub fn charge_owned_growth(&self, bytes: usize)
Charge bytes of owned growth — a collection’s backing buffer
reallocating — against the pacing counter.
§Why the spine is charged and not only the object
Heap::alloc_raw charges stride + owned_bytes_of(payload) once, at
construction. Growth after that point — a push that reallocates — has
to be charged too, or a program whose memory is mostly buffers barely
advances the counter at all: with scalars promoted out of the heap
(ADR-121), the arithmetic feeding a push is not itself a paced
allocation pacing the collector on the spine’s behalf. Measured on bfs,
whose adjacency lists are a Vec of Vecs: 41 collections with the
spine charged against 6 without, and a peak resident set of 61 MiB
against 224, for an identical live set. So the pacer’s input is the
memory the program actually took rather than the share of it that
happened to be shaped like an object.
Cheap by construction: callers invoke this only on the reallocation path,
which amortized doubling already makes rare, and it is a load, an add and
a store. It deliberately does not collect — the caller decides where
its safepoint is, and every one of them already polls
Heap::maybe_collect on entry.
Sourcepub fn pace(&self, roots: &RuntimeRoots<'_>) -> Safepoint<'_>
pub fn pace(&self, roots: &RuntimeRoots<'_>) -> Safepoint<'_>
Give the collector a chance to run, and hand back the Safepoint that
permits one allocation.
This is the only producer of a Safepoint, and it is what makes
“allocate without pacing” unwritable on the paced path: the token
Heap::alloc demands cannot be obtained except by performing the
Heap::maybe_collect that mints it, against the whole
RuntimeRoots.
Sourcepub fn alloc<T: Copy>(
&self,
_safepoint: Safepoint<'_>,
payload: Payload<T>,
value: T,
) -> GcRef
pub fn alloc<T: Copy>( &self, _safepoint: Safepoint<'_>, payload: Payload<T>, value: T, ) -> GcRef
Allocate an object with the given descriptor and a Copy payload value,
returning a reference to it.
Takes the Safepoint minted by Heap::pace: an allocation on this
path has necessarily given the collector its chance. For payloads that
own Rust resources (Box<str>, VecPayload) use Heap::alloc_with,
which writes the value via ptr::write so its Drop later runs
correctly.
The descriptor arrives as a Payload<T>, so “this value is not that
descriptor’s payload” is a type error at the call rather than an assert
here.
Sourcepub unsafe fn alloc_with(
&self,
_safepoint: Safepoint<'_>,
descriptor: &'static TypeDescriptor,
size: usize,
align: usize,
init: impl FnOnce(*mut u8),
) -> GcRef
pub unsafe fn alloc_with( &self, _safepoint: Safepoint<'_>, descriptor: &'static TypeDescriptor, size: usize, align: usize, init: impl FnOnce(*mut u8), ) -> GcRef
Allocate an object whose payload owns Rust resources, initializing it
with init. init receives a pointer to the uninitialized payload bytes
and must fully initialize them.
This path keeps its runtime layout assertions, deliberately: init is a
closure writing through a *mut u8, so there is no payload type for a
Payload<T> to carry, as there is on the Copy path. Every caller
here writes a specific non-Copy payload — a Box<str>, a VecPayload
— and passes that type’s own size_of/align_of.
Reach for Heap::alloc_payload instead unless init genuinely needs
the raw pointer: it derives both numbers and the write from the payload
type, leaving nothing for a caller to keep in agreement.
§Safety
init must initialize the payload in place and must not panic after
partial initialization (if it does, the payload’s Drop will not run,
leaking the partially-initialized resources). The descriptor’s size/
align must match the value init writes.
Sourcepub unsafe fn alloc_payload<P>(
&self,
safepoint: Safepoint<'_>,
descriptor: &'static TypeDescriptor,
payload: P,
) -> GcRef
pub unsafe fn alloc_payload<P>( &self, safepoint: Safepoint<'_>, descriptor: &'static TypeDescriptor, payload: P, ) -> GcRef
Heap::alloc_with for a payload the caller can hand over by value:
the size, the alignment and the write are all derived from P.
This is the shape a non-Copy allocation wants. alloc_with takes the
layout as two loose numbers and the write as a closure over a *mut u8,
so every caller names its payload type three times and nothing but the
assertions in Heap::alloc_with_unpaced holds the three together. Here
it is named once and the compiler derives the rest — what Payload<T>
does for the Copy path, carried as far as a payload that owns Rust
resources can carry it.
§Safety
descriptor must be P’s own descriptor. A mismatched layout is
caught by Heap::alloc_with_unpaced’s assertions; a same-layout
mismatch is not, and the descriptor’s drop_value, trace and format
callbacks are dispatched against these bytes.
Sourcepub fn collect(&self, roots: &RuntimeRoots<'_>)
pub fn collect(&self, roots: &RuntimeRoots<'_>)
Run a mark-and-sweep collection (§12.1, ADR-011).
Every GcRef reachable from roots (plus everything transitively
reachable through descriptor trace callbacks) is marked black and
survives; everything else is finalized via drop_value and reclaimed.
roots is passed twice, as both the strong and the weak set, because a
RuntimeRoots is both: five arms say what must survive and a sixth
says what must be told when something did not (ADR-106). The sealed set
being the source of both is what keeps them from disagreeing about which
collection they belong to.
Sourcepub fn maybe_collect(&self, roots: &RuntimeRoots<'_>) -> bool
pub fn maybe_collect(&self, roots: &RuntimeRoots<'_>) -> bool
Run a collection if allocation pressure has reached the threshold,
rooting from roots. Reached from every allocation through
Heap::pace (§12.4), so collection happens automatically inside JIT’d
code — this is what makes “nested vectors survive collection” (§19)
testable without the host forcing it.
Returns true if a collection ran.
Sourcepub fn collection_is_due(&self) -> bool
pub fn collection_is_due(&self) -> bool
Has allocation pressure reached the threshold? — the one statement of the pacing predicate, and the second-most-copied line in the runtime (ADR-113).
Heap::maybe_collect and [Heap::maybe_collect_with] are its two
callers here. Its third reader is not in this crate and cannot call it:
the Cranelift backend reproduces this expression inline (ADR-113)
in every Inst::Materialize { Int } it emits — two loads at
Heap::BYTES_SINCE_COLLECT_OFFSET and
Heap::COLLECT_THRESHOLD_OFFSET, an unsigned compare, and a branch to
a cold block that calls praxis_alloc_int — so that the overwhelmingly
common case (a loop counter inside crate::small_int’s range, on a
heap that is nowhere near its threshold) is a table read rather than a
guarded call into this module.
§The obligation, and what a third term would cost
ADR-040’s Safepoint exists so that “allocate on the paced path
without pacing” has no spelling. Generated code does not breach it,
because it takes that path only where this function answers false,
which is exactly the branch on which maybe_collect returns without
doing anything. That is the entire argument, and it holds only while this
expression is what the backend emits.
The branch this guards also allocates (ADR-119): generated code
claims a block and writes its header on the far side of it. What makes
that sound is that between this branch and the last store there is no
call, so not due here is not due throughout. See Safepoint, which
states all three parts.
So: a term added here must be added to emit_pacing_test in
crates/praxis-codegen-cranelift/src/lower.rs, or generated code
allocates on a branch where the collector was due. That is the one
place the backend transcribes this expression — emit_inline_intern and
emit_inline_claim_box both call it — and PacingOffsets is the one
value it reads the displacements off. The failure mode is not a wrong
answer — it is a collection that silently does not happen, which looks
like a memory leak in a program the reader will not connect to a pacer
change. Two things fire when it is forgotten:
the_pacing_predicate_is_one_unsigned_compare_of_the_two_exported_words
below, which compares this function’s answer against the two words the
backend loads, and the deliberately narrow export surface — a third term
has no offset constant to be baked from, so writing one is a decision
rather than an oversight.
It is pub because it is part of that contract, not an implementation
detail: what the backend inlines should be nameable, and a host that
wants to know whether the next allocation will collect should ask this
rather than reconstruct it.
Sourcepub fn reset(&mut self)
pub fn reset(&mut self)
Reset the heap to empty, dropping everything. Used by tests. Immortal
singletons must be re-allocated afterwards — the whole Immortals value,
not just the three singletons, because of the small-Int table
(crate::small_int): a RuntimeContext minted before the reset holds
unit_ref, true_ref, false_ref and a small_ints pointer, and
every one of them names storage the heap is now free to hand out again.
Trait Implementations§
Source§impl Drop for Heap
impl Drop for Heap
Source§fn drop(&mut self)
fn drop(&mut self)
Finalize whatever the program left live (RT-02), then release the pages.
Releasing the pages reclaims the [header|payload] blocks, and nothing
else: the Box<str> behind a Text, the Vec<GcRef> behind a Vec[T],
the HashMap behind a Map[K,V] are ordinary Rust allocations no page
ever owned. Without the finalize, every object still reachable at
teardown would leak its backing store.
A GcRef does not outlive the heap. Finalizing here makes reading
one afterwards a visible use-after-free rather than a quiet read of
stale-but-intact bytes. The two consumers that take a value out of the
runtime keep to that:
praxis-cli/src/run.rstakes the crash snapshot, renders it, then moves theRuntimeinto theDebugSessiontheReplowns.Repldeclaressnapshotbeforesession, so the snapshot is dropped first and nothing reads aGcRefafter teardown.praxis-debugger/src/repl.rsreplaces its snapshot after arestart/reloadwhile the runtime is still alive.
CrashSnapshot and ParseDetail hold GcRefs but have no Drop that
dereferences one, so field order within Runtime — where heap is
declared first and therefore dropped first — is safe either way. No
descriptor’s drop_value dereferences a GcRef either, so finalization
order among live objects does not matter.