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 state.closed = false;
369 state.attached = true;
370 state.current_line = current_line(vm);
371 state.pending_request = None;
372 self.inner.changed.notify_all();
375 }
376
377 loop {
378 let request = {
379 let mut state = self
380 .inner
381 .state
382 .lock()
383 .expect("debug command bridge lock poisoned");
384 while !state.closed && state.pending_request.is_none() {
385 state = self
386 .inner
387 .changed
388 .wait(state)
389 .expect("debug command bridge lock poisoned");
390 }
391 if state.closed {
392 state.attached = false;
393 state.current_line = None;
394 state.pending_request = None;
395 state.pending_response = None;
396 self.inner.changed.notify_all();
397 return true;
398 }
399 state
400 .pending_request
401 .take()
402 .expect("debug command request missing")
403 };
404
405 let mut output = Vec::<u8>::new();
406 let action = handle_command(
407 &request.command,
408 vm,
409 breakpoints,
410 line_breakpoints,
411 step,
412 &mut output,
413 );
414 let resumed = action.is_break();
415 let current_line = if resumed { None } else { current_line(vm) };
416 let attached = !resumed;
417 let output = String::from_utf8_lossy(&output).to_string();
418
419 let mut state = self
420 .inner
421 .state
422 .lock()
423 .expect("debug command bridge lock poisoned");
424 state.attached = attached;
425 state.current_line = current_line;
426 state.pending_response = Some(DebugCommandBridgeResponseInternal {
427 request_id: request.request_id,
428 output,
429 current_line,
430 attached,
431 resumed,
432 });
433 self.inner.changed.notify_all();
434
435 if resumed {
436 return false;
437 }
438 }
439 }
440}
441
442impl Default for DebugCommandBridge {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448impl std::fmt::Display for DebugCommandBridgeError {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 match self {
451 DebugCommandBridgeError::NotAttached => write!(f, "debugger is not attached"),
452 DebugCommandBridgeError::Timeout => write!(f, "timed out waiting for debugger"),
453 DebugCommandBridgeError::Closed => write!(f, "debugger bridge is closed"),
454 }
455 }
456}
457
458impl std::error::Error for DebugCommandBridgeError {}
459
460#[derive(Clone)]
461struct DebugCommandBridgeResponseInternal {
462 request_id: u64,
463 output: String,
464 current_line: Option<u32>,
465 attached: bool,
466 resumed: bool,
467}
468
469struct DebugServer {
470 listener: TcpListener,
471 stream: Option<TcpStream>,
472}
473
474impl DebugServer {
475 fn new(listener: TcpListener) -> Self {
476 Self {
477 listener,
478 stream: None,
479 }
480 }
481
482 fn ensure_client(&mut self) -> io::Result<()> {
483 if self.stream.is_none() {
484 let (stream, _) = self.listener.accept()?;
485 self.stream = Some(stream);
486 }
487 Ok(())
488 }
489
490 fn repl(
491 &mut self,
492 vm: &mut Vm,
493 breakpoints: &mut HashSet<usize>,
494 line_breakpoints: &mut HashSet<u32>,
495 step: &mut StepMode,
496 banner: Option<&str>,
497 ) -> bool {
498 if self.ensure_client().is_err() {
499 return false;
500 }
501 let Some(stream) = self.stream.as_mut() else {
502 return false;
503 };
504 let _ = writeln!(stream, "debugger attached. type 'help' for commands");
505 if let Some(text) = banner {
506 let _ = writeln!(stream, "{text}");
507 }
508 let Ok(clone) = stream.try_clone() else {
509 self.stream = None;
510 return true;
511 };
512 let mut reader = io::BufReader::new(clone);
513 loop {
514 if write_prompt(stream).is_err() {
515 self.stream = None;
516 return true;
517 }
518 let mut line = String::new();
519 match reader.read_line(&mut line) {
520 Ok(0) => {
521 self.stream = None;
522 return true;
523 }
524 Ok(_) => {}
525 Err(_) => {
526 self.stream = None;
527 return true;
528 }
529 }
530 if handle_command(&line, vm, breakpoints, line_breakpoints, step, stream).is_break() {
531 return false;
532 }
533 }
534 }
535}
536
537fn repl_stdio(
538 vm: &mut Vm,
539 breakpoints: &mut HashSet<usize>,
540 line_breakpoints: &mut HashSet<u32>,
541 step: &mut StepMode,
542 banner: Option<&str>,
543) {
544 let stdin = io::stdin();
545 let mut input = String::new();
546 if let Some(text) = banner {
547 println!("{text}");
548 }
549 loop {
550 input.clear();
551 print!("(pdb) ");
552 let _ = io::stdout().flush();
553 if stdin.read_line(&mut input).is_err() {
554 break;
555 }
556 if handle_command(
557 &input,
558 vm,
559 breakpoints,
560 line_breakpoints,
561 step,
562 &mut io::stdout(),
563 )
564 .is_break()
565 {
566 break;
567 }
568 }
569}
570
571fn write_prompt(stream: &mut TcpStream) -> io::Result<()> {
572 stream.write_all(b"(pdb) ")?;
573 stream.flush()
574}
575
576#[derive(Clone, Copy, Debug, PartialEq, Eq)]
577enum ReplAction {
578 Continue,
579 Break,
580}
581
582impl ReplAction {
583 fn is_break(self) -> bool {
584 matches!(self, ReplAction::Break)
585 }
586}
587
588fn handle_command(
589 line: &str,
590 vm: &mut Vm,
591 breakpoints: &mut HashSet<usize>,
592 line_breakpoints: &mut HashSet<u32>,
593 step: &mut StepMode,
594 out: &mut dyn Write,
595) -> ReplAction {
596 let mut parts = line.split_whitespace();
597 let Some(cmd) = parts.next() else {
598 return ReplAction::Continue;
599 };
600 match cmd {
601 "c" | "continue" => return ReplAction::Break,
602 "s" | "step" | "stepi" => {
603 *step = StepMode::Step;
604 return ReplAction::Break;
605 }
606 "n" | "next" => {
607 *step = StepMode::StepOver {
608 depth: vm.call_depth(),
609 ip: vm.ip(),
610 };
611 return ReplAction::Break;
612 }
613 "finish" | "out" => {
614 *step = StepMode::StepOut {
615 depth: vm.call_depth(),
616 };
617 return ReplAction::Break;
618 }
619 "b" | "break" => {
620 if let Some(arg) = parts.next() {
621 if arg == "line" {
622 if let Some(requested_line) = parse_u32(parts.next()) {
623 let line = vm
624 .debug_info()
625 .map(|info| resolve_executable_line(info, requested_line))
626 .unwrap_or(requested_line);
627 line_breakpoints.insert(line);
628 if line == requested_line {
629 let _ = writeln!(out, "line breakpoint set at {line}");
630 } else {
631 let _ = writeln!(
632 out,
633 "line breakpoint set at {line} (requested line {requested_line})"
634 );
635 }
636 } else {
637 let _ = writeln!(out, "usage: break line <number>");
638 }
639 return ReplAction::Continue;
640 }
641 if let Ok(offset) = arg.parse::<usize>() {
642 breakpoints.insert(offset);
643 let _ = writeln!(out, "breakpoint set at {offset}");
644 } else {
645 let _ = writeln!(out, "expected instruction offset");
646 }
647 } else {
648 let _ = writeln!(out, "usage: break <offset>");
649 }
650 }
651 "bl" => {
652 if let Some(requested_line) = parse_u32(parts.next()) {
653 let line = vm
654 .debug_info()
655 .map(|info| resolve_executable_line(info, requested_line))
656 .unwrap_or(requested_line);
657 line_breakpoints.insert(line);
658 if line == requested_line {
659 let _ = writeln!(out, "line breakpoint set at {line}");
660 } else {
661 let _ = writeln!(
662 out,
663 "line breakpoint set at {line} (requested line {requested_line})"
664 );
665 }
666 } else {
667 let _ = writeln!(out, "usage: bl <line>");
668 }
669 }
670 "clear" => {
671 if let Some(arg) = parts.next() {
672 if arg == "line" {
673 if let Some(requested_line) = parse_u32(parts.next()) {
674 let line = vm
675 .debug_info()
676 .map(|info| resolve_executable_line(info, requested_line))
677 .unwrap_or(requested_line);
678 line_breakpoints.remove(&line);
679 if line == requested_line {
680 let _ = writeln!(out, "line breakpoint cleared at {line}");
681 } else {
682 let _ = writeln!(
683 out,
684 "line breakpoint cleared at {line} (requested line {requested_line})"
685 );
686 }
687 } else {
688 let _ = writeln!(out, "usage: clear line <number>");
689 }
690 return ReplAction::Continue;
691 }
692 if let Ok(offset) = arg.parse::<usize>() {
693 breakpoints.remove(&offset);
694 let _ = writeln!(out, "breakpoint cleared at {offset}");
695 } else {
696 let _ = writeln!(out, "expected instruction offset");
697 }
698 } else {
699 let _ = writeln!(out, "usage: clear <offset>");
700 }
701 }
702 "cl" => {
703 if let Some(requested_line) = parse_u32(parts.next()) {
704 let line = vm
705 .debug_info()
706 .map(|info| resolve_executable_line(info, requested_line))
707 .unwrap_or(requested_line);
708 line_breakpoints.remove(&line);
709 if line == requested_line {
710 let _ = writeln!(out, "line breakpoint cleared at {line}");
711 } else {
712 let _ = writeln!(
713 out,
714 "line breakpoint cleared at {line} (requested line {requested_line})"
715 );
716 }
717 } else {
718 let _ = writeln!(out, "usage: cl <line>");
719 }
720 }
721 "breaks" => {
722 let _ = writeln!(out, "breakpoints: {:?}", breakpoints);
723 let _ = writeln!(out, "line breakpoints: {:?}", line_breakpoints);
724 }
725 "stack" => {
726 let _ = writeln!(out, "stack: {:?}", vm.stack());
727 }
728 "locals" => {
729 print_locals(vm, out);
730 }
731 "p" | "print" => {
732 if let Some(name) = parts.next() {
733 print_local_by_name(vm, name, out);
734 } else {
735 let _ = writeln!(out, "usage: print <local_name>");
736 }
737 }
738 "ip" => {
739 let _ = writeln!(out, "ip: {}", vm.ip());
740 }
741 "fuel" => {
742 let Some(sub) = parts.next() else {
743 print_fuel_state(vm, out);
744 return ReplAction::Continue;
745 };
746 match sub {
747 "set" => {
748 if let Some(value) = parse_u64(parts.next()) {
749 vm.set_fuel(value);
750 let _ = writeln!(out, "fuel set to {value}");
751 print_fuel_state(vm, out);
752 } else {
753 let _ = writeln!(out, "usage: fuel set <amount>");
754 }
755 }
756 "add" => {
757 if let Some(value) = parse_u64(parts.next()) {
758 match vm.add_fuel(value) {
759 Ok(()) => {
760 let _ = writeln!(out, "fuel added: {value}");
761 print_fuel_state(vm, out);
762 }
763 Err(err) => {
764 let _ = writeln!(out, "fuel add failed: {err}");
765 }
766 }
767 } else {
768 let _ = writeln!(out, "usage: fuel add <amount>");
769 }
770 }
771 "clear" => {
772 vm.clear_fuel();
773 let _ = writeln!(out, "fuel metering disabled");
774 print_fuel_state(vm, out);
775 }
776 "interval" => {
777 if let Some(raw) = parts.next() {
778 match raw.parse::<u32>() {
779 Ok(interval) => match vm.set_fuel_check_interval(interval) {
780 Ok(()) => {
781 let _ = writeln!(out, "fuel check interval set to {interval}");
782 print_fuel_state(vm, out);
783 }
784 Err(err) => {
785 let _ = writeln!(out, "fuel interval update failed: {err}");
786 }
787 },
788 Err(_) => {
789 let _ = writeln!(out, "usage: fuel interval <n>");
790 }
791 }
792 } else {
793 let _ = writeln!(out, "fuel check interval: {}", vm.fuel_check_interval());
794 }
795 }
796 _ => {
797 let _ = writeln!(
798 out,
799 "usage: fuel | fuel set <amount> | fuel add <amount> | fuel clear | fuel interval [n]"
800 );
801 }
802 }
803 }
804 "epoch" => {
805 let Some(sub) = parts.next() else {
806 print_epoch_state(vm, out);
807 return ReplAction::Continue;
808 };
809 match sub {
810 "tick" => {
811 let delta = parse_u64(parts.next()).unwrap_or(1);
812 let current = vm.increment_epoch_by(delta);
813 let _ = writeln!(out, "epoch advanced by {delta} to {current}");
814 print_epoch_state(vm, out);
815 }
816 "deadline" | "set" => {
817 if let Some(value) = parse_u64(parts.next()) {
818 match vm.set_epoch_deadline(value) {
819 Ok(()) => {
820 let _ = writeln!(
821 out,
822 "epoch deadline set {value} ticks beyond current epoch"
823 );
824 print_epoch_state(vm, out);
825 }
826 Err(err) => {
827 let _ = writeln!(out, "epoch deadline update failed: {err}");
828 }
829 }
830 } else {
831 let _ = writeln!(out, "usage: epoch deadline <ticks>");
832 }
833 }
834 "clear" => {
835 vm.clear_epoch_deadline();
836 let _ = writeln!(out, "epoch interruption disabled");
837 print_epoch_state(vm, out);
838 }
839 "interval" => {
840 if let Some(raw) = parts.next() {
841 match raw.parse::<u32>() {
842 Ok(interval) => match vm.set_epoch_check_interval(interval) {
843 Ok(()) => {
844 let _ = writeln!(out, "epoch check interval set to {interval}");
845 print_epoch_state(vm, out);
846 }
847 Err(err) => {
848 let _ = writeln!(out, "epoch interval update failed: {err}");
849 }
850 },
851 Err(_) => {
852 let _ = writeln!(out, "usage: epoch interval <n>");
853 }
854 }
855 } else {
856 let _ =
857 writeln!(out, "epoch check interval: {}", vm.epoch_check_interval());
858 }
859 }
860 _ => {
861 let _ = writeln!(
862 out,
863 "usage: epoch | epoch tick [n] | epoch deadline <ticks> | epoch clear | epoch interval [n]"
864 );
865 }
866 }
867 }
868 "where" => {
869 if let Some(info) = vm.debug_info() {
870 let line = info.line_for_offset(vm.ip());
871 if let Some(line) = line {
872 if let Some(text) = info.source_line(line) {
873 let _ = writeln!(out, "line {line}: {text}");
874 } else {
875 let _ = writeln!(out, "line: {line}");
876 }
877 } else {
878 let _ = writeln!(out, "line: unknown");
879 }
880 } else {
881 let _ = writeln!(out, "no debug info");
882 }
883 }
884 "funcs" => {
885 if let Some(info) = vm.debug_info() {
886 for func in &info.functions {
887 let _ = writeln!(out, "fn {}({})", func.name, format_args_list(func));
888 }
889 } else {
890 let _ = writeln!(out, "no debug info");
891 }
892 }
893 "help" => {
894 let _ = writeln!(
895 out,
896 "commands: break, break line, bl, clear, clear line, cl, breaks, continue, step, next, out, stack, locals, print, ip, where, funcs, fuel, epoch, help"
897 );
898 }
899 _ => {
900 let _ = writeln!(out, "unknown command");
901 }
902 }
903 ReplAction::Continue
904}
905
906fn format_args_list(func: &crate::debug_info::DebugFunction) -> String {
907 let mut parts = Vec::new();
908 for arg in &func.args {
909 parts.push(format!("{}:{}", arg.position, arg.name));
910 }
911 parts.join(", ")
912}
913
914fn print_locals(vm: &Vm, out: &mut dyn Write) {
915 let Some(info) = vm.debug_info() else {
916 let _ = writeln!(out, "locals: {:?}", vm.locals());
917 return;
918 };
919
920 if info.locals.is_empty() {
921 let _ = writeln!(out, "locals: {:?}", vm.locals());
922 return;
923 }
924
925 let current_line = info.line_for_offset(vm.ip());
926 for local in &info.locals {
927 if !local_visible_at_line(local, current_line) {
928 continue;
929 }
930 match vm.locals().get(local.index as usize) {
931 Some(value) => {
932 let _ = writeln!(out, "{} = {:?}", local.name, value);
933 }
934 None => {
935 let _ = writeln!(out, "{} = <unavailable>", local.name);
936 }
937 }
938 }
939}
940
941fn print_local_by_name(vm: &Vm, name: &str, out: &mut dyn Write) {
942 let Some(info) = vm.debug_info() else {
943 let _ = writeln!(out, "no debug info");
944 return;
945 };
946
947 let Some(local) = info.locals.iter().find(|local| local.name == name) else {
948 let _ = writeln!(out, "unknown local '{name}'");
949 return;
950 };
951 let current_line = info.line_for_offset(vm.ip());
952 if !local_visible_at_line(local, current_line) {
953 let _ = writeln!(out, "local '{name}' is not visible at this location");
954 return;
955 }
956
957 match vm.locals().get(local.index as usize) {
958 Some(value) => {
959 let _ = writeln!(out, "{name} = {:?}", value);
960 }
961 None => {
962 let _ = writeln!(out, "local '{name}' is out of range for this VM instance");
963 }
964 }
965}
966
967fn print_fuel_state(vm: &Vm, out: &mut dyn Write) {
968 let remaining = vm
969 .get_fuel()
970 .map(|value| value.to_string())
971 .unwrap_or_else(|| "disabled".to_string());
972 let _ = writeln!(
973 out,
974 "fuel: {remaining}, check_interval={}",
975 vm.fuel_check_interval()
976 );
977}
978
979fn print_epoch_state(vm: &Vm, out: &mut dyn Write) {
980 let deadline = vm
981 .epoch_deadline()
982 .map(|value| value.to_string())
983 .unwrap_or_else(|| "disabled".to_string());
984 let slice = vm
985 .epoch_deadline_delta()
986 .map(|value| value.to_string())
987 .unwrap_or_else(|| "disabled".to_string());
988 let _ = writeln!(
989 out,
990 "epoch: current={}, deadline={}, slice={}, check_interval={}",
991 vm.current_epoch(),
992 deadline,
993 slice,
994 vm.epoch_check_interval()
995 );
996}
997
998pub fn attach_with_debugger(vm: &mut Vm, debugger: &mut Debugger) {
999 debugger.on_instruction(vm);
1000}
1001
1002pub fn debug_info_from_vm(vm: &Vm) -> Option<&DebugInfo> {
1003 vm.debug_info()
1004}
1005
1006fn current_line(vm: &Vm) -> Option<u32> {
1007 vm.debug_info()
1008 .and_then(|info| info.line_for_offset(vm.ip()))
1009}
1010
1011fn parse_u32(token: Option<&str>) -> Option<u32> {
1012 token.and_then(|value| value.parse::<u32>().ok())
1013}
1014
1015fn parse_u64(token: Option<&str>) -> Option<u64> {
1016 token.and_then(|value| value.parse::<u64>().ok())
1017}
1018
1019fn resolve_executable_line(info: &DebugInfo, requested_line: u32) -> u32 {
1020 let mut next = None::<u32>;
1021 let mut prev = None::<u32>;
1022
1023 for line in info.lines.iter().map(|entry| entry.line) {
1024 if line >= requested_line && next.is_none_or(|candidate| line < candidate) {
1025 next = Some(line);
1026 }
1027 if line <= requested_line && prev.is_none_or(|candidate| line > candidate) {
1028 prev = Some(line);
1029 }
1030 }
1031
1032 next.or(prev).unwrap_or(requested_line)
1033}