Skip to main content

vm/debugger/
replay.rs

1use super::*;
2
3pub(super) enum ReplayAction {
4    Continue,
5    Exit,
6}
7
8impl ReplayAction {
9    pub(super) fn should_exit(self) -> bool {
10        matches!(self, ReplayAction::Exit)
11    }
12}
13
14pub(super) fn handle_replay_command(
15    line: &str,
16    recording: &VmRecording,
17    cursor: &mut usize,
18    replay_breakpoints: &mut ReplayBreakpoints,
19    out: &mut dyn Write,
20) -> ReplayAction {
21    let mut parts = line.split_whitespace();
22    let Some(cmd) = parts.next() else {
23        return ReplayAction::Continue;
24    };
25    match cmd {
26        "q" | "quit" | "exit" => return ReplayAction::Exit,
27        "c" | "continue" => {
28            if let Some(next) =
29                replay_breakpoints.next_pause_frame(recording, cursor.saturating_add(1))
30            {
31                *cursor = next;
32            } else {
33                *cursor = recording.frames.len().saturating_sub(1);
34            }
35            let _ = write_replay_position(recording, *cursor, out);
36            return ReplayAction::Continue;
37        }
38        "s" | "step" | "stepi" => {
39            if *cursor + 1 < recording.frames.len() {
40                *cursor += 1;
41            }
42            let _ = write_replay_position(recording, *cursor, out);
43            return ReplayAction::Continue;
44        }
45        "n" | "next" => {
46            *cursor = replay_step_over(recording, *cursor);
47            let _ = write_replay_position(recording, *cursor, out);
48            return ReplayAction::Continue;
49        }
50        "finish" | "out" => {
51            *cursor = replay_step_out(recording, *cursor);
52            let _ = write_replay_position(recording, *cursor, out);
53            return ReplayAction::Continue;
54        }
55        "b" | "break" => {
56            if let Some(arg) = parts.next() {
57                if arg == "line" {
58                    if let Some(requested_line) = parse_u32(parts.next()) {
59                        let line = recording
60                            .program
61                            .debug
62                            .as_ref()
63                            .map(|info| resolve_executable_line(info, requested_line))
64                            .unwrap_or(requested_line);
65                        replay_breakpoints.line_breakpoints.insert(line);
66                        if line == requested_line {
67                            let _ = writeln!(out, "replay pause point set at line {line}");
68                        } else {
69                            let _ = writeln!(
70                                out,
71                                "replay pause point set at line {line} (requested line {requested_line})"
72                            );
73                        }
74                    } else {
75                        let _ = writeln!(out, "usage: break line <number>");
76                    }
77                    return ReplayAction::Continue;
78                }
79                if let Ok(offset) = arg.parse::<usize>() {
80                    replay_breakpoints.offset_breakpoints.insert(offset);
81                    let _ = writeln!(out, "replay pause point set at offset {offset}");
82                } else {
83                    let _ = writeln!(out, "expected instruction offset");
84                }
85            } else {
86                let _ = writeln!(out, "usage: break <offset>");
87            }
88            return ReplayAction::Continue;
89        }
90        "bl" => {
91            if let Some(requested_line) = parse_u32(parts.next()) {
92                let line = recording
93                    .program
94                    .debug
95                    .as_ref()
96                    .map(|info| resolve_executable_line(info, requested_line))
97                    .unwrap_or(requested_line);
98                replay_breakpoints.line_breakpoints.insert(line);
99                if line == requested_line {
100                    let _ = writeln!(out, "replay pause point set at line {line}");
101                } else {
102                    let _ = writeln!(
103                        out,
104                        "replay pause point set at line {line} (requested line {requested_line})"
105                    );
106                }
107            } else {
108                let _ = writeln!(out, "usage: bl <line>");
109            }
110            return ReplayAction::Continue;
111        }
112        "clear" => {
113            if let Some(arg) = parts.next() {
114                if arg == "line" {
115                    if let Some(requested_line) = parse_u32(parts.next()) {
116                        let line = recording
117                            .program
118                            .debug
119                            .as_ref()
120                            .map(|info| resolve_executable_line(info, requested_line))
121                            .unwrap_or(requested_line);
122                        replay_breakpoints.line_breakpoints.remove(&line);
123                        if line == requested_line {
124                            let _ = writeln!(out, "replay pause point cleared at line {line}");
125                        } else {
126                            let _ = writeln!(
127                                out,
128                                "replay pause point cleared at line {line} (requested line {requested_line})"
129                            );
130                        }
131                    } else {
132                        let _ = writeln!(out, "usage: clear line <number>");
133                    }
134                    return ReplayAction::Continue;
135                }
136                if let Ok(offset) = arg.parse::<usize>() {
137                    replay_breakpoints.offset_breakpoints.remove(&offset);
138                    let _ = writeln!(out, "replay pause point cleared at offset {offset}");
139                } else {
140                    let _ = writeln!(out, "expected instruction offset");
141                }
142            } else {
143                let _ = writeln!(out, "usage: clear <offset>");
144            }
145            return ReplayAction::Continue;
146        }
147        "cl" => {
148            if let Some(requested_line) = parse_u32(parts.next()) {
149                let line = recording
150                    .program
151                    .debug
152                    .as_ref()
153                    .map(|info| resolve_executable_line(info, requested_line))
154                    .unwrap_or(requested_line);
155                replay_breakpoints.line_breakpoints.remove(&line);
156                if line == requested_line {
157                    let _ = writeln!(out, "replay pause point cleared at line {line}");
158                } else {
159                    let _ = writeln!(
160                        out,
161                        "replay pause point cleared at line {line} (requested line {requested_line})"
162                    );
163                }
164            } else {
165                let _ = writeln!(out, "usage: cl <line>");
166            }
167            return ReplayAction::Continue;
168        }
169        "breaks" => {
170            let _ = writeln!(
171                out,
172                "replay pause offsets: {:?}",
173                replay_breakpoints.offset_breakpoints
174            );
175            let _ = writeln!(
176                out,
177                "replay pause lines: {:?}",
178                replay_breakpoints.line_breakpoints
179            );
180            return ReplayAction::Continue;
181        }
182        _ => {}
183    }
184
185    let frame = &recording.frames[*cursor];
186    match cmd {
187        "stack" => {
188            let _ = writeln!(out, "stack: {:?}", frame.stack);
189        }
190        "locals" => {
191            print_replay_locals(recording, frame, out);
192        }
193        "p" | "print" => {
194            if let Some(name) = parts.next() {
195                print_replay_local_by_name(recording, frame, name, out);
196            } else {
197                let _ = writeln!(out, "usage: print <local_name>");
198            }
199        }
200        "ip" => {
201            let _ = writeln!(out, "ip: {}", frame.ip);
202        }
203        "where" => {
204            print_replay_where(recording, frame, out);
205        }
206        "funcs" => {
207            if let Some(info) = recording.program.debug.as_ref() {
208                for func in &info.functions {
209                    let _ = writeln!(out, "fn {}({})", func.name, format_args_list(func));
210                }
211            } else {
212                let _ = writeln!(out, "no debug info");
213            }
214        }
215        "help" => {
216            let _ = writeln!(
217                out,
218                "commands: break, break line, bl, clear, clear line, cl, breaks, continue, step, next, out, stack, locals, print, ip, where, funcs, help, quit"
219            );
220        }
221        _ => {
222            let _ = writeln!(out, "unknown command");
223        }
224    }
225    ReplayAction::Continue
226}
227
228pub(super) fn replay_step_over(recording: &VmRecording, cursor: usize) -> usize {
229    if cursor + 1 >= recording.frames.len() {
230        return cursor;
231    }
232    let start = &recording.frames[cursor];
233    for index in (cursor + 1)..recording.frames.len() {
234        let frame = &recording.frames[index];
235        if frame.call_depth <= start.call_depth && frame.ip != start.ip {
236            return index;
237        }
238    }
239    recording.frames.len().saturating_sub(1)
240}
241
242pub(super) fn replay_step_out(recording: &VmRecording, cursor: usize) -> usize {
243    if cursor + 1 >= recording.frames.len() {
244        return cursor;
245    }
246    let start = &recording.frames[cursor];
247    for index in (cursor + 1)..recording.frames.len() {
248        let frame = &recording.frames[index];
249        if frame.call_depth < start.call_depth {
250            return index;
251        }
252    }
253    recording.frames.len().saturating_sub(1)
254}
255
256pub(super) fn write_replay_position(
257    recording: &VmRecording,
258    cursor: usize,
259    out: &mut dyn Write,
260) -> io::Result<()> {
261    let last = recording.frames.len().saturating_sub(1);
262    let frame = &recording.frames[cursor];
263    write!(out, "frame {cursor}/{last}")?;
264    if replay_at_end(recording, cursor) {
265        match recording.terminal_status {
266            Some(status) => write!(out, " [end:{status:?}]")?,
267            None => write!(out, " [end]")?,
268        }
269    }
270    write!(out, ": ip={} depth={}", frame.ip, frame.call_depth)?;
271    if let Some(info) = recording.program.debug.as_ref()
272        && let Some(line) = info.line_for_offset(frame.ip)
273    {
274        if let Some(text) = info.source_line(line) {
275            writeln!(out, " line {line}: {text}")?;
276            return Ok(());
277        }
278        writeln!(out, " line: {line}")?;
279        return Ok(());
280    }
281    writeln!(out)?;
282    Ok(())
283}
284
285pub(super) fn replay_at_end(recording: &VmRecording, cursor: usize) -> bool {
286    cursor + 1 >= recording.frames.len()
287}
288
289#[derive(Default)]
290pub(super) struct ReplayBreakpoints {
291    offset_breakpoints: HashSet<usize>,
292    line_breakpoints: HashSet<u32>,
293}
294
295impl ReplayBreakpoints {
296    pub(super) fn next_pause_frame(
297        &self,
298        recording: &VmRecording,
299        start_index: usize,
300    ) -> Option<usize> {
301        if self.offset_breakpoints.is_empty() && self.line_breakpoints.is_empty() {
302            return None;
303        }
304        for index in start_index..recording.frames.len() {
305            let frame = &recording.frames[index];
306            if self.offset_breakpoints.contains(&frame.ip) {
307                return Some(index);
308            }
309            if let Some(info) = recording.program.debug.as_ref()
310                && let Some(line) = info.line_for_offset(frame.ip)
311                && self.line_breakpoints.contains(&line)
312            {
313                return Some(index);
314            }
315        }
316        None
317    }
318}
319
320pub fn run_recording_replay_command(
321    recording: &VmRecording,
322    state: &mut VmRecordingReplayState,
323    command: &str,
324) -> VmRecordingReplayResponse {
325    if recording.frames.is_empty() {
326        return VmRecordingReplayResponse {
327            output: "recording has no captured frames".to_string(),
328            current_line: None,
329            at_end: true,
330            exited: false,
331        };
332    }
333
334    if state.cursor >= recording.frames.len() {
335        state.cursor = recording.frames.len().saturating_sub(1);
336    }
337
338    let mut replay_breakpoints = ReplayBreakpoints {
339        offset_breakpoints: state.offset_breakpoints.clone(),
340        line_breakpoints: state.line_breakpoints.clone(),
341    };
342    let mut output = Vec::<u8>::new();
343    let action = handle_replay_command(
344        command,
345        recording,
346        &mut state.cursor,
347        &mut replay_breakpoints,
348        &mut output,
349    );
350
351    state.offset_breakpoints = replay_breakpoints.offset_breakpoints;
352    state.line_breakpoints = replay_breakpoints.line_breakpoints;
353    let current_line = replay_current_line(recording, state.cursor);
354
355    VmRecordingReplayResponse {
356        output: String::from_utf8_lossy(&output).to_string(),
357        current_line,
358        at_end: replay_at_end(recording, state.cursor),
359        exited: action.should_exit(),
360    }
361}
362
363pub fn replay_recording_stdio(recording: &VmRecording) {
364    if recording.frames.is_empty() {
365        println!("recording has no captured frames");
366        return;
367    }
368
369    let mut cursor = 0usize;
370    let mut replay_breakpoints = ReplayBreakpoints::default();
371    println!(
372        "recording loaded: frames={}, terminal={:?}",
373        recording.frames.len(),
374        recording.terminal_status
375    );
376    let _ = write_replay_position(recording, cursor, &mut io::stdout());
377
378    let stdin = io::stdin();
379    let mut input = String::new();
380    loop {
381        input.clear();
382        if replay_at_end(recording, cursor) {
383            print!("(pdb-rec:end) ");
384        } else {
385            print!("(pdb-rec) ");
386        }
387        let _ = io::stdout().flush();
388        if stdin.read_line(&mut input).is_err() {
389            break;
390        }
391        if handle_replay_command(
392            &input,
393            recording,
394            &mut cursor,
395            &mut replay_breakpoints,
396            &mut io::stdout(),
397        )
398        .should_exit()
399        {
400            break;
401        }
402    }
403}
404
405pub(super) fn replay_current_line(recording: &VmRecording, cursor: usize) -> Option<u32> {
406    let frame = recording.frames.get(cursor)?;
407    recording
408        .program
409        .debug
410        .as_ref()
411        .and_then(|info| info.line_for_offset(frame.ip))
412}
413
414pub(super) fn print_replay_locals(
415    recording: &VmRecording,
416    frame: &VmRecordingFrame,
417    out: &mut dyn Write,
418) {
419    let Some(info) = recording.program.debug.as_ref() else {
420        let _ = writeln!(out, "locals: {:?}", frame.locals);
421        return;
422    };
423
424    if info.locals.is_empty() {
425        let _ = writeln!(out, "locals: {:?}", frame.locals);
426        return;
427    }
428
429    let current_line = info.line_for_offset(frame.ip);
430    for local in &info.locals {
431        if !local_visible_at_line(local, current_line) {
432            continue;
433        }
434        match frame.locals.get(local.index as usize) {
435            Some(value) => {
436                let _ = writeln!(out, "{} = {:?}", local.name, value);
437            }
438            None => {
439                let _ = writeln!(out, "{} = <unavailable>", local.name);
440            }
441        }
442    }
443}
444
445pub(super) fn local_visible_at_line(local: &LocalInfo, line: Option<u32>) -> bool {
446    let Some(line) = line else {
447        return true;
448    };
449    if let Some(declared_line) = local.declared_line
450        && line < declared_line
451    {
452        return false;
453    }
454    if let Some(last_line) = local.last_line
455        && line > last_line
456    {
457        return false;
458    }
459    true
460}
461
462pub(super) fn print_replay_local_by_name(
463    recording: &VmRecording,
464    frame: &VmRecordingFrame,
465    name: &str,
466    out: &mut dyn Write,
467) {
468    let Some(info) = recording.program.debug.as_ref() else {
469        let _ = writeln!(out, "no debug info");
470        return;
471    };
472
473    let Some(local) = info.locals.iter().find(|local| local.name == name) else {
474        let _ = writeln!(out, "unknown local '{name}'");
475        return;
476    };
477    let current_line = info.line_for_offset(frame.ip);
478    if !local_visible_at_line(local, current_line) {
479        let _ = writeln!(out, "local '{name}' is not visible in this frame");
480        return;
481    }
482
483    match frame.locals.get(local.index as usize) {
484        Some(value) => {
485            let _ = writeln!(out, "{name} = {:?}", value);
486        }
487        None => {
488            let _ = writeln!(out, "local '{name}' is out of range for this frame");
489        }
490    }
491}
492
493pub(super) fn print_replay_where(
494    recording: &VmRecording,
495    frame: &VmRecordingFrame,
496    out: &mut dyn Write,
497) {
498    if let Some(info) = recording.program.debug.as_ref() {
499        if let Some(line) = info.line_for_offset(frame.ip) {
500            if let Some(text) = info.source_line(line) {
501                let _ = writeln!(out, "line {line}: {text}");
502            } else {
503                let _ = writeln!(out, "line: {line}");
504            }
505        } else {
506            let _ = writeln!(out, "line: unknown");
507        }
508    } else {
509        let _ = writeln!(out, "no debug info");
510    }
511}