1use std::collections::HashSet;
7use std::io::{self, BufRead, Write};
8
9use crate::scope::Scope;
10use crate::value::PerlValue;
11
12pub struct Debugger {
14 breakpoints: HashSet<usize>,
16 sub_breakpoints: HashSet<String>,
18 step_mode: bool,
20 step_over_depth: Option<usize>,
22 step_out_depth: Option<usize>,
24 call_depth: usize,
26 last_stop_line: Option<usize>,
28 pub file: String,
30 source_lines: Vec<String>,
32 enabled: bool,
34 watches: Vec<String>,
36 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 pub fn load_source(&mut self, source: &str) {
66 self.source_lines = source.lines().map(String::from).collect();
67 }
68
69 pub fn set_file(&mut self, file: &str) {
71 self.file = file.to_string();
72 }
73
74 pub fn should_stop(&mut self, line: usize) -> bool {
76 if !self.enabled {
77 return false;
78 }
79
80 if !self.step_mode && self.last_stop_line == Some(line) {
82 return false;
83 }
84
85 if self.breakpoints.contains(&line) {
87 return true;
88 }
89
90 if self.step_mode {
92 return true;
93 }
94
95 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 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 pub fn should_stop_at_sub(&self, name: &str) -> bool {
116 self.enabled && self.sub_breakpoints.contains(name)
117 }
118
119 pub fn enter_sub(&mut self, _name: &str) {
121 self.call_depth += 1;
122 }
123
124 pub fn leave_sub(&mut self) {
126 self.call_depth = self.call_depth.saturating_sub(1);
127 }
128
129 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 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 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 "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 "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 "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 "T" | "stack" | "bt" | "backtrace" => {
282 self.print_stack(call_stack, line);
283 DebugAction::Prompt
284 }
285
286 "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 "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 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 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; }
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#[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}