Skip to main content

torsh_jit/debugger/
interface.rs

1//! User interface for JIT debugging
2//!
3//! This module provides the interactive user interface for debugging including
4//! command parsing, user interaction, and display formatting.
5
6use super::core::{
7    Breakpoint, BreakpointId, BreakpointLocation, DebugCommand, DebugState, DebugValue,
8    DebuggerConfig, DisassemblyView, ExecutionLocation, InspectionResult, InspectionTarget,
9    MemoryView, Watch, WatchId,
10};
11use super::state::CallStack;
12use crate::{JitError, JitResult, NodeId};
13use std::collections::HashMap;
14use std::io::{self, Write};
15
16/// Debugger user interface
17pub struct DebuggerInterface {
18    config: DebuggerConfig,
19    command_history: Vec<String>,
20    command_aliases: HashMap<String, String>,
21}
22
23impl DebuggerInterface {
24    /// Create a new debugger interface
25    ///
26    /// # Arguments
27    /// * `config` - Configuration for the debugger interface
28    pub fn new(config: DebuggerConfig) -> Self {
29        let mut aliases = HashMap::new();
30        // Setup default command aliases
31        aliases.insert("s".to_string(), "step".to_string());
32        aliases.insert("n".to_string(), "next".to_string());
33        aliases.insert("i".to_string(), "into".to_string());
34        aliases.insert("o".to_string(), "out".to_string());
35        aliases.insert("c".to_string(), "continue".to_string());
36        aliases.insert("b".to_string(), "break".to_string());
37        aliases.insert("d".to_string(), "delete".to_string());
38        aliases.insert("w".to_string(), "watch".to_string());
39        aliases.insert("p".to_string(), "inspect".to_string());
40        aliases.insert("mem".to_string(), "memory".to_string());
41        aliases.insert("dis".to_string(), "disasm".to_string());
42        aliases.insert("h".to_string(), "help".to_string());
43        aliases.insert("q".to_string(), "quit".to_string());
44
45        Self {
46            config,
47            command_history: Vec::new(),
48            command_aliases: aliases,
49        }
50    }
51
52    /// Show welcome message
53    pub fn show_welcome_message(&self) {
54        println!("đŸĻ€ ToRSh JIT Debugger v1.0");
55        println!("Type 'help' for available commands");
56        println!("=====================================");
57    }
58
59    /// Display current debug state
60    ///
61    /// # Arguments
62    /// * `state` - The current debug state to display
63    pub fn display_current_state(&self, state: &DebugState) {
64        println!("\n--- Current State ---");
65        println!("📍 Location: {:?}", state.location);
66        println!("đŸ”ĸ Step: {}", state.execution_step);
67        println!("â–ļī¸  Running: {}", state.is_running);
68
69        if !state.call_stack.is_empty() {
70            println!("📞 Call depth: {}", state.call_stack.depth());
71        }
72
73        if !state.variables.is_empty() {
74            println!("🔍 Variables:");
75            for (name, value) in &state.variables {
76                println!("  {} = {:?}", name, value);
77            }
78        }
79    }
80
81    /// Get user command from input
82    ///
83    /// # Returns
84    /// The parsed debug command
85    pub fn get_user_command(&mut self) -> JitResult<DebugCommand> {
86        print!("(torsh-debug) ");
87        io::stdout().flush().expect("stdout flush should succeed");
88
89        let mut input = String::new();
90        io::stdin()
91            .read_line(&mut input)
92            .map_err(|e| JitError::RuntimeError(format!("Failed to read input: {}", e)))?;
93
94        let input = input.trim();
95
96        // Add to command history if not empty
97        if !input.is_empty() {
98            self.command_history.push(input.to_string());
99        }
100
101        self.parse_command(input)
102    }
103
104    /// Parse a command string into a DebugCommand
105    ///
106    /// # Arguments
107    /// * `input` - The input string to parse
108    ///
109    /// # Returns
110    /// The parsed debug command
111    pub fn parse_command(&self, input: &str) -> JitResult<DebugCommand> {
112        let parts: Vec<&str> = input.split_whitespace().collect();
113
114        if parts.is_empty() {
115            return Err(JitError::RuntimeError("Empty command".to_string()));
116        }
117
118        // Resolve aliases
119        let binding = parts[0].to_string();
120        let command = self.command_aliases.get(parts[0]).unwrap_or(&binding);
121
122        match command.as_str() {
123            "step" => Ok(DebugCommand::Step),
124            "next" => Ok(DebugCommand::StepOver),
125            "into" => Ok(DebugCommand::StepInto),
126            "out" => Ok(DebugCommand::StepOut),
127            "continue" => Ok(DebugCommand::Continue),
128            "break" => {
129                if let Some(&location_str) = parts.get(1) {
130                    let location = self.parse_breakpoint_location(location_str)?;
131                    Ok(DebugCommand::SetBreakpoint { location })
132                } else {
133                    Err(JitError::RuntimeError(
134                        "Breakpoint location required".to_string(),
135                    ))
136                }
137            }
138            "delete" => {
139                if let Some(&id_str) = parts.get(1) {
140                    let id = id_str
141                        .parse::<u64>()
142                        .map_err(|_| JitError::RuntimeError("Invalid breakpoint ID".to_string()))?;
143                    Ok(DebugCommand::RemoveBreakpoint {
144                        id: BreakpointId(id),
145                    })
146                } else {
147                    Err(JitError::RuntimeError("Breakpoint ID required".to_string()))
148                }
149            }
150            "breakpoints" => Ok(DebugCommand::ListBreakpoints),
151            "watch" => {
152                if parts.len() > 1 {
153                    let expression = parts[1..].join(" ");
154                    Ok(DebugCommand::Watch { expression })
155                } else {
156                    Err(JitError::RuntimeError(
157                        "Watch expression required".to_string(),
158                    ))
159                }
160            }
161            "unwatch" => {
162                if let Some(&id_str) = parts.get(1) {
163                    let id = id_str
164                        .parse::<u64>()
165                        .map_err(|_| JitError::RuntimeError("Invalid watch ID".to_string()))?;
166                    Ok(DebugCommand::Unwatch { id: WatchId(id) })
167                } else {
168                    Err(JitError::RuntimeError("Watch ID required".to_string()))
169                }
170            }
171            "watches" => Ok(DebugCommand::ListWatches),
172            "inspect" => {
173                if let Some(&target_str) = parts.get(1) {
174                    let target = self.parse_inspection_target(target_str)?;
175                    Ok(DebugCommand::Inspect { target })
176                } else {
177                    Err(JitError::RuntimeError(
178                        "Inspection target required".to_string(),
179                    ))
180                }
181            }
182            "stack" => Ok(DebugCommand::CallStack),
183            "locals" => Ok(DebugCommand::Locals),
184            "memory" => {
185                if let Some(&addr_str) = parts.get(1) {
186                    let address = self.parse_memory_address(addr_str)?;
187                    Ok(DebugCommand::Memory { address })
188                } else {
189                    Err(JitError::RuntimeError(
190                        "Memory address required".to_string(),
191                    ))
192                }
193            }
194            "disasm" => {
195                let location = if parts.len() > 1 {
196                    self.parse_execution_location(&parts[1..])?
197                } else {
198                    // Use current location placeholder
199                    ExecutionLocation::GraphNode(NodeId::new(0))
200                };
201                Ok(DebugCommand::Disassemble { location })
202            }
203            "help" => Ok(DebugCommand::Help),
204            "quit" | "exit" => Ok(DebugCommand::Quit),
205            "history" => {
206                self.show_command_history();
207                Ok(DebugCommand::Help) // Just show help after history
208            }
209            "clear" => {
210                self.clear_screen();
211                Ok(DebugCommand::Help)
212            }
213            _ => Err(JitError::RuntimeError(format!(
214                "Unknown command: {}. Type 'help' for available commands.",
215                command
216            ))),
217        }
218    }
219
220    /// Parse a breakpoint location string
221    fn parse_breakpoint_location(&self, location_str: &str) -> JitResult<BreakpointLocation> {
222        if location_str.starts_with("node_") {
223            let node_index = location_str[5..]
224                .parse::<usize>()
225                .map_err(|_| JitError::RuntimeError("Invalid node index".to_string()))?;
226            Ok(BreakpointLocation::GraphNode(NodeId::new(node_index)))
227        } else if location_str.contains(':') {
228            let parts: Vec<&str> = location_str.split(':').collect();
229            if parts.len() == 2 {
230                let function = parts[0].to_string();
231                let instruction = parts[1]
232                    .parse::<usize>()
233                    .map_err(|_| JitError::RuntimeError("Invalid instruction index".to_string()))?;
234                Ok(BreakpointLocation::Instruction {
235                    function,
236                    instruction,
237                })
238            } else {
239                Err(JitError::RuntimeError(
240                    "Invalid breakpoint location format. Use 'node_N' or 'function:instruction'"
241                        .to_string(),
242                ))
243            }
244        } else {
245            Err(JitError::RuntimeError(
246                "Invalid breakpoint location format. Use 'node_N' or 'function:instruction'"
247                    .to_string(),
248            ))
249        }
250    }
251
252    /// Parse an inspection target string
253    fn parse_inspection_target(&self, target_str: &str) -> JitResult<InspectionTarget> {
254        if target_str.starts_with("node_") {
255            let node_index = target_str[5..]
256                .parse::<usize>()
257                .map_err(|_| JitError::RuntimeError("Invalid node index".to_string()))?;
258            Ok(InspectionTarget::Node(NodeId::new(node_index)))
259        } else if target_str.starts_with("0x") {
260            let address = u64::from_str_radix(&target_str[2..], 16)
261                .map_err(|_| JitError::RuntimeError("Invalid memory address".to_string()))?;
262            Ok(InspectionTarget::Memory(address))
263        } else {
264            Ok(InspectionTarget::Variable(target_str.to_string()))
265        }
266    }
267
268    /// Parse memory address from string
269    fn parse_memory_address(&self, addr_str: &str) -> JitResult<u64> {
270        if addr_str.starts_with("0x") {
271            u64::from_str_radix(&addr_str[2..], 16)
272                .map_err(|_| JitError::RuntimeError("Invalid hexadecimal address".to_string()))
273        } else {
274            addr_str
275                .parse::<u64>()
276                .map_err(|_| JitError::RuntimeError("Invalid decimal address".to_string()))
277        }
278    }
279
280    /// Parse an execution location from command parts
281    fn parse_execution_location(&self, parts: &[&str]) -> JitResult<ExecutionLocation> {
282        if parts.is_empty() {
283            return Err(JitError::RuntimeError("Location required".to_string()));
284        }
285
286        if parts[0].starts_with("node_") {
287            let node_index = parts[0][5..]
288                .parse::<usize>()
289                .map_err(|_| JitError::RuntimeError("Invalid node index".to_string()))?;
290            Ok(ExecutionLocation::GraphNode(NodeId::new(node_index)))
291        } else if parts.len() >= 2 {
292            let function = parts[0].to_string();
293            let instruction_index = parts[1]
294                .parse::<usize>()
295                .map_err(|_| JitError::RuntimeError("Invalid instruction index".to_string()))?;
296            Ok(ExecutionLocation::Instruction {
297                function,
298                instruction_index,
299            })
300        } else {
301            Err(JitError::RuntimeError(
302                "Invalid location format. Use 'node_N' or 'function instruction_index'".to_string(),
303            ))
304        }
305    }
306
307    /// Show that a breakpoint was set
308    pub fn show_breakpoint_set(&self, location: BreakpointLocation) {
309        println!("✅ Breakpoint set at {:?}", location);
310    }
311
312    /// Show that a breakpoint was removed
313    pub fn show_breakpoint_removed(&self, id: BreakpointId) {
314        println!("❌ Breakpoint {} removed", id.0);
315    }
316
317    /// Show list of breakpoints
318    pub fn show_breakpoints(&self, breakpoints: &[&Breakpoint]) {
319        if breakpoints.is_empty() {
320            println!("📍 No breakpoints set");
321        } else {
322            println!("📍 Breakpoints:");
323            for bp in breakpoints {
324                let status = if bp.enabled {
325                    "✅ enabled"
326                } else {
327                    "❌ disabled"
328                };
329                let condition = if let Some(ref cond) = bp.condition {
330                    format!(" [condition: {}]", cond)
331                } else {
332                    String::new()
333                };
334                println!(
335                    "  {} - {:?} ({}) [hits: {}]{}",
336                    bp.id.0, bp.location, status, bp.hit_count, condition
337                );
338            }
339        }
340    }
341
342    /// Show that a watch was added
343    pub fn show_watch_added(&self, id: WatchId, expression: &str) {
344        println!("đŸ‘ī¸  Watch {} added: {}", id.0, expression);
345    }
346
347    /// Show that a watch was removed
348    pub fn show_watch_removed(&self, id: WatchId) {
349        println!("❌ Watch {} removed", id.0);
350    }
351
352    /// Show list of watch expressions
353    pub fn show_watches(&self, watches: &[&Watch]) {
354        if watches.is_empty() {
355            println!("đŸ‘ī¸  No watches set");
356        } else {
357            println!("đŸ‘ī¸  Watches:");
358            for watch in watches {
359                let status = if watch.enabled {
360                    "✅ enabled"
361                } else {
362                    "❌ disabled"
363                };
364                let value = if let Some(ref val) = watch.last_value {
365                    format!(" = {:?}", val)
366                } else {
367                    " = <not evaluated>".to_string()
368                };
369                println!(
370                    "  {} - {} ({}){}",
371                    watch.id.0, watch.expression, status, value
372                );
373            }
374        }
375    }
376
377    /// Show inspection result
378    pub fn show_inspection_result(&self, result: &InspectionResult) {
379        match result {
380            InspectionResult::Variable {
381                name,
382                value,
383                type_info,
384            } => {
385                println!(
386                    "🔍 Variable '{}': {:?} (type: {}, {} bytes)",
387                    name, value, type_info.type_name, type_info.size_bytes
388                );
389            }
390            InspectionResult::Node {
391                node_id,
392                value,
393                metadata,
394            } => {
395                println!("🔍 Node {:?}: {:?}", node_id, value);
396                println!("  Operation: {}", metadata.operation);
397                println!("  Inputs: {}", metadata.input_count);
398                println!("  Output shape: {:?}", metadata.output_shape);
399                println!("  Data type: {:?}", metadata.dtype);
400            }
401            InspectionResult::Memory {
402                address,
403                content,
404                size,
405            } => {
406                println!("🔍 Memory at 0x{:x} (size: {}):", address, size);
407                self.format_memory_dump(*address, content);
408            }
409        }
410    }
411
412    /// Show call stack
413    pub fn show_call_stack(&self, call_stack: &CallStack) {
414        if call_stack.is_empty() {
415            println!("📞 Call stack is empty");
416        } else {
417            println!("📞 Call stack (depth: {}):", call_stack.depth());
418            for (i, frame) in call_stack.frames().iter().rev().enumerate() {
419                let marker = if i == 0 { "â–ļī¸ " } else { "  " };
420                println!(
421                    "{}#{} - {} at {:?}",
422                    marker, i, frame.function_name, frame.location
423                );
424                if !frame.local_variables.is_empty() {
425                    println!("      locals: {} variables", frame.local_variables.len());
426                }
427            }
428        }
429    }
430
431    /// Show local variables
432    pub fn show_local_variables(&self, locals: &HashMap<String, DebugValue>) {
433        if locals.is_empty() {
434            println!("đŸ”ĸ No local variables");
435        } else {
436            println!("đŸ”ĸ Local variables ({}):", locals.len());
437            for (name, value) in locals {
438                println!("  {} = {:?}", name, value);
439            }
440        }
441    }
442
443    /// Show memory view
444    pub fn show_memory_view(&self, memory_view: &MemoryView) {
445        println!(
446            "🧠 Memory view starting at 0x{:x}:",
447            memory_view.start_address
448        );
449        self.format_memory_dump(memory_view.start_address, &memory_view.content);
450    }
451
452    /// Format memory dump for display
453    fn format_memory_dump(&self, start_address: u64, content: &[u8]) {
454        for (i, chunk) in content.chunks(16).enumerate() {
455            print!("  {:08x}: ", start_address + (i * 16) as u64);
456
457            // Hex bytes
458            for (j, byte) in chunk.iter().enumerate() {
459                if j == 8 {
460                    print!(" "); // Extra space in the middle
461                }
462                print!("{:02x} ", byte);
463            }
464
465            // Pad if less than 16 bytes
466            for j in chunk.len()..16 {
467                if j == 8 {
468                    print!(" ");
469                }
470                print!("   ");
471            }
472
473            print!(" |");
474
475            // ASCII representation
476            for byte in chunk {
477                let ch = if byte.is_ascii_graphic() || *byte == b' ' {
478                    *byte as char
479                } else {
480                    '.'
481                };
482                print!("{}", ch);
483            }
484
485            println!("|");
486        }
487    }
488
489    /// Show disassembly
490    pub fn show_disassembly(&self, disassembly: &DisassemblyView) {
491        println!("📋 Disassembly at {:?}:", disassembly.location);
492        for instruction in &disassembly.instructions {
493            print!(
494                "  {:08x}: {} {}",
495                instruction.address, instruction.opcode, instruction.operands
496            );
497            if let Some(comment) = &instruction.comment {
498                print!(" ; {}", comment);
499            }
500            println!();
501        }
502    }
503
504    /// Show help information
505    pub fn show_help(&self) {
506        println!("🆘 Available commands:");
507        println!("  step, s            - Execute one step");
508        println!("  next, n            - Step over function calls");
509        println!("  into, i            - Step into function calls");
510        println!("  out, o             - Step out of current function");
511        println!("  continue, c        - Continue execution");
512        println!("  break <loc>, b     - Set breakpoint at location");
513        println!("    Examples: break node_5, break main:10");
514        println!("  delete <id>, d     - Remove breakpoint by ID");
515        println!("  breakpoints        - List all breakpoints");
516        println!("  watch <expr>, w    - Watch expression");
517        println!("    Examples: watch variable_name, watch node_3");
518        println!("  unwatch <id>       - Remove watch by ID");
519        println!("  watches            - List all watches");
520        println!("  inspect <target>, p - Inspect variable/node/memory");
521        println!("    Examples: inspect var, inspect node_1, inspect 0x1000");
522        println!("  stack              - Show call stack");
523        println!("  locals             - Show local variables");
524        println!("  memory <addr>, mem - Show memory contents");
525        println!("    Examples: memory 0x1000, memory 4096");
526        println!("  disasm <loc>, dis  - Disassemble at location");
527        println!("  history            - Show command history");
528        println!("  clear              - Clear screen");
529        println!("  help, h            - Show this help");
530        println!("  quit, q            - Exit debugger");
531    }
532
533    /// Show execution complete message
534    pub fn show_execution_complete(&self) {
535        println!("✅ Execution completed successfully.");
536    }
537
538    /// Show command history
539    pub fn show_command_history(&self) {
540        if self.command_history.is_empty() {
541            println!("📜 No command history");
542        } else {
543            println!("📜 Command history:");
544            for (i, cmd) in self.command_history.iter().enumerate() {
545                println!("  {}: {}", i + 1, cmd);
546            }
547        }
548    }
549
550    /// Clear the screen
551    pub fn clear_screen(&self) {
552        // Clear screen using ANSI escape sequences
553        print!("\x1B[2J\x1B[1;1H");
554        io::stdout().flush().expect("stdout flush should succeed");
555    }
556
557    /// Show error message
558    pub fn show_error(&self, error: &str) {
559        println!("❌ Error: {}", error);
560    }
561
562    /// Show warning message
563    pub fn show_warning(&self, warning: &str) {
564        println!("âš ī¸  Warning: {}", warning);
565    }
566
567    /// Show information message
568    pub fn show_info(&self, info: &str) {
569        println!("â„šī¸  {}", info);
570    }
571
572    /// Show success message
573    pub fn show_success(&self, message: &str) {
574        println!("✅ {}", message);
575    }
576
577    /// Set a command alias
578    pub fn set_alias(&mut self, alias: String, command: String) {
579        self.command_aliases.insert(alias, command);
580    }
581
582    /// Remove a command alias
583    pub fn remove_alias(&mut self, alias: &str) {
584        self.command_aliases.remove(alias);
585    }
586
587    /// Get command history
588    pub fn get_command_history(&self) -> &[String] {
589        &self.command_history
590    }
591
592    /// Clear command history
593    pub fn clear_command_history(&mut self) {
594        self.command_history.clear();
595    }
596
597    /// Get configuration
598    pub fn config(&self) -> &DebuggerConfig {
599        &self.config
600    }
601
602    /// Update configuration
603    pub fn update_config(&mut self, config: DebuggerConfig) {
604        self.config = config;
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    fn create_test_interface() -> DebuggerInterface {
613        DebuggerInterface::new(DebuggerConfig::default())
614    }
615
616    #[test]
617    fn test_interface_creation() {
618        let interface = create_test_interface();
619        assert!(!interface.command_aliases.is_empty());
620        assert!(interface.command_history.is_empty());
621    }
622
623    #[test]
624    fn test_command_parsing_basic() {
625        let interface = create_test_interface();
626
627        assert!(matches!(
628            interface.parse_command("step"),
629            Ok(DebugCommand::Step)
630        ));
631        assert!(matches!(
632            interface.parse_command("s"),
633            Ok(DebugCommand::Step)
634        ));
635        assert!(matches!(
636            interface.parse_command("continue"),
637            Ok(DebugCommand::Continue)
638        ));
639        assert!(matches!(
640            interface.parse_command("c"),
641            Ok(DebugCommand::Continue)
642        ));
643        assert!(matches!(
644            interface.parse_command("help"),
645            Ok(DebugCommand::Help)
646        ));
647        assert!(matches!(
648            interface.parse_command("quit"),
649            Ok(DebugCommand::Quit)
650        ));
651    }
652
653    #[test]
654    fn test_breakpoint_location_parsing() {
655        let interface = create_test_interface();
656
657        let result = interface.parse_breakpoint_location("node_5");
658        assert!(matches!(result, Ok(BreakpointLocation::GraphNode(_))));
659
660        let result = interface.parse_breakpoint_location("main:10");
661        assert!(matches!(result, Ok(BreakpointLocation::Instruction { .. })));
662
663        let result = interface.parse_breakpoint_location("invalid");
664        assert!(result.is_err());
665    }
666
667    #[test]
668    fn test_inspection_target_parsing() {
669        let interface = create_test_interface();
670
671        let result = interface.parse_inspection_target("node_3");
672        assert!(matches!(result, Ok(InspectionTarget::Node(_))));
673
674        let result = interface.parse_inspection_target("0x1000");
675        assert!(matches!(result, Ok(InspectionTarget::Memory(4096))));
676
677        let result = interface.parse_inspection_target("variable_name");
678        assert!(matches!(result, Ok(InspectionTarget::Variable(_))));
679    }
680
681    #[test]
682    fn test_memory_address_parsing() {
683        let interface = create_test_interface();
684
685        assert_eq!(interface.parse_memory_address("0x1000").unwrap(), 4096);
686        assert_eq!(interface.parse_memory_address("1000").unwrap(), 1000);
687        assert!(interface.parse_memory_address("invalid").is_err());
688    }
689
690    #[test]
691    fn test_command_with_arguments() {
692        let interface = create_test_interface();
693
694        let result = interface.parse_command("break node_5");
695        assert!(matches!(result, Ok(DebugCommand::SetBreakpoint { .. })));
696
697        let result = interface.parse_command("watch variable_name");
698        assert!(matches!(result, Ok(DebugCommand::Watch { .. })));
699
700        let result = interface.parse_command("inspect node_3");
701        assert!(matches!(result, Ok(DebugCommand::Inspect { .. })));
702    }
703
704    #[test]
705    fn test_alias_management() {
706        let mut interface = create_test_interface();
707
708        interface.set_alias("test".to_string(), "step".to_string());
709        assert!(matches!(
710            interface.parse_command("test"),
711            Ok(DebugCommand::Step)
712        ));
713
714        interface.remove_alias("test");
715        assert!(interface.parse_command("test").is_err());
716    }
717
718    #[test]
719    fn test_invalid_commands() {
720        let interface = create_test_interface();
721
722        assert!(interface.parse_command("").is_err());
723        assert!(interface.parse_command("invalid_command").is_err());
724        assert!(interface.parse_command("break").is_err()); // Missing argument
725        assert!(interface.parse_command("watch").is_err()); // Missing argument
726    }
727}