1use 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#[derive(Debug, Clone)]
30pub enum CaptureMode {
31 FunctionBoundaries,
33 EveryNInstructions(u64),
35 Breakpoints(Vec<usize>),
37 Disabled,
39}
40
41impl Default for CaptureMode {
42 fn default() -> Self {
43 Self::Disabled
44 }
45}
46
47#[derive(Debug)]
56pub struct VmSnapshot {
57 pub index: u64,
59 pub ip: usize,
61 pub sp: usize,
63 pub call_depth: usize,
65 pub function_id: Option<u16>,
67 pub function_name: Option<String>,
69 pub instruction_count: u64,
71 pub stack_data: Vec<u64>,
74 pub stack_kinds: Vec<NativeKind>,
77 pub module_bindings_data: Vec<u64>,
80 pub module_bindings_kinds: Vec<NativeKind>,
83 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 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 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#[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#[derive(Debug, Clone)]
180pub struct TimeTravelConfig {
181 pub capture_mode: CaptureMode,
183 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
196pub struct TimeTravel {
198 config: TimeTravelConfig,
199 snapshots: VecDeque<VmSnapshot>,
201 cursor: usize,
203 next_index: u64,
205 instruction_counter: u64,
207 snapshot_store: Option<SnapshotStore>,
209}
210
211impl TimeTravel {
212 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 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 pub fn disabled() -> Self {
238 Self::with_config(TimeTravelConfig::default())
239 }
240
241 #[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 pub fn on_function_entry(&mut self) -> bool {
275 matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
276 }
277
278 pub fn on_function_exit(&mut self) -> bool {
280 matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
281 }
282
283 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 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 pub fn capture(&mut self, snapshot: VmSnapshot) {
338 if self.snapshots.len() >= self.config.max_snapshots {
339 self.snapshots.pop_front();
340 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 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 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 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 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 pub fn current(&self) -> Option<&VmSnapshot> {
446 self.snapshots.get(self.cursor)
447 }
448
449 pub fn latest(&self) -> Option<&VmSnapshot> {
451 self.snapshots.back()
452 }
453
454 pub fn snapshot_count(&self) -> usize {
456 self.snapshots.len()
457 }
458
459 pub fn cursor_position(&self) -> usize {
461 self.cursor
462 }
463
464 pub fn is_enabled(&self) -> bool {
466 !matches!(self.config.capture_mode, CaptureMode::Disabled)
467 }
468
469 pub fn clear(&mut self) {
471 self.snapshots.clear();
472 self.cursor = 0;
473 }
474
475 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)); assert!(!tt.should_capture(1, 2, false)); assert!(tt.should_capture(2, 3, false)); assert!(!tt.should_capture(3, 4, false)); }
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 assert!(!tt.should_capture(0, 1, false));
546 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); let prev = tt.step_back().unwrap();
569 assert_eq!(prev.index, 3);
570 assert_eq!(tt.cursor_position(), 3);
571
572 let next = tt.step_forward().unwrap();
574 assert_eq!(next.index, 4);
575
576 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 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); 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}