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}