Skip to main content

Heap

Struct Heap 

Source
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

Source

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.

Source

pub const COLLECT_THRESHOLD_OFFSET: usize

Source

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).

Source

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.

Source

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.

Source

pub fn page_count(&self) -> usize

How many pages this heap holds, live or pooled.

Source

pub fn id(&self) -> HeapId

This heap’s identity. Every header it allocates carries it.

Source

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.

Source

pub fn stats(&self) -> HeapStats

Current allocation count, and what the last sweep measured.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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 Default for Heap

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Drop for Heap

Source§

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.rs takes the crash snapshot, renders it, then moves the Runtime into the DebugSession the Repl owns. Repl declares snapshot before session, so the snapshot is dropped first and nothing reads a GcRef after teardown.
  • praxis-debugger/src/repl.rs replaces its snapshot after a restart/reload while 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.

Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl Send for Heap

Auto Trait Implementations§

§

impl !Freeze for Heap

§

impl !RefUnwindSafe for Heap

§

impl !Sync for Heap

§

impl !UnwindSafe for Heap

§

impl Unpin for Heap

§

impl UnsafeUnpin for Heap

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.