1use std::collections::HashSet;
17use std::io::{self, BufRead, Write};
18use std::sync::{Arc, Mutex};
19
20use crate::scope::Scope;
21use crate::value::StrykeValue;
22
23pub struct Debugger {
25 breakpoints: HashSet<usize>,
27 sub_breakpoints: HashSet<String>,
29 step_mode: bool,
31 step_over_depth: Option<usize>,
33 step_out_depth: Option<usize>,
35 call_depth: usize,
37 last_stop_line: Option<usize>,
39 last_stop_depth: usize,
44 pub file: String,
46 source_lines: Vec<String>,
48 enabled: bool,
50 watches: Vec<String>,
52 history: Vec<String>,
54 dap_backend: Option<DapBackendHandle>,
57}
58
59pub struct DapBackendHandle {
63 pub shared: Arc<crate::dap::DapShared>,
65 pub bp_state: Arc<Mutex<crate::dap::BreakpointState>>,
67}
68
69impl Default for Debugger {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl Debugger {
76 pub fn new() -> Self {
78 Self {
79 breakpoints: HashSet::new(),
80 sub_breakpoints: HashSet::new(),
81 step_mode: true,
82 step_over_depth: None,
83 step_out_depth: None,
84 call_depth: 0,
85 last_stop_line: None,
86 last_stop_depth: 0,
87 file: String::new(),
88 source_lines: Vec::new(),
89 enabled: true,
90 watches: Vec::new(),
91 history: Vec::new(),
92 dap_backend: None,
93 }
94 }
95
96 pub fn add_breakpoint_line(&mut self, line: usize) {
99 self.breakpoints.insert(line);
100 }
101
102 pub fn add_breakpoint_sub(&mut self, name: &str) {
104 self.sub_breakpoints.insert(name.to_string());
105 }
106
107 pub fn clear_line_breakpoints(&mut self) {
110 self.breakpoints.clear();
111 }
112
113 pub fn set_line_breakpoints(&mut self, lines: &[usize]) {
115 self.breakpoints = lines.iter().copied().collect();
116 }
117
118 pub fn set_step_mode(&mut self, on: bool) {
121 self.step_mode = on;
122 }
123
124 pub fn request_step_over(&mut self) {
126 self.step_over_depth = Some(self.call_depth);
127 }
128
129 pub fn request_step_out(&mut self) {
131 self.step_out_depth = Some(self.call_depth);
132 }
133
134 pub fn set_dap_backend(
137 &mut self,
138 shared: Arc<crate::dap::DapShared>,
139 bp_state: Arc<Mutex<crate::dap::BreakpointState>>,
140 ) {
141 self.dap_backend = Some(DapBackendHandle { shared, bp_state });
142 }
143
144 #[inline]
146 pub fn is_dap(&self) -> bool {
147 self.dap_backend.is_some()
148 }
149
150 pub fn breakpoint_lines(&self) -> Vec<usize> {
152 let mut v: Vec<usize> = self.breakpoints.iter().copied().collect();
153 v.sort_unstable();
154 v
155 }
156
157 fn build_snapshot(
160 &self,
161 line: usize,
162 scope: &Scope,
163 call_stack: &[(String, usize)],
164 reason: &str,
165 ) -> crate::dap::PauseSnapshot {
166 let mut frames: Vec<crate::dap::FrameSnap> = Vec::new();
167 frames.push(crate::dap::FrameSnap {
169 name: "<current>".to_string(),
170 file: self.file.clone(),
171 line,
172 });
173 for (name, l) in call_stack.iter().rev() {
174 frames.push(crate::dap::FrameSnap {
175 name: name.clone(),
176 file: self.file.clone(),
177 line: *l,
178 });
179 }
180 let mut var_ref_map = std::collections::HashMap::new();
181 let locals = crate::dap::capture_locals_with_map(scope, &mut var_ref_map);
182 crate::dap::PauseSnapshot {
183 file: self.file.clone(),
184 line,
185 reason: reason.to_string(),
186 frames,
187 locals,
188 globals: Vec::new(),
189 var_ref_map,
190 }
191 }
192
193 pub fn load_source(&mut self, source: &str) {
195 self.source_lines = source.lines().map(String::from).collect();
196 }
197
198 pub fn set_file(&mut self, file: &str) {
200 self.file = file.to_string();
201 }
202
203 pub fn should_stop(&mut self, line: usize) -> bool {
205 if !self.enabled {
206 return false;
207 }
208
209 if std::env::var("STRYKE_DBG_TRACE").is_ok() {
210 eprintln!(
211 "[ss] line={} bp_set={:?}",
212 line,
213 self.breakpoints.iter().collect::<Vec<_>>()
214 );
215 }
216
217 if line == 0 {
221 return false;
222 }
223
224 if self.last_stop_line == Some(line) && self.call_depth == self.last_stop_depth {
230 return false;
231 }
232
233 if self.breakpoints.contains(&line) {
235 return true;
236 }
237
238 if self.step_mode {
240 return true;
241 }
242
243 if let Some(depth) = self.step_over_depth {
245 if self.call_depth <= depth {
246 self.step_over_depth = None;
247 return true;
248 }
249 }
250
251 if let Some(depth) = self.step_out_depth {
253 if self.call_depth < depth {
254 self.step_out_depth = None;
255 return true;
256 }
257 }
258
259 false
260 }
261
262 pub fn should_stop_at_sub(&self, name: &str) -> bool {
264 self.enabled && self.sub_breakpoints.contains(name)
265 }
266
267 pub fn enter_sub(&mut self, _name: &str) {
269 self.call_depth += 1;
270 }
271
272 pub fn leave_sub(&mut self) {
274 self.call_depth = self.call_depth.saturating_sub(1);
275 }
276
277 pub fn prompt(
285 &mut self,
286 line: usize,
287 scope: &Scope,
288 call_stack: &[(String, usize)],
289 ) -> DebugAction {
290 self.last_stop_line = Some(line);
291 self.last_stop_depth = self.call_depth;
292 self.step_mode = false;
293
294 if let Some(backend) = self.dap_backend.as_ref() {
296 let reason = if self.breakpoints.contains(&line) {
297 "breakpoint"
298 } else {
299 "step"
300 };
301 let snap = self.build_snapshot(line, scope, call_stack, reason);
302 let shared = backend.shared.clone();
303 let bp = backend.bp_state.clone();
304 let action = shared.pause(snap);
305 if let Ok(mut g) = bp.lock() {
307 if let Some(kind) = g.pending_step.take() {
308 match kind {
309 crate::dap::StepKind::Over => self.step_over_depth = Some(self.call_depth),
310 crate::dap::StepKind::Into => self.step_mode = true,
311 crate::dap::StepKind::Out => self.step_out_depth = Some(self.call_depth),
312 }
313 }
314 if let Some(lines) = g.line_breakpoints.get(&self.file) {
316 let lines = lines.clone();
317 self.set_line_breakpoints(&lines);
318 }
319 }
320 return action;
321 }
322
323 self.print_location(line);
325 self.print_watches(scope);
326
327 loop {
328 eprint!(" DB<{}> ", self.history.len() + 1);
329 io::stderr().flush().ok();
330
331 let mut input = String::new();
332 if io::stdin().lock().read_line(&mut input).is_err() {
333 return DebugAction::Quit;
334 }
335 let input = input.trim();
336
337 if input.is_empty() {
338 if let Some(last) = self.history.last().cloned() {
340 return self.execute_command(&last, line, scope, call_stack);
341 }
342 self.step_mode = true;
343 return DebugAction::Continue;
344 }
345
346 self.history.push(input.to_string());
347 let action = self.execute_command(input, line, scope, call_stack);
348 if !matches!(action, DebugAction::Prompt) {
349 return action;
350 }
351 }
352 }
353
354 fn execute_command(
355 &mut self,
356 input: &str,
357 line: usize,
358 scope: &Scope,
359 call_stack: &[(String, usize)],
360 ) -> DebugAction {
361 let parts: Vec<&str> = input.splitn(2, ' ').collect();
362 let cmd = parts[0];
363 let arg = parts.get(1).map(|s| s.trim()).unwrap_or("");
364
365 match cmd {
366 "s" | "step" | "n" | "next" => {
368 self.step_mode = true;
369 DebugAction::Continue
370 }
371 "o" | "over" => {
372 self.step_over_depth = Some(self.call_depth);
373 DebugAction::Continue
374 }
375 "out" | "finish" | "r" => {
376 self.step_out_depth = Some(self.call_depth);
377 DebugAction::Continue
378 }
379 "c" | "cont" | "continue" => {
380 self.step_mode = false;
381 DebugAction::Continue
382 }
383
384 "b" | "break" => {
386 if arg.is_empty() {
387 self.breakpoints.insert(line);
388 eprintln!("Breakpoint set at line {}", line);
389 } else if let Ok(n) = arg.parse::<usize>() {
390 self.breakpoints.insert(n);
391 eprintln!("Breakpoint set at line {}", n);
392 } else {
393 self.sub_breakpoints.insert(arg.to_string());
394 eprintln!("Breakpoint set at fn {}", arg);
395 }
396 DebugAction::Prompt
397 }
398 "B" | "delete" => {
399 if arg.is_empty() || arg == "*" {
400 self.breakpoints.clear();
401 self.sub_breakpoints.clear();
402 eprintln!("All breakpoints deleted");
403 } else if let Ok(n) = arg.parse::<usize>() {
404 self.breakpoints.remove(&n);
405 eprintln!("Breakpoint at line {} deleted", n);
406 } else {
407 self.sub_breakpoints.remove(arg);
408 eprintln!("Breakpoint at fn {} deleted", arg);
409 }
410 DebugAction::Prompt
411 }
412 "L" | "breakpoints" => {
413 if self.breakpoints.is_empty() && self.sub_breakpoints.is_empty() {
414 eprintln!("No breakpoints set");
415 } else {
416 eprintln!("Breakpoints:");
417 for &bp in &self.breakpoints {
418 eprintln!(" line {}", bp);
419 }
420 for bp in &self.sub_breakpoints {
421 eprintln!(" fn {}", bp);
422 }
423 }
424 DebugAction::Prompt
425 }
426
427 "p" | "print" | "x" => {
429 if arg.is_empty() {
430 eprintln!("Usage: p <var> (e.g., p $x, p @arr, p %hash)");
431 } else {
432 self.print_variable(arg, scope);
433 }
434 DebugAction::Prompt
435 }
436 "V" | "vars" => {
437 self.print_all_vars(scope);
438 DebugAction::Prompt
439 }
440 "w" | "watch" => {
441 if arg.is_empty() {
442 if self.watches.is_empty() {
443 eprintln!("No watches set");
444 } else {
445 eprintln!("Watches: {}", self.watches.join(", "));
446 }
447 } else {
448 self.watches.push(arg.to_string());
449 eprintln!("Watching: {}", arg);
450 }
451 DebugAction::Prompt
452 }
453 "W" => {
454 if arg.is_empty() || arg == "*" {
455 self.watches.clear();
456 eprintln!("All watches cleared");
457 } else {
458 self.watches.retain(|w| w != arg);
459 eprintln!("Watch {} removed", arg);
460 }
461 DebugAction::Prompt
462 }
463
464 "T" | "stack" | "bt" | "backtrace" => {
466 self.print_stack(call_stack, line);
467 DebugAction::Prompt
468 }
469
470 "l" | "list" => {
472 let target = if arg.is_empty() {
473 line
474 } else {
475 arg.parse().unwrap_or(line)
476 };
477 self.list_source(target, 10);
478 DebugAction::Prompt
479 }
480 "." => {
481 self.print_location(line);
482 DebugAction::Prompt
483 }
484
485 "q" | "quit" | "exit" => DebugAction::Quit,
487 "h" | "help" | "?" => {
488 self.print_help();
489 DebugAction::Prompt
490 }
491 "D" | "disable" => {
492 self.enabled = false;
493 eprintln!("Debugger disabled (use -d to re-enable on next run)");
494 DebugAction::Continue
495 }
496
497 _ => {
498 eprintln!("Unknown command: {}. Type 'h' for help.", cmd);
499 DebugAction::Prompt
500 }
501 }
502 }
503
504 fn print_location(&self, line: usize) {
505 let file_display = if self.file.is_empty() {
506 "<eval>"
507 } else {
508 &self.file
509 };
510 eprintln!();
511 eprintln!("{}:{}", file_display, line);
512
513 let start = line.saturating_sub(2);
515 let end = (line + 2).min(self.source_lines.len());
516 for i in start..end {
517 let marker = if i + 1 == line { "==>" } else { " " };
518 if let Some(src) = self.source_lines.get(i) {
519 eprintln!("{} {:4}: {}", marker, i + 1, src);
520 }
521 }
522 }
523
524 fn print_watches(&self, scope: &Scope) {
525 if self.watches.is_empty() {
526 return;
527 }
528 eprintln!("Watches:");
529 for w in &self.watches {
530 eprint!(" {} = ", w);
531 self.print_variable(w, scope);
532 }
533 }
534
535 fn print_variable(&self, var: &str, scope: &Scope) {
536 let var = var.trim();
537 if let Some(name) = var.strip_prefix('$') {
538 let val = scope.get_scalar(name);
539 eprintln!("{}", format_value(&val));
540 } else if let Some(name) = var.strip_prefix('@') {
541 let val = scope.get_array(name);
542 eprintln!(
543 "({})",
544 val.iter().map(format_value).collect::<Vec<_>>().join(", ")
545 );
546 } else if let Some(name) = var.strip_prefix('%') {
547 let val = scope.get_hash(name);
548 let pairs: Vec<String> = val
549 .iter()
550 .map(|(k, v)| format!("{} => {}", k, format_value(v)))
551 .collect();
552 eprintln!("({})", pairs.join(", "));
553 } else {
554 let val = scope.get_scalar(var);
556 eprintln!("{}", format_value(&val));
557 }
558 }
559
560 fn print_all_vars(&self, scope: &Scope) {
561 let vars = scope.all_scalar_names();
562 if vars.is_empty() {
563 eprintln!("No variables in scope");
564 return;
565 }
566 eprintln!("Variables:");
567 for name in vars {
568 if name.starts_with('^') || name.starts_with('_') && name.len() > 2 {
569 continue; }
571 let val = scope.get_scalar(&name);
572 if !val.is_undef() {
573 eprintln!(" ${} = {}", name, format_value(&val));
574 }
575 }
576 }
577
578 fn print_stack(&self, call_stack: &[(String, usize)], current_line: usize) {
579 eprintln!("Call stack:");
580 if call_stack.is_empty() {
581 eprintln!(" #0 <main> at line {}", current_line);
582 } else {
583 for (i, (name, line)) in call_stack.iter().enumerate().rev() {
584 eprintln!(" #{} {} at line {}", call_stack.len() - i, name, line);
585 }
586 eprintln!(" #0 <current> at line {}", current_line);
587 }
588 }
589
590 fn list_source(&self, center: usize, radius: usize) {
591 let start = center.saturating_sub(radius);
592 let end = (center + radius).min(self.source_lines.len());
593 for i in start..end {
594 let marker = if i + 1 == center { "==>" } else { " " };
595 let bp = if self.breakpoints.contains(&(i + 1)) {
596 "b"
597 } else {
598 " "
599 };
600 if let Some(src) = self.source_lines.get(i) {
601 eprintln!("{}{} {:4}: {}", marker, bp, i + 1, src);
602 }
603 }
604 }
605
606 fn print_help(&self) {
607 eprintln!(
608 r#"
609Debugger Commands:
610 s, step, n, next Step to next statement
611 o, over Step over (don't descend into subs)
612 out, finish, r Step out (run until sub returns)
613 c, cont, continue Continue execution
614
615 b [line|sub] Set breakpoint (current line if no arg)
616 B [line|sub|*] Delete breakpoint(s)
617 L, breakpoints List all breakpoints
618
619 p, print, x <var> Print variable ($x, @arr, %hash)
620 V, vars Print all variables in scope
621 w <var> Add watch expression
622 W [var|*] Remove watch expression(s)
623
624 T, stack, bt Print call stack backtrace
625 l [line] List source around line
626 . Show current location
627
628 q, quit, exit Quit program
629 h, help, ? Show this help
630 D, disable Disable debugger (continue without stops)
631
632 <Enter> Repeat last command or step
633"#
634 );
635 }
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
640pub enum DebugAction {
641 Continue,
643 Quit,
645 Prompt,
647}
648
649pub(crate) fn format_value(val: &StrykeValue) -> String {
650 if val.is_undef() {
651 "undef".to_string()
652 } else if let Some(s) = val.as_str() {
653 if s.parse::<f64>().is_ok() {
654 s.to_string()
655 } else {
656 format!("\"{}\"", s.escape_default())
657 }
658 } else if let Some(n) = val.as_integer() {
659 n.to_string()
660 } else if let Some(f) = val.as_float() {
661 f.to_string()
662 } else if val.as_array_ref().is_some() || val.as_array_vec().is_some() {
663 let list = val.to_list();
664 let items: Vec<String> = list.iter().map(format_value).collect();
665 format!("[{}]", items.join(", "))
666 } else if val.as_hash_ref().is_some() {
667 if let Some(map) = val.as_hash_map() {
668 let pairs: Vec<String> = map
669 .iter()
670 .map(|(k, v)| format!("{} => {}", k, format_value(v)))
671 .collect();
672 format!("{{{}}}", pairs.join(", "))
673 } else {
674 "HASH(?)".to_string()
675 }
676 } else if val.as_code_ref().is_some() {
677 "CODE(...)".to_string()
678 } else {
679 val.type_name()
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn debugger_new_defaults() {
689 let dbg = Debugger::new();
690 assert!(dbg.breakpoints.is_empty());
691 assert!(dbg.sub_breakpoints.is_empty());
692 assert!(dbg.step_mode);
693 assert!(dbg.enabled);
694 assert!(dbg.watches.is_empty());
695 assert_eq!(dbg.call_depth, 0);
696 }
697
698 #[test]
699 fn debugger_load_source_splits_lines() {
700 let mut dbg = Debugger::new();
701 dbg.load_source("line1\nline2\nline3");
702 assert_eq!(dbg.source_lines.len(), 3);
703 assert_eq!(dbg.source_lines[0], "line1");
704 assert_eq!(dbg.source_lines[2], "line3");
705 }
706
707 #[test]
708 fn debugger_set_file() {
709 let mut dbg = Debugger::new();
710 dbg.set_file("test.pl");
711 assert_eq!(dbg.file, "test.pl");
712 }
713
714 #[test]
715 fn debugger_should_stop_at_breakpoint() {
716 let mut dbg = Debugger::new();
717 dbg.step_mode = false;
718 dbg.breakpoints.insert(10);
719 assert!(dbg.should_stop(10));
720 assert!(!dbg.should_stop(11));
721 }
722
723 #[test]
724 fn debugger_should_stop_in_step_mode() {
725 let mut dbg = Debugger::new();
726 dbg.step_mode = true;
727 assert!(dbg.should_stop(1));
728 assert!(dbg.should_stop(999));
729 }
730
731 #[test]
732 fn debugger_should_stop_disabled() {
733 let mut dbg = Debugger::new();
734 dbg.enabled = false;
735 dbg.step_mode = true;
736 assert!(!dbg.should_stop(1));
737 }
738
739 #[test]
740 fn debugger_should_stop_at_sub() {
741 let mut dbg = Debugger::new();
742 dbg.sub_breakpoints.insert("foo".to_string());
743 assert!(dbg.should_stop_at_sub("foo"));
744 assert!(!dbg.should_stop_at_sub("bar"));
745 }
746
747 #[test]
748 fn debugger_enter_leave_sub_tracks_depth() {
749 let mut dbg = Debugger::new();
750 assert_eq!(dbg.call_depth, 0);
751 dbg.enter_sub("foo");
752 assert_eq!(dbg.call_depth, 1);
753 dbg.enter_sub("bar");
754 assert_eq!(dbg.call_depth, 2);
755 dbg.leave_sub();
756 assert_eq!(dbg.call_depth, 1);
757 dbg.leave_sub();
758 assert_eq!(dbg.call_depth, 0);
759 dbg.leave_sub();
760 assert_eq!(dbg.call_depth, 0);
761 }
762
763 #[test]
764 fn debugger_step_over_depth() {
765 let mut dbg = Debugger::new();
766 dbg.step_mode = false;
767 dbg.enter_sub("outer");
768 dbg.step_over_depth = Some(1);
769 dbg.enter_sub("inner");
770 assert!(!dbg.should_stop(5));
771 dbg.leave_sub();
772 assert!(dbg.should_stop(6));
773 assert!(dbg.step_over_depth.is_none());
774 }
775
776 #[test]
777 fn debugger_step_out_depth() {
778 let mut dbg = Debugger::new();
779 dbg.step_mode = false;
780 dbg.enter_sub("outer");
781 dbg.enter_sub("inner");
782 dbg.step_out_depth = Some(2);
783 assert!(!dbg.should_stop(5));
784 dbg.leave_sub();
785 assert!(dbg.should_stop(6));
786 assert!(dbg.step_out_depth.is_none());
787 }
788
789 #[test]
790 fn debugger_avoids_repeated_stops_on_same_line() {
791 let mut dbg = Debugger::new();
792 dbg.step_mode = false;
793 dbg.breakpoints.insert(10);
794 assert!(dbg.should_stop(10));
795 dbg.last_stop_line = Some(10);
796 assert!(!dbg.should_stop(10));
797 }
798
799 #[test]
805 fn same_line_guard_yields_to_depth_change_on_step_in() {
806 let mut dbg = Debugger::new();
807 dbg.last_stop_line = Some(10);
809 dbg.last_stop_depth = 0;
810 dbg.step_mode = true;
812 assert!(!dbg.should_stop(10));
814 dbg.enter_sub("callee");
816 assert!(dbg.should_stop(10));
817 }
818
819 #[test]
823 fn step_out_fires_when_returning_to_same_line() {
824 let mut dbg = Debugger::new();
825 dbg.enter_sub("callee");
827 dbg.last_stop_line = Some(5);
828 dbg.last_stop_depth = 1;
829 dbg.step_out_depth = Some(1);
830 assert!(!dbg.should_stop(5));
832 dbg.leave_sub();
834 assert!(dbg.should_stop(5));
836 }
837
838 #[test]
843 fn step_over_skips_into_nested_frame_and_resumes_after_return() {
844 let mut dbg = Debugger::new();
845 dbg.step_mode = false;
846 dbg.step_over_depth = Some(0);
848 dbg.enter_sub("callee");
849 assert!(!dbg.should_stop(20));
851 assert!(!dbg.should_stop(21));
852 dbg.leave_sub();
855 assert!(dbg.should_stop(11));
856 }
857
858 #[test]
859 fn format_value_undef() {
860 assert_eq!(format_value(&StrykeValue::UNDEF), "undef");
861 }
862
863 #[test]
864 fn format_value_integer() {
865 assert_eq!(format_value(&StrykeValue::integer(42)), "42");
866 assert_eq!(format_value(&StrykeValue::integer(-100)), "-100");
867 }
868
869 #[test]
870 fn format_value_float() {
871 let f = format_value(&StrykeValue::float(2.71));
873 assert!(f.starts_with("2.71"));
874 }
875
876 #[test]
877 fn format_value_string() {
878 assert_eq!(
879 format_value(&StrykeValue::string("hello".into())),
880 "\"hello\""
881 );
882 }
883
884 #[test]
885 fn format_value_numeric_string() {
886 assert_eq!(format_value(&StrykeValue::string("42".into())), "42");
887 assert_eq!(format_value(&StrykeValue::string("3.14".into())), "3.14");
888 }
889
890 #[test]
891 fn format_value_array() {
892 let arr = StrykeValue::array(vec![
893 StrykeValue::integer(1),
894 StrykeValue::integer(2),
895 StrykeValue::integer(3),
896 ]);
897 assert_eq!(format_value(&arr), "[1, 2, 3]");
898 }
899
900 #[test]
901 fn debug_action_eq() {
902 assert_eq!(DebugAction::Continue, DebugAction::Continue);
903 assert_ne!(DebugAction::Continue, DebugAction::Quit);
904 assert_ne!(DebugAction::Quit, DebugAction::Prompt);
905 }
906}