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