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 bytecode frame.
44    fn observe_enter_bytecode_frame(
45        &mut self,
46        _arg_count: usize,
47        _: &Rc<Lambda>,
48        _call_depth: usize,
49    ) {
50    }
51
52    /// Called when the runtime exits a bytecode frame.
53    fn observe_exit_bytecode_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
54
55    /// Called when the runtime suspends a bytecode frame.
56    fn observe_suspend_bytecode_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
57
58    /// Called when the runtime enters a generator frame.
59    fn observe_enter_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
60
61    /// Called when the runtime exits a generator frame.
62    fn observe_exit_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
63
64    /// Called when the runtime suspends a generator frame.
65    fn observe_suspend_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
66
67    /// Called when a generator requests an action from the VM.
68    fn observe_generator_request(&mut self, _name: &str, _msg: &VMRequest) {}
69
70    /// Called when the runtime replaces the current bytecode frame for a
71    /// tail call.
72    fn observe_tail_call(&mut self, _frame_at: usize, _: &Rc<Lambda>) {}
73
74    /// Called when the runtime enters a builtin.
75    fn observe_enter_builtin(&mut self, _name: &'static str) {}
76
77    /// Called when the runtime exits a builtin.
78    fn observe_exit_builtin(&mut self, _name: &'static str, _stack: &[Value]) {}
79
80    /// Called when the runtime *begins* executing an instruction. The
81    /// provided stack is the state at the beginning of the operation.
82    fn observe_execute_op(&mut self, _ip: CodeIdx, _: &Op, _: &[Value]) {}
83}
84
85#[derive(Default)]
86pub struct NoOpObserver {}
87
88impl CompilerObserver for NoOpObserver {}
89impl RuntimeObserver for NoOpObserver {}
90
91/// Compiler observer that is optimised for the case where no observer is being used.
92///
93/// The trait RuntimeObserver is implemented on the Optional<dyn
94/// RuntimeObserver>. This removes the dynamic dispatch overhead when
95/// no observer is being used.
96#[derive(Default)]
97pub struct OptionalCompilerObserver<'o>(pub Option<&'o mut dyn CompilerObserver>);
98impl<'o> CompilerObserver for OptionalCompilerObserver<'o> {
99    fn observe_compiled_toplevel(&mut self, lambda: &Rc<Lambda>) {
100        if let Some(ref mut obs) = self.0 {
101            obs.observe_compiled_toplevel(lambda);
102        }
103    }
104
105    fn observe_compiled_lambda(&mut self, lambda: &Rc<Lambda>) {
106        if let Some(ref mut obs) = self.0 {
107            obs.observe_compiled_lambda(lambda);
108        }
109    }
110
111    fn observe_compiled_thunk(&mut self, lambda: &Rc<Lambda>) {
112        if let Some(ref mut obs) = self.0 {
113            obs.observe_compiled_thunk(lambda)
114        }
115    }
116}
117
118impl<'o> From<&'o mut dyn CompilerObserver> for OptionalCompilerObserver<'o> {
119    fn from(val: &'o mut dyn CompilerObserver) -> Self {
120        OptionalCompilerObserver(Some(val))
121    }
122}
123
124impl<'o> From<Option<&'o mut dyn CompilerObserver>> for OptionalCompilerObserver<'o> {
125    fn from(val: Option<&'o mut dyn CompilerObserver>) -> Self {
126        Self(val)
127    }
128}
129
130/// Runtime observer that is optimised for the case where no observer is being used.
131///
132/// The trait RuntimeObserver is implemented on the Optional<dyn
133/// RuntimeObserver>. This removes the dynamic dispatch overhead when
134/// no observer is being used.
135pub struct OptionalRuntimeObserver<'o>(pub Option<&'o mut dyn RuntimeObserver>);
136
137impl<'o> From<&'o mut dyn RuntimeObserver> for OptionalRuntimeObserver<'o> {
138    fn from(val: &'o mut dyn RuntimeObserver) -> Self {
139        OptionalRuntimeObserver(Some(val))
140    }
141}
142
143impl<'o> RuntimeObserver for OptionalRuntimeObserver<'o> {
144    #[inline(always)]
145    fn observe_enter_bytecode_frame(
146        &mut self,
147        arg_count: usize,
148        lambda: &Rc<Lambda>,
149        call_depth: usize,
150    ) {
151        if let Some(ref mut obs) = self.0 {
152            obs.observe_enter_bytecode_frame(arg_count, lambda, call_depth);
153        }
154    }
155
156    #[inline(always)]
157    fn observe_exit_bytecode_frame(&mut self, frame_at: usize, stack: &[Value]) {
158        if let Some(ref mut obs) = self.0 {
159            obs.observe_exit_bytecode_frame(frame_at, stack);
160        }
161    }
162
163    #[inline(always)]
164    fn observe_suspend_bytecode_frame(&mut self, frame_at: usize, stack: &[Value]) {
165        if let Some(ref mut obs) = self.0 {
166            obs.observe_suspend_bytecode_frame(frame_at, stack);
167        }
168    }
169
170    #[inline(always)]
171    fn observe_enter_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
172        if let Some(ref mut obs) = self.0 {
173            obs.observe_enter_generator(frame_at, name, stack);
174        }
175    }
176
177    #[inline(always)]
178    fn observe_exit_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
179        if let Some(ref mut obs) = self.0 {
180            obs.observe_exit_generator(frame_at, name, stack);
181        }
182    }
183
184    #[inline(always)]
185    fn observe_suspend_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
186        if let Some(ref mut obs) = self.0 {
187            obs.observe_suspend_generator(frame_at, name, stack);
188        }
189    }
190
191    #[inline(always)]
192    fn observe_generator_request(&mut self, name: &str, msg: &VMRequest) {
193        if let Some(ref mut obs) = self.0 {
194            obs.observe_generator_request(name, msg);
195        }
196    }
197
198    #[inline(always)]
199    fn observe_tail_call(&mut self, frame_at: usize, lambda: &Rc<Lambda>) {
200        if let Some(ref mut obs) = self.0 {
201            obs.observe_tail_call(frame_at, lambda);
202        }
203    }
204
205    #[inline(always)]
206    fn observe_enter_builtin(&mut self, name: &'static str) {
207        if let Some(ref mut obs) = self.0 {
208            obs.observe_enter_builtin(name);
209        }
210    }
211
212    #[inline(always)]
213    fn observe_exit_builtin(&mut self, name: &'static str, stack: &[Value]) {
214        if let Some(ref mut obs) = self.0 {
215            obs.observe_exit_builtin(name, stack);
216        }
217    }
218
219    #[inline(always)]
220    fn observe_execute_op(&mut self, ip: CodeIdx, op: &Op, stack: &[Value]) {
221        if let Some(ref mut obs) = self.0 {
222            obs.observe_execute_op(ip, op, stack);
223        }
224    }
225}
226
227/// An observer that prints disassembled chunk information to its
228/// internal writer whenwever the compiler emits a toplevel function,
229/// closure or thunk.
230pub struct DisassemblingObserver<W: Write> {
231    source: SourceCode,
232    writer: TabWriter<W>,
233}
234
235impl<W: Write> DisassemblingObserver<W> {
236    pub fn new(source: SourceCode, writer: W) -> Self {
237        Self {
238            source,
239            writer: TabWriter::new(writer),
240        }
241    }
242
243    fn lambda_header(&mut self, kind: &str, lambda: &Rc<Lambda>) {
244        let _ = writeln!(
245            &mut self.writer,
246            "=== compiled {} @ {:p} ({} ops, {} length) ===",
247            kind,
248            *lambda,
249            lambda.chunk.op_count(),
250            lambda.chunk.code.len(),
251        );
252    }
253
254    fn disassemble_chunk(&mut self, chunk: &Chunk) {
255        // calculate width of the widest address in the chunk
256        let width = format!("{:#x}", chunk.code.len() - 1).len();
257
258        let mut idx = 0;
259        while idx < chunk.code.len() {
260            let size = chunk
261                .disassemble_op(&mut self.writer, &self.source, width, CodeIdx(idx))
262                .expect("writing debug output should work");
263            idx += size;
264        }
265    }
266}
267
268impl<W: Write> CompilerObserver for DisassemblingObserver<W> {
269    fn observe_compiled_toplevel(&mut self, lambda: &Rc<Lambda>) {
270        self.lambda_header("toplevel", lambda);
271        self.disassemble_chunk(&lambda.chunk);
272        let _ = self.writer.flush();
273    }
274
275    fn observe_compiled_lambda(&mut self, lambda: &Rc<Lambda>) {
276        self.lambda_header("lambda", lambda);
277        self.disassemble_chunk(&lambda.chunk);
278        let _ = self.writer.flush();
279    }
280
281    fn observe_compiled_thunk(&mut self, lambda: &Rc<Lambda>) {
282        self.lambda_header("thunk", lambda);
283        self.disassemble_chunk(&lambda.chunk);
284        let _ = self.writer.flush();
285    }
286}
287
288/// An observer that collects a textual representation of an entire
289/// runtime execution.
290pub struct TracingObserver<W: Write> {
291    // If timing is enabled, contains the timestamp of the last-emitted trace event
292    last_event: Option<Instant>,
293    writer: TabWriter<W>,
294}
295
296impl<W: Write> TracingObserver<W> {
297    pub fn new(writer: W) -> Self {
298        Self {
299            last_event: None,
300            writer: TabWriter::new(writer),
301        }
302    }
303
304    /// Write the time of each runtime event, relative to when this method is called
305    pub fn enable_timing(&mut self) {
306        self.last_event = Some(Instant::now());
307    }
308
309    fn maybe_write_time(&mut self) {
310        if let Some(last_event) = &mut self.last_event {
311            let _ = write!(&mut self.writer, "+{}ns\t", last_event.elapsed().as_nanos());
312            *last_event = Instant::now();
313        }
314    }
315
316    fn write_value(&mut self, val: &Value) {
317        let _ = match val {
318            // Potentially large types which we only want to print
319            // the type of (and avoid recursing).
320            Value::List(l) => write!(&mut self.writer, "list[{}] ", l.len()),
321            Value::Attrs(a) => write!(&mut self.writer, "attrs[{}] ", a.len()),
322            Value::Thunk(t) if t.is_evaluated() => {
323                self.write_value(&t.value());
324                Ok(())
325            }
326
327            // For other value types, defer to the standard value printer.
328            _ => write!(&mut self.writer, "{val} "),
329        };
330    }
331
332    fn write_stack(&mut self, stack: &[Value]) {
333        let _ = write!(&mut self.writer, "[ ");
334
335        // Print out a maximum of 6 values from the top of the stack,
336        // before abbreviating it to `...`.
337        for (i, val) in stack.iter().rev().enumerate() {
338            if i == 6 {
339                let _ = write!(&mut self.writer, "...");
340                break;
341            }
342
343            self.write_value(val);
344        }
345
346        let _ = writeln!(&mut self.writer, "]");
347    }
348}
349
350impl<W: Write> RuntimeObserver for TracingObserver<W> {
351    fn observe_enter_bytecode_frame(
352        &mut self,
353        arg_count: usize,
354        lambda: &Rc<Lambda>,
355        call_depth: usize,
356    ) {
357        self.maybe_write_time();
358
359        let _ = write!(&mut self.writer, "=== entering ");
360
361        let _ = if arg_count == 0 {
362            write!(&mut self.writer, "thunk ")
363        } else {
364            write!(&mut self.writer, "closure ")
365        };
366
367        if let Some(name) = &lambda.name {
368            let _ = write!(&mut self.writer, "'{name}' ");
369        }
370
371        let _ = writeln!(
372            &mut self.writer,
373            "in frame[{}] @ {:p} ===",
374            call_depth, *lambda
375        );
376    }
377
378    /// Called when the runtime exits a bytecode frame.
379    fn observe_exit_bytecode_frame(&mut self, frame_at: usize, stack: &[Value]) {
380        self.maybe_write_time();
381        let _ = write!(&mut self.writer, "=== exiting frame {frame_at} ===\t ");
382        self.write_stack(stack);
383    }
384
385    fn observe_suspend_bytecode_frame(&mut self, frame_at: usize, stack: &[Value]) {
386        self.maybe_write_time();
387        let _ = write!(&mut self.writer, "=== suspending frame {frame_at} ===\t");
388
389        self.write_stack(stack);
390    }
391
392    fn observe_enter_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
393        self.maybe_write_time();
394        let _ = write!(
395            &mut self.writer,
396            "=== entering generator frame '{name}' [{frame_at}] ===\t",
397        );
398
399        self.write_stack(stack);
400    }
401
402    fn observe_exit_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
403        self.maybe_write_time();
404        let _ = write!(
405            &mut self.writer,
406            "=== exiting generator '{name}' [{frame_at}] ===\t"
407        );
408
409        self.write_stack(stack);
410    }
411
412    fn observe_suspend_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
413        self.maybe_write_time();
414        let _ = write!(
415            &mut self.writer,
416            "=== suspending generator '{name}' [{frame_at}] ===\t"
417        );
418
419        self.write_stack(stack);
420    }
421
422    fn observe_generator_request(&mut self, name: &str, msg: &VMRequest) {
423        self.maybe_write_time();
424        let _ = writeln!(
425            &mut self.writer,
426            "=== generator '{name}' requested {msg} ==="
427        );
428    }
429
430    fn observe_enter_builtin(&mut self, name: &'static str) {
431        self.maybe_write_time();
432        let _ = writeln!(&mut self.writer, "=== entering builtin {name} ===");
433    }
434
435    fn observe_exit_builtin(&mut self, name: &'static str, stack: &[Value]) {
436        self.maybe_write_time();
437        let _ = write!(&mut self.writer, "=== exiting builtin {name} ===\t");
438        self.write_stack(stack);
439    }
440
441    fn observe_tail_call(&mut self, frame_at: usize, lambda: &Rc<Lambda>) {
442        self.maybe_write_time();
443        let _ = writeln!(
444            &mut self.writer,
445            "=== tail-calling {:p} in frame[{}] ===",
446            *lambda, frame_at
447        );
448    }
449
450    fn observe_execute_op(&mut self, ip: CodeIdx, op: &Op, stack: &[Value]) {
451        self.maybe_write_time();
452        let _ = write!(&mut self.writer, "{:04} {:?}\t", ip.0, op);
453        self.write_stack(stack);
454    }
455}
456
457impl<W: Write> Drop for TracingObserver<W> {
458    fn drop(&mut self) {
459        let _ = self.writer.flush();
460    }
461}