1use std::collections::HashSet;
2use std::io::{self, BufRead, Write};
3use std::net::{TcpListener, TcpStream};
4use std::sync::{Arc, Condvar, Mutex};
5use std::time::{Duration, Instant};
6
7use crate::debug_info::{DebugInfo, LocalInfo};
8use crate::vm::{Program, Vm, VmError, VmStatus};
9
10mod recording;
11mod replay;
12#[cfg(test)]
13mod tests;
14
15use self::recording::VmRecordingBuilder;
16pub use self::recording::{
17 VmRecording, VmRecordingError, VmRecordingFrame, VmRecordingReplayResponse,
18 VmRecordingReplayState,
19};
20use self::replay::*;
21pub use self::replay::{replay_recording_stdio, run_recording_replay_command};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum StepMode {
25 Running,
26 Step,
27 StepOver { depth: usize, ip: usize },
28 StepOut { depth: usize },
29}
30
31pub struct Debugger {
32 breakpoints: HashSet<usize>,
33 line_breakpoints: HashSet<u32>,
34 step_mode: StepMode,
35 server: Option<DebugServer>,
36 bridge: Option<DebugCommandBridge>,
37 recording: Option<VmRecordingBuilder>,
38 client_detached: bool,
39}
40
41#[derive(Clone)]
42pub struct DebugCommandBridge {
43 inner: Arc<DebugCommandBridgeInner>,
44}
45
46struct DebugCommandBridgeInner {
47 state: Mutex<DebugCommandBridgeState>,
48 changed: Condvar,
49}
50
51struct DebugCommandBridgeState {
52 attached: bool,
53 current_line: Option<u32>,
54 closed: bool,
55 next_request_id: u64,
56 pending_request: Option<DebugCommandBridgeRequest>,
57 pending_response: Option<DebugCommandBridgeResponseInternal>,
58}
59
60struct DebugCommandBridgeRequest {
61 request_id: u64,
62 command: String,
63}
64
65#[derive(Clone, Debug)]
66pub struct DebugCommandBridgeResponse {
67 pub output: String,
68 pub current_line: Option<u32>,
69 pub attached: bool,
70 pub resumed: bool,
71}
72pub struct DebugCommandBridgeStatus {
73 pub attached: bool,
74 pub current_line: Option<u32>,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub enum DebugCommandBridgeError {
79 NotAttached,
80 Timeout,
81 Closed,
82}
83
84impl Default for Debugger {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl Debugger {
91 pub fn new() -> Self {
92 Self {
93 breakpoints: HashSet::new(),
94 line_breakpoints: HashSet::new(),
95 step_mode: StepMode::Running,
96 server: None,
97 bridge: None,
98 recording: None,
99 client_detached: false,
100 }
101 }
102
103 pub fn with_tcp(addr: &str) -> io::Result<Self> {
104 let listener = TcpListener::bind(addr)?;
105 listener.set_nonblocking(false)?;
106 Ok(Self {
107 breakpoints: HashSet::new(),
108 line_breakpoints: HashSet::new(),
109 step_mode: StepMode::Running,
110 server: Some(DebugServer::new(listener)),
111 bridge: None,
112 recording: None,
113 client_detached: false,
114 })
115 }
116
117 pub fn with_command_bridge(bridge: DebugCommandBridge) -> Self {
118 Self {
119 breakpoints: HashSet::new(),
120 line_breakpoints: HashSet::new(),
121 step_mode: StepMode::Running,
122 server: None,
123 bridge: Some(bridge),
124 recording: None,
125 client_detached: false,
126 }
127 }
128
129 pub fn with_recording(program: Program) -> Self {
130 Self {
131 breakpoints: HashSet::new(),
132 line_breakpoints: HashSet::new(),
133 step_mode: StepMode::Running,
134 server: None,
135 bridge: None,
136 recording: Some(VmRecordingBuilder::new(program)),
137 client_detached: false,
138 }
139 }
140
141 pub fn stop_on_entry(&mut self) {
142 self.step_mode = StepMode::Step;
143 }
144
145 pub fn add_breakpoint(&mut self, offset: usize) {
146 self.breakpoints.insert(offset);
147 }
148
149 pub fn remove_breakpoint(&mut self, offset: usize) {
150 self.breakpoints.remove(&offset);
151 }
152
153 pub fn on_instruction(&mut self, vm: &mut Vm) {
154 if let Some(recording) = self.recording.as_mut() {
155 recording.record_state(vm);
156 }
157
158 let ip = vm.ip();
159 let mut should_break = self.breakpoints.contains(&ip);
160
161 if !should_break
162 && let Some(line) = current_line(vm)
163 && self.line_breakpoints.contains(&line)
164 {
165 should_break = true;
166 }
167
168 if !should_break {
169 match self.step_mode {
170 StepMode::Step => {
171 should_break = true;
172 }
173 StepMode::StepOver {
174 depth,
175 ip: start_ip,
176 } => {
177 if vm.call_depth() <= depth && ip != start_ip {
178 should_break = true;
179 }
180 }
181 StepMode::StepOut { depth } => {
182 if vm.call_depth() < depth {
183 should_break = true;
184 }
185 }
186 StepMode::Running => {}
187 }
188 }
189 if should_break {
190 self.step_mode = StepMode::Running;
191 self.client_detached = self.repl(vm, None);
192 }
193 }
194
195 pub fn on_vm_status(&mut self, vm: &Vm, status: VmStatus) {
196 if let Some(recording) = self.recording.as_mut() {
197 recording.on_terminal_status(vm, status);
198 }
199 }
200
201 pub fn on_vm_error(&mut self, vm: &mut Vm, err: &VmError) -> bool {
202 let banner = match err {
203 VmError::OutOfFuel { needed, remaining } => Some(format!(
204 "execution interrupted: out of fuel (needed {needed}, remaining {remaining}). use `fuel set <n>` or `fuel add <n>`, then `continue`"
205 )),
206 VmError::EpochDeadlineReached { current, deadline } => Some(format!(
207 "execution interrupted: epoch deadline reached (current {current}, deadline {deadline}). `continue` will re-arm the same deadline automatically; use `epoch tick <n>` to advance the global epoch or `epoch deadline <ticks>` to change the slice size first"
208 )),
209 _ => None,
210 };
211 self.client_detached = self.repl(vm, banner.as_deref());
212 !self.client_detached
213 }
214
215 pub fn take_recording(&mut self) -> Option<VmRecording> {
216 self.recording.take().map(VmRecordingBuilder::finish)
217 }
218
219 pub fn take_detach_event(&mut self) -> bool {
220 std::mem::take(&mut self.client_detached)
221 }
222
223 fn repl(&mut self, vm: &mut Vm, banner: Option<&str>) -> bool {
224 if let Some(server) = self.server.as_mut() {
225 return server.repl(
226 vm,
227 &mut self.breakpoints,
228 &mut self.line_breakpoints,
229 &mut self.step_mode,
230 banner,
231 );
232 }
233 if let Some(bridge) = self.bridge.as_ref() {
234 return bridge.repl(
235 vm,
236 &mut self.breakpoints,
237 &mut self.line_breakpoints,
238 &mut self.step_mode,
239 banner,
240 );
241 }
242 repl_stdio(
243 vm,
244 &mut self.breakpoints,
245 &mut self.line_breakpoints,
246 &mut self.step_mode,
247 banner,
248 );
249 false
250 }
251}
252
253impl DebugCommandBridge {
254 pub fn new() -> Self {
255 Self {
256 inner: Arc::new(DebugCommandBridgeInner {
257 state: Mutex::new(DebugCommandBridgeState {
258 attached: false,
259 current_line: None,
260 closed: false,
261 next_request_id: 0,
262 pending_request: None,
263 pending_response: None,
264 }),
265 changed: Condvar::new(),
266 }),
267 }
268 }
269
270 pub fn status(&self) -> DebugCommandBridgeStatus {
271 let state = self
272 .inner
273 .state
274 .lock()
275 .expect("debug command bridge lock poisoned");
276 DebugCommandBridgeStatus {
277 attached: state.attached,
278 current_line: state.current_line,
279 }
280 }
281
282 pub fn close(&self) {
283 let mut state = self
284 .inner
285 .state
286 .lock()
287 .expect("debug command bridge lock poisoned");
288 state.closed = true;
289 state.attached = false;
290 state.current_line = None;
291 state.pending_request = None;
292 state.pending_response = None;
293 self.inner.changed.notify_all();
294 }
295
296 pub fn execute(
297 &self,
298 command: impl Into<String>,
299 timeout: Duration,
300 ) -> Result<DebugCommandBridgeResponse, DebugCommandBridgeError> {
301 let mut state = self
302 .inner
303 .state
304 .lock()
305 .expect("debug command bridge lock poisoned");
306 if state.closed {
307 return Err(DebugCommandBridgeError::Closed);
308 }
309 if !state.attached {
310 return Err(DebugCommandBridgeError::NotAttached);
311 }
312
313 state.next_request_id = state.next_request_id.saturating_add(1);
314 let request_id = state.next_request_id;
315 state.pending_request = Some(DebugCommandBridgeRequest {
316 request_id,
317 command: command.into(),
318 });
319 self.inner.changed.notify_all();
320
321 let deadline = Instant::now() + timeout;
322 loop {
323 if state.closed {
324 return Err(DebugCommandBridgeError::Closed);
325 }
326 if let Some(response) = state.pending_response.clone()
327 && response.request_id == request_id
328 {
329 state.pending_response = None;
330 return Ok(DebugCommandBridgeResponse {
331 output: response.output,
332 current_line: response.current_line,
333 attached: response.attached,
334 resumed: response.resumed,
335 });
336 }
337 let now = Instant::now();
338 if now >= deadline {
339 return Err(DebugCommandBridgeError::Timeout);
340 }
341 let wait_for = deadline.saturating_duration_since(now);
342 let (next_state, wait_result) = self
343 .inner
344 .changed
345 .wait_timeout(state, wait_for)
346 .expect("debug command bridge lock poisoned");
347 state = next_state;
348 if wait_result.timed_out() {
349 return Err(DebugCommandBridgeError::Timeout);
350 }
351 }
352 }
353
354 fn repl(
355 &self,
356 vm: &mut Vm,
357 breakpoints: &mut HashSet<usize>,
358 line_breakpoints: &mut HashSet<u32>,
359 step: &mut StepMode,
360 _banner: Option<&str>,
361 ) -> bool {
362 {
363 let mut state = self
364 .inner
365 .state
366 .lock()
367 .expect("debug command bridge lock poisoned");
368 if state.closed {
369 state.attached = false;
370 state.current_line = None;
371 state.pending_request = None;
372 state.pending_response = None;
373 self.inner.changed.notify_all();
374 return true;
375 }
376 state.attached = true;
377 state.current_line = current_line(vm);
378 state.pending_request = None;
379 self.inner.changed.notify_all();
382 }
383
384 loop {
385 let request = {
386 let mut state = self
387 .inner
388 .state
389 .lock()
390 .expect("debug command bridge lock poisoned");
391 while !state.closed && state.pending_request.is_none() {
392 state = self
393 .inner
394 .changed
395 .wait(state)
396 .expect("debug command bridge lock poisoned");
397 }
398 if state.closed {
399 state.attached = false;
400 state.current_line = None;
401 state.pending_request = None;
402 state.pending_response = None;
403 self.inner.changed.notify_all();
404 return true;
405 }
406 state
407 .pending_request
408 .take()
409 .expect("debug command request missing")
410 };
411
412 let mut output = Vec::<u8>::new();
413 let action = handle_command(
414 &request.command,
415 vm,
416 breakpoints,
417 line_breakpoints,
418 step,
419 &mut output,
420 );
421 let resumed = action.is_break();
422 let current_line = if resumed { None } else { current_line(vm) };
423 let attached = !resumed;
424 let output = String::from_utf8_lossy(&output).to_string();
425
426 let mut state = self
427 .inner
428 .state
429 .lock()
430 .expect("debug command bridge lock poisoned");
431 state.attached = attached;
432 state.current_line = current_line;
433 state.pending_response = Some(DebugCommandBridgeResponseInternal {
434 request_id: request.request_id,
435 output,
436 current_line,
437 attached,
438 resumed,
439 });
440 self.inner.changed.notify_all();
441
442 if resumed {
443 return false;
444 }
445 }
446 }
447}
448
449impl Default for DebugCommandBridge {
450 fn default() -> Self {
451 Self::new()
452 }
453}
454
455impl std::fmt::Display for DebugCommandBridgeError {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 match self {
458 DebugCommandBridgeError::NotAttached => write!(f, "debugger is not attached"),
459 DebugCommandBridgeError::Timeout => write!(f, "timed out waiting for debugger"),
460 DebugCommandBridgeError::Closed => write!(f, "debugger bridge is closed"),
461 }
462 }
463}
464
465impl std::error::Error for DebugCommandBridgeError {}
466
467#[derive(Clone)]
468struct DebugCommandBridgeResponseInternal {
469 request_id: u64,
470 output: String,
471 current_line: Option<u32>,
472 attached: bool,
473 resumed: bool,
474}
475
476struct DebugServer {
477 listener: TcpListener,
478 stream: Option<TcpStream>,
479}
480
481impl DebugServer {
482 fn new(listener: TcpListener) -> Self {
483 Self {
484 listener,
485 stream: None,
486 }
487 }
488
489 fn ensure_client(&mut self) -> io::Result<()> {
490 if self.stream.is_none() {
491 let (stream, _) = self.listener.accept()?;
492 self.stream = Some(stream);
493 }
494 Ok(())
495 }
496
497 fn repl(
498 &mut self,
499 vm: &mut Vm,
500 breakpoints: &mut HashSet<usize>,
501 line_breakpoints: &mut HashSet<u32>,
502 step: &mut StepMode,
503 banner: Option<&str>,
504 ) -> bool {
505 if self.ensure_client().is_err() {
506 return false;
507 }
508 let Some(stream) = self.stream.as_mut() else {
509 return false;
510 };
511 let _ = writeln!(stream, "debugger attached. type 'help' for commands");
512 if let Some(text) = banner {
513 let _ = writeln!(stream, "{text}");
514 }
515 let Ok(clone) = stream.try_clone() else {
516 self.stream = None;
517 return true;
518 };
519 let mut reader = io::BufReader::new(clone);
520 loop {
521 if write_prompt(stream).is_err() {
522 self.stream = None;
523 return true;
524 }
525 let mut line = String::new();
526 match reader.read_line(&mut line) {
527 Ok(0) => {
528 self.stream = None;
529 return true;
530 }
531 Ok(_) => {}
532 Err(_) => {
533 self.stream = None;
534 return true;
535 }
536 }
537 if handle_command(&line, vm, breakpoints, line_breakpoints, step, stream).is_break() {
538 return false;
539 }
540 }
541 }
542}
543
544fn repl_stdio(
545 vm: &mut Vm,
546 breakpoints: &mut HashSet<usize>,
547 line_breakpoints: &mut HashSet<u32>,
548 step: &mut StepMode,
549 banner: Option<&str>,
550) {
551 let stdin = io::stdin();
552 let mut input = String::new();
553 if let Some(text) = banner {
554 println!("{text}");
555 }
556 loop {
557 input.clear();
558 print!("(pdb) ");
559 let _ = io::stdout().flush();
560 if stdin.read_line(&mut input).is_err() {
561 break;
562 }
563 if handle_command(
564 &input,
565 vm,
566 breakpoints,
567 line_breakpoints,
568 step,
569 &mut io::stdout(),
570 )
571 .is_break()
572 {
573 break;
574 }
575 }
576}
577
578fn write_prompt(stream: &mut TcpStream) -> io::Result<()> {
579 stream.write_all(b"(pdb) ")?;
580 stream.flush()
581}
582
583#[derive(Clone, Copy, Debug, PartialEq, Eq)]
584enum ReplAction {
585 Continue,
586 Break,
587}
588
589impl ReplAction {
590 fn is_break(self) -> bool {
591 matches!(self, ReplAction::Break)
592 }
593}
594
595fn handle_command(
596 line: &str,
597 vm: &mut Vm,
598 breakpoints: &mut HashSet<usize>,
599 line_breakpoints: &mut HashSet<u32>,
600 step: &mut StepMode,
601 out: &mut dyn Write,
602) -> ReplAction {
603 let mut parts = line.split_whitespace();
604 let Some(cmd) = parts.next() else {
605 return ReplAction::Continue;
606 };
607 match cmd {
608 "c" | "continue" => return ReplAction::Break,
609 "s" | "step" | "stepi" => {
610 *step = StepMode::Step;
611 return ReplAction::Break;
612 }
613 "n" | "next" => {
614 *step = StepMode::StepOver {
615 depth: vm.call_depth(),
616 ip: vm.ip(),
617 };
618 return ReplAction::Break;
619 }
620 "finish" | "out" => {
621 *step = StepMode::StepOut {
622 depth: vm.call_depth(),
623 };
624 return ReplAction::Break;
625 }
626 "b" | "break" => {
627 if let Some(arg) = parts.next() {
628 if arg == "line" {
629 if let Some(requested_line) = parse_u32(parts.next()) {
630 let line = vm
631 .debug_info()
632 .map(|info| resolve_executable_line(info, requested_line))
633 .unwrap_or(requested_line);
634 line_breakpoints.insert(line);
635 if line == requested_line {
636 let _ = writeln!(out, "line breakpoint set at {line}");
637 } else {
638 let _ = writeln!(
639 out,
640 "line breakpoint set at {line} (requested line {requested_line})"
641 );
642 }
643 } else {
644 let _ = writeln!(out, "usage: break line <number>");
645 }
646 return ReplAction::Continue;
647 }
648 if let Ok(offset) = arg.parse::<usize>() {
649 breakpoints.insert(offset);
650 let _ = writeln!(out, "breakpoint set at {offset}");
651 } else {
652 let _ = writeln!(out, "expected instruction offset");
653 }
654 } else {
655 let _ = writeln!(out, "usage: break <offset>");
656 }
657 }
658 "bl" => {
659 if let Some(requested_line) = parse_u32(parts.next()) {
660 let line = vm
661 .debug_info()
662 .map(|info| resolve_executable_line(info, requested_line))
663 .unwrap_or(requested_line);
664 line_breakpoints.insert(line);
665 if line == requested_line {
666 let _ = writeln!(out, "line breakpoint set at {line}");
667 } else {
668 let _ = writeln!(
669 out,
670 "line breakpoint set at {line} (requested line {requested_line})"
671 );
672 }
673 } else {
674 let _ = writeln!(out, "usage: bl <line>");
675 }
676 }
677 "clear" => {
678 if let Some(arg) = parts.next() {
679 if arg == "line" {
680 if let Some(requested_line) = parse_u32(parts.next()) {
681 let line = vm
682 .debug_info()
683 .map(|info| resolve_executable_line(info, requested_line))
684 .unwrap_or(requested_line);
685 line_breakpoints.remove(&line);
686 if line == requested_line {
687 let _ = writeln!(out, "line breakpoint cleared at {line}");
688 } else {
689 let _ = writeln!(
690 out,
691 "line breakpoint cleared at {line} (requested line {requested_line})"
692 );
693 }
694 } else {
695 let _ = writeln!(out, "usage: clear line <number>");
696 }
697 return ReplAction::Continue;
698 }
699 if let Ok(offset) = arg.parse::<usize>() {
700 breakpoints.remove(&offset);
701 let _ = writeln!(out, "breakpoint cleared at {offset}");
702 } else {
703 let _ = writeln!(out, "expected instruction offset");
704 }
705 } else {
706 let _ = writeln!(out, "usage: clear <offset>");
707 }
708 }
709 "cl" => {
710 if let Some(requested_line) = parse_u32(parts.next()) {
711 let line = vm
712 .debug_info()
713 .map(|info| resolve_executable_line(info, requested_line))
714 .unwrap_or(requested_line);
715 line_breakpoints.remove(&line);
716 if line == requested_line {
717 let _ = writeln!(out, "line breakpoint cleared at {line}");
718 } else {
719 let _ = writeln!(
720 out,
721 "line breakpoint cleared at {line} (requested line {requested_line})"
722 );
723 }
724 } else {
725 let _ = writeln!(out, "usage: cl <line>");
726 }
727 }
728 "breaks" => {
729 let _ = writeln!(out, "breakpoints: {:?}", breakpoints);
730 let _ = writeln!(out, "line breakpoints: {:?}", line_breakpoints);
731 }
732 "stack" => {
733 let _ = writeln!(out, "stack: {:?}", vm.stack());
734 }
735 "locals" => {
736 print_locals(vm, out);
737 }
738 "p" | "print" => {
739 if let Some(name) = parts.next() {
740 print_local_by_name(vm, name, out);
741 } else {
742 let _ = writeln!(out, "usage: print <local_name>");
743 }
744 }
745 "ip" => {
746 let _ = writeln!(out, "ip: {}", vm.ip());
747 }
748 "fuel" => {
749 let Some(sub) = parts.next() else {
750 print_fuel_state(vm, out);
751 return ReplAction::Continue;
752 };
753 match sub {
754 "set" => {
755 if let Some(value) = parse_u64(parts.next()) {
756 vm.set_fuel(value);
757 let _ = writeln!(out, "fuel set to {value}");
758 print_fuel_state(vm, out);
759 } else {
760 let _ = writeln!(out, "usage: fuel set <amount>");
761 }
762 }
763 "add" => {
764 if let Some(value) = parse_u64(parts.next()) {
765 match vm.add_fuel(value) {
766 Ok(()) => {
767 let _ = writeln!(out, "fuel added: {value}");
768 print_fuel_state(vm, out);
769 }
770 Err(err) => {
771 let _ = writeln!(out, "fuel add failed: {err}");
772 }
773 }
774 } else {
775 let _ = writeln!(out, "usage: fuel add <amount>");
776 }
777 }
778 "clear" => {
779 vm.clear_fuel();
780 let _ = writeln!(out, "fuel metering disabled");
781 print_fuel_state(vm, out);
782 }
783 "interval" => {
784 if let Some(raw) = parts.next() {
785 match raw.parse::<u32>() {
786 Ok(interval) => match vm.set_fuel_check_interval(interval) {
787 Ok(()) => {
788 let _ = writeln!(out, "fuel check interval set to {interval}");
789 print_fuel_state(vm, out);
790 }
791 Err(err) => {
792 let _ = writeln!(out, "fuel interval update failed: {err}");
793 }
794 },
795 Err(_) => {
796 let _ = writeln!(out, "usage: fuel interval <n>");
797 }
798 }
799 } else {
800 let _ = writeln!(out, "fuel check interval: {}", vm.fuel_check_interval());
801 }
802 }
803 _ => {
804 let _ = writeln!(
805 out,
806 "usage: fuel | fuel set <amount> | fuel add <amount> | fuel clear | fuel interval [n]"
807 );
808 }
809 }
810 }
811 "epoch" => {
812 let Some(sub) = parts.next() else {
813 print_epoch_state(vm, out);
814 return ReplAction::Continue;
815 };
816 match sub {
817 "tick" => {
818 let delta = parse_u64(parts.next()).unwrap_or(1);
819 let current = vm.increment_epoch_by(delta);
820 let _ = writeln!(out, "epoch advanced by {delta} to {current}");
821 print_epoch_state(vm, out);
822 }
823 "deadline" | "set" => {
824 if let Some(value) = parse_u64(parts.next()) {
825 match vm.set_epoch_deadline(value) {
826 Ok(()) => {
827 let _ = writeln!(
828 out,
829 "epoch deadline set {value} ticks beyond current epoch"
830 );
831 print_epoch_state(vm, out);
832 }
833 Err(err) => {
834 let _ = writeln!(out, "epoch deadline update failed: {err}");
835 }
836 }
837 } else {
838 let _ = writeln!(out, "usage: epoch deadline <ticks>");
839 }
840 }
841 "clear" => {
842 vm.clear_epoch_deadline();
843 let _ = writeln!(out, "epoch interruption disabled");
844 print_epoch_state(vm, out);
845 }
846 "interval" => {
847 if let Some(raw) = parts.next() {
848 match raw.parse::<u32>() {
849 Ok(interval) => match vm.set_epoch_check_interval(interval) {
850 Ok(()) => {
851 let _ = writeln!(out, "epoch check interval set to {interval}");
852 print_epoch_state(vm, out);
853 }
854 Err(err) => {
855 let _ = writeln!(out, "epoch interval update failed: {err}");
856 }
857 },
858 Err(_) => {
859 let _ = writeln!(out, "usage: epoch interval <n>");
860 }
861 }
862 } else {
863 let _ =
864 writeln!(out, "epoch check interval: {}", vm.epoch_check_interval());
865 }
866 }
867 _ => {
868 let _ = writeln!(
869 out,
870 "usage: epoch | epoch tick [n] | epoch deadline <ticks> | epoch clear | epoch interval [n]"
871 );
872 }
873 }
874 }
875 "where" => {
876 if let Some(info) = vm.debug_info() {
877 let line = info.line_for_offset(vm.ip());
878 if let Some(line) = line {
879 if let Some(text) = info.source_line(line) {
880 let _ = writeln!(out, "line {line}: {text}");
881 } else {
882 let _ = writeln!(out, "line: {line}");
883 }
884 } else {
885 let _ = writeln!(out, "line: unknown");
886 }
887 } else {
888 let _ = writeln!(out, "no debug info");
889 }
890 }
891 "funcs" => {
892 if let Some(info) = vm.debug_info() {
893 for func in &info.functions {
894 let _ = writeln!(out, "fn {}({})", func.name, format_args_list(func));
895 }
896 } else {
897 let _ = writeln!(out, "no debug info");
898 }
899 }
900 "help" => {
901 let _ = writeln!(
902 out,
903 "commands: break, break line, bl, clear, clear line, cl, breaks, continue, step, next, out, stack, locals, print, ip, where, funcs, fuel, epoch, help"
904 );
905 }
906 _ => {
907 let _ = writeln!(out, "unknown command");
908 }
909 }
910 ReplAction::Continue
911}
912
913fn format_args_list(func: &crate::debug_info::DebugFunction) -> String {
914 let mut parts = Vec::new();
915 for arg in &func.args {
916 parts.push(format!("{}:{}", arg.position, arg.name));
917 }
918 parts.join(", ")
919}
920
921fn print_locals(vm: &Vm, out: &mut dyn Write) {
922 let Some(info) = vm.debug_info() else {
923 let _ = writeln!(out, "locals: {:?}", vm.locals());
924 return;
925 };
926
927 if info.locals.is_empty() {
928 let _ = writeln!(out, "locals: {:?}", vm.locals());
929 return;
930 }
931
932 let current_line = info.line_for_offset(vm.ip());
933 for local in &info.locals {
934 if !local_visible_at_line(local, current_line) {
935 continue;
936 }
937 match vm.locals().get(local.index as usize) {
938 Some(value) => {
939 let _ = writeln!(out, "{} = {:?}", local.name, value);
940 }
941 None => {
942 let _ = writeln!(out, "{} = <unavailable>", local.name);
943 }
944 }
945 }
946}
947
948fn print_local_by_name(vm: &Vm, name: &str, out: &mut dyn Write) {
949 let Some(info) = vm.debug_info() else {
950 let _ = writeln!(out, "no debug info");
951 return;
952 };
953
954 let Some(local) = info.locals.iter().find(|local| local.name == name) else {
955 let _ = writeln!(out, "unknown local '{name}'");
956 return;
957 };
958 let current_line = info.line_for_offset(vm.ip());
959 if !local_visible_at_line(local, current_line) {
960 let _ = writeln!(out, "local '{name}' is not visible at this location");
961 return;
962 }
963
964 match vm.locals().get(local.index as usize) {
965 Some(value) => {
966 let _ = writeln!(out, "{name} = {:?}", value);
967 }
968 None => {
969 let _ = writeln!(out, "local '{name}' is out of range for this VM instance");
970 }
971 }
972}
973
974fn print_fuel_state(vm: &Vm, out: &mut dyn Write) {
975 let remaining = vm
976 .get_fuel()
977 .map(|value| value.to_string())
978 .unwrap_or_else(|| "disabled".to_string());
979 let _ = writeln!(
980 out,
981 "fuel: {remaining}, check_interval={}",
982 vm.fuel_check_interval()
983 );
984}
985
986fn print_epoch_state(vm: &Vm, out: &mut dyn Write) {
987 let deadline = vm
988 .epoch_deadline()
989 .map(|value| value.to_string())
990 .unwrap_or_else(|| "disabled".to_string());
991 let slice = vm
992 .epoch_deadline_delta()
993 .map(|value| value.to_string())
994 .unwrap_or_else(|| "disabled".to_string());
995 let _ = writeln!(
996 out,
997 "epoch: current={}, deadline={}, slice={}, check_interval={}",
998 vm.current_epoch(),
999 deadline,
1000 slice,
1001 vm.epoch_check_interval()
1002 );
1003}
1004
1005pub fn attach_with_debugger(vm: &mut Vm, debugger: &mut Debugger) {
1006 debugger.on_instruction(vm);
1007}
1008
1009pub fn debug_info_from_vm(vm: &Vm) -> Option<&DebugInfo> {
1010 vm.debug_info()
1011}
1012
1013fn current_line(vm: &Vm) -> Option<u32> {
1014 vm.debug_info()
1015 .and_then(|info| info.line_for_offset(vm.ip()))
1016}
1017
1018fn parse_u32(token: Option<&str>) -> Option<u32> {
1019 token.and_then(|value| value.parse::<u32>().ok())
1020}
1021
1022fn parse_u64(token: Option<&str>) -> Option<u64> {
1023 token.and_then(|value| value.parse::<u64>().ok())
1024}
1025
1026fn resolve_executable_line(info: &DebugInfo, requested_line: u32) -> u32 {
1027 let mut next = None::<u32>;
1028 let mut prev = None::<u32>;
1029
1030 for line in info.lines.iter().map(|entry| entry.line) {
1031 if line >= requested_line && next.is_none_or(|candidate| line < candidate) {
1032 next = Some(line);
1033 }
1034 if line <= requested_line && prev.is_none_or(|candidate| line > candidate) {
1035 prev = Some(line);
1036 }
1037 }
1038
1039 next.or(prev).unwrap_or(requested_line)
1040}
1041
1042#[cfg(test)]
1043mod bridge_close_tests {
1044 use std::time::Duration;
1045
1046 use crate::vm::{Program, Vm, VmStatus};
1047
1048 use super::{DebugCommandBridge, DebugCommandBridgeError, Debugger};
1049
1050 #[test]
1051 fn closed_bridge_does_not_reopen_when_debugger_reaches_stop() {
1052 let program = Program::new(
1053 vec![],
1054 vec![crate::vm::OpCode::Nop as u8, crate::vm::OpCode::Ret as u8],
1055 );
1056 let bridge = DebugCommandBridge::new();
1057 bridge.close();
1058 let thread_bridge = bridge.clone();
1059
1060 let join = std::thread::spawn(move || {
1061 let mut debugger = Debugger::with_command_bridge(thread_bridge);
1062 debugger.stop_on_entry();
1063 let mut vm = Vm::new(program);
1064 vm.run_with_debugger(&mut debugger)
1065 .expect("closed debugger bridge should detach without blocking")
1066 });
1067
1068 assert_eq!(
1069 join.join().expect("debugger thread should join"),
1070 VmStatus::Halted
1071 );
1072 assert!(!bridge.status().attached);
1073 assert_eq!(
1074 bridge
1075 .execute("where", Duration::from_millis(5))
1076 .expect_err("closed bridge should remain closed"),
1077 DebugCommandBridgeError::Closed
1078 );
1079 }
1080}