praxis_runtime/context.rs
1//! The [`RuntimeContext`] handed to every generated function (§10.3, Appendix B)
2//! and the [`Runtime`] that owns the heap + immortals.
3//!
4//! Every generated function receives a hidden first parameter — a pointer to
5//! `RuntimeContext` — followed only by `GcRef` arguments, and returns one
6//! `GcRef`. The context is the single channel through which generated code
7//! reaches the GC heap, the pending fault, the debug frame chain, the input
8//! source, and so on.
9
10use crate::crash_snapshot::{CrashSnapshot, SnapshotSlot};
11use crate::debug::{
12 DEBUG_FRAME_STACK_SLOTS, DEBUG_VALUE_STACK_SLOTS, DebugFrameEntry, DebugFrameStack,
13 DebugFrameStackHeader, DebugValueStack, DebugValueStackHeader,
14};
15use crate::gc::GcRef;
16use crate::heap::Heap;
17use crate::immortal::{Immortals, read_bool};
18use crate::parse_detail::ParseDetail;
19#[cfg(test)]
20use crate::roots::RootSet;
21use crate::shadow_stack::{SHADOW_STACK_SLOTS, ShadowStack, ShadowStackHeader};
22use crate::{
23 collections::VecPayload,
24 descriptor::{BuiltinTypeId, TypeDescriptor, builtin_descriptor_addresses},
25};
26
27/// What *any* call spends, however narrow: the floor of [`frame_cost`], in
28/// bytes (ADR-105).
29///
30/// Measured, not assumed, and measured on **both** targets this backend
31/// supports — which is the whole of why it is 160 and not the 134 ADR-105
32/// landed.
33///
34/// - **arm64.** Bisecting the abort depth of recursive Praxis programs under
35/// `ulimit -s` (release) gives a native frame of `99 + 1.06 × gc_locals`
36/// bytes: 86 B for a minimal frame, 294 B for one carrying twenty-two live
37/// collections. 134 was that fit at [`REFERENCE_FRAME_SLOTS`], rounded up.
38/// - **x86_64.** Read straight off Cranelift's own `frame_layout` by
39/// `audit_frame_cost` over every function the test suite and the book compile
40/// — 2669 frames, which is a census rather than a fit. Narrow frames are
41/// *wider* here than arm64's: the x64 backend has half the registers to
42/// allocate out of and spills accordingly, so a function of eleven `Gc` locals
43/// or fewer reaches 128 B and one of twenty-seven reaches 176 B, against a
44/// 134 B charge. That is the assert ADR-105 decision 5 exists to fire, and it
45/// fired.
46///
47/// 160 covers the worst frame in that census with a 16-byte margin — one full
48/// stack quantum on a target that aligns to 16 — so the model still over-charges
49/// every real frame and the budget below is still a ceiling rather than an
50/// estimate.
51///
52/// **One constant, not one per target, and that is deliberate.** ADR-105
53/// decision 3 refused to derive the budget from the host's `getrlimit` because
54/// it would make the same program fault at different depths on different
55/// machines, "and the depth at which a Praxis program stops recursing would stop
56/// being a property of Praxis". A per-target charge gives that away just as
57/// surely: [`STACK_BUDGET_BYTES`] scales with this constant, so a target-varying
58/// base leaves a *reference* frame at [`MAX_RECURSION_DEPTH`] everywhere but
59/// moves the depth of every wider one. The charge is therefore the high-water
60/// mark across targets, which costs arm64 only headroom it does not use.
61///
62/// **[`FRAME_BYTES_PER_SLOT`] is left alone at 2**, though raising it would have
63/// bought the same margin. The x86_64 census finds no slope worth the name — a
64/// 450-slot function frames in 192 B and a 105-slot one in 288 B, because `Gc`
65/// locals live in the shadow and debug slot stacks rather than in the native
66/// frame — so a steeper per-slot term would be an invented number that took
67/// depth from wide frames to fix a floor that was too low.
68///
69/// **It is a floor and not merely a base, and that is load-bearing.** A frame
70/// narrower than the reference is charged the same, so no call can ever cost
71/// less than this — which is what makes
72/// [`DEBUG_FRAME_STACK_SLOTS`](crate::debug::DEBUG_FRAME_STACK_SLOTS) sound at
73/// `MAX_RECURSION_DEPTH + 1`. A cost that was proportional from zero would let
74/// the budget buy more minimum-width frames than that reservation covers.
75///
76/// The backend checks the model rather than trusting it: after Cranelift has
77/// compiled a function it knows the real frame size, and a `debug_assert`
78/// there fails the build's test run if any function's actual frame outgrows
79/// what [`frame_cost`] charged for it.
80pub const FRAME_BYTES_BASE: u32 = 160;
81
82/// What each `Gc` local *past* [`REFERENCE_FRAME_SLOTS`] adds to a frame's cost,
83/// in bytes (ADR-105). Rounds up arm64's measured 1.06 B per local; x86_64
84/// measures no slope at all, and [`FRAME_BYTES_BASE`] says why that left this
85/// constant where it was.
86pub const FRAME_BYTES_PER_SLOT: u32 = 2;
87
88/// The deepest recursion a *reference-width* function reaches, and the figure
89/// [`STACK_BUDGET_BYTES`] is derived from.
90///
91/// What runs out is bytes, not calls, and a frame's byte cost varies by a
92/// factor of three with its width — so the guard spends a byte budget rather
93/// than counting calls, because a count calibrated for a narrow frame lets a
94/// wide one abort the host (ADR-105). This constant is the *anchor* of that
95/// budget: a reference frame recurses exactly this deep.
96pub const MAX_RECURSION_DEPTH: u32 = 8000;
97
98/// The frame width [`MAX_RECURSION_DEPTH`] is calibrated against: the `Gc`
99/// local count of
100///
101/// ```praxis
102/// fn count(n: Int) -> Int { if n == 0 { 0 } else { 1 + count(n - 1) } }
103/// ```
104///
105/// — the program that constant was chosen for, and the one
106/// `adv_deep_recursion_over_limit_faults_cleanly` uses.
107///
108/// **Anchoring the budget here rather than at zero is deliberate.** A zero-slot
109/// function is a hypothetical: every real Praxis function boxes something, and
110/// the simplest recursive one there is takes eleven `Gc` locals. Deriving the
111/// budget from `frame_cost(0)` would let *that* function recurse only 6686 deep
112/// rather than 8000 — a 16% cut to every ordinary program, to bound a cost only
113/// wide frames incur. Anchoring at the reference frame takes depth only from
114/// the frames that over-reach.
115///
116/// `a_reference_frame_still_recurses_as_deep_as_the_call_count_allowed` is the
117/// end-to-end gate; if a codegen change makes `count` wider, that test fails and
118/// this constant is what to re-measure.
119pub const REFERENCE_FRAME_SLOTS: u32 = 11;
120
121/// The native stack, in bytes, that Praxis frames may occupy — and the largest
122/// budget a host may install (ADR-105).
123///
124/// **Why this number and not the real stack limit.** The two stacks Praxis
125/// actually runs on are 8 MiB (a macOS main thread, where `praxis run` calls the
126/// JIT entry) and 2 MiB (std's default for a spawned thread, which is what the
127/// whole `cargo test` suite runs on). `getrlimit` answers for the first and not
128/// the second, so asking the OS gives a number that is wrong exactly where the
129/// suite lives. Choosing one figure that fits under *both*, with room to spare,
130/// removes the question instead of answering it: it is what
131/// `MAX_RECURSION_DEPTH` reference frames cost — about 1.22 MiB charged — and no
132/// frame shape can exceed it, because the guard charges by shape.
133///
134/// What is *consumed* is below that by however much the model over-charges the
135/// shape doing the recursing, and the model over-charges every shape: the
136/// reference program frames in 80 bytes on x86_64 against a 160-byte charge, so
137/// it reaches 8000 deep on 625 KiB. The tightest ratio in the census behind
138/// [`FRAME_BYTES_BASE`] — 176 actual bytes against 192 charged — puts the true
139/// worst case at 1.12 MiB, which is the figure to hold against the 2 MiB thread
140/// stack rather than the charged one.
141///
142/// A host that knows better may lower it through
143/// [`Runtime::set_stack_budget`](crate::Runtime::set_stack_budget). It may not
144/// raise it: [`SHADOW_STACK_SLOTS`](crate::SHADOW_STACK_SLOTS) is sized from
145/// this constant, and a larger budget would make shadow-stack exhaustion
146/// reachable again. [`StackBudget`] is what makes that unrepresentable rather
147/// than documented.
148pub const STACK_BUDGET_BYTES: u32 = MAX_RECURSION_DEPTH * FRAME_BYTES_BASE;
149
150/// What one call spends of [`StackBudget`]: a floor, plus a per-slot term for
151/// every `Gc` local past the reference width (ADR-105).
152///
153/// The backend knows `slots` before it emits the prologue, so this folds to one
154/// immediate and the guard is four instructions.
155///
156/// The `saturating_sub` is the floor, and it does two jobs. It keeps an ordinary
157/// function at [`MAX_RECURSION_DEPTH`] — see [`REFERENCE_FRAME_SLOTS`] — and it
158/// makes [`FRAME_BYTES_BASE`] the *minimum* any call can spend, which is the
159/// premise [`DEBUG_FRAME_STACK_SLOTS`](crate::debug::DEBUG_FRAME_STACK_SLOTS)
160/// is sized on.
161///
162/// **`slots` is the count of `Gc` locals, not the count of shadow slots**
163/// (ADR-128 decision 4) — a [`DebugSlotCount`](crate::DebugSlotCount) at the one
164/// real call site, so at most [`MAX_DEBUG_VALUE_SLOTS`](crate::MAX_DEBUG_VALUE_SLOTS).
165/// Colouring makes that a different number from
166/// [`MAX_SHADOW_SLOTS`](crate::MAX_SHADOW_SLOTS), and the charge deliberately
167/// rides the larger one: `FRAME_BYTES_PER_SLOT` is not rent on a shadow slot,
168/// it is a calibrated proxy for the *native* frame, and under-reporting that is
169/// the SIGABRT ADR-105 exists to remove.
170///
171/// So the bound this must not overflow is `frame_cost(4096)` = `160 + 2 × 4085`
172/// = 8330, comfortably inside `u32` and inside [`STACK_BUDGET_BYTES`] — and the
173/// saturating arithmetic is belt-and-braces for a caller that has not proved
174/// even that.
175#[must_use]
176pub const fn frame_cost(slots: u32) -> u32 {
177 let over = slots.saturating_sub(REFERENCE_FRAME_SLOTS);
178 FRAME_BYTES_BASE.saturating_add(FRAME_BYTES_PER_SLOT.saturating_mul(over))
179}
180
181/// A native-stack budget a [`RuntimeContext`] may be minted with: a `u32` proven
182/// no larger than [`STACK_BUDGET_BYTES`] at construction.
183///
184/// The proof is the point. `SHADOW_STACK_SLOTS` is sized from
185/// `STACK_BUDGET_BYTES`, on the strength of "a frame spends at least
186/// `FRAME_BYTES_PER_SLOT` per slot it claims, so the slots of every live frame
187/// sum to at most `budget / FRAME_BYTES_PER_SLOT`". A host that could install a
188/// larger budget would make shadow-stack overflow reachable from generated
189/// code — silently, because generated code does not check the reservation. It
190/// cannot: [`StackBudget::new`] is the only constructor and it refuses.
191///
192/// Same shape as [`SlotCount`](crate::SlotCount), and for the same reason: the
193/// bound is checked once, where the value is made, and every consumer downstream
194/// may assume it.
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct StackBudget(u32);
197
198impl StackBudget {
199 /// The budget every [`Runtime`] starts with: the whole of
200 /// [`STACK_BUDGET_BYTES`].
201 pub const DEFAULT: StackBudget = StackBudget(STACK_BUDGET_BYTES);
202
203 /// `Some` iff `bytes` is a budget the shadow-stack reservation covers.
204 ///
205 /// `const` so a caller can prove a literal at compile time.
206 #[must_use]
207 pub const fn new(bytes: u32) -> Option<StackBudget> {
208 if bytes <= STACK_BUDGET_BYTES {
209 Some(StackBudget(bytes))
210 } else {
211 None
212 }
213 }
214
215 /// The budget in bytes, which is `<= STACK_BUDGET_BYTES` by construction.
216 #[must_use]
217 pub const fn get(self) -> u32 {
218 self.0
219 }
220}
221
222impl Default for StackBudget {
223 fn default() -> Self {
224 StackBudget::DEFAULT
225 }
226}
227
228/// What kind of runtime fault occurred (§9.2, §10.4). Set by the runtime
229/// wrapper that detected it; read by the host after the generated code unwinds
230/// to its fault epilogue.
231#[repr(C)]
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum FaultKind {
234 /// No fault pending. The zero state.
235 None = 0,
236 /// Integer arithmetic overflowed (§4.12).
237 IntOverflow = 1,
238 /// Division or remainder by zero (§4.12).
239 DivByZero = 2,
240 /// A collection index was out of bounds (§9.2). Raised by `Vec.get` /
241 /// indexing and similar accessors.
242 IndexOutOfBounds = 3,
243 /// An input parse mismatch (§7.11). Raised by the input-parser interpreter
244 /// when the input does not match a parser expression. The interpreter also
245 /// records the deepest mismatch in the runtime's [`crate::ParseDetail`]
246 /// slot, which the host reads to render the input/parser spans.
247 ParseFailed = 4,
248 /// An operation required a non-empty collection but found an empty one
249 /// (§9.2). Raised by `Deque.pop_front`/`pop_back`, heap `pop`/`peek`, and
250 /// similar accessors on an empty collection.
251 EmptyCollection = 5,
252 /// Recursion exhausted the native-stack budget (§9.2, §17.4). Raised by the
253 /// prologue guard in every generated function when `stack_left` is less than
254 /// this frame's [`frame_cost`], so the host survives deep recursion
255 /// (`count(100000)` and similar) instead of overflowing the native stack and
256 /// aborting (SIGABRT).
257 StackOverflow = 6,
258 /// A `Float` value could not be converted to `Int`: NaN, ±infinity, or a
259 /// finite value outside the signed 64-bit range (§4.12). `Float` arithmetic
260 /// itself never faults (per IEEE-754 it produces inf/nan); only the
261 /// narrowing `to_int` conversion does.
262 FloatToInt = 7,
263 /// A code point was not a Unicode scalar value: negative, above
264 /// `0x10FFFF`, or in the surrogate range `D800..=DFFF` (§4.3). Raised by
265 /// `praxis_alloc_char`.
266 InvalidChar = 8,
267 /// Host input that had to be `Text` was not valid UTF-8 (§4.3). Raised by
268 /// `praxis_get_input`, which is its only producer (ADR-111).
269 ///
270 /// The validation sits at the one caller holding bytes it did not author. A
271 /// `Text` *literal*'s bytes come from a Rust `String` and cannot fail, so
272 /// its `Alloc` is non-faulting, and a violated precondition in
273 /// `praxis_alloc_text` aborts rather than faulting, the way
274 /// `praxis_int_load`'s does.
275 ///
276 /// Generated code reads `FaultKind` directly since ADR-102, so renumbering
277 /// a variant is an ABI change and not a tidy-up. This one is unreachable
278 /// from `praxis run`, whose `lazy_stdin::read` validates stdin and exits 2
279 /// — it exists for an embedder that does not.
280 InvalidText = 9,
281 /// A size or extent the runtime cannot honour: a negative `Grid` width or
282 /// height, a `width * height` that overflows or exceeds
283 /// [`GridExtent::MAX_CELLS`](crate::collections::GridExtent::MAX_CELLS), or
284 /// a `BitSet` member outside [`BitIndex`](crate::bitset::BitIndex)'s range
285 /// (§9.2). Each is checked *before* the `usize` cast, where a negative
286 /// extent would otherwise land near `usize::MAX` and become an OOM abort or
287 /// a capacity-overflow panic across `extern "C"`.
288 InvalidSize = 10,
289 /// A value did not have the type its destination declared: pushing a
290 /// `Float` into a `Vec[Int]`, or constructing a `Grid[T]` whose cell type
291 /// has no default value to fill with (§9.2).
292 TypeMismatch = 11,
293 /// The program called `panic(value)` (§9.1). The value it passed is
294 /// rendered through its descriptor into the runtime's [`FaultMessage`]
295 /// slot, so the fault says *what* the program stopped for.
296 Panic = 12,
297 /// An `assert(condition)` found its condition false (§9.1). Carries no
298 /// message: `assert` takes a condition and nothing else, so any text would
299 /// only restate the kind. `panic` is the name that carries words.
300 AssertFailed = 13,
301 /// A range with no members was asked for a member: `clamp(v, low, high)`
302 /// with `low > high` (ADR-058), which names an empty inclusive range and so
303 /// has no value to clamp to.
304 ///
305 /// `praxis_range_len`'s uncountable range is deliberately *not* this kind:
306 /// it raises [`IntOverflow`](Self::IntOverflow), because `Int::MIN..Int::MAX`
307 /// is the *fullest* range there is and calling it empty would be a fault
308 /// message that lies (ADR-059, ADR-075).
309 EmptyRange = 14,
310 /// An argument this algorithm has no answer for: a negative edge weight in
311 /// the Dijkstra and A\* searches, whose settle-once-and-never-reconsider
312 /// shape makes a negative edge silently overstate the answer, and a
313 /// negative heuristic, which makes `f = g + h` decrease along a path
314 /// (ADR-060).
315 ///
316 /// The operand is well-formed and the graph is well-formed; what is absent
317 /// is a *correct answer this algorithm could produce*, which is why neither
318 /// [`InvalidSize`](Self::InvalidSize) nor
319 /// [`TypeMismatch`](Self::TypeMismatch) fits: an answer the walk cannot
320 /// compute is a fault, not a wrong number (ADR-060).
321 NoAnswer = 15,
322}
323
324/// The message a [`FaultKind::Panic`] or [`FaultKind::AssertFailed`] carries.
325///
326/// A fault kind alone cannot say what the program stopped *for*, and `panic`'s
327/// whole contract is an explicit message (§9.1). The wrapper renders the value
328/// the program passed — through its descriptor, exactly as `out` would — and
329/// leaves the text here; the host reads it when it renders the fault.
330///
331/// Rendering happens in the wrapper rather than at report time on purpose: the
332/// argument is a `GcRef` into a heap that the host tears down, so keeping the
333/// reference would make the message outlive what it points at. A `String` does
334/// not.
335///
336/// Host-managed, like [`crate::ParseDetail`]: generated code never reads or
337/// writes it.
338#[derive(Debug, Default)]
339pub struct FaultMessage {
340 text: Option<String>,
341}
342
343impl FaultMessage {
344 /// An empty slot.
345 #[must_use]
346 pub fn new() -> FaultMessage {
347 FaultMessage { text: None }
348 }
349
350 /// Record `text` as the message for the fault being raised.
351 pub fn set(&mut self, text: String) {
352 self.text = Some(text);
353 }
354
355 /// The recorded message, or `None` when the pending fault carries none.
356 #[must_use]
357 pub fn get(&self) -> Option<&str> {
358 self.text.as_deref()
359 }
360
361 /// Forget any recorded message.
362 pub fn clear(&mut self) {
363 self.text = None;
364 }
365}
366
367/// A [`FaultKind`] that is actually a fault.
368///
369/// [`Fault::set`] takes one of these, so "raise the absence of a fault" has no
370/// spelling.
371///
372/// The associated constants are the whole raisable set. There is no
373/// `RaisedFault(FaultKind::None)` to construct — [`RaisedFault::new`] is the
374/// only fallible route in, and it is for a kind that arrives as data.
375#[derive(Clone, Copy, PartialEq, Eq, Debug)]
376pub struct RaisedFault(FaultKind);
377
378impl RaisedFault {
379 /// Integer arithmetic overflowed (§4.12).
380 pub const INT_OVERFLOW: RaisedFault = RaisedFault(FaultKind::IntOverflow);
381 /// Division or remainder by zero (§4.12).
382 pub const DIV_BY_ZERO: RaisedFault = RaisedFault(FaultKind::DivByZero);
383 /// A collection index was out of bounds (§9.2).
384 pub const INDEX_OUT_OF_BOUNDS: RaisedFault = RaisedFault(FaultKind::IndexOutOfBounds);
385 /// An input parse mismatch (§7.11).
386 pub const PARSE_FAILED: RaisedFault = RaisedFault(FaultKind::ParseFailed);
387 /// An operation required a non-empty collection (§9.2).
388 pub const EMPTY_COLLECTION: RaisedFault = RaisedFault(FaultKind::EmptyCollection);
389 /// Recursion exceeded the depth limit (§9.2, §17.4).
390 pub const STACK_OVERFLOW: RaisedFault = RaisedFault(FaultKind::StackOverflow);
391 /// A `Float` had no exact `Int` (§4.12).
392 pub const FLOAT_TO_INT: RaisedFault = RaisedFault(FaultKind::FloatToInt);
393 /// A code point was not a Unicode scalar value (§4.3).
394 pub const INVALID_CHAR: RaisedFault = RaisedFault(FaultKind::InvalidChar);
395 /// A byte buffer that had to be `Text` was not valid UTF-8 (§4.3).
396 pub const INVALID_TEXT: RaisedFault = RaisedFault(FaultKind::InvalidText);
397 /// A size or extent the runtime cannot honour (§9.2).
398 pub const INVALID_SIZE: RaisedFault = RaisedFault(FaultKind::InvalidSize);
399 /// A value did not have the type its destination declared (§9.2).
400 pub const TYPE_MISMATCH: RaisedFault = RaisedFault(FaultKind::TypeMismatch);
401 /// The program called `panic(value)` (§9.1).
402 pub const PANIC: RaisedFault = RaisedFault(FaultKind::Panic);
403 /// An `assert(condition)` found its condition false (§9.1).
404 pub const ASSERT_FAILED: RaisedFault = RaisedFault(FaultKind::AssertFailed);
405 /// A range with no members was asked for a member (ADR-058).
406 pub const EMPTY_RANGE: RaisedFault = RaisedFault(FaultKind::EmptyRange);
407 /// An argument this algorithm has no answer for (ADR-060).
408 pub const NO_ANSWER: RaisedFault = RaisedFault(FaultKind::NoAnswer);
409
410 /// The raisable fault `kind` names, or `None` for [`FaultKind::None`] —
411 /// which is the *absence* of a fault and cannot be raised.
412 #[must_use]
413 pub const fn new(kind: FaultKind) -> Option<RaisedFault> {
414 match kind {
415 FaultKind::None => None,
416 raisable => Some(RaisedFault(raisable)),
417 }
418 }
419
420 /// The kind this raises.
421 #[inline]
422 #[must_use]
423 pub const fn kind(self) -> FaultKind {
424 self.0
425 }
426}
427
428impl std::fmt::Display for FaultKind {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 match self {
431 FaultKind::None => write!(f, "no fault"),
432 FaultKind::IntOverflow => write!(f, "integer overflow"),
433 FaultKind::DivByZero => write!(f, "division by zero"),
434 FaultKind::IndexOutOfBounds => write!(f, "index out of bounds"),
435 FaultKind::ParseFailed => write!(f, "input parse mismatch"),
436 FaultKind::EmptyCollection => write!(f, "empty collection"),
437 FaultKind::StackOverflow => write!(f, "stack overflow (recursion limit)"),
438 FaultKind::FloatToInt => write!(f, "float-to-int conversion out of range"),
439 FaultKind::InvalidChar => write!(f, "not a Unicode scalar value"),
440 FaultKind::InvalidText => write!(f, "invalid UTF-8 in Text"),
441 FaultKind::InvalidSize => write!(f, "size or extent out of range"),
442 FaultKind::TypeMismatch => write!(f, "value does not have the declared type"),
443 FaultKind::Panic => write!(f, "panic"),
444 FaultKind::AssertFailed => write!(f, "assertion failed"),
445 FaultKind::EmptyRange => write!(f, "empty range"),
446 FaultKind::NoAnswer => write!(f, "an argument this algorithm has no answer for"),
447 }
448 }
449}
450
451/// The fault record a [`RuntimeContext`] points at. `pending_fault` is non-null
452/// and points at the owning runtime's slot.
453///
454/// **The kind is the whole state.** There is no `pending: bool` mirroring
455/// `kind != None` beside it: one field cannot contradict itself, and generated
456/// code branches on the kind word directly (ADR-102).
457#[repr(C)]
458pub struct Fault {
459 /// The pending fault, or [`FaultKind::None`] for no fault. Private: the
460 /// only way to raise one is [`Fault::set`], which takes a [`RaisedFault`].
461 kind: FaultKind,
462}
463
464impl Fault {
465 /// Where the kind sits within the record, and how wide it is.
466 ///
467 /// **Generated code reads the kind directly (ADR-102).** An
468 /// `Inst::CheckFault` is a load of `ctx.pending_fault`, a load of the kind
469 /// at this offset, and a `brif` — which works only because
470 /// [`FaultKind::None`] is 0 and every raisable kind is not, so the loaded
471 /// word *is* [`Fault::is_pending`].
472 ///
473 /// So a repr change to `Fault` or to [`FaultKind`] is a generated-code
474 /// change and owes a
475 /// [`RUNTIME_ABI_VERSION`](crate::abi::RUNTIME_ABI_VERSION) bump. The
476 /// backend asserts `KIND_SIZE` at compile time against the width it loads,
477 /// so a `#[repr(u8)]` or a `#[repr(C)]` that grows the enum fails the build
478 /// rather than reading three bytes of something else.
479 ///
480 /// Both are minted here rather than reached for with `offset_of!` from the
481 /// backend because `kind` is private — the field is private so that
482 /// [`Fault::set`] is the only way to raise, which is what makes
483 /// "raise no fault" unspellable.
484 pub const KIND_OFFSET: usize = core::mem::offset_of!(Fault, kind);
485
486 /// The width of the kind, in bytes. See [`Fault::KIND_OFFSET`].
487 pub const KIND_SIZE: usize = core::mem::size_of::<FaultKind>();
488
489 /// A fresh, clear fault record (no fault pending).
490 pub fn clear() -> Self {
491 Fault {
492 kind: FaultKind::None,
493 }
494 }
495
496 /// Raise `fault`.
497 ///
498 /// Takes a [`RaisedFault`] rather than a `FaultKind` so that "raise no
499 /// fault" has no spelling.
500 pub fn set(&mut self, fault: RaisedFault) {
501 self.kind = fault.kind();
502 }
503
504 /// The pending fault kind, or [`FaultKind::None`].
505 #[inline]
506 #[must_use]
507 pub fn kind(&self) -> FaultKind {
508 self.kind
509 }
510
511 /// True iff a fault is pending.
512 pub fn is_pending(&self) -> bool {
513 self.kind != FaultKind::None
514 }
515}
516
517impl Default for Fault {
518 fn default() -> Self {
519 Self::clear()
520 }
521}
522
523/// One local variable in a debug frame snapshot (§9.3).
524///
525/// Carries the source name, the compiler-assigned `symbol_id` (which
526/// disambiguates shadowed bindings — two `var a` in the same scope get distinct
527/// ids, §4.2), the local's type descriptor, and the current `GcRef` value. The
528/// prologue and epilogue push and pop the frames, the spill updates the values,
529/// and the crash debugger reads them to display locals.
530#[repr(C)]
531#[derive(Clone, Copy, Debug)]
532pub struct DebugLocal {
533 /// The source name as written (e.g. `a`). Not owned by the frame; points at
534 /// a `'static` string the compiler embedded.
535 pub source_name: *const u8,
536 /// The name's byte length.
537 pub name_len: u32,
538 /// The compiler-assigned symbol id (disambiguates shadowed bindings, §4.2).
539 pub symbol_id: u32,
540 /// The local's static type descriptor (§9.3 "local type descriptors"), so
541 /// the debugger can render a local without re-deriving its type. Embedded
542 /// by the backend at push time from the MIR local's `Type`. Null when the
543 /// local has no static type (`MirType::Opaque` — a pipeline accumulator, a
544 /// fused-loop item), alongside
545 /// [`NO_STATIC_TYPE`](crate::debug::NO_STATIC_TYPE) in `type_id`; and null
546 /// on its own when the type has no runtime descriptor (`Never`, an
547 /// unresolved inference variable), where `type_id` is still a real handle.
548 /// Either way the debugger omits the type column.
549 pub descriptor: *const crate::TypeDescriptor,
550 /// The current value of the local, or `None` for a slot no value has been
551 /// written into yet.
552 ///
553 /// Decoded from the slot's one machine word by
554 /// [`DebugLocalMeta::read`](crate::debug::DebugLocalMeta::read), under the
555 /// [`DebugSlotKind`](crate::debug::DebugSlotKind) the compiler recorded for
556 /// this local — so a temp whose box ADR-120 elided is a
557 /// [`DebugValue::Scalar`](crate::debug::DebugValue::Scalar) carrying its
558 /// payload and *no* reference, and everything else is a
559 /// [`DebugValue::Reference`](crate::debug::DebugValue::Reference). A
560 /// consumer that means to follow the value into the heap says so with
561 /// [`DebugValue::reference`](crate::debug::DebugValue::reference), which is
562 /// the one door a scalar cannot pass.
563 ///
564 /// The word itself is `Option<GcRef>`-shaped in the slot, and the zeroed
565 /// slot a fresh frame starts with is the `None` niche (F18).
566 pub value: Option<crate::debug::DebugValue>,
567 /// The full static `Type` id (a `praxis_typeck::Type(u32)` handle), so the
568 /// crash debugger can reconstruct the local's *exact* type — including
569 /// collection element types (`Vec[Int]`, `Map[Text, Int]`) and record field
570 /// shapes — which the runtime `descriptor` alone loses. The debugger pairs
571 /// this id with the live `TypeDb` to type-check `p EXPR` against the
572 /// selected frame (§9.5). [`NO_STATIC_TYPE`](crate::debug::NO_STATIC_TYPE)
573 /// when the local has none; every other `u32` is a valid arena index, so
574 /// there is no in-band zero sentinel.
575 pub type_id: u32,
576 /// The debugger classification: `LOCAL_KIND_USER` (a binding the programmer
577 /// wrote) or `LOCAL_KIND_TEMP` (a compiler intermediate). See
578 /// [`crate::debug::LOCAL_KIND_USER`].
579 pub kind: u8,
580 /// The local's source span start (byte offset), paired with `span_end`.
581 pub span_start: u32,
582 /// The local's source span end (byte offset). `(span_start, span_end) ==
583 /// (0, 0)` means "no span" (the return slot, span-less captures).
584 pub span_end: u32,
585 /// The function a direct call defines this local from, copied from
586 /// [`DebugLocalMeta::callee_name`](crate::debug::DebugLocalMeta::callee_name).
587 /// Null for every local that is not a direct call's result.
588 pub callee_name: *const u8,
589 /// The callee name's byte length; `0` where there is no name.
590 pub callee_name_len: u32,
591}
592
593/// The hidden first argument to every generated function.
594///
595/// Matches the sketch in Appendix B. Fields are raw pointers because generated
596/// Cranelift code reads them at a fixed offset with a fixed calling convention;
597/// Rust borrows would not survive across the ABI boundary.
598#[repr(C)]
599pub struct RuntimeContext {
600 pub heap: *mut Heap,
601 /// The runtime's one fault slot. **Non-null in every context generated code
602 /// is ever handed** — [`Runtime::context`] is its only producer and wires
603 /// it to the runtime's own `Fault`; the sole null-wiring constructor,
604 /// [`RuntimeContext::placeholder`], is `unsafe` and test-only.
605 ///
606 /// That invariant is load-bearing (ADR-102): an `Inst::CheckFault` is a
607 /// load of this pointer and a load of the [`Fault::KIND_OFFSET`] word
608 /// behind it, with no null test. A host that hand-built a context with a
609 /// null here and called generated code would fault the process rather than
610 /// silently never observing a Praxis fault. `a_wired_context_has_a_fault_slot`
611 /// is the gate.
612 pub pending_fault: *mut Fault,
613 /// The header of the runtime's one crash-debugger frame stack (§9.3,
614 /// ADR-021, ADR-104). Generated code claims one [`DebugFrameEntry`] in the
615 /// prologue and restores the `top` in the epilogue;
616 /// [`crate::crash_snapshot::praxis_snapshot_debug_chain`] reads `[base, top)`
617 /// innermost-first to build the frames the crash REPL renders.
618 ///
619 /// §11.6's discipline in this struct is *append at the end, never reorder*:
620 /// a field generated code reads that comes to point at something else keeps
621 /// its position and bumps
622 /// [`RUNTIME_ABI_VERSION`](crate::abi::RUNTIME_ABI_VERSION), because
623 /// deleting it and appending a replacement would shift every field below.
624 pub debug_frames: *mut DebugFrameStackHeader,
625 /// The header of the runtime's one compiler-managed shadow stack (§12.3,
626 /// ADR-019, ADR-101). Generated code claims a run of slots in the prologue
627 /// by bumping the header's `top`, spills live `GcRef`s into that run at
628 /// safepoints, and restores `top` in the epilogue. The collector scans
629 /// `[base, top)` via [`RootSet`].
630 pub shadow: *mut ShadowStackHeader,
631 pub input_source: GcRef,
632 /// The cached immortal `Unit` — the "defined dummy" returned on fault paths
633 /// (§10.4). Separate from `input_source` (which holds the read-in buffer
634 /// when present), so fault sentinels are stable regardless of input.
635 pub unit_ref: GcRef,
636 pub current_generation: u64,
637 /// How much of the native-stack budget the live Praxis frames have *not*
638 /// yet spent, in bytes (§9.2, §17.4, ADR-105). A prologue subtracts its own
639 /// [`frame_cost`]; its epilogue stores back the value it found. A prologue
640 /// guard faults with [`FaultKind::StackOverflow`] when what is left will not
641 /// cover this frame, so deep recursion faults cleanly instead of overflowing
642 /// the native stack and aborting the host (SIGABRT). Read by generated
643 /// Cranelift code at a fixed offset, like the other `#[repr(C)]` fields
644 /// above.
645 ///
646 /// **It counts down, and the direction is the design.** Counting up needs
647 /// the limit in generated code, which fixes it at compile time for every
648 /// host. Counting down puts the limit in this field, so [`Runtime::context`]
649 /// — the one producer every caller of generated code goes through — is the
650 /// single place a stack size enters the system, and the backend never learns
651 /// it. It also makes zero mean *exhausted*, which is the right thing for
652 /// [`RuntimeContext::placeholder`] to say.
653 pub stack_left: u32,
654 /// Host-managed pointer to the runtime's [`crate::ParseDetail`] slot
655 /// (§7.11). The parser interpreter writes the richest parse mismatch into
656 /// it on `ParseFailed`; the host (CLI / crash debugger) reads it after the
657 /// fault. Generated code never touches this field — it is appended at the
658 /// end of `RuntimeContext` so the offsets of all generated-code-read fields
659 /// above are unchanged (§11.6 ABI stability).
660 pub parse_detail: *mut crate::ParseDetail,
661 /// Host-managed pointer to the runtime's [`crate::SnapshotSlot`] (§9.3).
662 /// The first fault epilogue deep-copies the debug-frame chain into it
663 /// before unwinding; the host reads the snapshot after the fault. Like
664 /// `parse_detail`, generated code only passes it to
665 /// `praxis_snapshot_debug_chain` — it is appended at the end of
666 /// `RuntimeContext` for ABI stability.
667 pub crash_snapshot: *mut crate::SnapshotSlot,
668 /// The runtime's one native root store (ADR-114): what the runtime's own
669 /// Rust code holds live across an allocation, in one contiguous array.
670 ///
671 /// Claimed and released by [`crate::roots::NativeScope`], never by generated
672 /// code — which is why it, like `parse_detail` and `crash_snapshot`, is
673 /// appended at the end of the struct. It is the fifth arm of
674 /// [`crate::roots::RuntimeRoots`], which scans `[0, len)`.
675 ///
676 /// It has no reader outside `praxis-runtime` at all, so changing what it
677 /// points at is not the ABI-version event it would be for `shadow` or
678 /// `debug_frames`, which every generated prologue bump-allocates from. See
679 /// ADR-114.
680 pub native_roots: *mut crate::roots::NativeRootStore,
681 /// The cached immortal `true`, alongside [`Self::unit_ref`] (§4.3). There
682 /// are exactly two `Bool` values; the runtime allocates them once, so no
683 /// comparison in a loop consumes arena storage.
684 pub true_ref: GcRef,
685 /// The cached immortal `false`. See [`Self::true_ref`].
686 pub false_ref: GcRef,
687 /// Host-managed pointer to the runtime's [`FaultMessage`] slot (§9.1).
688 /// `praxis_panic` and `praxis_assert` write the message the program gave;
689 /// the host reads it when it renders the fault. Like `parse_detail` and
690 /// `crash_snapshot`, generated code never touches it — it is appended at
691 /// the end of the struct so every generated-code-read offset above is
692 /// unchanged (§11.6 ABI stability).
693 pub fault_message: *mut FaultMessage,
694 /// The base of the interned small-`Int` table (`Immortals::small_ints`),
695 /// alongside [`Self::true_ref`] and [`Self::unit_ref`] (§4.3,
696 /// [`crate::small_int`]).
697 ///
698 /// Unlike those, this is a *pointer to* the objects rather than one of
699 /// them: there are [`crate::SMALL_INT_COUNT`] of them, so generated code
700 /// takes two loads — the base from here, then the element at a byte offset
701 /// it computed at compile time from the literal's value. That is what
702 /// `Inst::ConstGc` emits, and it is why an in-range `Int` literal in a loop
703 /// body is not a call, an allocation and a shadow-frame spill per iteration
704 /// (docs/handovers/21-where-the-time-goes.md §3.5).
705 ///
706 /// Generated code *does* read this one, so it would be a compatibility
707 /// break if it moved — but it is appended like `fault_message` and its
708 /// neighbours, so every offset above is unchanged.
709 pub small_ints: *const GcRef,
710 /// The header of the runtime's one crash-debugger value stack (§9.3,
711 /// ADR-104). Generated code claims one slot per `Gc` local in the prologue,
712 /// stores each local's value there at the instruction that defines it, and
713 /// restores the `top` in the epilogue. Each [`DebugFrameEntry`] in
714 /// `debug_frames` names the base of its own call's run.
715 ///
716 /// **The collector never *traces* this** — it is the weak arm of
717 /// [`crate::roots::RuntimeRoots`] (ADR-106), not a strong one. The slot type
718 /// is `Option<GcRef>` rather than the shadow stack's `*mut GcHeader`
719 /// precisely so that `impl RootSet for SlotStackHeader<*mut GcHeader>`
720 /// cannot reach it: the debug set is over-approximate and never cleared
721 /// (MIR-16), and rooting it would undo MIR-01's clears.
722 ///
723 /// It *is* scanned, once per collection, immediately after the sweep: every
724 /// slot naming storage that sweep just reclaimed becomes `None`. That is
725 /// what makes a debug value always a live object or an absence, and never a
726 /// reference to a block the allocator has since reissued as something else
727 /// — which would render as a well-formed value of another type under the
728 /// dead local's own name, sharper than a dangling read.
729 ///
730 /// Appended after `small_ints`, so every offset above is unchanged.
731 pub debug_values: *mut DebugValueStackHeader,
732 /// The base of the interned ASCII-`Char` table (`Immortals::small_chars`),
733 /// alongside `small_ints` (§4.3, [`crate::small_char`], ADR-107).
734 ///
735 /// **Generated code never reads this one**, which is what separates it from
736 /// `small_ints`: the language has no character literal, so there is no
737 /// `GcConst::Char` and nothing lowers to a load of this base. Its readers are
738 /// the runtime's own — `abi.rs`'s `char_ref` and the parser interpreter's
739 /// `Rt::alloc_char`, both of which reach the runtime only through a
740 /// `*mut RuntimeContext`. It is therefore the `native_roots`/`fault_message`
741 /// class of field, and it is appended at the end of the struct for their
742 /// reason: every generated-code-read offset above stays where it was
743 /// (§11.6 ABI stability).
744 pub small_chars: *const GcRef,
745 /// Every built-in descriptor's address, indexed by [`BuiltinTypeId`]
746 /// (ADR-116). ADR-102's inline type proof loads one slot of this and
747 /// compares the object header's descriptor word against it.
748 ///
749 /// **By value, and that is the decision.** A pointer to
750 /// [`crate::descriptor::BUILTINS`] would make the proof two *dependent*
751 /// loads; the array makes it one load at a displacement the backend folds
752 /// from [`RuntimeContext::descriptor_offset`]. Baking the address in as an
753 /// `iconst` instead would be no load at all, but on aarch64 it costs
754 /// `movz`+`movk`+`movk` (a `static` in this binary lives above 2³²) and it
755 /// would make the compiler name a descriptor *address*, which it otherwise
756 /// never does (docs/handovers/25-two-mallocs-per-runtime-call.md §3 F-4).
757 ///
758 /// Filled by [`crate::descriptor::builtin_descriptor_addresses`], which
759 /// derives it from `BUILTINS` — so "slot `i` holds the descriptor whose id
760 /// is `i`" has no second place it could be written wrong, and
761 /// `builtins_are_indexed_by_their_id` stays the one gate on it.
762 ///
763 /// Appended after `small_chars`, so every offset above is unchanged
764 /// (§11.6 ABI stability). Generated code *does* read this one.
765 pub descriptors: [*const TypeDescriptor; BuiltinTypeId::COUNT],
766}
767
768/// The table is appended, so every field generated code reads sits where it
769/// did. Pinned rather than described: the proof site's displacement is only a
770/// compile-time immediate because the array starts right after `small_chars`.
771const _: () = assert!(
772 RuntimeContext::descriptor_offset(BuiltinTypeId::Unit)
773 == core::mem::offset_of!(RuntimeContext, small_chars)
774 + core::mem::size_of::<*const GcRef>(),
775 "the descriptor table is appended after `small_chars`, not spliced in"
776);
777
778/// A slot is one pointer wide and the table is exactly the built-ins, so the
779/// last slot is in bounds and there is no room for a twenty-third.
780const _: () = assert!(
781 RuntimeContext::descriptor_offset(BuiltinTypeId::Range)
782 + core::mem::size_of::<*const TypeDescriptor>()
783 == core::mem::size_of::<RuntimeContext>(),
784 "`Range` is the last built-in and its slot is the last word of the context"
785);
786
787impl RuntimeContext {
788 /// The byte displacement of `id`'s slot in [`Self::descriptors`], from the
789 /// base of a `RuntimeContext`.
790 ///
791 /// **The one authority for the address ADR-102's proof compares against**,
792 /// and the reason the backend holds no descriptor address at all: it folds
793 /// this displacement, loads whatever the runtime put there, and compares.
794 /// *Which* descriptor that is is the runtime's answer rather than a pointer
795 /// the compiler carried across the ABI — so the two cannot disagree about
796 /// the address `Int`'s descriptor has, only about which slot it is in, and
797 /// that is the enum discriminant.
798 ///
799 /// Minted here rather than reached for with `offset_of!` from the backend
800 /// for [`Fault::KIND_OFFSET`]'s reason one step further on: the element
801 /// stride is part of the answer, and a backend that multiplied by its own
802 /// `size_of::<*const _>()` would be a second statement of this layout.
803 #[must_use]
804 pub const fn descriptor_offset(id: BuiltinTypeId) -> usize {
805 core::mem::offset_of!(RuntimeContext, descriptors)
806 + (id as usize) * core::mem::size_of::<*const TypeDescriptor>()
807 }
808
809 /// Construct a context with all pointers null and the input source set to
810 /// the canonical placeholder. Real runtime setup (rooting the heap,
811 /// installing a fault sink) is done via [`Runtime::context`].
812 ///
813 /// **Generated code must never be run against a placeholder.** The prologue
814 /// is inline (ADR-101): it dereferences `shadow` unconditionally and without
815 /// a null check, because the check cost every call in the language and
816 /// `Runtime::context` is the only producer of a context generated code is
817 /// ever handed.
818 ///
819 /// # Safety
820 /// `input_source` must be a valid `GcRef` (or the caller must ensure no
821 /// generated code dereferences it before the runtime is fully initialized).
822 pub unsafe fn placeholder(input_source: GcRef) -> RuntimeContext {
823 RuntimeContext {
824 heap: std::ptr::null_mut(),
825 pending_fault: std::ptr::null_mut(),
826 debug_frames: std::ptr::null_mut(),
827 shadow: std::ptr::null_mut(),
828 input_source,
829 // Placeholder: reuse the input_source ref as the Unit sentinel too,
830 // since this constructor is only for not-yet-wired test scaffolding.
831 unit_ref: input_source,
832 current_generation: 0,
833 // Zero is *exhausted*, not "fresh" — so if generated code ever did
834 // reach a placeholder in spite of the paragraph above, its first
835 // prologue faults `StackOverflow` rather than running with a full
836 // budget over a stack nobody sized. Counting down is what makes the
837 // safe default and the zero value the same number (ADR-105).
838 stack_left: 0,
839 parse_detail: std::ptr::null_mut(),
840 crash_snapshot: std::ptr::null_mut(),
841 native_roots: std::ptr::null_mut(),
842 // As for `unit_ref`: this constructor is not-yet-wired scaffolding.
843 true_ref: input_source,
844 false_ref: input_source,
845 fault_message: std::ptr::null_mut(),
846 // Null, not a dangling table: a placeholder context is scaffolding
847 // no generated code runs against, and a null here faults loudly at
848 // the first `Inst::ConstGc` rather than reading whatever the
849 // `input_source` trick would have aliased.
850 small_ints: std::ptr::null(),
851 debug_values: std::ptr::null_mut(),
852 // Null for `small_ints`' reason: a null faults loudly at the first
853 // read rather than aliasing whatever the `input_source` trick above
854 // would have pointed at. The exposure is the parser interpreter's
855 // `Rt::alloc_char`, which is the standing one `Rt::alloc_int`
856 // already has — a parse is never run against a placeholder.
857 small_chars: std::ptr::null(),
858 // Real addresses, where every neighbour above is null — and the
859 // difference is that there is nothing here for a runtime to wire.
860 // These are `static`s of this binary; they are valid before `main`
861 // and depend on no `Runtime`. A null table would be a trap for a
862 // state that cannot exist, and it would make the *only* difference
863 // between a placeholder and a wired context at ADR-102's proof site
864 // a null dereference rather than an honest comparison (ADR-116).
865 descriptors: builtin_descriptor_addresses(),
866 }
867 }
868
869 /// True iff a fault is currently pending on this context. Generated code
870 /// checks this at safepoints after potentially-faulting operations (§10.4).
871 ///
872 /// `pending_fault` is non-null once the context is wired to a runtime; a
873 /// fault is pending when the pointed-at [`Fault`] slot says so.
874 #[inline]
875 pub fn has_pending_fault(&self) -> bool {
876 if self.pending_fault.is_null() {
877 return false;
878 }
879 // SAFETY: a non-null `pending_fault` points at a live `Fault` owned by
880 // the runtime for as long as the context is in use.
881 unsafe { (*self.pending_fault).is_pending() }
882 }
883}
884
885/// Read the current fault kind from a context's `pending_fault` slot (§9.2).
886/// Returns [`FaultKind::None`] if no fault is pending or the slot is null. Used
887/// by [`crate::crash_snapshot::praxis_snapshot_debug_chain`] to record which
888/// fault kind triggered the snapshot.
889///
890/// # Safety
891/// `ctx` must be live and wired (a null `pending_fault` yields `None`).
892pub unsafe fn current_fault_kind(ctx: *mut RuntimeContext) -> FaultKind {
893 if ctx.is_null() || unsafe { (*ctx).pending_fault.is_null() } {
894 return FaultKind::None;
895 }
896 // SAFETY: caller guarantees the context is live; a non-null pending_fault
897 // points at a live Fault owned by the runtime.
898 unsafe { (*(*ctx).pending_fault).kind }
899}
900
901/// The owner of the heap and the immortal singletons.
902///
903/// The entry point for runtime code: construct a `Runtime`, allocate values
904/// through it, root them in a [`crate::RootScope`], and collect when needed.
905/// [`Runtime::context`] produces the `RuntimeContext` handed to generated code.
906pub struct Runtime {
907 heap: Heap,
908 immortals: Immortals,
909 /// The fault slot generated code signals through (§10.4). Owned here so its
910 /// address is stable for the lifetime of the runtime.
911 fault: Fault,
912 /// The rich parse-failure detail slot (§7.11). Owned here so its address is
913 /// stable; `Runtime::context` installs it on every context. The parser
914 /// interpreter writes the deepest mismatch into it; the host reads it after
915 /// a `FaultKind::ParseFailed`.
916 parse_detail: ParseDetail,
917 /// The crash-snapshot slot (§9.3). Owned here so its address is stable; the
918 /// first fault epilogue deep-copies the debug-frame chain into it before
919 /// unwinding. The host reads it (and roots it for GC) after a fault.
920 crash_snapshot: SnapshotSlot,
921 /// The message slot a `panic`/`assert` fault carries (§9.1). Owned here so
922 /// its address is stable; `Runtime::context` installs it on every context.
923 fault_message: FaultMessage,
924 /// The one shadow stack every generated frame bump-allocates from
925 /// (ADR-101). Owned here, sized once, never resized: generated code holds
926 /// the header's address for the whole program and a frame's base pointer
927 /// for the duration of a call, so a reallocation would be a use-after-free.
928 shadow_stack: ShadowStack,
929 /// The one native root store every [`crate::roots::NativeScope`] claims from
930 /// (ADR-114). Owned here so its address is stable, like `fault` and
931 /// `parse_detail`, and for the same reason: `Runtime::context` hands out a
932 /// raw pointer to it.
933 ///
934 /// Unlike `shadow_stack` this one **grows**, and it can, because the only
935 /// address anything holds is the store's own — never the array's. A scope
936 /// saves a `usize` watermark; the collector re-reads the slice at every
937 /// collection. ADR-114 prices the asymmetry: how deep the scopes nest is
938 /// bounded, how many roots one of them holds is the program's input.
939 native_roots: crate::roots::NativeRootStore,
940 /// The crash debugger's two stacks (§9.3, ADR-104), owned and sized here
941 /// for the same reason and under the same never-resize rule as
942 /// `shadow_stack`. `debug_frames` holds one entry per live call — which
943 /// function, and where its values are — and `debug_values` one slot per `Gc`
944 /// local per live call.
945 debug_frames: DebugFrameStack,
946 debug_values: DebugValueStack,
947 /// The native-stack budget every context this runtime mints starts with
948 /// (ADR-105).
949 ///
950 /// Owned here rather than baked into generated code because the budget is a
951 /// property of the *stack the program runs on*, which the backend cannot
952 /// know and the host sometimes can. [`Runtime::context`] is the only reader,
953 /// which makes it the one door a stack size enters through.
954 stack_budget: StackBudget,
955}
956
957impl Runtime {
958 /// Create a runtime with a fresh heap and the immortal singletons allocated.
959 pub fn new() -> Self {
960 let heap = Heap::new();
961 // Immortals must be allocated before any collection can run.
962 let immortals = Immortals::new(&heap);
963 Runtime {
964 heap,
965 immortals,
966 fault: Fault::clear(),
967 parse_detail: ParseDetail::new(),
968 crash_snapshot: SnapshotSlot::new(),
969 fault_message: FaultMessage::new(),
970 // 3.42 MiB of address space, allocated zeroed — one `mmap` of
971 // untouched pages, faulted in only as deep as the program actually
972 // recurses. See `SHADOW_STACK_SLOTS` for why it can be sized once
973 // and never checked.
974 shadow_stack: ShadowStack::new(SHADOW_STACK_SLOTS, std::ptr::null_mut()),
975 // 8 KiB of reservation, one `malloc`, and a growable one — the
976 // asymmetry ADR-114 records: how deep the native scopes nest is
977 // bounded, how many roots one of them holds is not.
978 native_roots: crate::roots::NativeRootStore::new(),
979 debug_frames: DebugFrameStack::new(DEBUG_FRAME_STACK_SLOTS, DebugFrameEntry::empty()),
980 debug_values: DebugValueStack::new(DEBUG_VALUE_STACK_SLOTS, None),
981 stack_budget: StackBudget::DEFAULT,
982 }
983 }
984
985 /// Lower the native-stack budget every context this runtime mints will start
986 /// with (ADR-105).
987 ///
988 /// For a host that knows its stack is smaller than the one
989 /// [`STACK_BUDGET_BYTES`] assumes, and for tests that want to reach the
990 /// guard without recursing eight thousand times. It cannot be *raised* past
991 /// the default — [`StackBudget::new`] refuses, because the shadow-stack
992 /// reservation is sized from that figure.
993 pub fn set_stack_budget(&mut self, budget: StackBudget) {
994 self.stack_budget = budget;
995 }
996
997 /// The native-stack budget this runtime hands to a new context.
998 #[must_use]
999 pub fn stack_budget(&self) -> StackBudget {
1000 self.stack_budget
1001 }
1002
1003 /// Borrow the heap.
1004 #[inline]
1005 pub fn heap(&self) -> &Heap {
1006 &self.heap
1007 }
1008
1009 /// The immortal singletons (§4.3).
1010 #[inline]
1011 pub fn immortals(&self) -> &Immortals {
1012 &self.immortals
1013 }
1014
1015 /// Force a mark-and-sweep collection (§12.1) rooted from everything this
1016 /// runtime owns — the shadow stack, the ambient input buffer, a parse
1017 /// failure's partial value, the crash snapshot, and the native root store.
1018 ///
1019 /// This is the host's collection entry point. It takes no root set: a host
1020 /// that could name its own would be choosing which of the runtime's owners
1021 /// to honour, and choosing wrong frees a live object.
1022 pub fn collect_now(&mut self) {
1023 let mut ctx = self.context();
1024 // SAFETY: `ctx` is a fresh view of this live runtime, and the arms it
1025 // points at (parse detail, snapshot slot) are owned by `self`, which
1026 // outlives the borrow.
1027 let roots = unsafe { crate::roots::RuntimeRoots::from_context(&mut ctx) };
1028 self.heap.collect(&roots);
1029 }
1030
1031 /// Run a mark-and-sweep collection (§12.1) against an arbitrary root set.
1032 ///
1033 /// Test-only. Production collection goes through
1034 /// [`Heap::collect`](crate::Heap::collect), which accepts only a
1035 /// [`RuntimeRoots`](crate::roots::RuntimeRoots) read out of a live context
1036 /// — a host that could pass its own `&dyn RootSet` could collect against a
1037 /// set that omits the runtime's own owners, and so free a live object.
1038 #[cfg(test)]
1039 pub fn collect_with(&self, roots: &dyn RootSet) {
1040 self.heap.collect_with(roots);
1041 }
1042
1043 /// A `RuntimeContext` view of this runtime, suitable for generated code.
1044 /// `pending_fault` points at this runtime's fault slot; `shadow` points at
1045 /// this runtime's shadow-stack header, which every generated prologue
1046 /// bump-allocates from and which the collector scans; `debug_frames` and
1047 /// `debug_values` point at the crash debugger's two stacks, which the
1048 /// prologue claims from and which `praxis_snapshot_debug_chain` reads.
1049 /// `parse_detail` points at this runtime's [`ParseDetail`] slot so the
1050 /// parser interpreter can record the richest `ParseFailed` detail.
1051 ///
1052 /// Every context this mints shares the three stacks **and the native root
1053 /// store**, so a context taken while generated code or a runtime wrapper is
1054 /// running (as [`Runtime::collect_now`] does) sees the frames and scopes
1055 /// already on them.
1056 ///
1057 /// **Two contexts must never execute over these stacks concurrently.** That
1058 /// holds because a Praxis program is single-threaded and every host that
1059 /// mints a second context ([`crate::Runtime::collect_now`], the debugger's
1060 /// `p EXPR` and `restart`) does so only when the previous run has fully
1061 /// unwound. A second context therefore starts with the *full* stack
1062 /// budget rather than the running one's remainder, which is correct for the
1063 /// two callers that mint one while frames are live: both do so from the host,
1064 /// on the host's own stack, not from underneath the frames.
1065 pub fn context(&mut self) -> RuntimeContext {
1066 RuntimeContext {
1067 heap: &mut self.heap as *mut Heap,
1068 pending_fault: &mut self.fault as *mut Fault,
1069 debug_frames: self.debug_frames.header_ptr(),
1070 shadow: self.shadow_stack.header_ptr(),
1071 input_source: self.immortals.unit(),
1072 unit_ref: self.immortals.unit(),
1073 current_generation: 0,
1074 // The one door a native-stack size enters the system through
1075 // (ADR-105). Generated code never learns the budget; it only ever
1076 // subtracts from what it finds here.
1077 stack_left: self.stack_budget.get(),
1078 parse_detail: &mut self.parse_detail as *mut ParseDetail,
1079 crash_snapshot: &mut self.crash_snapshot as *mut SnapshotSlot,
1080 // The one store, shared by every context this runtime mints — so a
1081 // context taken while native code is running (as `collect_now` does)
1082 // sees the scopes already open on it.
1083 native_roots: &mut self.native_roots as *mut crate::roots::NativeRootStore,
1084 true_ref: self.immortals.true_(),
1085 false_ref: self.immortals.false_(),
1086 fault_message: &mut self.fault_message as *mut FaultMessage,
1087 small_ints: self.immortals.small_ints_ptr(),
1088 debug_values: self.debug_values.header_ptr(),
1089 small_chars: self.immortals.small_chars_ptr(),
1090 // Not `self`'s: the built-in descriptors are `static`s shared by
1091 // every runtime in the process, so this is the same table in every
1092 // context and copying it costs one 176-byte block move per program
1093 // run (ADR-116).
1094 descriptors: builtin_descriptor_addresses(),
1095 }
1096 }
1097
1098 /// The current fault state (§10.4). `FaultKind::None` when no fault is set.
1099 pub fn fault(&self) -> FaultKind {
1100 self.fault.kind()
1101 }
1102
1103 /// True iff a fault is pending.
1104 pub fn has_pending_fault(&self) -> bool {
1105 self.fault.is_pending()
1106 }
1107
1108 /// Clear any pending fault, returning the kind that was pending (if any).
1109 pub fn take_fault(&mut self) -> Option<FaultKind> {
1110 let kind = self.fault.kind();
1111 if self.fault.is_pending() {
1112 self.fault = Fault::clear();
1113 self.fault_message.clear();
1114 Some(kind)
1115 } else {
1116 None
1117 }
1118 }
1119
1120 /// The message a `panic`/`assert` fault carried (§9.1), or `None` for a
1121 /// fault kind that carries none. The host renders it beside the fault line.
1122 #[must_use]
1123 pub fn fault_message(&self) -> Option<&str> {
1124 self.fault_message.get()
1125 }
1126
1127 /// Borrow the rich parse-failure detail slot (§7.11). The host reads this
1128 /// after a `FaultKind::ParseFailed` to render the input/parser span, the
1129 /// expected description, and the actual preview. Returns `None` when no
1130 /// detail was recorded (e.g. a non-parser `ParseFailed` path).
1131 #[must_use]
1132 pub fn parse_detail(&self) -> &ParseDetail {
1133 &self.parse_detail
1134 }
1135
1136 /// Mutably borrow the parse-detail slot (so the host can clear it before a
1137 /// rerun, or the debugger can read the partial root value).
1138 pub fn parse_detail_mut(&mut self) -> &mut ParseDetail {
1139 &mut self.parse_detail
1140 }
1141
1142 /// Borrow the crash-snapshot slot (§9.3). `None` when no fault snapshotted
1143 /// this run (the program completed cleanly, or faulted before any debug
1144 /// frame was pushed). The host reads this after a fault for the
1145 /// noninteractive render / crash REPL.
1146 #[must_use]
1147 pub fn crash_snapshot(&self) -> Option<&CrashSnapshot> {
1148 self.crash_snapshot.get()
1149 }
1150
1151 /// Take the crash snapshot out of the runtime (the host owns it after).
1152 /// Returns `None` when no snapshot was taken.
1153 pub fn take_crash_snapshot(&mut self) -> Option<CrashSnapshot> {
1154 self.crash_snapshot.take()
1155 }
1156
1157 /// Reset the fault, crash-snapshot, and parse-detail slots so the next
1158 /// `main` call starts from a clean slate (§9.7 `restart`/`reload`). The
1159 /// heap is *not* collected — old allocations (and the snapshot the host
1160 /// may still hold as a root set) survive until an explicit `collect`.
1161 /// Call this before re-executing `main`.
1162 pub fn clear_for_rerun(&mut self) {
1163 self.fault = Fault::clear();
1164 self.crash_snapshot.clear();
1165 self.parse_detail.clear();
1166 self.fault_message.clear();
1167 // Every epilogue — including every fault epilogue — restores the `top`
1168 // its prologue saved, so a completed run leaves the stacks exactly as it
1169 // found them. A non-empty stack here is an unbalanced prologue, which is
1170 // a codegen bug and not something a rerun should paper over silently.
1171 debug_assert!(
1172 self.shadow_stack.is_empty(),
1173 "the shadow stack is {} slots deep between runs; some prologue was \
1174 not balanced by an epilogue",
1175 self.shadow_stack.len()
1176 );
1177 debug_assert!(
1178 self.debug_frames.is_empty() && self.debug_values.is_empty(),
1179 "the debug stacks are {} frames / {} values deep between runs; some \
1180 prologue was not balanced by an epilogue",
1181 self.debug_frames.len(),
1182 self.debug_values.len()
1183 );
1184 // The same statement for the fourth region, and a sharper one: a
1185 // `NativeScope` is RAII on the *Rust* stack, so between runs there is no
1186 // frame that could still be holding a claim. A non-empty store is a
1187 // scope that was leaked or `mem::forget`ten, and every root in it is one
1188 // the next run's collections would keep alive forever.
1189 debug_assert!(
1190 self.native_roots.is_empty(),
1191 "the native root store holds {} roots between runs; some \
1192 `NativeScope` was not dropped",
1193 self.native_roots.len()
1194 );
1195 self.shadow_stack.reset();
1196 // Length only. The capacity is deliberately kept: a `restart` re-parses
1197 // the same input, so a store that grew to hold one root per line wants
1198 // to be exactly that big again, and shrinking here would put the whole
1199 // doubling schedule back on the next run's parse.
1200 self.native_roots.reset();
1201 self.debug_frames.reset();
1202 self.debug_values.reset();
1203 }
1204
1205 /// The shadow stack every generated frame bump-allocates from (ADR-101).
1206 ///
1207 /// Read-only, and the reason it is exposed at all is that "the stack is
1208 /// empty again" is the observable form of "every prologue was balanced by
1209 /// an epilogue" — an unbalanced prologue must be a test failure, not a slow
1210 /// leak that only shows up as a wrong root set thousands of calls later.
1211 #[must_use]
1212 pub fn shadow_stack(&self) -> &ShadowStack {
1213 &self.shadow_stack
1214 }
1215
1216 /// The native root store every [`crate::roots::NativeScope`] claims from
1217 /// (ADR-114). Read-only, and exposed for [`Runtime::shadow_stack`]'s reason
1218 /// — "the store is empty again" is the observable form of "every scope was
1219 /// dropped" — plus one this region has and the others do not: its
1220 /// [`capacity`](crate::roots::NativeRootStore::capacity) is the observable
1221 /// form of "this program made the store grow", which is the state a
1222 /// pointer-shaped watermark would not have survived.
1223 #[must_use]
1224 pub fn native_root_store(&self) -> &crate::roots::NativeRootStore {
1225 &self.native_roots
1226 }
1227
1228 /// The crash debugger's frame stack (§9.3, ADR-104). Read-only, and exposed
1229 /// for the same reason as [`Runtime::shadow_stack`]: "the stack is empty
1230 /// again" is the observable form of "every prologue was balanced".
1231 #[must_use]
1232 pub fn debug_frame_stack(&self) -> &DebugFrameStack {
1233 &self.debug_frames
1234 }
1235
1236 /// The crash debugger's value stack (§9.3, ADR-104). See
1237 /// [`Runtime::debug_frame_stack`].
1238 #[must_use]
1239 pub fn debug_value_stack(&self) -> &DebugValueStack {
1240 &self.debug_values
1241 }
1242
1243 /// Consume the runtime, drop the heap, and return the proof that no live
1244 /// object can still name a JIT generation's arena (F13, hazard H15).
1245 ///
1246 /// This is the *only* constructor of [`HeapDrained`], and reclaiming a
1247 /// generation requires one. Dropping the heap runs every finalizer
1248 /// (`Heap::drop`), so after this call no `RecordPayload` or `TuplePayload`
1249 /// survives to dereference a schema pointer.
1250 ///
1251 /// A host that never calls this loses nothing but memory: an un-retired
1252 /// generation leaks its arena.
1253 #[must_use]
1254 pub fn teardown(self) -> crate::teardown::HeapDrained {
1255 drop(self);
1256 crate::teardown::HeapDrained::new()
1257 }
1258}
1259
1260impl Default for Runtime {
1261 fn default() -> Self {
1262 Self::new()
1263 }
1264}
1265
1266// ---- typed allocation helpers --------------------------------------------
1267
1268impl Runtime {
1269 /// Allocate an `Int` (§4.3), or answer the interned immortal when `value` is
1270 /// small ([`crate::small_int`]).
1271 ///
1272 /// The interning is here and not only in `praxis_alloc_int` so that the host
1273 /// helper and the ABI wrapper answer the *same object* for the same small
1274 /// value, exactly as [`Runtime::alloc_bool`] already does. Two allocators
1275 /// disagreeing about whether `5` is shared would be a wart with no upside:
1276 /// nothing can observe the sharing (that is `small_int`'s argument), so the
1277 /// only thing a split would buy is two behaviours to remember.
1278 pub fn alloc_int(&self, value: i64) -> GcRef {
1279 match self.immortals.small_int(value) {
1280 Some(interned) => interned,
1281 None => self.heap.alloc_unpaced(crate::scalars::INT_PAYLOAD, value),
1282 }
1283 }
1284
1285 /// Allocate a `Bool` as the corresponding immortal singleton (§4.3). Booleans
1286 /// are always the immortals — there is never a fresh `Bool` allocation.
1287 pub fn alloc_bool(&self, value: bool) -> GcRef {
1288 self.immortals.bool_(value)
1289 }
1290
1291 /// Allocate a `Byte` (§4.3).
1292 pub fn alloc_byte(&self, value: u8) -> GcRef {
1293 self.heap.alloc_unpaced(crate::scalars::BYTE_PAYLOAD, value)
1294 }
1295
1296 /// Allocate a `Char` (§4.3), or answer the interned immortal when `value` is
1297 /// ASCII ([`crate::small_char`]). Panics if `value` is not a valid scalar
1298 /// value.
1299 ///
1300 /// The validity assert stays in front of the table lookup rather than being
1301 /// absorbed into it: `index_of` answers "is it interned", which for a value
1302 /// above the range is `None` and therefore says nothing at all about
1303 /// validity. An out-of-range invalid code point must still panic here.
1304 ///
1305 /// The interning is here and not only in `praxis_alloc_char` for
1306 /// [`Runtime::alloc_int`]'s reason — the host helper and the ABI wrapper must
1307 /// answer the *same object* for the same small value. Nothing can observe the
1308 /// sharing (that is `small_char`'s argument), so a split would buy nothing
1309 /// but two behaviours to remember.
1310 pub fn alloc_char(&self, value: u32) -> GcRef {
1311 assert!(
1312 crate::scalars::is_valid_char(value),
1313 "{value:#x} is not a valid Unicode scalar"
1314 );
1315 match self.immortals.small_char(value) {
1316 Some(interned) => interned,
1317 None => self.heap.alloc_unpaced(crate::scalars::CHAR_PAYLOAD, value),
1318 }
1319 }
1320
1321 /// Allocate a `Float` (§4.3, §4.12). All finite values, ±infinity, and NaN
1322 /// are valid payloads — `Float` arithmetic never faults (IEEE-754).
1323 pub fn alloc_float(&self, value: f64) -> GcRef {
1324 self.heap
1325 .alloc_unpaced(crate::scalars::FLOAT_PAYLOAD, value)
1326 }
1327
1328 /// The immortal `Unit` (§4.3).
1329 pub fn alloc_unit(&self) -> GcRef {
1330 self.immortals.unit()
1331 }
1332
1333 /// Allocate an owned `Text` (§4.3, ADR-013).
1334 pub fn alloc_text(&self, value: &str) -> GcRef {
1335 // SAFETY: TextPayload is TEXT's payload type.
1336 unsafe {
1337 self.heap
1338 .alloc_payload_unpaced(&crate::text::TEXT, crate::text::TextPayload::owned(value))
1339 }
1340 }
1341
1342 /// Allocate a source-slice `Text` — a zero-copy view into `owner`'s bytes
1343 /// spanning `[start, start+len)` (§7.10, ADR-013). The slice's descriptor
1344 /// traces `owner`, keeping the backing alive.
1345 ///
1346 /// Returns `None` if the range is not a `Text`: past the owner's end, an
1347 /// overflowing length, or ends that split a multi-byte scalar. The check is
1348 /// unconditional, not a `debug_assert` — a release build must not slice out
1349 /// of range.
1350 ///
1351 /// # Safety
1352 /// `owner` must be a live `Text` `GcRef`.
1353 #[must_use]
1354 pub unsafe fn alloc_text_slice(&self, owner: GcRef, start: usize, len: usize) -> Option<GcRef> {
1355 // SAFETY: caller guarantees `owner` is a live Text.
1356 let slice = unsafe { crate::text::SourceSlice::new(owner, start, len) }?;
1357 let payload = crate::text::TextPayload::Slice(slice);
1358 // SAFETY: TextPayload is TEXT's payload type.
1359 Some(unsafe { self.heap.alloc_payload_unpaced(&crate::text::TEXT, payload) })
1360 }
1361
1362 /// Allocate a `Vec[T]` from a slice of already-allocated element refs and the
1363 /// element descriptor (§11.2, ADR-013).
1364 pub fn alloc_vec(
1365 &self,
1366 element_descriptor: &'static TypeDescriptor,
1367 items: Vec<GcRef>,
1368 ) -> GcRef {
1369 // SAFETY: VecPayload is VEC's payload type.
1370 unsafe {
1371 self.heap.alloc_payload_unpaced(
1372 &crate::collections::VEC,
1373 VecPayload {
1374 element_descriptor,
1375 items: items.into(),
1376 },
1377 )
1378 }
1379 }
1380
1381 /// Allocate a `Grid[T]` from a flat row-major list of cells, the element
1382 /// descriptor, and the column count (§7.5). `items.len()` must be a
1383 /// multiple of `width`.
1384 pub fn alloc_grid(
1385 &self,
1386 element_descriptor: &'static TypeDescriptor,
1387 items: Vec<GcRef>,
1388 width: usize,
1389 ) -> GcRef {
1390 debug_assert!(
1391 width == 0 || items.len().is_multiple_of(width),
1392 "grid items ({}) not a multiple of width ({})",
1393 items.len(),
1394 width
1395 );
1396 // SAFETY: GridPayload is GRID's payload type.
1397 unsafe {
1398 self.heap.alloc_payload_unpaced(
1399 &crate::collections::GRID,
1400 crate::collections::GridPayload {
1401 element_descriptor,
1402 items,
1403 width,
1404 },
1405 )
1406 }
1407 }
1408
1409 /// Allocate a provisional structural `Record` from field values and a static
1410 /// schema (§7.8). `items.len()` must equal `schema.arity()`.
1411 pub fn alloc_record(
1412 &self,
1413 schema: &'static crate::records::RecordSchema,
1414 items: Vec<GcRef>,
1415 ) -> GcRef {
1416 debug_assert_eq!(
1417 items.len(),
1418 schema.arity(),
1419 "record field count ({}) != schema arity ({})",
1420 items.len(),
1421 schema.arity()
1422 );
1423 // SAFETY: RecordPayload is RECORD's payload type.
1424 unsafe {
1425 self.heap.alloc_payload_unpaced(
1426 &crate::records::RECORD,
1427 crate::records::RecordPayload { schema, items },
1428 )
1429 }
1430 }
1431}
1432
1433// ---- typed payload access helpers ----------------------------------------
1434
1435impl GcRef {
1436 /// Read an `Int` payload (§4.3).
1437 ///
1438 /// Panics if this reference's descriptor is not `Int`.
1439 pub fn as_int(&self) -> i64 {
1440 assert_eq!(
1441 self.descriptor().id(),
1442 crate::scalars::INT.id(),
1443 "not an Int"
1444 );
1445 // SAFETY: descriptor check confirms payload is i64.
1446 unsafe { *self.payload::<i64>() }
1447 }
1448
1449 /// Read a `Bool` payload as a Rust `bool` (§4.3).
1450 ///
1451 /// Panics if this reference's descriptor is not `Bool`.
1452 pub fn as_bool(&self) -> bool {
1453 assert_eq!(
1454 self.descriptor().id(),
1455 crate::scalars::BOOL.id(),
1456 "not a Bool"
1457 );
1458 // SAFETY: descriptor check confirms payload is BoolPayload.
1459 unsafe { read_bool(*self) }
1460 }
1461
1462 /// Read a `Byte` payload (§4.3).
1463 pub fn as_byte(&self) -> u8 {
1464 assert_eq!(
1465 self.descriptor().id(),
1466 crate::scalars::BYTE.id(),
1467 "not a Byte"
1468 );
1469 // SAFETY: descriptor check confirms payload is u8.
1470 unsafe { *self.payload::<u8>() }
1471 }
1472
1473 /// Read a `Char` payload as a Rust `char` (§4.3).
1474 pub fn as_char(&self) -> char {
1475 assert_eq!(
1476 self.descriptor().id(),
1477 crate::scalars::CHAR.id(),
1478 "not a Char"
1479 );
1480 let raw = unsafe { *self.payload::<u32>() };
1481 char::from_u32(raw).expect("Char payload was not a valid scalar; memory corrupted")
1482 }
1483
1484 /// Read a `Float` payload as an `f64` (§4.3).
1485 pub fn as_float(&self) -> f64 {
1486 assert_eq!(
1487 self.descriptor().id(),
1488 crate::scalars::FLOAT.id(),
1489 "not a Float"
1490 );
1491 // SAFETY: descriptor check confirms payload is FloatPayload (f64).
1492 unsafe { *self.payload::<f64>() }
1493 }
1494
1495 /// Read a `Text` payload as a `&str` (§4.3).
1496 ///
1497 /// The lifetime is tied to the `GcRef`'s borrow; the text stays valid as long
1498 /// as the object is reachable. Handles both owned and source-slice payloads
1499 /// (ADR-013): a slice reads through its owner.
1500 pub fn as_text(&self) -> &str {
1501 assert_eq!(self.descriptor().id(), crate::text::TEXT.id(), "not Text");
1502 // SAFETY: descriptor check confirms payload is a TextPayload; the
1503 // reference is valid while the object lives (non-moving GC, ADR-011).
1504 let payload = self.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload;
1505 unsafe { crate::text::text_str(payload) }
1506 }
1507
1508 /// Read a `Vec[T]` payload as a slice of element refs (§11.2).
1509 pub fn as_vec(&self) -> &[GcRef] {
1510 assert_eq!(
1511 self.descriptor().id(),
1512 crate::collections::VEC.id(),
1513 "not a Vec"
1514 );
1515 // SAFETY: descriptor check confirms payload is VecPayload.
1516 let p: &VecPayload = unsafe { &*self.payload::<VecPayload>() };
1517 &p.items
1518 }
1519
1520 /// Format this value through its descriptor into `out` (§11.4), in the
1521 /// program's own rendering — what `out(v)` writes and what `"{v}"` splices.
1522 pub fn format(&self, out: &mut dyn std::fmt::Write) {
1523 self.format_styled(&mut crate::FormatSink::display(out));
1524 }
1525
1526 /// Format this value into `out` in the **debugger's** rendering
1527 /// ([`FormatStyle::Debug`](crate::FormatStyle::Debug)): a `Text` is a quoted
1528 /// literal, at every depth.
1529 ///
1530 /// The pair exists because the two callers want opposite things from the
1531 /// same value. A program printing a string means its characters; a debugger
1532 /// showing a local means "this is a string, and here is exactly which one" —
1533 /// and on a locals row the difference between `""` and no output at all is
1534 /// the difference between a value and a bug report.
1535 pub fn format_debug(&self, out: &mut dyn std::fmt::Write) {
1536 self.format_styled(&mut crate::FormatSink::debug(out));
1537 }
1538
1539 /// Format this value into an existing sink, keeping its style.
1540 ///
1541 /// The shared body of the two above, and the entry point for a caller that
1542 /// already has a sink — a descriptor callback rendering a part of itself.
1543 pub fn format_styled(&self, out: &mut crate::FormatSink<'_>) {
1544 let desc = self.descriptor();
1545 // SAFETY: `self`'s payload matches its descriptor.
1546 unsafe { (desc.format)(self.payload::<u8>() as *const u8, out) };
1547 }
1548
1549 /// Structural equality through the descriptors (§5.5). Returns `false` if
1550 /// either side's type is not equatable, or if the descriptors differ.
1551 pub fn equals(&self, other: &GcRef) -> bool {
1552 let a = self.descriptor();
1553 let b = other.descriptor();
1554 if a.id() != b.id() {
1555 return false;
1556 }
1557 let Some(eq) = a.equals else {
1558 return false;
1559 };
1560 // SAFETY: both payloads match the shared descriptor.
1561 unsafe {
1562 eq(
1563 self.payload::<u8>() as *const u8,
1564 other.payload::<u8>() as *const u8,
1565 )
1566 }
1567 }
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572
1573 /// Nothing can reset the heap a runtime's immortals live in: `Runtime`
1574 /// exposes only `&Heap`, never `&mut Heap`, so no safe call can tear down
1575 /// the arena and mint a fresh `HeapId` underneath `Runtime.immortals`. This
1576 /// pins the invariant that would make such an accessor dangerous — every
1577 /// context's cached `unit_ref` / `true_ref` / `false_ref` is live storage
1578 /// in this runtime's own heap.
1579 #[test]
1580 fn a_runtimes_immortals_belong_to_its_own_live_heap() {
1581 let mut rt = Runtime::new();
1582 let ctx = rt.context();
1583 for cached in [ctx.unit_ref, ctx.true_ref, ctx.false_ref] {
1584 assert!(
1585 rt.heap().owns(cached),
1586 "a cached immortal must be live storage in this runtime's heap"
1587 );
1588 }
1589 assert_eq!(ctx.unit_ref.as_ptr(), rt.immortals().unit().as_ptr());
1590 assert_eq!(ctx.true_ref.as_ptr(), rt.immortals().true_().as_ptr());
1591 assert_eq!(ctx.false_ref.as_ptr(), rt.immortals().false_().as_ptr());
1592 }
1593 use super::*;
1594 use crate::gc::GcHeader;
1595 use crate::roots::RootScope;
1596 use std::ptr::NonNull;
1597
1598 /// ADR-102: generated code loads the fault kind rather than calling
1599 /// `praxis_check_fault`, so what `is_pending()` encapsulates is baked into
1600 /// emitted instructions and must be pinned here.
1601 ///
1602 /// The `brif` the backend emits treats the loaded word as the predicate, so
1603 /// "a fault is pending" and "the word is non-zero" have to be the same
1604 /// statement. That holds because `None` is 0 and no other kind is — and the
1605 /// second half needs no loop here: [`FaultKind`] gives every variant an
1606 /// explicit discriminant, and Rust rejects an enum that assigns one twice.
1607 /// So pinning `None == 0` is the whole of what is left to check.
1608 #[test]
1609 fn the_fault_record_is_one_kind_at_offset_zero() {
1610 assert_eq!(Fault::KIND_OFFSET, 0);
1611 assert_eq!(
1612 Fault::KIND_SIZE,
1613 4,
1614 "a `#[repr(C)]` fieldless enum is a C `int`, and the backend loads \
1615 this width"
1616 );
1617 assert_eq!(
1618 std::mem::size_of::<Fault>(),
1619 Fault::KIND_SIZE,
1620 "the kind is the whole record; a second field would make the \
1621 inline load read half of it"
1622 );
1623 assert_eq!(FaultKind::None as u32, 0, "the zero word means no fault");
1624
1625 // And the load really is the predicate: raise, then read the record's
1626 // first four bytes the way generated code does.
1627 let mut fault = Fault::clear();
1628 let word = |f: &Fault| {
1629 let base = f as *const Fault as *const u8;
1630 // SAFETY: `KIND_OFFSET`/`KIND_SIZE` bound a `FaultKind` inside a
1631 // live `Fault`, and `u32` is that width with no alignment demand
1632 // the record does not already meet.
1633 unsafe { base.add(Fault::KIND_OFFSET).cast::<u32>().read() }
1634 };
1635 assert_eq!(word(&fault), 0, "a clear record loads as zero");
1636 fault.set(RaisedFault::INT_OVERFLOW);
1637 assert_ne!(word(&fault), 0, "a raised record loads as non-zero");
1638 assert!(fault.is_pending());
1639 }
1640
1641 /// The invariant the inline fault check depends on, stated as a test rather
1642 /// than as prose in ADR-017's Consequences: a context generated code can be
1643 /// handed has a fault slot to read.
1644 ///
1645 /// The two loads generated code emits do not test for null, so a null here
1646 /// is a segfault rather than a program that never observes a fault.
1647 #[test]
1648 fn a_wired_context_has_a_fault_slot() {
1649 let mut rt = Runtime::new();
1650 let ctx = rt.context();
1651 assert!(
1652 !ctx.pending_fault.is_null(),
1653 "`Runtime::context` is the only producer of a context generated code \
1654 sees, and generated code dereferences this without testing it"
1655 );
1656 // SAFETY: non-null as just asserted, and it points at `rt`'s own slot,
1657 // which outlives this borrow.
1658 assert!(!unsafe { (*ctx.pending_fault).is_pending() });
1659 }
1660
1661 /// **ADR-116's whole correctness argument, as one assertion.** Generated
1662 /// code proves a value's type by loading
1663 /// `[ctx + RuntimeContext::descriptor_offset(id)]` and comparing it against
1664 /// the header's descriptor word (ADR-102). If a slot held a neighbour's
1665 /// descriptor, that proof would accept an object of the wrong type and the
1666 /// payload read behind it would be a wrong-type read at whatever width the
1667 /// backend folded — so the correspondence between the slot index and the
1668 /// descriptor is the one thing this table has to get right.
1669 ///
1670 /// It is checked here at the offset generated code reads, in bytes, rather
1671 /// than by indexing the Rust array: indexing would re-derive the stride
1672 /// from `size_of` and prove that `descriptor_offset` and the compiler agree
1673 /// only if they were both wrong in the same way.
1674 #[test]
1675 fn every_descriptor_slot_holds_the_builtin_whose_id_indexes_it() {
1676 let mut rt = Runtime::new();
1677 let ctx = rt.context();
1678 let base = &ctx as *const RuntimeContext as *const u8;
1679 for index in 0..BuiltinTypeId::COUNT {
1680 let id = BuiltinTypeId::from_u32(index as u32).expect("index is in range");
1681 // SAFETY: the two `const _` blocks beside the field bound the table
1682 // inside the context, and `ctx` is live for this borrow.
1683 let read = unsafe {
1684 base.add(RuntimeContext::descriptor_offset(id))
1685 .cast::<*const TypeDescriptor>()
1686 .read()
1687 };
1688 assert!(
1689 std::ptr::eq(read, id.descriptor()),
1690 "the slot generated code reads for {id:?} holds `{}`",
1691 // SAFETY: every slot holds a `&'static TypeDescriptor`'s address.
1692 unsafe { (*read).name }
1693 );
1694 }
1695 }
1696
1697 /// The table is the same in every context a process mints, which is what
1698 /// lets the backend fold a displacement and nothing else.
1699 ///
1700 /// Two runtimes are two heaps, two fault slots and two shadow stacks; they
1701 /// are not two sets of built-in descriptors, because those are `static`s of
1702 /// this binary. A future runtime that minted per-`Runtime` descriptors
1703 /// would make code compiled for one unusable against another, and this is
1704 /// where that would be noticed.
1705 #[test]
1706 fn two_runtimes_agree_on_every_descriptor_address() {
1707 let mut first = Runtime::new();
1708 let mut second = Runtime::new();
1709 assert_eq!(first.context().descriptors, second.context().descriptors);
1710 }
1711
1712 /// A placeholder carries the real table, alone among its fields.
1713 ///
1714 /// Every pointer `placeholder` nulls is one a `Runtime` has to wire. These
1715 /// are not: they are addresses of `static`s, valid before `main`. Nulling
1716 /// them would be a trap for a state that cannot arise, and it would make a
1717 /// placeholder fail ADR-102's proof by segfaulting on the load rather than
1718 /// by comparing unequal.
1719 #[test]
1720 fn a_placeholder_context_still_knows_every_builtin_descriptor() {
1721 let mut header = GcHeader::detached();
1722 let nn = NonNull::from(&mut header);
1723 // SAFETY: local live header for the duration of this test.
1724 let gcref = unsafe { GcRef::from_non_null(nn) };
1725 let ctx = unsafe { RuntimeContext::placeholder(gcref) };
1726 assert!(std::ptr::eq(
1727 ctx.descriptors[BuiltinTypeId::Int as usize],
1728 &crate::scalars::INT
1729 ));
1730 assert!(ctx.descriptors.iter().all(|d| !d.is_null()));
1731 }
1732
1733 #[test]
1734 fn placeholder_reports_no_fault() {
1735 let mut header = GcHeader::detached();
1736 let nn = NonNull::from(&mut header);
1737 // SAFETY: local live header for the duration of this test.
1738 let gcref = unsafe { GcRef::from_non_null(nn) };
1739 let ctx = unsafe { RuntimeContext::placeholder(gcref) };
1740 assert!(!ctx.has_pending_fault());
1741 assert_eq!(ctx.current_generation, 0);
1742 }
1743
1744 #[test]
1745 fn has_pending_fault_flips_with_non_null_pointer() {
1746 let mut header = GcHeader::detached();
1747 let nn = NonNull::from(&mut header);
1748 let gcref = unsafe { GcRef::from_non_null(nn) };
1749 let mut ctx = unsafe { RuntimeContext::placeholder(gcref) };
1750 assert!(!ctx.has_pending_fault());
1751 let mut fault = Fault::clear();
1752 fault.set(RaisedFault::INT_OVERFLOW);
1753 ctx.pending_fault = &mut fault;
1754 assert!(ctx.has_pending_fault());
1755 }
1756
1757 /// `set` takes a [`RaisedFault`] and there is no `RaisedFault` for `None`,
1758 /// so `fault.set(FaultKind::None)` does not compile: the property is
1759 /// structural. What is left to test is the one place a `FaultKind` arriving
1760 /// as data becomes a raisable one — and that it rejects the absence of a
1761 /// fault.
1762 #[test]
1763 fn setting_none_cannot_create_a_pending_fault() {
1764 assert!(
1765 RaisedFault::new(FaultKind::None).is_none(),
1766 "FaultKind::None represents the absence of a fault and cannot be raised"
1767 );
1768
1769 let mut fault = Fault::clear();
1770 assert!(!fault.is_pending());
1771 assert_eq!(fault.kind(), FaultKind::None);
1772
1773 // Every other kind round-trips, and raising one is what makes a fault
1774 // pending — there is no second field to disagree with the kind.
1775 for kind in [
1776 FaultKind::IntOverflow,
1777 FaultKind::DivByZero,
1778 FaultKind::IndexOutOfBounds,
1779 FaultKind::ParseFailed,
1780 FaultKind::EmptyCollection,
1781 FaultKind::StackOverflow,
1782 FaultKind::FloatToInt,
1783 FaultKind::InvalidChar,
1784 FaultKind::InvalidText,
1785 ] {
1786 let raised = RaisedFault::new(kind).expect("every non-None kind is raisable");
1787 assert_eq!(raised.kind(), kind);
1788 fault.set(raised);
1789 assert!(fault.is_pending(), "{kind} must be pending once raised");
1790 assert_eq!(fault.kind(), kind);
1791 }
1792 }
1793
1794 #[test]
1795 fn runtime_allocates_and_reads_scalars() {
1796 let rt = Runtime::new();
1797 let i = rt.alloc_int(-123);
1798 assert_eq!(i.as_int(), -123);
1799 let b = rt.alloc_bool(true);
1800 assert!(b.as_bool());
1801 let by = rt.alloc_byte(200);
1802 assert_eq!(by.as_byte(), 200);
1803 let c = rt.alloc_char('€' as u32);
1804 assert_eq!(c.as_char(), '€');
1805 let t = rt.alloc_text("héllo");
1806 assert_eq!(t.as_text(), "héllo");
1807 assert_eq!(rt.alloc_unit().as_ptr(), rt.immortals().unit().as_ptr());
1808 }
1809
1810 #[test]
1811 fn runtime_formats_and_compares() {
1812 let rt = Runtime::new();
1813 let a = rt.alloc_int(42);
1814 let b = rt.alloc_int(42);
1815 let c = rt.alloc_int(43);
1816 assert!(a.equals(&b));
1817 assert!(!a.equals(&c));
1818
1819 let mut out = String::new();
1820 a.format(&mut out);
1821 assert_eq!(out, "42");
1822 }
1823
1824 #[test]
1825 fn runtime_vec_allocates_and_reads() {
1826 let rt = Runtime::new();
1827 let e0 = rt.alloc_int(1);
1828 let e1 = rt.alloc_int(2);
1829 let v = rt.alloc_vec(&crate::scalars::INT, vec![e0, e1]);
1830 assert_eq!(v.descriptor().name, "Vec");
1831 assert_eq!(v.as_vec().len(), 2);
1832
1833 let mut out = String::new();
1834 v.format(&mut out);
1835 assert_eq!(out, "[1, 2]");
1836 }
1837
1838 #[test]
1839 fn runtime_collect_keeps_immortals_alive_unrooted() {
1840 // Immortals are out-of-band; a collection with no roots must not touch
1841 // them. Capture each singleton's address before the collection and assert
1842 // the same address afterwards (a self-comparison would assert nothing).
1843 let rt = Runtime::new();
1844 let unit_before = rt.immortals().unit().as_ptr();
1845 let true_before = rt.immortals().true_().as_ptr();
1846 let false_before = rt.immortals().false_().as_ptr();
1847 let roots = RootScope::new();
1848 rt.collect_with(&roots);
1849 assert_eq!(rt.immortals().unit().as_ptr(), unit_before);
1850 assert_eq!(rt.immortals().true_().as_ptr(), true_before);
1851 assert_eq!(rt.immortals().false_().as_ptr(), false_before);
1852 }
1853
1854 #[test]
1855 #[should_panic(expected = "not an Int")]
1856 fn as_int_rejects_wrong_descriptor() {
1857 let rt = Runtime::new();
1858 let b = rt.alloc_bool(false);
1859 let _ = b.as_int();
1860 }
1861
1862 #[test]
1863 fn a_rerun_starts_from_an_empty_shadow_stack() {
1864 // The `restart`/`reload` path (§9.7): the debugger reruns `main` against
1865 // the same `Runtime`. A run that faulted still restored every frame on
1866 // the way out — the fault epilogue is an epilogue — so the stack is
1867 // already empty, and `clear_for_rerun` says so with a `debug_assert`
1868 // before resetting it. That reset is the backstop, not the mechanism.
1869 let mut rt = Runtime::new();
1870 let mut ctx = rt.context();
1871 // SAFETY: `ctx` is wired to `rt`, which outlives the guard.
1872 let guard = unsafe {
1873 crate::shadow_stack::push_frame(
1874 &mut ctx as *mut RuntimeContext,
1875 crate::shadow_stack::SlotCount::new(5).unwrap(),
1876 )
1877 };
1878 assert_eq!(rt.shadow_stack().len(), 5);
1879 drop(guard);
1880 assert!(rt.shadow_stack().is_empty());
1881 rt.clear_for_rerun();
1882 assert!(rt.shadow_stack().is_empty());
1883 }
1884}