Skip to main content

stryke/
debugger.rs

1//! Interactive debugger for stryke programs.
2//!
3//! Provides breakpoint-based debugging with single-stepping, variable inspection,
4//! and call stack display. Activated via `-d` flag.
5
6use std::collections::HashSet;
7use std::io::{self, BufRead, Write};
8
9use crate::scope::Scope;
10use crate::value::PerlValue;
11
12/// Debugger state, shared between VM and interpreter.
13pub struct Debugger {
14    /// Breakpoints by line number.
15    breakpoints: HashSet<usize>,
16    /// Breakpoints by subroutine name.
17    sub_breakpoints: HashSet<String>,
18    /// Single-step mode: stop at every statement/opcode.
19    step_mode: bool,
20    /// Step-over mode: stop at next statement at same or lower call depth.
21    step_over_depth: Option<usize>,
22    /// Step-out mode: stop when returning to this call depth.
23    step_out_depth: Option<usize>,
24    /// Current call depth for step-over/step-out.
25    call_depth: usize,
26    /// Last line we stopped at (avoid repeated stops on same line).
27    last_stop_line: Option<usize>,
28    /// Current source file name.
29    pub file: String,
30    /// Source lines (for display).
31    source_lines: Vec<String>,
32    /// Whether debugger is enabled.
33    enabled: bool,
34    /// Watch expressions (variable names to display on each stop).
35    watches: Vec<String>,
36    /// Command history.
37    history: Vec<String>,
38}
39
40impl Default for Debugger {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl Debugger {
47    pub fn new() -> Self {
48        Self {
49            breakpoints: HashSet::new(),
50            sub_breakpoints: HashSet::new(),
51            step_mode: true,
52            step_over_depth: None,
53            step_out_depth: None,
54            call_depth: 0,
55            last_stop_line: None,
56            file: String::new(),
57            source_lines: Vec::new(),
58            enabled: true,
59            watches: Vec::new(),
60            history: Vec::new(),
61        }
62    }
63
64    /// Load source for display in debugger.
65    pub fn load_source(&mut self, source: &str) {
66        self.source_lines = source.lines().map(String::from).collect();
67    }
68
69    /// Set source file name.
70    pub fn set_file(&mut self, file: &str) {
71        self.file = file.to_string();
72    }
73
74    /// Check if debugger should stop at this line.
75    pub fn should_stop(&mut self, line: usize) -> bool {
76        if !self.enabled {
77            return false;
78        }
79
80        // Avoid stopping on the same line repeatedly (unless stepping)
81        if !self.step_mode && self.last_stop_line == Some(line) {
82            return false;
83        }
84
85        // Check breakpoints
86        if self.breakpoints.contains(&line) {
87            return true;
88        }
89
90        // Check step mode
91        if self.step_mode {
92            return true;
93        }
94
95        // Check step-over (stop at same or lower depth)
96        if let Some(depth) = self.step_over_depth {
97            if self.call_depth <= depth {
98                self.step_over_depth = None;
99                return true;
100            }
101        }
102
103        // Check step-out (stop when returning)
104        if let Some(depth) = self.step_out_depth {
105            if self.call_depth < depth {
106                self.step_out_depth = None;
107                return true;
108            }
109        }
110
111        false
112    }
113
114    /// Check if we should stop at subroutine entry.
115    pub fn should_stop_at_sub(&self, name: &str) -> bool {
116        self.enabled && self.sub_breakpoints.contains(name)
117    }
118
119    /// Notify debugger of subroutine call.
120    pub fn enter_sub(&mut self, _name: &str) {
121        self.call_depth += 1;
122    }
123
124    /// Notify debugger of subroutine return.
125    pub fn leave_sub(&mut self) {
126        self.call_depth = self.call_depth.saturating_sub(1);
127    }
128
129    /// Interactive debugger prompt. Returns true to continue, false to quit.
130    pub fn prompt(
131        &mut self,
132        line: usize,
133        scope: &Scope,
134        call_stack: &[(String, usize)],
135    ) -> DebugAction {
136        self.last_stop_line = Some(line);
137        self.step_mode = false;
138
139        // Print location and source context
140        self.print_location(line);
141        self.print_watches(scope);
142
143        loop {
144            eprint!("  DB<{}> ", self.history.len() + 1);
145            io::stderr().flush().ok();
146
147            let mut input = String::new();
148            if io::stdin().lock().read_line(&mut input).is_err() {
149                return DebugAction::Quit;
150            }
151            let input = input.trim();
152
153            if input.is_empty() {
154                // Repeat last command or step
155                if let Some(last) = self.history.last().cloned() {
156                    return self.execute_command(&last, line, scope, call_stack);
157                }
158                self.step_mode = true;
159                return DebugAction::Continue;
160            }
161
162            self.history.push(input.to_string());
163            let action = self.execute_command(input, line, scope, call_stack);
164            if !matches!(action, DebugAction::Prompt) {
165                return action;
166            }
167        }
168    }
169
170    fn execute_command(
171        &mut self,
172        input: &str,
173        line: usize,
174        scope: &Scope,
175        call_stack: &[(String, usize)],
176    ) -> DebugAction {
177        let parts: Vec<&str> = input.splitn(2, ' ').collect();
178        let cmd = parts[0];
179        let arg = parts.get(1).map(|s| s.trim()).unwrap_or("");
180
181        match cmd {
182            // Step commands
183            "s" | "step" | "n" | "next" => {
184                self.step_mode = true;
185                DebugAction::Continue
186            }
187            "o" | "over" => {
188                self.step_over_depth = Some(self.call_depth);
189                DebugAction::Continue
190            }
191            "out" | "finish" | "r" => {
192                self.step_out_depth = Some(self.call_depth);
193                DebugAction::Continue
194            }
195            "c" | "cont" | "continue" => {
196                self.step_mode = false;
197                DebugAction::Continue
198            }
199
200            // Breakpoints
201            "b" | "break" => {
202                if arg.is_empty() {
203                    self.breakpoints.insert(line);
204                    eprintln!("Breakpoint set at line {}", line);
205                } else if let Ok(n) = arg.parse::<usize>() {
206                    self.breakpoints.insert(n);
207                    eprintln!("Breakpoint set at line {}", n);
208                } else {
209                    self.sub_breakpoints.insert(arg.to_string());
210                    eprintln!("Breakpoint set at fn {}", arg);
211                }
212                DebugAction::Prompt
213            }
214            "B" | "delete" => {
215                if arg.is_empty() || arg == "*" {
216                    self.breakpoints.clear();
217                    self.sub_breakpoints.clear();
218                    eprintln!("All breakpoints deleted");
219                } else if let Ok(n) = arg.parse::<usize>() {
220                    self.breakpoints.remove(&n);
221                    eprintln!("Breakpoint at line {} deleted", n);
222                } else {
223                    self.sub_breakpoints.remove(arg);
224                    eprintln!("Breakpoint at fn {} deleted", arg);
225                }
226                DebugAction::Prompt
227            }
228            "L" | "breakpoints" => {
229                if self.breakpoints.is_empty() && self.sub_breakpoints.is_empty() {
230                    eprintln!("No breakpoints set");
231                } else {
232                    eprintln!("Breakpoints:");
233                    for &bp in &self.breakpoints {
234                        eprintln!("  line {}", bp);
235                    }
236                    for bp in &self.sub_breakpoints {
237                        eprintln!("  fn {}", bp);
238                    }
239                }
240                DebugAction::Prompt
241            }
242
243            // Inspection
244            "p" | "print" | "x" => {
245                if arg.is_empty() {
246                    eprintln!("Usage: p <var> (e.g., p $x, p @arr, p %hash)");
247                } else {
248                    self.print_variable(arg, scope);
249                }
250                DebugAction::Prompt
251            }
252            "V" | "vars" => {
253                self.print_all_vars(scope);
254                DebugAction::Prompt
255            }
256            "w" | "watch" => {
257                if arg.is_empty() {
258                    if self.watches.is_empty() {
259                        eprintln!("No watches set");
260                    } else {
261                        eprintln!("Watches: {}", self.watches.join(", "));
262                    }
263                } else {
264                    self.watches.push(arg.to_string());
265                    eprintln!("Watching: {}", arg);
266                }
267                DebugAction::Prompt
268            }
269            "W" => {
270                if arg.is_empty() || arg == "*" {
271                    self.watches.clear();
272                    eprintln!("All watches cleared");
273                } else {
274                    self.watches.retain(|w| w != arg);
275                    eprintln!("Watch {} removed", arg);
276                }
277                DebugAction::Prompt
278            }
279
280            // Stack
281            "T" | "stack" | "bt" | "backtrace" => {
282                self.print_stack(call_stack, line);
283                DebugAction::Prompt
284            }
285
286            // Source listing
287            "l" | "list" => {
288                let target = if arg.is_empty() {
289                    line
290                } else {
291                    arg.parse().unwrap_or(line)
292                };
293                self.list_source(target, 10);
294                DebugAction::Prompt
295            }
296            "." => {
297                self.print_location(line);
298                DebugAction::Prompt
299            }
300
301            // Control
302            "q" | "quit" | "exit" => DebugAction::Quit,
303            "h" | "help" | "?" => {
304                self.print_help();
305                DebugAction::Prompt
306            }
307            "D" | "disable" => {
308                self.enabled = false;
309                eprintln!("Debugger disabled (use -d to re-enable on next run)");
310                DebugAction::Continue
311            }
312
313            _ => {
314                eprintln!("Unknown command: {}. Type 'h' for help.", cmd);
315                DebugAction::Prompt
316            }
317        }
318    }
319
320    fn print_location(&self, line: usize) {
321        let file_display = if self.file.is_empty() {
322            "<eval>"
323        } else {
324            &self.file
325        };
326        eprintln!();
327        eprintln!("{}:{}", file_display, line);
328
329        // Print surrounding lines
330        let start = line.saturating_sub(2);
331        let end = (line + 2).min(self.source_lines.len());
332        for i in start..end {
333            let marker = if i + 1 == line { "==>" } else { "   " };
334            if let Some(src) = self.source_lines.get(i) {
335                eprintln!("{} {:4}:  {}", marker, i + 1, src);
336            }
337        }
338    }
339
340    fn print_watches(&self, scope: &Scope) {
341        if self.watches.is_empty() {
342            return;
343        }
344        eprintln!("Watches:");
345        for w in &self.watches {
346            eprint!("  {} = ", w);
347            self.print_variable(w, scope);
348        }
349    }
350
351    fn print_variable(&self, var: &str, scope: &Scope) {
352        let var = var.trim();
353        if let Some(name) = var.strip_prefix('$') {
354            let val = scope.get_scalar(name);
355            eprintln!("{}", format_value(&val));
356        } else if let Some(name) = var.strip_prefix('@') {
357            let val = scope.get_array(name);
358            eprintln!(
359                "({})",
360                val.iter().map(format_value).collect::<Vec<_>>().join(", ")
361            );
362        } else if let Some(name) = var.strip_prefix('%') {
363            let val = scope.get_hash(name);
364            let pairs: Vec<String> = val
365                .iter()
366                .map(|(k, v)| format!("{} => {}", k, format_value(v)))
367                .collect();
368            eprintln!("({})", pairs.join(", "));
369        } else {
370            // Assume scalar
371            let val = scope.get_scalar(var);
372            eprintln!("{}", format_value(&val));
373        }
374    }
375
376    fn print_all_vars(&self, scope: &Scope) {
377        let vars = scope.all_scalar_names();
378        if vars.is_empty() {
379            eprintln!("No variables in scope");
380            return;
381        }
382        eprintln!("Variables:");
383        for name in vars {
384            if name.starts_with('^') || name.starts_with('_') && name.len() > 2 {
385                continue; // Skip special vars
386            }
387            let val = scope.get_scalar(&name);
388            if !val.is_undef() {
389                eprintln!("  ${} = {}", name, format_value(&val));
390            }
391        }
392    }
393
394    fn print_stack(&self, call_stack: &[(String, usize)], current_line: usize) {
395        eprintln!("Call stack:");
396        if call_stack.is_empty() {
397            eprintln!("  #0  <main> at line {}", current_line);
398        } else {
399            for (i, (name, line)) in call_stack.iter().enumerate().rev() {
400                eprintln!("  #{}  {} at line {}", call_stack.len() - i, name, line);
401            }
402            eprintln!("  #0  <current> at line {}", current_line);
403        }
404    }
405
406    fn list_source(&self, center: usize, radius: usize) {
407        let start = center.saturating_sub(radius);
408        let end = (center + radius).min(self.source_lines.len());
409        for i in start..end {
410            let marker = if i + 1 == center { "==>" } else { "   " };
411            let bp = if self.breakpoints.contains(&(i + 1)) {
412                "b"
413            } else {
414                " "
415            };
416            if let Some(src) = self.source_lines.get(i) {
417                eprintln!("{}{} {:4}:  {}", marker, bp, i + 1, src);
418            }
419        }
420    }
421
422    fn print_help(&self) {
423        eprintln!(
424            r#"
425Debugger Commands:
426  s, step, n, next    Step to next statement
427  o, over             Step over (don't descend into subs)
428  out, finish, r      Step out (run until sub returns)
429  c, cont, continue   Continue execution
430
431  b [line|sub]        Set breakpoint (current line if no arg)
432  B [line|sub|*]      Delete breakpoint(s)
433  L, breakpoints      List all breakpoints
434
435  p, print, x <var>   Print variable ($x, @arr, %hash)
436  V, vars             Print all variables in scope
437  w <var>             Add watch expression
438  W [var|*]           Remove watch expression(s)
439
440  T, stack, bt        Print call stack backtrace
441  l [line]            List source around line
442  .                   Show current location
443
444  q, quit, exit       Quit program
445  h, help, ?          Show this help
446  D, disable          Disable debugger (continue without stops)
447
448  <Enter>             Repeat last command or step
449"#
450        );
451    }
452}
453
454/// Action to take after debugger prompt.
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub enum DebugAction {
457    Continue,
458    Quit,
459    Prompt,
460}
461
462fn format_value(val: &PerlValue) -> String {
463    if val.is_undef() {
464        "undef".to_string()
465    } else if let Some(s) = val.as_str() {
466        if s.parse::<f64>().is_ok() {
467            s.to_string()
468        } else {
469            format!("\"{}\"", s.escape_default())
470        }
471    } else if let Some(n) = val.as_integer() {
472        n.to_string()
473    } else if let Some(f) = val.as_float() {
474        f.to_string()
475    } else if val.as_array_ref().is_some() || val.as_array_vec().is_some() {
476        let list = val.to_list();
477        let items: Vec<String> = list.iter().map(format_value).collect();
478        format!("[{}]", items.join(", "))
479    } else if val.as_hash_ref().is_some() {
480        if let Some(map) = val.as_hash_map() {
481            let pairs: Vec<String> = map
482                .iter()
483                .map(|(k, v)| format!("{} => {}", k, format_value(v)))
484                .collect();
485            format!("{{{}}}", pairs.join(", "))
486        } else {
487            "HASH(?)".to_string()
488        }
489    } else if val.as_code_ref().is_some() {
490        "CODE(...)".to_string()
491    } else {
492        val.type_name()
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn debugger_new_defaults() {
502        let dbg = Debugger::new();
503        assert!(dbg.breakpoints.is_empty());
504        assert!(dbg.sub_breakpoints.is_empty());
505        assert!(dbg.step_mode);
506        assert!(dbg.enabled);
507        assert!(dbg.watches.is_empty());
508        assert_eq!(dbg.call_depth, 0);
509    }
510
511    #[test]
512    fn debugger_load_source_splits_lines() {
513        let mut dbg = Debugger::new();
514        dbg.load_source("line1\nline2\nline3");
515        assert_eq!(dbg.source_lines.len(), 3);
516        assert_eq!(dbg.source_lines[0], "line1");
517        assert_eq!(dbg.source_lines[2], "line3");
518    }
519
520    #[test]
521    fn debugger_set_file() {
522        let mut dbg = Debugger::new();
523        dbg.set_file("test.pl");
524        assert_eq!(dbg.file, "test.pl");
525    }
526
527    #[test]
528    fn debugger_should_stop_at_breakpoint() {
529        let mut dbg = Debugger::new();
530        dbg.step_mode = false;
531        dbg.breakpoints.insert(10);
532        assert!(dbg.should_stop(10));
533        assert!(!dbg.should_stop(11));
534    }
535
536    #[test]
537    fn debugger_should_stop_in_step_mode() {
538        let mut dbg = Debugger::new();
539        dbg.step_mode = true;
540        assert!(dbg.should_stop(1));
541        assert!(dbg.should_stop(999));
542    }
543
544    #[test]
545    fn debugger_should_stop_disabled() {
546        let mut dbg = Debugger::new();
547        dbg.enabled = false;
548        dbg.step_mode = true;
549        assert!(!dbg.should_stop(1));
550    }
551
552    #[test]
553    fn debugger_should_stop_at_sub() {
554        let mut dbg = Debugger::new();
555        dbg.sub_breakpoints.insert("foo".to_string());
556        assert!(dbg.should_stop_at_sub("foo"));
557        assert!(!dbg.should_stop_at_sub("bar"));
558    }
559
560    #[test]
561    fn debugger_enter_leave_sub_tracks_depth() {
562        let mut dbg = Debugger::new();
563        assert_eq!(dbg.call_depth, 0);
564        dbg.enter_sub("foo");
565        assert_eq!(dbg.call_depth, 1);
566        dbg.enter_sub("bar");
567        assert_eq!(dbg.call_depth, 2);
568        dbg.leave_sub();
569        assert_eq!(dbg.call_depth, 1);
570        dbg.leave_sub();
571        assert_eq!(dbg.call_depth, 0);
572        dbg.leave_sub();
573        assert_eq!(dbg.call_depth, 0);
574    }
575
576    #[test]
577    fn debugger_step_over_depth() {
578        let mut dbg = Debugger::new();
579        dbg.step_mode = false;
580        dbg.enter_sub("outer");
581        dbg.step_over_depth = Some(1);
582        dbg.enter_sub("inner");
583        assert!(!dbg.should_stop(5));
584        dbg.leave_sub();
585        assert!(dbg.should_stop(6));
586        assert!(dbg.step_over_depth.is_none());
587    }
588
589    #[test]
590    fn debugger_step_out_depth() {
591        let mut dbg = Debugger::new();
592        dbg.step_mode = false;
593        dbg.enter_sub("outer");
594        dbg.enter_sub("inner");
595        dbg.step_out_depth = Some(2);
596        assert!(!dbg.should_stop(5));
597        dbg.leave_sub();
598        assert!(dbg.should_stop(6));
599        assert!(dbg.step_out_depth.is_none());
600    }
601
602    #[test]
603    fn debugger_avoids_repeated_stops_on_same_line() {
604        let mut dbg = Debugger::new();
605        dbg.step_mode = false;
606        dbg.breakpoints.insert(10);
607        assert!(dbg.should_stop(10));
608        dbg.last_stop_line = Some(10);
609        assert!(!dbg.should_stop(10));
610    }
611
612    #[test]
613    fn format_value_undef() {
614        assert_eq!(format_value(&PerlValue::UNDEF), "undef");
615    }
616
617    #[test]
618    fn format_value_integer() {
619        assert_eq!(format_value(&PerlValue::integer(42)), "42");
620        assert_eq!(format_value(&PerlValue::integer(-100)), "-100");
621    }
622
623    #[test]
624    fn format_value_float() {
625        let f = format_value(&PerlValue::float(3.14));
626        assert!(f.starts_with("3.14"));
627    }
628
629    #[test]
630    fn format_value_string() {
631        assert_eq!(
632            format_value(&PerlValue::string("hello".into())),
633            "\"hello\""
634        );
635    }
636
637    #[test]
638    fn format_value_numeric_string() {
639        assert_eq!(format_value(&PerlValue::string("42".into())), "42");
640        assert_eq!(format_value(&PerlValue::string("3.14".into())), "3.14");
641    }
642
643    #[test]
644    fn format_value_array() {
645        let arr = PerlValue::array(vec![
646            PerlValue::integer(1),
647            PerlValue::integer(2),
648            PerlValue::integer(3),
649        ]);
650        assert_eq!(format_value(&arr), "[1, 2, 3]");
651    }
652
653    #[test]
654    fn debug_action_eq() {
655        assert_eq!(DebugAction::Continue, DebugAction::Continue);
656        assert_ne!(DebugAction::Continue, DebugAction::Quit);
657        assert_ne!(DebugAction::Quit, DebugAction::Prompt);
658    }
659}