aether/debugger/
session.rs

1// src/debugger/session.rs
2//! Debugger session and command processing
3
4use crate::debugger::breakpoint::BreakpointType;
5use crate::debugger::state::{DebuggerState, ExecutionMode};
6use crate::evaluator::Evaluator;
7use std::cell::RefCell;
8use std::rc::Rc;
9
10/// Action to take after executing a command
11#[derive(Debug, Clone, PartialEq)]
12pub enum CommandAction {
13    /// Continue execution (e.g., after step/next/continue)
14    Continue,
15    /// Stay in debugger REPL
16    Stay,
17    /// Quit debugger
18    Quit,
19}
20
21/// Debugger session
22pub struct DebuggerSession {
23    evaluator: Rc<RefCell<Evaluator>>,
24    state: DebuggerState,
25    source_code: Option<String>,
26    source_file: Option<String>,
27}
28
29impl DebuggerSession {
30    /// Create a new debugger session
31    pub fn new(evaluator: Rc<RefCell<Evaluator>>) -> Self {
32        DebuggerSession {
33            evaluator,
34            state: DebuggerState::new(),
35            source_code: None,
36            source_file: None,
37        }
38    }
39
40    /// Set the source code for listing
41    pub fn set_source(&mut self, source: String, file: String) {
42        self.source_code = Some(source);
43        self.source_file = Some(file);
44    }
45
46    /// Start the debugger session
47    pub fn start(&mut self) {
48        self.state.activate();
49        println!("Aether Debugger v1.0");
50        if let Some(file) = &self.source_file {
51            println!("Debugging: {}", file);
52        }
53        println!("Type 'help' for available commands\n");
54    }
55
56    /// Get a mutable reference to the debugger state
57    pub fn state_mut(&mut self) -> &mut DebuggerState {
58        &mut self.state
59    }
60
61    /// Get a reference to the debugger state
62    pub fn state(&self) -> &DebuggerState {
63        &self.state
64    }
65
66    /// Handle a debugger command, returning (message, action)
67    pub fn handle_command(&mut self, cmd: &str) -> (String, CommandAction) {
68        let parts: Vec<&str> = cmd.split_whitespace().collect();
69        if parts.is_empty() {
70            return (String::new(), CommandAction::Stay);
71        }
72
73        let command = parts[0].to_lowercase();
74        let args = &parts[1..];
75
76        let (msg, action) = match command.as_str() {
77            "break" | "b" => self.cmd_break(args),
78            "delete" | "d" => self.cmd_delete(args),
79            "disable" => self.cmd_disable(args),
80            "enable" => self.cmd_enable(args),
81            "info" => self.cmd_info(args),
82            "step" | "s" => self.cmd_step(args),
83            "next" | "n" => self.cmd_next(args),
84            "finish" | "f" => self.cmd_finish(args),
85            "continue" | "c" => self.cmd_continue(args),
86            "print" | "p" => self.cmd_print(args),
87            "backtrace" | "bt" => self.cmd_backtrace(args),
88            "frame" => self.cmd_frame(args),
89            "list" | "l" => self.cmd_list(args),
90            "help" | "h" | "?" => self.cmd_help(args),
91            "quit" | "q" => self.cmd_quit(args),
92            _ => (
93                format!(
94                    "Unknown command: {}. Type 'help' for available commands.",
95                    command
96                ),
97                CommandAction::Stay,
98            ),
99        };
100
101        (msg, action)
102    }
103
104    // Command implementations
105
106    fn cmd_break(&mut self, args: &[&str]) -> (String, CommandAction) {
107        if args.is_empty() {
108            return (
109                "Usage: break [file:]line | break function_name".to_string(),
110                CommandAction::Stay,
111            );
112        }
113
114        let loc = args[0];
115
116        // Try parsing as line number first
117        if let Ok(line) = loc.parse::<usize>() {
118            // Line number only - use current file
119            let file = self
120                .state
121                .current_location()
122                .map(|(f, _)| f.clone())
123                .or_else(|| self.source_file.clone())
124                .unwrap_or_else(|| "<unknown>".to_string());
125
126            let id = self.state.set_breakpoint(BreakpointType::Line {
127                file: file.clone(),
128                line,
129            });
130            return (
131                format!("Breakpoint {} set at {}:{}", id, file, line),
132                CommandAction::Stay,
133            );
134        }
135
136        // Try file:line format
137        if let Some(pos) = loc.find(':') {
138            let file = loc[..pos].to_string();
139            if let Ok(line) = loc[pos + 1..].parse::<usize>() {
140                let id = self.state.set_breakpoint(BreakpointType::Line {
141                    file: file.clone(),
142                    line,
143                });
144                return (
145                    format!("Breakpoint {} set at {}:{}", id, file, line),
146                    CommandAction::Stay,
147                );
148            }
149        }
150
151        // Otherwise treat as function name
152        let id = self.state.set_breakpoint(BreakpointType::Function {
153            name: loc.to_string(),
154        });
155        (
156            format!("Breakpoint {} set at function '{}'", id, loc),
157            CommandAction::Stay,
158        )
159    }
160
161    fn cmd_delete(&mut self, args: &[&str]) -> (String, CommandAction) {
162        if args.is_empty() {
163            let count = self.state.list_breakpoints().len();
164            self.state.remove_all_breakpoints();
165            return (
166                format!("All breakpoints deleted ({})", count),
167                CommandAction::Stay,
168            );
169        }
170
171        if let Ok(id) = args[0].parse::<usize>() {
172            if self.state.remove_breakpoint(id) {
173                (format!("Breakpoint {} deleted", id), CommandAction::Stay)
174            } else {
175                (format!("Breakpoint {} not found", id), CommandAction::Stay)
176            }
177        } else {
178            ("Invalid breakpoint ID".to_string(), CommandAction::Stay)
179        }
180    }
181
182    fn cmd_disable(&mut self, args: &[&str]) -> (String, CommandAction) {
183        if args.is_empty() {
184            return (
185                "Usage: disable <breakpoint_id>".to_string(),
186                CommandAction::Stay,
187            );
188        }
189
190        if let Ok(id) = args[0].parse::<usize>() {
191            if self.state.toggle_breakpoint(id, false) {
192                (format!("Breakpoint {} disabled", id), CommandAction::Stay)
193            } else {
194                (format!("Breakpoint {} not found", id), CommandAction::Stay)
195            }
196        } else {
197            ("Invalid breakpoint ID".to_string(), CommandAction::Stay)
198        }
199    }
200
201    fn cmd_enable(&mut self, args: &[&str]) -> (String, CommandAction) {
202        if args.is_empty() {
203            return (
204                "Usage: enable <breakpoint_id>".to_string(),
205                CommandAction::Stay,
206            );
207        }
208
209        if let Ok(id) = args[0].parse::<usize>() {
210            if self.state.toggle_breakpoint(id, true) {
211                (format!("Breakpoint {} enabled", id), CommandAction::Stay)
212            } else {
213                (format!("Breakpoint {} not found", id), CommandAction::Stay)
214            }
215        } else {
216            ("Invalid breakpoint ID".to_string(), CommandAction::Stay)
217        }
218    }
219
220    fn cmd_info(&mut self, args: &[&str]) -> (String, CommandAction) {
221        if args.is_empty() {
222            return (
223                "Usage: info breakpoints | info locals | info args".to_string(),
224                CommandAction::Stay,
225            );
226        }
227
228        match args[0] {
229            "breakpoints" | "break" | "bp" => {
230                let breakpoints = self.state.list_breakpoints();
231                if breakpoints.is_empty() {
232                    return ("No breakpoints".to_string(), CommandAction::Stay);
233                }
234
235                let mut result = String::from("Breakpoints:\n");
236                for bp in breakpoints {
237                    let status = if bp.enabled { " enabled" } else { " disabled" };
238                    result.push_str(&format!(
239                        "  ID: {:3}{} | {} | hits: {} | {}\n",
240                        bp.id,
241                        status,
242                        bp.location_string(),
243                        bp.hit_count,
244                        if bp.ignore_count > 0 {
245                            format!("(ignore first {})", bp.ignore_count)
246                        } else {
247                            String::new()
248                        }
249                    ));
250                }
251                (result, CommandAction::Stay)
252            }
253            "locals" => {
254                // TODO: Need to add API to Evaluator to get all variables
255                (
256                    "Local variables: Not yet implemented".to_string(),
257                    CommandAction::Stay,
258                )
259            }
260            "args" => (
261                "Arguments: Not yet implemented".to_string(),
262                CommandAction::Stay,
263            ),
264            _ => (
265                format!("Unknown info command: {}", args[0]),
266                CommandAction::Stay,
267            ),
268        }
269    }
270
271    fn cmd_step(&mut self, args: &[&str]) -> (String, CommandAction) {
272        let _count = if args.is_empty() {
273            1
274        } else {
275            args[0].parse::<usize>().unwrap_or(1)
276        };
277
278        self.state.set_execution_mode(ExecutionMode::StepInto);
279        ("Stepping...".to_string(), CommandAction::Continue)
280    }
281
282    fn cmd_next(&mut self, args: &[&str]) -> (String, CommandAction) {
283        let _count = if args.is_empty() {
284            1
285        } else {
286            args[0].parse::<usize>().unwrap_or(1)
287        };
288
289        let evaluator = self.evaluator.borrow();
290        let depth = evaluator.get_call_stack_depth();
291        drop(evaluator);
292
293        self.state.set_execution_mode(ExecutionMode::StepOver);
294        self.state.set_step_over_depth(depth);
295        ("Next...".to_string(), CommandAction::Continue)
296    }
297
298    fn cmd_finish(&mut self, _args: &[&str]) -> (String, CommandAction) {
299        let evaluator = self.evaluator.borrow();
300        let depth = evaluator.get_call_stack_depth();
301        drop(evaluator);
302
303        self.state.set_execution_mode(ExecutionMode::StepOut);
304        self.state.set_step_over_depth(depth);
305        (
306            "Running until current function returns...".to_string(),
307            CommandAction::Continue,
308        )
309    }
310
311    fn cmd_continue(&mut self, _args: &[&str]) -> (String, CommandAction) {
312        self.state.set_execution_mode(ExecutionMode::Continue);
313        ("Continuing...".to_string(), CommandAction::Continue)
314    }
315
316    fn cmd_print(&mut self, args: &[&str]) -> (String, CommandAction) {
317        if args.is_empty() {
318            return (
319                "Usage: print <variable_name>".to_string(),
320                CommandAction::Stay,
321            );
322        }
323
324        let var_name = args[0];
325        let evaluator = self.evaluator.borrow();
326
327        match evaluator.get_global(var_name) {
328            Some(value) => (format!("{} = {}", var_name, value), CommandAction::Stay),
329            None => (
330                format!("Variable '{}' not found", var_name),
331                CommandAction::Stay,
332            ),
333        }
334    }
335
336    fn cmd_backtrace(&mut self, args: &[&str]) -> (String, CommandAction) {
337        let evaluator = self.evaluator.borrow();
338        let call_stack = evaluator.get_call_stack();
339
340        if call_stack.is_empty() {
341            drop(evaluator);
342            return ("No stack.".to_string(), CommandAction::Stay);
343        }
344
345        let max_frames = if args.is_empty() {
346            call_stack.len()
347        } else {
348            args[0].parse::<usize>().unwrap_or(call_stack.len())
349        };
350
351        let mut result = String::from("Call stack:\n");
352        for (i, frame) in call_stack.iter().take(max_frames).enumerate() {
353            result.push_str(&format!("#{} {}\n", i, frame.signature));
354        }
355        drop(evaluator);
356        (result, CommandAction::Stay)
357    }
358
359    fn cmd_frame(&mut self, args: &[&str]) -> (String, CommandAction) {
360        if args.is_empty() {
361            return (
362                "Usage: frame <frame_number>".to_string(),
363                CommandAction::Stay,
364            );
365        }
366
367        if let Ok(_frame_num) = args[0].parse::<usize>() {
368            (
369                "Frame selection not yet implemented".to_string(),
370                CommandAction::Stay,
371            )
372        } else {
373            ("Invalid frame number".to_string(), CommandAction::Stay)
374        }
375    }
376
377    fn cmd_list(&mut self, args: &[&str]) -> (String, CommandAction) {
378        let count = if args.is_empty() {
379            10
380        } else {
381            args[0].parse::<usize>().unwrap_or(10)
382        };
383
384        if let Some(source) = &self.source_code {
385            let current_line = self
386                .state
387                .current_location()
388                .map(|(_, line)| *line)
389                .unwrap_or(1);
390
391            let lines: Vec<&str> = source.lines().collect();
392            let start = current_line.saturating_sub(count / 2);
393            let end = (start + count).min(lines.len());
394
395            let mut result = String::new();
396            for (idx, line) in lines.iter().enumerate().take(end).skip(start) {
397                let marker = if idx + 1 == current_line { "=>" } else { "  " };
398                result.push_str(&format!("{} {:4}: {}\n", marker, idx + 1, line));
399            }
400            (result, CommandAction::Stay)
401        } else {
402            ("No source code available".to_string(), CommandAction::Stay)
403        }
404    }
405
406    fn cmd_help(&mut self, _args: &[&str]) -> (String, CommandAction) {
407        (HELP_TEXT.to_string(), CommandAction::Stay)
408    }
409
410    fn cmd_quit(&mut self, _args: &[&str]) -> (String, CommandAction) {
411        ("Exiting debugger...".to_string(), CommandAction::Quit)
412    }
413}
414
415const HELP_TEXT: &str = r#"
416Aether Debugger Commands
417
418Execution Control:
419  step [N]        Step N times (default 1), stepping into function calls
420  next [N]        Step N times (default 1), stepping over function calls
421  finish          Execute until the current function returns
422  continue        Continue execution until next breakpoint
423
424Breakpoints:
425  break [file:]line  Set breakpoint at line
426  break function     Set breakpoint at function entry
427  delete [N]         Delete breakpoint N (or all if N not specified)
428  disable [N]        Disable breakpoint N
429  enable [N]         Enable breakpoint N
430  info breakpoints   List all breakpoints
431
432Stack & Variables:
433  backtrace [N]      Print backtrace of N frames (all if N not specified)
434  frame N            Select and print stack frame N
435  print expr         Print value of expression/variable
436  info locals        Print local variables
437
438Source:
439  list [N]           List N lines of source (default 10)
440
441Miscellaneous:
442  help               Show this help message
443  quit               Exit debugger
444
445Examples:
446  (aether-debug) break 15           # Set breakpoint at line 15
447  (aether-debug) break calc.aether:20  # Set at file:line
448  (aether-debug) break processData  # Set at function entry
449  (aether-debug) next               # Step over
450  (aether-debug) step               # Step into
451  (aether-debug) print X            # Show variable X
452  (aether-debug) backtrace          # Show call stack
453"#;
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::environment::Environment;
459    use std::cell::RefCell;
460
461    fn create_test_session() -> DebuggerSession {
462        let env = Rc::new(RefCell::new(Environment::new()));
463        let evaluator = Rc::new(RefCell::new(Evaluator::with_env(env)));
464        let mut session = DebuggerSession::new(evaluator);
465        session.set_source(
466            "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n".to_string(),
467            "test.aether".to_string(),
468        );
469        session
470    }
471
472    #[test]
473    fn test_break_command() {
474        let mut session = create_test_session();
475
476        let (result, _) = session.handle_command("break 10");
477        assert!(result.contains("Breakpoint"));
478    }
479
480    #[test]
481    fn test_step_command() {
482        let mut session = create_test_session();
483
484        let (_, action) = session.handle_command("step");
485        assert_eq!(action, CommandAction::Continue);
486        assert_eq!(session.state().execution_mode(), &ExecutionMode::StepInto);
487    }
488
489    #[test]
490    fn test_next_command() {
491        let mut session = create_test_session();
492
493        let (_, action) = session.handle_command("next");
494        assert_eq!(action, CommandAction::Continue);
495        assert_eq!(session.state().execution_mode(), &ExecutionMode::StepOver);
496    }
497
498    #[test]
499    fn test_continue_command() {
500        let mut session = create_test_session();
501
502        let (_, action) = session.handle_command("continue");
503        assert_eq!(action, CommandAction::Continue);
504        assert_eq!(session.state().execution_mode(), &ExecutionMode::Continue);
505    }
506}