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            state.closed = false;
369            state.attached = true;
370            state.current_line = current_line(vm);
371            state.pending_request = None;
372            // Keep any in-flight response visible to execute() to avoid
373            // races when the debugger resumes and immediately re-attaches.
374            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}