snix_eval/
observer.rs

1//! Implements traits for things that wish to observe internal state
2//! changes of snix-eval.
3//!
4//! This can be used to gain insights from compilation, to trace the
5//! runtime, and so on.
6//!
7//! All methods are optional, that is, observers can implement only
8/// what they are interested in observing.
9use std::io::Write;
10use std::rc::Rc;
11use std::time::Instant;
12use tabwriter::TabWriter;
13
14use crate::SourceCode;
15use crate::Value;
16use crate::chunk::Chunk;
17use crate::generators::VMRequest;
18use crate::opcode::{CodeIdx, Op};
19use crate::value::Lambda;
20
21/// Implemented by types that wish to observe internal happenings of
22/// the Snix compiler.
23pub trait CompilerObserver {
24    /// Called when the compiler finishes compilation of the top-level
25    /// of an expression (usually the root Nix expression of a file).
26    fn observe_compiled_toplevel(&mut self, _: &Rc<Lambda>) {}
27
28    /// Called when the compiler finishes compilation of a
29    /// user-defined function.
30    ///
31    /// Note that in Nix there are only single argument functions, so
32    /// in an expression like `a: b: c: ...` this method will be
33    /// called three times.
34    fn observe_compiled_lambda(&mut self, _: &Rc<Lambda>) {}
35
36    /// Called when the compiler finishes compilation of a thunk.
37    fn observe_compiled_thunk(&mut self, _: &Rc<Lambda>) {}
38}
39
40/// Implemented by types that wish to observe internal happenings of
41/// the Snix virtual machine at runtime.
42pub trait RuntimeObserver {
43    /// Called when the runtime enters a new call frame.
44    fn observe_enter_call_frame(&mut self, _arg_count: usize, _: &Rc<Lambda>, _call_depth: usize) {}
45
46    /// Called when the runtime exits a call frame.
47    fn observe_exit_call_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
48
49    /// Called when the runtime suspends a call frame.
50    fn observe_suspend_call_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
51
52    /// Called when the runtime enters a generator frame.
53    fn observe_enter_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
54
55    /// Called when the runtime exits a generator frame.
56    fn observe_exit_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
57
58    /// Called when the runtime suspends a generator frame.
59    fn observe_suspend_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
60
61    /// Called when a generator requests an action from the VM.
62    fn observe_generator_request(&mut self, _name: &str, _msg: &VMRequest) {}
63
64    /// Called when the runtime replaces the current call frame for a
65    /// tail call.
66    fn observe_tail_call(&mut self, _frame_at: usize, _: &Rc<Lambda>) {}
67
68    /// Called when the runtime enters a builtin.
69    fn observe_enter_builtin(&mut self, _name: &'static str) {}
70
71    /// Called when the runtime exits a builtin.
72    fn observe_exit_builtin(&mut self, _name: &'static str, _stack: &[Value]) {}
73
74    /// Called when the runtime *begins* executing an instruction. The
75    /// provided stack is the state at the beginning of the operation.
76    fn observe_execute_op(&mut self, _ip: CodeIdx, _: &Op, _: &[Value]) {}
77}
78
79#[derive(Default)]
80pub struct NoOpObserver {}
81
82impl CompilerObserver for NoOpObserver {}
83impl RuntimeObserver for NoOpObserver {}
84
85/// An observer that prints disassembled chunk information to its
86/// internal writer whenwever the compiler emits a toplevel function,
87/// closure or thunk.
88pub struct DisassemblingObserver<W: Write> {
89    source: SourceCode,
90    writer: TabWriter<W>,
91}
92
93impl<W: Write> DisassemblingObserver<W> {
94    pub fn new(source: SourceCode, writer: W) -> Self {
95        Self {
96            source,
97            writer: TabWriter::new(writer),
98        }
99    }
100
101    fn lambda_header(&mut self, kind: &str, lambda: &Rc<Lambda>) {
102        let _ = writeln!(
103            &mut self.writer,
104            "=== compiled {} @ {:p} ({} ops, {} length) ===",
105            kind,
106            *lambda,
107            lambda.chunk.op_count(),
108            lambda.chunk.code.len(),
109        );
110    }
111
112    fn disassemble_chunk(&mut self, chunk: &Chunk) {
113        // calculate width of the widest address in the chunk
114        let width = format!("{:#x}", chunk.code.len() - 1).len();
115
116        let mut idx = 0;
117        while idx < chunk.code.len() {
118            let size = chunk
119                .disassemble_op(&mut self.writer, &self.source, width, CodeIdx(idx))
120                .expect("writing debug output should work");
121            idx += size;
122        }
123    }
124}
125
126impl<W: Write> CompilerObserver for DisassemblingObserver<W> {
127    fn observe_compiled_toplevel(&mut self, lambda: &Rc<Lambda>) {
128        self.lambda_header("toplevel", lambda);
129        self.disassemble_chunk(&lambda.chunk);
130        let _ = self.writer.flush();
131    }
132
133    fn observe_compiled_lambda(&mut self, lambda: &Rc<Lambda>) {
134        self.lambda_header("lambda", lambda);
135        self.disassemble_chunk(&lambda.chunk);
136        let _ = self.writer.flush();
137    }
138
139    fn observe_compiled_thunk(&mut self, lambda: &Rc<Lambda>) {
140        self.lambda_header("thunk", lambda);
141        self.disassemble_chunk(&lambda.chunk);
142        let _ = self.writer.flush();
143    }
144}
145
146/// An observer that collects a textual representation of an entire
147/// runtime execution.
148pub struct TracingObserver<W: Write> {
149    // If timing is enabled, contains the timestamp of the last-emitted trace event
150    last_event: Option<Instant>,
151    writer: TabWriter<W>,
152}
153
154impl<W: Write> TracingObserver<W> {
155    pub fn new(writer: W) -> Self {
156        Self {
157            last_event: None,
158            writer: TabWriter::new(writer),
159        }
160    }
161
162    /// Write the time of each runtime event, relative to when this method is called
163    pub fn enable_timing(&mut self) {
164        self.last_event = Some(Instant::now());
165    }
166
167    fn maybe_write_time(&mut self) {
168        if let Some(last_event) = &mut self.last_event {
169            let _ = write!(&mut self.writer, "+{}ns\t", last_event.elapsed().as_nanos());
170            *last_event = Instant::now();
171        }
172    }
173
174    fn write_value(&mut self, val: &Value) {
175        let _ = match val {
176            // Potentially large types which we only want to print
177            // the type of (and avoid recursing).
178            Value::List(l) => write!(&mut self.writer, "list[{}] ", l.len()),
179            Value::Attrs(a) => write!(&mut self.writer, "attrs[{}] ", a.len()),
180            Value::Thunk(t) if t.is_evaluated() => {
181                self.write_value(&t.value());
182                Ok(())
183            }
184
185            // For other value types, defer to the standard value printer.
186            _ => write!(&mut self.writer, "{val} "),
187        };
188    }
189
190    fn write_stack(&mut self, stack: &[Value]) {
191        let _ = write!(&mut self.writer, "[ ");
192
193        // Print out a maximum of 6 values from the top of the stack,
194        // before abbreviating it to `...`.
195        for (i, val) in stack.iter().rev().enumerate() {
196            if i == 6 {
197                let _ = write!(&mut self.writer, "...");
198                break;
199            }
200
201            self.write_value(val);
202        }
203
204        let _ = writeln!(&mut self.writer, "]");
205    }
206}
207
208impl<W: Write> RuntimeObserver for TracingObserver<W> {
209    fn observe_enter_call_frame(
210        &mut self,
211        arg_count: usize,
212        lambda: &Rc<Lambda>,
213        call_depth: usize,
214    ) {
215        self.maybe_write_time();
216
217        let _ = write!(&mut self.writer, "=== entering ");
218
219        let _ = if arg_count == 0 {
220            write!(&mut self.writer, "thunk ")
221        } else {
222            write!(&mut self.writer, "closure ")
223        };
224
225        if let Some(name) = &lambda.name {
226            let _ = write!(&mut self.writer, "'{name}' ");
227        }
228
229        let _ = writeln!(
230            &mut self.writer,
231            "in frame[{}] @ {:p} ===",
232            call_depth, *lambda
233        );
234    }
235
236    /// Called when the runtime exits a call frame.
237    fn observe_exit_call_frame(&mut self, frame_at: usize, stack: &[Value]) {
238        self.maybe_write_time();
239        let _ = write!(&mut self.writer, "=== exiting frame {frame_at} ===\t ");
240        self.write_stack(stack);
241    }
242
243    fn observe_suspend_call_frame(&mut self, frame_at: usize, stack: &[Value]) {
244        self.maybe_write_time();
245        let _ = write!(&mut self.writer, "=== suspending frame {frame_at} ===\t");
246
247        self.write_stack(stack);
248    }
249
250    fn observe_enter_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
251        self.maybe_write_time();
252        let _ = write!(
253            &mut self.writer,
254            "=== entering generator frame '{name}' [{frame_at}] ===\t",
255        );
256
257        self.write_stack(stack);
258    }
259
260    fn observe_exit_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
261        self.maybe_write_time();
262        let _ = write!(
263            &mut self.writer,
264            "=== exiting generator '{name}' [{frame_at}] ===\t"
265        );
266
267        self.write_stack(stack);
268    }
269
270    fn observe_suspend_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
271        self.maybe_write_time();
272        let _ = write!(
273            &mut self.writer,
274            "=== suspending generator '{name}' [{frame_at}] ===\t"
275        );
276
277        self.write_stack(stack);
278    }
279
280    fn observe_generator_request(&mut self, name: &str, msg: &VMRequest) {
281        self.maybe_write_time();
282        let _ = writeln!(
283            &mut self.writer,
284            "=== generator '{name}' requested {msg} ==="
285        );
286    }
287
288    fn observe_enter_builtin(&mut self, name: &'static str) {
289        self.maybe_write_time();
290        let _ = writeln!(&mut self.writer, "=== entering builtin {name} ===");
291    }
292
293    fn observe_exit_builtin(&mut self, name: &'static str, stack: &[Value]) {
294        self.maybe_write_time();
295        let _ = write!(&mut self.writer, "=== exiting builtin {name} ===\t");
296        self.write_stack(stack);
297    }
298
299    fn observe_tail_call(&mut self, frame_at: usize, lambda: &Rc<Lambda>) {
300        self.maybe_write_time();
301        let _ = writeln!(
302            &mut self.writer,
303            "=== tail-calling {:p} in frame[{}] ===",
304            *lambda, frame_at
305        );
306    }
307
308    fn observe_execute_op(&mut self, ip: CodeIdx, op: &Op, stack: &[Value]) {
309        self.maybe_write_time();
310        let _ = write!(&mut self.writer, "{:04} {:?}\t", ip.0, op);
311        self.write_stack(stack);
312    }
313}
314
315impl<W: Write> Drop for TracingObserver<W> {
316    fn drop(&mut self) {
317        let _ = self.writer.flush();
318    }
319}