Skip to main content

praxis_runtime/
breakpoint.rs

1//! Breakpoint stops: what a `:bp` marker does at runtime (§9.8).
2//!
3//! A fault snapshot is taken while the stack is *unwinding* and is read after
4//! every language frame has gone (see [`crate::crash_snapshot`]). A breakpoint is
5//! the other case: the frames are still claimed, the program is still in the
6//! middle of them, and the whole point is to go back afterwards. So this module
7//! is deliberately not a second fault path — it is a call out to the host and a
8//! return.
9//!
10//! ## The handler is given a snapshot and nothing else, and that is the design
11//!
12//! [`BreakpointHandler`] takes a `&`[`BreakpointStop`] and no `*mut
13//! RuntimeContext`. It has no heap pointer, no allocator, no way to run
14//! generated code — so **it cannot trigger a collection**, and that is a
15//! structural fact rather than a rule a host has to remember.
16//!
17//! That matters because of what the stop hands over. [`BreakpointStop::frames`]
18//! is a deep copy of the live debug chain, and its `GcRef`s are only as valid as
19//! the objects they name. Values *below* the debug stack's `top` are ADR-106's
20//! weak arm — every collection clears the slots whose objects it reclaimed — so
21//! at the instant the copy is taken every reference in it is live. Keeping it
22//! that way for the duration of the stop needs one thing: that no collection
23//! happens while the host is looking. Withholding the context is how that is
24//! guaranteed, and it is why the snapshot does not need to be registered as a
25//! root set the way a *fault* snapshot does (which the host holds across
26//! `restart`, `p EXPR` and everything else that allocates).
27//!
28//! ## Detaching, and why there is no `kill`
29//!
30//! [`Resume`] has two arms. `Continue` returns to the program. `Detach` returns
31//! to the program *and disarms every later stop*, which is what "quit the
32//! debugger" means from inside a live run.
33//!
34//! There is no third arm that ends the program, because there is nothing sound
35//! to do with the frames: §9.2 forbids unwinding Rust through JIT frames, and
36//! the fault epilogue that *can* unwind them is reached by raising a fault —
37//! which would report the program as having failed when it did not. A host that
38//! wants the run over can exit the process from its handler; the language will
39//! not pretend a stop is a fault.
40
41use std::cell::Cell;
42
43use crate::crash_snapshot::CrashSnapshot;
44
45/// One `:bp` stop, as the host sees it.
46///
47/// Carries no context, no heap and no way back into the runtime — see the module
48/// header for why that absence is the point.
49///
50/// **Handed to the handler by value, and it must not survive the call.** The
51/// `GcRef`s in [`frames`](Self::frames) name objects that are live *at the
52/// instant of the stop*; the moment the handler returns, the program runs on and
53/// the next allocation may collect any of them. Every other root set in the
54/// system is registered with the collector, and this one deliberately is not —
55/// the handler holds it for the length of one call, which is the whole window in
56/// which no collection can happen (module header). A host that stashes it is
57/// keeping a root set the collector cannot see.
58pub struct BreakpointStop {
59    /// The frame chain at the stop, innermost first: frame 0 is the function
60    /// holding the marker, the last is the program's entry point.
61    ///
62    /// A [`CrashSnapshot`] because it is the same deep copy of the same debug
63    /// chain, produced by the same walk. Its
64    /// [`fault_kind`](CrashSnapshot::fault_kind) is
65    /// [`FaultKind::None`](crate::FaultKind::None), which is the honest answer:
66    /// nothing went wrong, the program stopped because it was asked to.
67    pub frames: CrashSnapshot,
68    /// The `:bp` marker's own source span `[start, end)`, so the host can point
69    /// at the line the program stopped on rather than at the function's extent.
70    pub span: (u32, u32),
71    /// How many stops this run has made, counting this one. `1` on the first.
72    ///
73    /// The host's cheapest way to say "the tenth time round the loop" without
74    /// keeping its own counter, and the runtime's already: the count is what
75    /// decides whether a stop is the first one, which is the only thing the
76    /// runtime itself does with it.
77    pub hits: u64,
78}
79
80/// What the host wants done after a stop.
81#[derive(Clone, Copy, PartialEq, Eq, Debug)]
82pub enum Resume {
83    /// Return to the program. The next `:bp` stops again.
84    Continue,
85    /// Return to the program and stop at nothing else this run.
86    Detach,
87}
88
89/// A host's breakpoint handler: given a stop, answers what to do next.
90///
91/// A plain `fn` for [`InputReader`](crate::InputReader)'s reason: it is stored
92/// across the ABI boundary and called from generated code's stack, so it carries
93/// no captured state and no lifetime. A host that needs state puts it in its own
94/// thread-local, which is what `praxis-cli` does.
95///
96/// The stop arrives **by value** because the debugger the CLI builds from it
97/// owns its snapshot, and it must be dropped before the handler returns — see
98/// [`BreakpointStop`].
99pub type BreakpointHandler = fn(BreakpointStop) -> Resume;
100
101thread_local! {
102    /// The installed handler, or none. Thread-local for
103    /// [`crate::input`]'s reason: the runtime is single-threaded (§12.1) and
104    /// there is one program per process.
105    static HANDLER: Cell<Option<BreakpointHandler>> = const { Cell::new(None) };
106    /// Stops made this run — [`BreakpointStop::hits`]'s source.
107    static HITS: Cell<u64> = const { Cell::new(0) };
108    /// Set by a [`Resume::Detach`]; makes every later stop a no-op until
109    /// [`install_breakpoint_handler`] arms a fresh run.
110    static DETACHED: Cell<bool> = const { Cell::new(false) };
111}
112
113/// Install the host's breakpoint handler, armed and with a fresh hit count.
114///
115/// A program compiled with `:bp` markers and run without one stops at nothing:
116/// the marker's call finds no handler and returns, which is the right behaviour
117/// for an embedder that has no debugger, and for every JIT test.
118pub fn install_breakpoint_handler(handler: BreakpointHandler) {
119    HANDLER.with(|slot| slot.set(Some(handler)));
120    HITS.with(|slot| slot.set(0));
121    DETACHED.with(|slot| slot.set(false));
122}
123
124/// Forget any installed handler, so every later `:bp` is a no-op.
125///
126/// The crash debugger's path needs this: a fault hands the terminal to the
127/// debugger, and `restart` (§9.7) re-runs the program *from inside that screen*
128/// — a stop handler firing there would take the terminal from the debugger
129/// already holding it. Disarming before the hand-off is what keeps the two
130/// surfaces from fighting over it.
131pub fn clear_breakpoint_handler() {
132    HANDLER.with(|slot| slot.set(None));
133}
134
135/// Whether a stop has detached (a [`Resume::Detach`] answer). The host's way to
136/// tell a run the user walked away from apart from one that simply had no
137/// markers left.
138#[must_use]
139pub fn breakpoints_detached() -> bool {
140    DETACHED.with(Cell::get)
141}
142
143/// Take the stop: deep-copy the live debug chain, hand it to the installed
144/// handler, and act on its answer.
145///
146/// # Safety
147/// `ctx` must be live and wired, and every claimed debug frame entry must
148/// satisfy `copy_stack`'s contract — which every generated prologue establishes.
149pub(crate) unsafe fn stop(ctx: *mut crate::RuntimeContext, span: (u32, u32)) {
150    if DETACHED.with(Cell::get) {
151        return;
152    }
153    // Taken rather than borrowed for the duration, so a handler that somehow
154    // re-entered this function finds none and returns instead of recursing.
155    let Some(handler) = HANDLER.with(Cell::take) else {
156        return;
157    };
158    let hits = HITS.with(|slot| {
159        let n = slot.get().saturating_add(1);
160        slot.set(n);
161        n
162    });
163    // SAFETY: the caller guarantees `ctx` is live and wired.
164    let frames = unsafe { crate::crash_snapshot::copy_live_chain(ctx) };
165    let resume = handler(BreakpointStop { frames, span, hits });
166    HANDLER.with(|slot| slot.set(Some(handler)));
167    if resume == Resume::Detach {
168        DETACHED.with(|slot| slot.set(true));
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    fn keep_going(_: BreakpointStop) -> Resume {
177        Resume::Continue
178    }
179
180    /// Installing arms: a handler installed after a detached run stops again.
181    #[test]
182    fn installing_resets_the_detach_and_the_count() {
183        DETACHED.with(|slot| slot.set(true));
184        HITS.with(|slot| slot.set(7));
185        assert!(breakpoints_detached());
186        install_breakpoint_handler(keep_going);
187        assert!(!breakpoints_detached());
188        assert_eq!(HITS.with(Cell::get), 0);
189        clear_breakpoint_handler();
190    }
191
192    /// A program run with no handler installed stops at nothing — the state an
193    /// embedder that never asked for a debugger is in.
194    #[test]
195    fn no_handler_means_no_stop() {
196        clear_breakpoint_handler();
197        assert!(HANDLER.with(Cell::get).is_none());
198    }
199}