Skip to main content

tatara_lisp_eval/
observe.rs

1//! Watching closure calls: the one place a profiler, a coverage counter, a
2//! tracer and an error's call stack all read from.
3//!
4//! Every closure call the tree-walker makes passes one site,
5//! `eval::call_closure_body`, once per activation — a tail call included,
6//! since the trampoline re-enters the same loop. That site has two arms:
7//!
8//! - **the observer arm**, an optional [`CallObserver`] the host installs with
9//!   [`crate::Interpreter::set_call_observer`]. It sees each activation enter
10//!   with its arguments and leave with how it left ([`Exit`]), and may answer
11//!   a call itself instead of letting the body run. With none installed, the
12//!   cost is one `Option` test per call;
13//! - **the unwind arm**, always on, because it costs nothing until something
14//!   fails: an error leaving an activation records that activation as a
15//!   [`TraceFrame`], innermost first, and the host reads the result with
16//!   [`crate::Interpreter::take_trace`]. A `try` that catches the error
17//!   discards what it recorded, since the frames belong to an error nobody
18//!   will report.
19//!
20//! The tree-walker only. The VM's frames are its own and report neither arm.
21
22use std::sync::{Arc, Mutex, PoisonError};
23
24use tatara_lisp::Span;
25
26use crate::error::EvalError;
27use crate::value::Value;
28
29/// One closure activation as the evaluator sees it.
30#[derive(Clone, Copy, Debug)]
31pub struct Call<'a> {
32    /// The name `(define (name …) …)` gave the closure; `None` for a lambda.
33    pub name: Option<&'a Arc<str>>,
34    /// Where the closure was defined: the `define` or `lambda` form.
35    pub source: Span,
36    /// Where this activation was called from.
37    pub at: Span,
38}
39
40/// How an activation left.
41#[derive(Clone, Copy, Debug)]
42pub enum Exit<'a> {
43    Returned(&'a Value),
44    /// Its last form called another closure in tail position, which replaces
45    /// it: the callee enters next, as a sibling rather than a child.
46    TailCalled,
47    Raised(&'a EvalError),
48    /// The observer's `enter` answered for it, and its body never ran.
49    Supplied(&'a Value),
50}
51
52/// A host's view of closure calls, and its one say in them. Each `enter` is
53/// followed by exactly one `leave` for the same activation, on every path out
54/// of it.
55pub trait CallObserver: Send + Sync {
56    /// Before the body runs, with the arguments bound. `None` lets it run;
57    /// `Some(value)` makes `value` the call's result without running the body
58    /// (a memo table answering), and `leave` then sees [`Exit::Supplied`].
59    fn enter(&self, call: &Call<'_>, args: &[Value]) -> Option<Value>;
60    fn leave(&self, call: &Call<'_>, exit: Exit<'_>);
61}
62
63/// One activation an error unwound through.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct TraceFrame {
66    pub name: Option<Arc<str>>,
67    pub source: Span,
68    pub at: Span,
69}
70
71/// The activations an error unwound through, innermost first.
72///
73/// Bounded: past [`TRACE_FRAMES`] only the count grows, so a recursion refused
74/// at a depth of 100,000 costs a counter, not 100,000 frames.
75#[derive(Clone, Debug, Default, PartialEq, Eq)]
76pub struct Trace {
77    pub frames: Vec<TraceFrame>,
78    /// Every activation unwound, kept or not.
79    pub depth: usize,
80}
81
82/// How many frames a [`Trace`] keeps.
83pub const TRACE_FRAMES: usize = 256;
84
85/// The unwind arm's record. Per incarnation, like the budget's counters: a
86/// fork starts with none.
87#[derive(Default)]
88pub(crate) struct Unwind(Mutex<Trace>);
89
90impl Unwind {
91    pub(crate) fn push(&self, call: &Call<'_>) {
92        let mut t = self.0.lock().unwrap_or_else(PoisonError::into_inner);
93        t.depth += 1;
94        if t.frames.len() < TRACE_FRAMES {
95            t.frames.push(TraceFrame {
96                name: call.name.cloned(),
97                source: call.source,
98                at: call.at,
99            });
100        }
101    }
102
103    pub(crate) fn clear(&self) {
104        let mut t = self.0.lock().unwrap_or_else(PoisonError::into_inner);
105        if t.depth > 0 {
106            *t = Trace::default();
107        }
108    }
109
110    pub(crate) fn take(&self) -> Trace {
111        std::mem::take(&mut *self.0.lock().unwrap_or_else(PoisonError::into_inner))
112    }
113}