Skip to main content

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}