Skip to main content

shape_vm/executor/
time_travel.rs

1//! Time-travel debugging support for the Shape VM.
2//!
3//! Captures VM state snapshots at configurable intervals during execution,
4//! allowing forward and backward navigation through execution history.
5
6use shape_runtime::snapshot::SnapshotStore;
7use shape_value::ValueWord;
8use std::collections::VecDeque;
9
10/// When to capture VM snapshots.
11#[derive(Debug, Clone)]
12pub enum CaptureMode {
13    /// Capture at every function entry and exit.
14    FunctionBoundaries,
15    /// Capture every N instructions.
16    EveryNInstructions(u64),
17    /// Capture at explicit breakpoints (instruction pointers).
18    Breakpoints(Vec<usize>),
19    /// Disabled (no captures).
20    Disabled,
21}
22
23impl Default for CaptureMode {
24    fn default() -> Self {
25        Self::Disabled
26    }
27}
28
29/// A snapshot of VM state at a point in time.
30#[derive(Debug, Clone)]
31pub struct VmSnapshot {
32    /// Monotonically increasing snapshot index.
33    pub index: u64,
34    /// Instruction pointer at time of capture.
35    pub ip: usize,
36    /// Stack pointer at time of capture.
37    pub sp: usize,
38    /// Call stack depth at time of capture.
39    pub call_depth: usize,
40    /// Function being executed (if known).
41    pub function_id: Option<u16>,
42    /// Function name (if known).
43    pub function_name: Option<String>,
44    /// Instruction count at time of capture.
45    pub instruction_count: u64,
46    /// Copy of the stack up to sp.
47    pub stack_snapshot: Vec<ValueWord>,
48    /// Module bindings snapshot.
49    pub module_bindings: Vec<ValueWord>,
50    /// Capture reason for display/debugging.
51    pub reason: CaptureReason,
52}
53
54/// Why a snapshot was captured.
55#[derive(Debug, Clone)]
56pub enum CaptureReason {
57    FunctionEntry(String),
58    FunctionExit(String),
59    InstructionInterval(u64),
60    Breakpoint(usize),
61    Manual,
62}
63
64impl std::fmt::Display for CaptureReason {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        match self {
67            Self::FunctionEntry(name) => write!(f, "function entry: {name}"),
68            Self::FunctionExit(name) => write!(f, "function exit: {name}"),
69            Self::InstructionInterval(n) => write!(f, "every {n} instructions"),
70            Self::Breakpoint(ip) => write!(f, "breakpoint at ip={ip}"),
71            Self::Manual => write!(f, "manual capture"),
72        }
73    }
74}
75
76/// Configuration for the time-travel debugger.
77#[derive(Debug, Clone)]
78pub struct TimeTravelConfig {
79    /// When to capture snapshots.
80    pub capture_mode: CaptureMode,
81    /// Maximum number of snapshots to retain (ring buffer).
82    pub max_snapshots: usize,
83}
84
85impl Default for TimeTravelConfig {
86    fn default() -> Self {
87        Self {
88            capture_mode: CaptureMode::Disabled,
89            max_snapshots: 10_000,
90        }
91    }
92}
93
94/// Time-travel debugger state.
95pub struct TimeTravel {
96    config: TimeTravelConfig,
97    /// Ring buffer of captured snapshots.
98    snapshots: VecDeque<VmSnapshot>,
99    /// Current position in the snapshot history (for navigation).
100    cursor: usize,
101    /// Next snapshot index.
102    next_index: u64,
103    /// Instruction counter for interval-based capture.
104    instruction_counter: u64,
105    /// Snapshot store for serialization (lazily initialized).
106    snapshot_store: Option<SnapshotStore>,
107}
108
109impl TimeTravel {
110    /// Create a new time-travel debugger with the given configuration.
111    pub fn with_config(config: TimeTravelConfig) -> Self {
112        Self {
113            config,
114            snapshots: VecDeque::new(),
115            cursor: 0,
116            next_index: 0,
117            instruction_counter: 0,
118            snapshot_store: None,
119        }
120    }
121
122    /// Create a new time-travel debugger with the given capture mode and
123    /// maximum history size.
124    ///
125    /// This constructor preserves backward compatibility with the existing
126    /// `VirtualMachine` API.
127    pub fn new(mode: CaptureMode, max_entries: usize) -> Self {
128        Self::with_config(TimeTravelConfig {
129            capture_mode: mode,
130            max_snapshots: max_entries,
131        })
132    }
133
134    /// Create a disabled (no-op) time-travel debugger.
135    pub fn disabled() -> Self {
136        Self::with_config(TimeTravelConfig::default())
137    }
138
139    /// Check whether a capture should happen at the current instruction.
140    ///
141    /// Called from the dispatch loop. Returns `true` if a snapshot should
142    /// be captured at this point.
143    ///
144    /// # Arguments
145    /// * `ip` - current instruction pointer
146    /// * `instruction_count` - total instructions executed so far (reserved)
147    /// * `is_call_or_return` - true if the current instruction is a Call/Return
148    #[inline]
149    pub fn should_capture(
150        &mut self,
151        ip: usize,
152        _instruction_count: u64,
153        is_call_or_return: bool,
154    ) -> bool {
155        match &self.config.capture_mode {
156            CaptureMode::Disabled => false,
157            CaptureMode::FunctionBoundaries => is_call_or_return,
158            CaptureMode::EveryNInstructions(n) => {
159                self.instruction_counter += 1;
160                if self.instruction_counter >= *n {
161                    self.instruction_counter = 0;
162                    true
163                } else {
164                    false
165                }
166            }
167            CaptureMode::Breakpoints(bps) => bps.contains(&ip),
168        }
169    }
170
171    /// Notify that a function was entered. Captures if in FunctionBoundaries mode.
172    pub fn on_function_entry(&mut self) -> bool {
173        matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
174    }
175
176    /// Notify that a function was exited. Captures if in FunctionBoundaries mode.
177    pub fn on_function_exit(&mut self) -> bool {
178        matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
179    }
180
181    // --- Backward-compatible dispatch.rs integration methods ---
182
183    /// Record a `shape_runtime::snapshot::VmSnapshot` into the history.
184    ///
185    /// This method wraps the runtime snapshot type used by `dispatch.rs`.
186    /// The snapshot is stored in the ring buffer alongside metadata.
187    pub fn record(
188        &mut self,
189        _snapshot: shape_runtime::snapshot::VmSnapshot,
190        ip: usize,
191        instruction_count: u64,
192        call_depth: usize,
193    ) -> usize {
194        let internal = VmSnapshot {
195            index: self.next_index,
196            ip,
197            sp: 0,
198            call_depth,
199            function_id: None,
200            function_name: None,
201            instruction_count,
202            stack_snapshot: vec![],
203            module_bindings: vec![],
204            reason: CaptureReason::Manual,
205        };
206        self.capture(internal);
207        self.snapshots.len().saturating_sub(1)
208    }
209
210    /// Get the snapshot store, creating it lazily.
211    ///
212    /// Used by `dispatch.rs` to obtain a `SnapshotStore` reference for
213    /// serializing VM state before recording.
214    pub fn snapshot_store(&mut self) -> Result<&SnapshotStore, String> {
215        if self.snapshot_store.is_none() {
216            let tmp = std::env::temp_dir().join("shape_time_travel");
217            self.snapshot_store = Some(
218                SnapshotStore::new(&tmp)
219                    .map_err(|e| format!("failed to create snapshot store: {}", e))?,
220            );
221        }
222        Ok(self.snapshot_store.as_ref().unwrap())
223    }
224
225    /// Store a snapshot.
226    pub fn capture(&mut self, snapshot: VmSnapshot) {
227        if self.snapshots.len() >= self.config.max_snapshots {
228            self.snapshots.pop_front();
229            // Adjust cursor if it would go out of bounds.
230            if self.cursor > 0 {
231                self.cursor -= 1;
232            }
233        }
234        self.snapshots.push_back(snapshot);
235        self.cursor = self.snapshots.len().saturating_sub(1);
236        self.next_index += 1;
237    }
238
239    /// Build a snapshot from raw VM state.
240    pub fn build_snapshot(
241        &self,
242        ip: usize,
243        sp: usize,
244        call_depth: usize,
245        function_id: Option<u16>,
246        function_name: Option<String>,
247        instruction_count: u64,
248        stack: &[ValueWord],
249        module_bindings: &[ValueWord],
250        reason: CaptureReason,
251    ) -> VmSnapshot {
252        VmSnapshot {
253            index: self.next_index,
254            ip,
255            sp,
256            call_depth,
257            function_id,
258            function_name,
259            instruction_count,
260            stack_snapshot: stack[..sp.min(stack.len())].to_vec(),
261            module_bindings: module_bindings.to_vec(),
262            reason,
263        }
264    }
265
266    // --- Navigation ---
267
268    /// Move to the previous snapshot. Returns the snapshot if available.
269    pub fn step_back(&mut self) -> Option<&VmSnapshot> {
270        if self.cursor > 0 {
271            self.cursor -= 1;
272        }
273        self.snapshots.get(self.cursor)
274    }
275
276    /// Move to the next snapshot. Returns the snapshot if available.
277    pub fn step_forward(&mut self) -> Option<&VmSnapshot> {
278        if self.cursor + 1 < self.snapshots.len() {
279            self.cursor += 1;
280        }
281        self.snapshots.get(self.cursor)
282    }
283
284    /// Jump to a specific snapshot index.
285    pub fn goto(&mut self, index: u64) -> Option<&VmSnapshot> {
286        if let Some(pos) = self.snapshots.iter().position(|s| s.index == index) {
287            self.cursor = pos;
288            self.snapshots.get(self.cursor)
289        } else {
290            None
291        }
292    }
293
294    /// Get the current snapshot (at cursor position).
295    pub fn current(&self) -> Option<&VmSnapshot> {
296        self.snapshots.get(self.cursor)
297    }
298
299    /// Get the most recent snapshot.
300    pub fn latest(&self) -> Option<&VmSnapshot> {
301        self.snapshots.back()
302    }
303
304    /// Number of captured snapshots.
305    pub fn snapshot_count(&self) -> usize {
306        self.snapshots.len()
307    }
308
309    /// Current cursor position.
310    pub fn cursor_position(&self) -> usize {
311        self.cursor
312    }
313
314    /// Whether the debugger is actively capturing.
315    pub fn is_enabled(&self) -> bool {
316        !matches!(self.config.capture_mode, CaptureMode::Disabled)
317    }
318
319    /// Clear all captured snapshots.
320    pub fn clear(&mut self) {
321        self.snapshots.clear();
322        self.cursor = 0;
323    }
324
325    /// Get a range of snapshots around the cursor for display.
326    pub fn context_window(&self, radius: usize) -> Vec<&VmSnapshot> {
327        let start = self.cursor.saturating_sub(radius);
328        let end = (self.cursor + radius + 1).min(self.snapshots.len());
329        self.snapshots.range(start..end).collect()
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    fn make_snapshot(_tt: &TimeTravel, idx_override: u64, reason: CaptureReason) -> VmSnapshot {
338        VmSnapshot {
339            index: idx_override,
340            ip: 0,
341            sp: 0,
342            call_depth: 0,
343            function_id: None,
344            function_name: None,
345            instruction_count: 0,
346            stack_snapshot: vec![],
347            module_bindings: vec![],
348            reason,
349        }
350    }
351
352    #[test]
353    fn test_disabled_no_captures() {
354        let mut tt = TimeTravel::disabled();
355        assert!(!tt.should_capture(0, 0, false));
356        assert!(!tt.is_enabled());
357    }
358
359    #[test]
360    fn test_interval_capture() {
361        let mut tt = TimeTravel::with_config(TimeTravelConfig {
362            capture_mode: CaptureMode::EveryNInstructions(3),
363            max_snapshots: 100,
364        });
365
366        assert!(!tt.should_capture(0, 1, false)); // 1
367        assert!(!tt.should_capture(1, 2, false)); // 2
368        assert!(tt.should_capture(2, 3, false)); // 3 -> trigger
369        assert!(!tt.should_capture(3, 4, false)); // 1 again
370    }
371
372    #[test]
373    fn test_breakpoint_capture() {
374        let mut tt = TimeTravel::with_config(TimeTravelConfig {
375            capture_mode: CaptureMode::Breakpoints(vec![10, 20, 30]),
376            max_snapshots: 100,
377        });
378
379        assert!(!tt.should_capture(5, 1, false));
380        assert!(tt.should_capture(10, 2, false));
381        assert!(!tt.should_capture(15, 3, false));
382        assert!(tt.should_capture(20, 4, false));
383    }
384
385    #[test]
386    fn test_function_boundary_capture() {
387        let mut tt = TimeTravel::with_config(TimeTravelConfig {
388            capture_mode: CaptureMode::FunctionBoundaries,
389            max_snapshots: 100,
390        });
391
392        // Non-call/return instructions should not trigger
393        assert!(!tt.should_capture(0, 1, false));
394        // Call/return instructions should trigger
395        assert!(tt.should_capture(0, 2, true));
396    }
397
398    #[test]
399    fn test_navigation() {
400        let mut tt = TimeTravel::with_config(TimeTravelConfig {
401            capture_mode: CaptureMode::FunctionBoundaries,
402            max_snapshots: 100,
403        });
404
405        for i in 0..5 {
406            let snap = make_snapshot(&tt, i, CaptureReason::FunctionEntry(format!("fn_{i}")));
407            tt.capture(VmSnapshot { index: i, ..snap });
408            tt.next_index = i + 1;
409        }
410
411        assert_eq!(tt.snapshot_count(), 5);
412        assert_eq!(tt.cursor_position(), 4); // at latest
413
414        // Step back
415        let prev = tt.step_back().unwrap();
416        assert_eq!(prev.index, 3);
417        assert_eq!(tt.cursor_position(), 3);
418
419        // Step forward
420        let next = tt.step_forward().unwrap();
421        assert_eq!(next.index, 4);
422
423        // Goto
424        let target = tt.goto(1).unwrap();
425        assert_eq!(target.index, 1);
426    }
427
428    #[test]
429    fn test_ring_buffer_eviction() {
430        let mut tt = TimeTravel::with_config(TimeTravelConfig {
431            capture_mode: CaptureMode::FunctionBoundaries,
432            max_snapshots: 3,
433        });
434
435        for i in 0..5u64 {
436            tt.capture(VmSnapshot {
437                index: i,
438                ip: i as usize,
439                sp: 0,
440                call_depth: 0,
441                function_id: None,
442                function_name: None,
443                instruction_count: i,
444                stack_snapshot: vec![],
445                module_bindings: vec![],
446                reason: CaptureReason::Manual,
447            });
448        }
449
450        assert_eq!(tt.snapshot_count(), 3);
451        // Oldest snapshots (0, 1) should have been evicted
452        assert_eq!(tt.snapshots.front().unwrap().index, 2);
453    }
454
455    #[test]
456    fn test_context_window() {
457        let mut tt = TimeTravel::with_config(TimeTravelConfig {
458            capture_mode: CaptureMode::FunctionBoundaries,
459            max_snapshots: 100,
460        });
461
462        for i in 0..10u64 {
463            tt.capture(VmSnapshot {
464                index: i,
465                ip: 0,
466                sp: 0,
467                call_depth: 0,
468                function_id: None,
469                function_name: None,
470                instruction_count: 0,
471                stack_snapshot: vec![],
472                module_bindings: vec![],
473                reason: CaptureReason::Manual,
474            });
475        }
476
477        tt.goto(5);
478        let window = tt.context_window(2);
479        assert_eq!(window.len(), 5); // indices 3,4,5,6,7
480        assert_eq!(window[0].index, 3);
481        assert_eq!(window[4].index, 7);
482    }
483
484    #[test]
485    fn test_function_boundary_mode() {
486        let mut tt = TimeTravel::with_config(TimeTravelConfig {
487            capture_mode: CaptureMode::FunctionBoundaries,
488            max_snapshots: 100,
489        });
490
491        assert!(tt.on_function_entry());
492        assert!(tt.on_function_exit());
493        assert!(tt.is_enabled());
494    }
495
496    #[test]
497    fn test_clear() {
498        let mut tt = TimeTravel::with_config(TimeTravelConfig {
499            capture_mode: CaptureMode::FunctionBoundaries,
500            max_snapshots: 100,
501        });
502
503        tt.capture(VmSnapshot {
504            index: 0,
505            ip: 0,
506            sp: 0,
507            call_depth: 0,
508            function_id: None,
509            function_name: None,
510            instruction_count: 0,
511            stack_snapshot: vec![],
512            module_bindings: vec![],
513            reason: CaptureReason::Manual,
514        });
515
516        assert_eq!(tt.snapshot_count(), 1);
517        tt.clear();
518        assert_eq!(tt.snapshot_count(), 0);
519    }
520}