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//!
6//! ## Wave 6.5 R-async-time migration (ADR-006 §2.7.7 / §2.7.8)
7//!
8//! Pre-bulldozer the snapshot stored `Vec<ValueWord>` for `stack_snapshot`
9//! and `module_bindings`, with a manual `Clone` that walked each element
10//! through the deleted `value_word_drop::vw_clone` and a `Drop` that ran
11//! `vw_drop_slice`. `ValueWord` is deleted (CLAUDE.md "Forbidden Patterns")
12//! and the §2.7.7 stack ABI carries data in a `Vec<u64>` data track plus
13//! a parallel `Vec<NativeKind>` kinds track. The snapshot tracks adopt the
14//! same lockstep shape: `*_data: Vec<u64>` plus `*_kinds: Vec<NativeKind>`,
15//! with `clone_with_kind` / `drop_with_kind` (replacing `vw_clone` /
16//! `vw_drop_slice`) handling refcount discipline.
17//!
18//! Index invariant — for every snapshot, `stack_data.len() == stack_kinds.len()`
19//! and `module_bindings_data.len() == module_bindings_kinds.len()`. The
20//! `Clone` and `Drop` impls walk both tracks in lockstep per ADR-006 §2.7.7.
21
22use shape_runtime::snapshot::SnapshotStore;
23use shape_value::NativeKind;
24use std::collections::VecDeque;
25
26use crate::executor::vm_impl::stack::{clone_with_kind, drop_with_kind};
27
28/// When to capture VM snapshots.
29#[derive(Debug, Clone)]
30pub enum CaptureMode {
31    /// Capture at every function entry and exit.
32    FunctionBoundaries,
33    /// Capture every N instructions.
34    EveryNInstructions(u64),
35    /// Capture at explicit breakpoints (instruction pointers).
36    Breakpoints(Vec<usize>),
37    /// Disabled (no captures).
38    Disabled,
39}
40
41impl Default for CaptureMode {
42    fn default() -> Self {
43        Self::Disabled
44    }
45}
46
47/// A snapshot of VM state at a point in time.
48///
49/// **WB2.5 retain-on-read.** `stack_data` / `module_bindings_data` carry
50/// raw 8-byte slot bits; `stack_kinds` / `module_bindings_kinds` carry the
51/// parallel-track `NativeKind` interpretation per slot (ADR-006 §2.7.7).
52/// The manual `Clone` bumps each element's refcount via `clone_with_kind`,
53/// keyed on the parallel-track kind; `Drop` releases via `drop_with_kind`
54/// so replaying / evicting a snapshot is refcount-neutral.
55#[derive(Debug)]
56pub struct VmSnapshot {
57    /// Monotonically increasing snapshot index.
58    pub index: u64,
59    /// Instruction pointer at time of capture.
60    pub ip: usize,
61    /// Stack pointer at time of capture.
62    pub sp: usize,
63    /// Call stack depth at time of capture.
64    pub call_depth: usize,
65    /// Function being executed (if known).
66    pub function_id: Option<u16>,
67    /// Function name (if known).
68    pub function_name: Option<String>,
69    /// Instruction count at time of capture.
70    pub instruction_count: u64,
71    /// Raw 8-byte slot bits for the live stack at capture time
72    /// (post-§2.7.7 lockstep with `stack_kinds`).
73    pub stack_data: Vec<u64>,
74    /// Parallel `NativeKind` track for `stack_data`.
75    /// Invariant: `stack_data.len() == stack_kinds.len()`.
76    pub stack_kinds: Vec<NativeKind>,
77    /// Raw 8-byte slot bits for module bindings at capture time
78    /// (post-§2.7.7 lockstep with `module_bindings_kinds`).
79    pub module_bindings_data: Vec<u64>,
80    /// Parallel `NativeKind` track for `module_bindings_data`.
81    /// Invariant: `module_bindings_data.len() == module_bindings_kinds.len()`.
82    pub module_bindings_kinds: Vec<NativeKind>,
83    /// Capture reason for display/debugging.
84    pub reason: CaptureReason,
85}
86
87impl Clone for VmSnapshot {
88    fn clone(&self) -> Self {
89        debug_assert_eq!(
90            self.stack_data.len(),
91            self.stack_kinds.len(),
92            "ADR-006 §2.7.7 lockstep invariant: stack_data and stack_kinds must agree"
93        );
94        debug_assert_eq!(
95            self.module_bindings_data.len(),
96            self.module_bindings_kinds.len(),
97            "ADR-006 §2.7.7 lockstep invariant: module_bindings_data and module_bindings_kinds must agree"
98        );
99        // Bump the strong-count for every heap-bearing element on both
100        // tracks before producing the new vectors. `clone_with_kind` is the
101        // post-§2.7.7 replacement for the deleted `vw_clone(bits)`.
102        for (&bits, &kind) in self.stack_data.iter().zip(self.stack_kinds.iter()) {
103            clone_with_kind(bits, kind);
104        }
105        for (&bits, &kind) in self
106            .module_bindings_data
107            .iter()
108            .zip(self.module_bindings_kinds.iter())
109        {
110            clone_with_kind(bits, kind);
111        }
112        VmSnapshot {
113            index: self.index,
114            ip: self.ip,
115            sp: self.sp,
116            call_depth: self.call_depth,
117            function_id: self.function_id,
118            function_name: self.function_name.clone(),
119            instruction_count: self.instruction_count,
120            stack_data: self.stack_data.clone(),
121            stack_kinds: self.stack_kinds.clone(),
122            module_bindings_data: self.module_bindings_data.clone(),
123            module_bindings_kinds: self.module_bindings_kinds.clone(),
124            reason: self.reason.clone(),
125        }
126    }
127}
128
129impl Drop for VmSnapshot {
130    fn drop(&mut self) {
131        // Release every owned share on both tracks. The post-§2.7.7
132        // replacement for the deleted `vw_drop_slice(slice)`.
133        debug_assert_eq!(
134            self.stack_data.len(),
135            self.stack_kinds.len(),
136            "ADR-006 §2.7.7 lockstep invariant violated at Drop"
137        );
138        debug_assert_eq!(
139            self.module_bindings_data.len(),
140            self.module_bindings_kinds.len(),
141            "ADR-006 §2.7.7 lockstep invariant violated at Drop"
142        );
143        for (&bits, &kind) in self.stack_data.iter().zip(self.stack_kinds.iter()) {
144            drop_with_kind(bits, kind);
145        }
146        for (&bits, &kind) in self
147            .module_bindings_data
148            .iter()
149            .zip(self.module_bindings_kinds.iter())
150        {
151            drop_with_kind(bits, kind);
152        }
153    }
154}
155
156/// Why a snapshot was captured.
157#[derive(Debug, Clone)]
158pub enum CaptureReason {
159    FunctionEntry(String),
160    FunctionExit(String),
161    InstructionInterval(u64),
162    Breakpoint(usize),
163    Manual,
164}
165
166impl std::fmt::Display for CaptureReason {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::FunctionEntry(name) => write!(f, "function entry: {name}"),
170            Self::FunctionExit(name) => write!(f, "function exit: {name}"),
171            Self::InstructionInterval(n) => write!(f, "every {n} instructions"),
172            Self::Breakpoint(ip) => write!(f, "breakpoint at ip={ip}"),
173            Self::Manual => write!(f, "manual capture"),
174        }
175    }
176}
177
178/// Configuration for the time-travel debugger.
179#[derive(Debug, Clone)]
180pub struct TimeTravelConfig {
181    /// When to capture snapshots.
182    pub capture_mode: CaptureMode,
183    /// Maximum number of snapshots to retain (ring buffer).
184    pub max_snapshots: usize,
185}
186
187impl Default for TimeTravelConfig {
188    fn default() -> Self {
189        Self {
190            capture_mode: CaptureMode::Disabled,
191            max_snapshots: 10_000,
192        }
193    }
194}
195
196/// Time-travel debugger state.
197pub struct TimeTravel {
198    config: TimeTravelConfig,
199    /// Ring buffer of captured snapshots.
200    snapshots: VecDeque<VmSnapshot>,
201    /// Current position in the snapshot history (for navigation).
202    cursor: usize,
203    /// Next snapshot index.
204    next_index: u64,
205    /// Instruction counter for interval-based capture.
206    instruction_counter: u64,
207    /// Snapshot store for serialization (lazily initialized).
208    snapshot_store: Option<SnapshotStore>,
209}
210
211impl TimeTravel {
212    /// Create a new time-travel debugger with the given configuration.
213    pub fn with_config(config: TimeTravelConfig) -> Self {
214        Self {
215            config,
216            snapshots: VecDeque::new(),
217            cursor: 0,
218            next_index: 0,
219            instruction_counter: 0,
220            snapshot_store: None,
221        }
222    }
223
224    /// Create a new time-travel debugger with the given capture mode and
225    /// maximum history size.
226    ///
227    /// This constructor preserves backward compatibility with the existing
228    /// `VirtualMachine` API.
229    pub fn new(mode: CaptureMode, max_entries: usize) -> Self {
230        Self::with_config(TimeTravelConfig {
231            capture_mode: mode,
232            max_snapshots: max_entries,
233        })
234    }
235
236    /// Create a disabled (no-op) time-travel debugger.
237    pub fn disabled() -> Self {
238        Self::with_config(TimeTravelConfig::default())
239    }
240
241    /// Check whether a capture should happen at the current instruction.
242    ///
243    /// Called from the dispatch loop. Returns `true` if a snapshot should
244    /// be captured at this point.
245    ///
246    /// # Arguments
247    /// * `ip` - current instruction pointer
248    /// * `instruction_count` - total instructions executed so far (reserved)
249    /// * `is_call_or_return` - true if the current instruction is a Call/Return
250    #[inline]
251    pub fn should_capture(
252        &mut self,
253        ip: usize,
254        _instruction_count: u64,
255        is_call_or_return: bool,
256    ) -> bool {
257        match &self.config.capture_mode {
258            CaptureMode::Disabled => false,
259            CaptureMode::FunctionBoundaries => is_call_or_return,
260            CaptureMode::EveryNInstructions(n) => {
261                self.instruction_counter += 1;
262                if self.instruction_counter >= *n {
263                    self.instruction_counter = 0;
264                    true
265                } else {
266                    false
267                }
268            }
269            CaptureMode::Breakpoints(bps) => bps.contains(&ip),
270        }
271    }
272
273    /// Notify that a function was entered. Captures if in FunctionBoundaries mode.
274    pub fn on_function_entry(&mut self) -> bool {
275        matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
276    }
277
278    /// Notify that a function was exited. Captures if in FunctionBoundaries mode.
279    pub fn on_function_exit(&mut self) -> bool {
280        matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
281    }
282
283    // --- Backward-compatible dispatch.rs integration methods ---
284
285    /// Record a `shape_runtime::snapshot::VmSnapshot` into the history.
286    ///
287    /// This method wraps the runtime snapshot type used by `dispatch.rs`.
288    /// The snapshot is stored in the ring buffer alongside metadata.
289    ///
290    /// **Phase-2c data capture pending — ADR-006 §2.7.4.** The runtime-side
291    /// `VmSnapshot` type is itself §2.7.4-deferred (its body is `todo!`),
292    /// so we record only the metadata (ip, instruction_count, call_depth)
293    /// and leave the post-§2.7.7 stack / module-binding tracks empty. When
294    /// the Phase-2c snapshot rebuild lands, the parallel kinds tracks will
295    /// be threaded through here.
296    pub fn record(
297        &mut self,
298        _snapshot: shape_runtime::snapshot::VmSnapshot,
299        ip: usize,
300        instruction_count: u64,
301        call_depth: usize,
302    ) -> usize {
303        let internal = VmSnapshot {
304            index: self.next_index,
305            ip,
306            sp: 0,
307            call_depth,
308            function_id: None,
309            function_name: None,
310            instruction_count,
311            stack_data: Vec::new(),
312            stack_kinds: Vec::new(),
313            module_bindings_data: Vec::new(),
314            module_bindings_kinds: Vec::new(),
315            reason: CaptureReason::Manual,
316        };
317        self.capture(internal);
318        self.snapshots.len().saturating_sub(1)
319    }
320
321    /// Get the snapshot store, creating it lazily.
322    ///
323    /// Used by `dispatch.rs` to obtain a `SnapshotStore` reference for
324    /// serializing VM state before recording.
325    pub fn snapshot_store(&mut self) -> Result<&SnapshotStore, String> {
326        if self.snapshot_store.is_none() {
327            let tmp = std::env::temp_dir().join("shape_time_travel");
328            self.snapshot_store = Some(
329                SnapshotStore::new(&tmp)
330                    .map_err(|e| format!("failed to create snapshot store: {}", e))?,
331            );
332        }
333        Ok(self.snapshot_store.as_ref().unwrap())
334    }
335
336    /// Store a snapshot.
337    pub fn capture(&mut self, snapshot: VmSnapshot) {
338        if self.snapshots.len() >= self.config.max_snapshots {
339            self.snapshots.pop_front();
340            // Adjust cursor if it would go out of bounds.
341            if self.cursor > 0 {
342                self.cursor -= 1;
343            }
344        }
345        self.snapshots.push_back(snapshot);
346        self.cursor = self.snapshots.len().saturating_sub(1);
347        self.next_index += 1;
348    }
349
350    /// Build a snapshot from raw VM state.
351    ///
352    /// WB2.5 retain-on-read: the slices are views over live VM slots
353    /// (the `Vec<u64>` data track + `Vec<NativeKind>` kinds track per
354    /// ADR-006 §2.7.7). Each captured element is `clone_with_kind`'d so
355    /// the snapshot owns an independent share per heap-bearing slot.
356    ///
357    /// # Panics
358    ///
359    /// Debug-asserts the lockstep invariant `stack_data.len() ==
360    /// stack_kinds.len()` and `module_bindings_data.len() ==
361    /// module_bindings_kinds.len()`.
362    pub fn build_snapshot(
363        &self,
364        ip: usize,
365        sp: usize,
366        call_depth: usize,
367        function_id: Option<u16>,
368        function_name: Option<String>,
369        instruction_count: u64,
370        stack_data: &[u64],
371        stack_kinds: &[NativeKind],
372        module_bindings_data: &[u64],
373        module_bindings_kinds: &[NativeKind],
374        reason: CaptureReason,
375    ) -> VmSnapshot {
376        debug_assert_eq!(
377            stack_data.len(),
378            stack_kinds.len(),
379            "ADR-006 §2.7.7 lockstep invariant: stack_data and stack_kinds must agree at build_snapshot"
380        );
381        debug_assert_eq!(
382            module_bindings_data.len(),
383            module_bindings_kinds.len(),
384            "ADR-006 §2.7.7 lockstep invariant: module_bindings tracks must agree at build_snapshot"
385        );
386        let live = sp.min(stack_data.len());
387        let stack_data_owned: Vec<u64> = stack_data[..live].to_vec();
388        let stack_kinds_owned: Vec<NativeKind> = stack_kinds[..live].to_vec();
389        for (&bits, &kind) in stack_data_owned.iter().zip(stack_kinds_owned.iter()) {
390            clone_with_kind(bits, kind);
391        }
392        let module_bindings_data_owned: Vec<u64> = module_bindings_data.to_vec();
393        let module_bindings_kinds_owned: Vec<NativeKind> = module_bindings_kinds.to_vec();
394        for (&bits, &kind) in module_bindings_data_owned
395            .iter()
396            .zip(module_bindings_kinds_owned.iter())
397        {
398            clone_with_kind(bits, kind);
399        }
400        VmSnapshot {
401            index: self.next_index,
402            ip,
403            sp,
404            call_depth,
405            function_id,
406            function_name,
407            instruction_count,
408            stack_data: stack_data_owned,
409            stack_kinds: stack_kinds_owned,
410            module_bindings_data: module_bindings_data_owned,
411            module_bindings_kinds: module_bindings_kinds_owned,
412            reason,
413        }
414    }
415
416    // --- Navigation ---
417
418    /// Move to the previous snapshot. Returns the snapshot if available.
419    pub fn step_back(&mut self) -> Option<&VmSnapshot> {
420        if self.cursor > 0 {
421            self.cursor -= 1;
422        }
423        self.snapshots.get(self.cursor)
424    }
425
426    /// Move to the next snapshot. Returns the snapshot if available.
427    pub fn step_forward(&mut self) -> Option<&VmSnapshot> {
428        if self.cursor + 1 < self.snapshots.len() {
429            self.cursor += 1;
430        }
431        self.snapshots.get(self.cursor)
432    }
433
434    /// Jump to a specific snapshot index.
435    pub fn goto(&mut self, index: u64) -> Option<&VmSnapshot> {
436        if let Some(pos) = self.snapshots.iter().position(|s| s.index == index) {
437            self.cursor = pos;
438            self.snapshots.get(self.cursor)
439        } else {
440            None
441        }
442    }
443
444    /// Get the current snapshot (at cursor position).
445    pub fn current(&self) -> Option<&VmSnapshot> {
446        self.snapshots.get(self.cursor)
447    }
448
449    /// Get the most recent snapshot.
450    pub fn latest(&self) -> Option<&VmSnapshot> {
451        self.snapshots.back()
452    }
453
454    /// Number of captured snapshots.
455    pub fn snapshot_count(&self) -> usize {
456        self.snapshots.len()
457    }
458
459    /// Current cursor position.
460    pub fn cursor_position(&self) -> usize {
461        self.cursor
462    }
463
464    /// Whether the debugger is actively capturing.
465    pub fn is_enabled(&self) -> bool {
466        !matches!(self.config.capture_mode, CaptureMode::Disabled)
467    }
468
469    /// Clear all captured snapshots.
470    pub fn clear(&mut self) {
471        self.snapshots.clear();
472        self.cursor = 0;
473    }
474
475    /// Get a range of snapshots around the cursor for display.
476    pub fn context_window(&self, radius: usize) -> Vec<&VmSnapshot> {
477        let start = self.cursor.saturating_sub(radius);
478        let end = (self.cursor + radius + 1).min(self.snapshots.len());
479        self.snapshots.range(start..end).collect()
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    fn make_snapshot(_tt: &TimeTravel, idx_override: u64, reason: CaptureReason) -> VmSnapshot {
488        VmSnapshot {
489            index: idx_override,
490            ip: 0,
491            sp: 0,
492            call_depth: 0,
493            function_id: None,
494            function_name: None,
495            instruction_count: 0,
496            stack_data: vec![],
497            stack_kinds: vec![],
498            module_bindings_data: vec![],
499            module_bindings_kinds: vec![],
500            reason,
501        }
502    }
503
504    #[test]
505    fn test_disabled_no_captures() {
506        let mut tt = TimeTravel::disabled();
507        assert!(!tt.should_capture(0, 0, false));
508        assert!(!tt.is_enabled());
509    }
510
511    #[test]
512    fn test_interval_capture() {
513        let mut tt = TimeTravel::with_config(TimeTravelConfig {
514            capture_mode: CaptureMode::EveryNInstructions(3),
515            max_snapshots: 100,
516        });
517
518        assert!(!tt.should_capture(0, 1, false)); // 1
519        assert!(!tt.should_capture(1, 2, false)); // 2
520        assert!(tt.should_capture(2, 3, false)); // 3 -> trigger
521        assert!(!tt.should_capture(3, 4, false)); // 1 again
522    }
523
524    #[test]
525    fn test_breakpoint_capture() {
526        let mut tt = TimeTravel::with_config(TimeTravelConfig {
527            capture_mode: CaptureMode::Breakpoints(vec![10, 20, 30]),
528            max_snapshots: 100,
529        });
530
531        assert!(!tt.should_capture(5, 1, false));
532        assert!(tt.should_capture(10, 2, false));
533        assert!(!tt.should_capture(15, 3, false));
534        assert!(tt.should_capture(20, 4, false));
535    }
536
537    #[test]
538    fn test_function_boundary_capture() {
539        let mut tt = TimeTravel::with_config(TimeTravelConfig {
540            capture_mode: CaptureMode::FunctionBoundaries,
541            max_snapshots: 100,
542        });
543
544        // Non-call/return instructions should not trigger
545        assert!(!tt.should_capture(0, 1, false));
546        // Call/return instructions should trigger
547        assert!(tt.should_capture(0, 2, true));
548    }
549
550    #[test]
551    fn test_navigation() {
552        let mut tt = TimeTravel::with_config(TimeTravelConfig {
553            capture_mode: CaptureMode::FunctionBoundaries,
554            max_snapshots: 100,
555        });
556
557        for i in 0..5 {
558            let mut snap = make_snapshot(&tt, i, CaptureReason::FunctionEntry(format!("fn_{i}")));
559            snap.index = i;
560            tt.capture(snap);
561            tt.next_index = i + 1;
562        }
563
564        assert_eq!(tt.snapshot_count(), 5);
565        assert_eq!(tt.cursor_position(), 4); // at latest
566
567        // Step back
568        let prev = tt.step_back().unwrap();
569        assert_eq!(prev.index, 3);
570        assert_eq!(tt.cursor_position(), 3);
571
572        // Step forward
573        let next = tt.step_forward().unwrap();
574        assert_eq!(next.index, 4);
575
576        // Goto
577        let target = tt.goto(1).unwrap();
578        assert_eq!(target.index, 1);
579    }
580
581    #[test]
582    fn test_ring_buffer_eviction() {
583        let mut tt = TimeTravel::with_config(TimeTravelConfig {
584            capture_mode: CaptureMode::FunctionBoundaries,
585            max_snapshots: 3,
586        });
587
588        for i in 0..5u64 {
589            tt.capture(VmSnapshot {
590                index: i,
591                ip: i as usize,
592                sp: 0,
593                call_depth: 0,
594                function_id: None,
595                function_name: None,
596                instruction_count: i,
597                stack_data: vec![],
598                stack_kinds: vec![],
599                module_bindings_data: vec![],
600                module_bindings_kinds: vec![],
601                reason: CaptureReason::Manual,
602            });
603        }
604
605        assert_eq!(tt.snapshot_count(), 3);
606        // Oldest snapshots (0, 1) should have been evicted
607        assert_eq!(tt.snapshots.front().unwrap().index, 2);
608    }
609
610    #[test]
611    fn test_context_window() {
612        let mut tt = TimeTravel::with_config(TimeTravelConfig {
613            capture_mode: CaptureMode::FunctionBoundaries,
614            max_snapshots: 100,
615        });
616
617        for i in 0..10u64 {
618            tt.capture(VmSnapshot {
619                index: i,
620                ip: 0,
621                sp: 0,
622                call_depth: 0,
623                function_id: None,
624                function_name: None,
625                instruction_count: 0,
626                stack_data: vec![],
627                stack_kinds: vec![],
628                module_bindings_data: vec![],
629                module_bindings_kinds: vec![],
630                reason: CaptureReason::Manual,
631            });
632        }
633
634        tt.goto(5);
635        let window = tt.context_window(2);
636        assert_eq!(window.len(), 5); // indices 3,4,5,6,7
637        assert_eq!(window[0].index, 3);
638        assert_eq!(window[4].index, 7);
639    }
640
641    #[test]
642    fn test_function_boundary_mode() {
643        let mut tt = TimeTravel::with_config(TimeTravelConfig {
644            capture_mode: CaptureMode::FunctionBoundaries,
645            max_snapshots: 100,
646        });
647
648        assert!(tt.on_function_entry());
649        assert!(tt.on_function_exit());
650        assert!(tt.is_enabled());
651    }
652
653    #[test]
654    fn test_clear() {
655        let mut tt = TimeTravel::with_config(TimeTravelConfig {
656            capture_mode: CaptureMode::FunctionBoundaries,
657            max_snapshots: 100,
658        });
659
660        tt.capture(VmSnapshot {
661            index: 0,
662            ip: 0,
663            sp: 0,
664            call_depth: 0,
665            function_id: None,
666            function_name: None,
667            instruction_count: 0,
668            stack_data: vec![],
669            stack_kinds: vec![],
670            module_bindings_data: vec![],
671            module_bindings_kinds: vec![],
672            reason: CaptureReason::Manual,
673        });
674
675        assert_eq!(tt.snapshot_count(), 1);
676        tt.clear();
677        assert_eq!(tt.snapshot_count(), 0);
678    }
679}