#[repr(C)]pub struct RuntimeContext {Show 18 fields
pub heap: *mut Heap,
pub pending_fault: *mut Fault,
pub debug_frames: *mut DebugFrameStackHeader,
pub shadow: *mut ShadowStackHeader,
pub input_source: GcRef,
pub unit_ref: GcRef,
pub current_generation: u64,
pub stack_left: u32,
pub parse_detail: *mut ParseDetail,
pub crash_snapshot: *mut SnapshotSlot,
pub native_roots: *mut NativeRootStore,
pub true_ref: GcRef,
pub false_ref: GcRef,
pub fault_message: *mut FaultMessage,
pub small_ints: *const GcRef,
pub debug_values: *mut DebugValueStackHeader,
pub small_chars: *const GcRef,
pub descriptors: [*const TypeDescriptor; 22],
}Expand description
The hidden first argument to every generated function.
Matches the sketch in Appendix B. Fields are raw pointers because generated Cranelift code reads them at a fixed offset with a fixed calling convention; Rust borrows would not survive across the ABI boundary.
Fields§
§heap: *mut Heap§pending_fault: *mut FaultThe runtime’s one fault slot. Non-null in every context generated code
is ever handed — Runtime::context is its only producer and wires
it to the runtime’s own Fault; the sole null-wiring constructor,
RuntimeContext::placeholder, is unsafe and test-only.
That invariant is load-bearing (ADR-102): an Inst::CheckFault is a
load of this pointer and a load of the Fault::KIND_OFFSET word
behind it, with no null test. A host that hand-built a context with a
null here and called generated code would fault the process rather than
silently never observing a Praxis fault. a_wired_context_has_a_fault_slot
is the gate.
debug_frames: *mut DebugFrameStackHeaderThe header of the runtime’s one crash-debugger frame stack (§9.3,
ADR-021, ADR-104). Generated code claims one DebugFrameEntry in the
prologue and restores the top in the epilogue;
crate::crash_snapshot::praxis_snapshot_debug_chain reads [base, top)
innermost-first to build the frames the crash REPL renders.
§11.6’s discipline in this struct is append at the end, never reorder:
a field generated code reads that comes to point at something else keeps
its position and bumps
RUNTIME_ABI_VERSION, because
deleting it and appending a replacement would shift every field below.
shadow: *mut ShadowStackHeaderThe header of the runtime’s one compiler-managed shadow stack (§12.3,
ADR-019, ADR-101). Generated code claims a run of slots in the prologue
by bumping the header’s top, spills live GcRefs into that run at
safepoints, and restores top in the epilogue. The collector scans
[base, top) via [RootSet].
input_source: GcRef§unit_ref: GcRefThe cached immortal Unit — the “defined dummy” returned on fault paths
(§10.4). Separate from input_source (which holds the read-in buffer
when present), so fault sentinels are stable regardless of input.
current_generation: u64§stack_left: u32How much of the native-stack budget the live Praxis frames have not
yet spent, in bytes (§9.2, §17.4, ADR-105). A prologue subtracts its own
frame_cost; its epilogue stores back the value it found. A prologue
guard faults with FaultKind::StackOverflow when what is left will not
cover this frame, so deep recursion faults cleanly instead of overflowing
the native stack and aborting the host (SIGABRT). Read by generated
Cranelift code at a fixed offset, like the other #[repr(C)] fields
above.
It counts down, and the direction is the design. Counting up needs
the limit in generated code, which fixes it at compile time for every
host. Counting down puts the limit in this field, so Runtime::context
— the one producer every caller of generated code goes through — is the
single place a stack size enters the system, and the backend never learns
it. It also makes zero mean exhausted, which is the right thing for
RuntimeContext::placeholder to say.
parse_detail: *mut ParseDetailHost-managed pointer to the runtime’s crate::ParseDetail slot
(§7.11). The parser interpreter writes the richest parse mismatch into
it on ParseFailed; the host (CLI / crash debugger) reads it after the
fault. Generated code never touches this field — it is appended at the
end of RuntimeContext so the offsets of all generated-code-read fields
above are unchanged (§11.6 ABI stability).
crash_snapshot: *mut SnapshotSlotHost-managed pointer to the runtime’s crate::SnapshotSlot (§9.3).
The first fault epilogue deep-copies the debug-frame chain into it
before unwinding; the host reads the snapshot after the fault. Like
parse_detail, generated code only passes it to
praxis_snapshot_debug_chain — it is appended at the end of
RuntimeContext for ABI stability.
native_roots: *mut NativeRootStoreThe runtime’s one native root store (ADR-114): what the runtime’s own Rust code holds live across an allocation, in one contiguous array.
Claimed and released by crate::roots::NativeScope, never by generated
code — which is why it, like parse_detail and crash_snapshot, is
appended at the end of the struct. It is the fifth arm of
crate::roots::RuntimeRoots, which scans [0, len).
It has no reader outside praxis-runtime at all, so changing what it
points at is not the ABI-version event it would be for shadow or
debug_frames, which every generated prologue bump-allocates from. See
ADR-114.
true_ref: GcRefThe cached immortal true, alongside Self::unit_ref (§4.3). There
are exactly two Bool values; the runtime allocates them once, so no
comparison in a loop consumes arena storage.
false_ref: GcRefThe cached immortal false. See Self::true_ref.
fault_message: *mut FaultMessageHost-managed pointer to the runtime’s FaultMessage slot (§9.1).
praxis_panic and praxis_assert write the message the program gave;
the host reads it when it renders the fault. Like parse_detail and
crash_snapshot, generated code never touches it — it is appended at
the end of the struct so every generated-code-read offset above is
unchanged (§11.6 ABI stability).
small_ints: *const GcRefThe base of the interned small-Int table (Immortals::small_ints),
alongside Self::true_ref and Self::unit_ref (§4.3,
crate::small_int).
Unlike those, this is a pointer to the objects rather than one of
them: there are crate::SMALL_INT_COUNT of them, so generated code
takes two loads — the base from here, then the element at a byte offset
it computed at compile time from the literal’s value. That is what
Inst::ConstGc emits, and it is why an in-range Int literal in a loop
body is not a call, an allocation and a shadow-frame spill per iteration
(docs/handovers/21-where-the-time-goes.md §3.5).
Generated code does read this one, so it would be a compatibility
break if it moved — but it is appended like fault_message and its
neighbours, so every offset above is unchanged.
debug_values: *mut DebugValueStackHeaderThe header of the runtime’s one crash-debugger value stack (§9.3,
ADR-104). Generated code claims one slot per Gc local in the prologue,
stores each local’s value there at the instruction that defines it, and
restores the top in the epilogue. Each DebugFrameEntry in
debug_frames names the base of its own call’s run.
The collector never traces this — it is the weak arm of
crate::roots::RuntimeRoots (ADR-106), not a strong one. The slot type
is Option<GcRef> rather than the shadow stack’s *mut GcHeader
precisely so that impl RootSet for SlotStackHeader<*mut GcHeader>
cannot reach it: the debug set is over-approximate and never cleared
(MIR-16), and rooting it would undo MIR-01’s clears.
It is scanned, once per collection, immediately after the sweep: every
slot naming storage that sweep just reclaimed becomes None. That is
what makes a debug value always a live object or an absence, and never a
reference to a block the allocator has since reissued as something else
— which would render as a well-formed value of another type under the
dead local’s own name, sharper than a dangling read.
Appended after small_ints, so every offset above is unchanged.
small_chars: *const GcRefThe base of the interned ASCII-Char table (Immortals::small_chars),
alongside small_ints (§4.3, crate::small_char, ADR-107).
Generated code never reads this one, which is what separates it from
small_ints: the language has no character literal, so there is no
GcConst::Char and nothing lowers to a load of this base. Its readers are
the runtime’s own — abi.rs’s char_ref and the parser interpreter’s
Rt::alloc_char, both of which reach the runtime only through a
*mut RuntimeContext. It is therefore the native_roots/fault_message
class of field, and it is appended at the end of the struct for their
reason: every generated-code-read offset above stays where it was
(§11.6 ABI stability).
descriptors: [*const TypeDescriptor; 22]Every built-in descriptor’s address, indexed by BuiltinTypeId
(ADR-116). ADR-102’s inline type proof loads one slot of this and
compares the object header’s descriptor word against it.
By value, and that is the decision. A pointer to
crate::descriptor::BUILTINS would make the proof two dependent
loads; the array makes it one load at a displacement the backend folds
from RuntimeContext::descriptor_offset. Baking the address in as an
iconst instead would be no load at all, but on aarch64 it costs
movz+movk+movk (a static in this binary lives above 2³²) and it
would make the compiler name a descriptor address, which it otherwise
never does (docs/handovers/25-two-mallocs-per-runtime-call.md §3 F-4).
Filled by crate::descriptor::builtin_descriptor_addresses, which
derives it from BUILTINS — so “slot i holds the descriptor whose id
is i” has no second place it could be written wrong, and
builtins_are_indexed_by_their_id stays the one gate on it.
Appended after small_chars, so every offset above is unchanged
(§11.6 ABI stability). Generated code does read this one.
Implementations§
Source§impl RuntimeContext
impl RuntimeContext
Sourcepub const fn descriptor_offset(id: BuiltinTypeId) -> usize
pub const fn descriptor_offset(id: BuiltinTypeId) -> usize
The byte displacement of id’s slot in Self::descriptors, from the
base of a RuntimeContext.
The one authority for the address ADR-102’s proof compares against,
and the reason the backend holds no descriptor address at all: it folds
this displacement, loads whatever the runtime put there, and compares.
Which descriptor that is is the runtime’s answer rather than a pointer
the compiler carried across the ABI — so the two cannot disagree about
the address Int’s descriptor has, only about which slot it is in, and
that is the enum discriminant.
Minted here rather than reached for with offset_of! from the backend
for Fault::KIND_OFFSET’s reason one step further on: the element
stride is part of the answer, and a backend that multiplied by its own
size_of::<*const _>() would be a second statement of this layout.
Sourcepub unsafe fn placeholder(input_source: GcRef) -> RuntimeContext
pub unsafe fn placeholder(input_source: GcRef) -> RuntimeContext
Construct a context with all pointers null and the input source set to
the canonical placeholder. Real runtime setup (rooting the heap,
installing a fault sink) is done via Runtime::context.
Generated code must never be run against a placeholder. The prologue
is inline (ADR-101): it dereferences shadow unconditionally and without
a null check, because the check cost every call in the language and
Runtime::context is the only producer of a context generated code is
ever handed.
§Safety
input_source must be a valid GcRef (or the caller must ensure no
generated code dereferences it before the runtime is fully initialized).
Sourcepub fn has_pending_fault(&self) -> bool
pub fn has_pending_fault(&self) -> bool
True iff a fault is currently pending on this context. Generated code checks this at safepoints after potentially-faulting operations (§10.4).
pending_fault is non-null once the context is wired to a runtime; a
fault is pending when the pointed-at Fault slot says so.