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}