Skip to main content

vm/debugger/
mod.rs

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            // Keep any in-flight response visible to execute() to avoid
380            // races when the debugger resumes and immediately re-attaches.
381            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}