leptos_helios/
debugger.rs

1//! Interactive Debugger
2//!
3//! This module provides interactive debugging capabilities for Helios,
4//! including breakpoints, step-through debugging, and state inspection.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::Instant;
10use tokio::sync::broadcast;
11
12/// Debugger errors
13#[derive(Debug, thiserror::Error)]
14pub enum DebuggerError {
15    #[error("Debugger not started")]
16    NotStarted,
17
18    #[error("Breakpoint not found: {0}")]
19    BreakpointNotFound(String),
20
21    #[error("Invalid debug command: {0}")]
22    InvalidCommand(String),
23
24    #[error("Execution failed: {0}")]
25    ExecutionFailed(String),
26
27    #[error("State inspection failed: {0}")]
28    StateInspectionFailed(String),
29}
30
31/// Debug breakpoint configuration
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Breakpoint {
34    pub id: String,
35    pub location: BreakpointLocation,
36    pub condition: Option<String>,
37    pub enabled: bool,
38    pub hit_count: usize,
39    pub created_at: u64,
40}
41
42/// Breakpoint location types
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub enum BreakpointLocation {
45    Function { name: String },
46    Line { file: String, line: u32 },
47    Render { chart_type: String },
48    DataProcessing { operation: String },
49    Performance { threshold_ms: u64 },
50}
51
52/// Debug execution state
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum ExecutionState {
55    Running,
56    Paused { location: String, reason: String },
57    Stopped,
58    Error { message: String },
59}
60
61/// Debug step commands
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum DebugCommand {
64    Continue,
65    StepOver,
66    StepInto,
67    StepOut,
68    Stop,
69    Restart,
70}
71
72/// Variable information for state inspection
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct VariableInfo {
75    pub name: String,
76    pub value: String,
77    pub type_name: String,
78    pub children: Vec<VariableInfo>,
79    pub memory_address: Option<String>,
80}
81
82/// Call stack frame
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct StackFrame {
85    pub function_name: String,
86    pub file: Option<String>,
87    pub line: Option<u32>,
88    pub variables: Vec<VariableInfo>,
89}
90
91/// Debug session state
92#[derive(Debug, Clone)]
93pub struct DebugSession {
94    pub id: String,
95    pub name: String,
96    pub state: ExecutionState,
97    pub breakpoints: HashMap<String, Breakpoint>,
98    pub call_stack: Vec<StackFrame>,
99    pub start_time: Instant,
100}
101
102/// Enhanced error message with debugging context
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct EnhancedError {
105    pub original_error: String,
106    pub error_type: String,
107    pub location: Option<String>,
108    pub context: HashMap<String, String>,
109    pub suggestions: Vec<String>,
110    pub related_documentation: Vec<String>,
111    pub severity: ErrorSeverity,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum ErrorSeverity {
116    Info,
117    Warning,
118    Error,
119    Critical,
120}
121
122/// Main interactive debugger
123pub struct InteractiveDebugger {
124    sessions: Arc<Mutex<HashMap<String, DebugSession>>>,
125    current_session: Arc<Mutex<Option<String>>>,
126    command_sender: broadcast::Sender<DebugCommand>,
127    state_sender: broadcast::Sender<ExecutionState>,
128    enabled: bool,
129    enhanced_errors: bool,
130}
131
132impl InteractiveDebugger {
133    /// Create a new interactive debugger
134    pub fn new() -> Self {
135        let (command_sender, _) = broadcast::channel(100);
136        let (state_sender, _) = broadcast::channel(100);
137
138        Self {
139            sessions: Arc::new(Mutex::new(HashMap::new())),
140            current_session: Arc::new(Mutex::new(None)),
141            command_sender,
142            state_sender,
143            enabled: true,
144            enhanced_errors: true,
145        }
146    }
147
148    /// Start a new debug session
149    pub fn start_session(&mut self, name: &str) -> Result<String, DebuggerError> {
150        if !self.enabled {
151            return Err(DebuggerError::NotStarted);
152        }
153
154        let session_id = uuid::Uuid::new_v4().to_string();
155        let session = DebugSession {
156            id: session_id.clone(),
157            name: name.to_string(),
158            state: ExecutionState::Running,
159            breakpoints: HashMap::new(),
160            call_stack: Vec::new(),
161            start_time: Instant::now(),
162        };
163
164        let mut sessions = self.sessions.lock().unwrap();
165        sessions.insert(session_id.clone(), session);
166
167        let mut current = self.current_session.lock().unwrap();
168        *current = Some(session_id.clone());
169
170        println!("🐛 Started debug session: {} ({})", name, session_id);
171        Ok(session_id)
172    }
173
174    /// Add a breakpoint
175    pub fn add_breakpoint(
176        &mut self,
177        location: BreakpointLocation,
178        condition: Option<String>,
179    ) -> Result<String, DebuggerError> {
180        let current_session = self.current_session.lock().unwrap();
181        let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
182
183        let breakpoint_id = uuid::Uuid::new_v4().to_string();
184        let breakpoint = Breakpoint {
185            id: breakpoint_id.clone(),
186            location,
187            condition,
188            enabled: true,
189            hit_count: 0,
190            created_at: Instant::now().elapsed().as_millis() as u64,
191        };
192
193        let mut sessions = self.sessions.lock().unwrap();
194        if let Some(session) = sessions.get_mut(session_id) {
195            session
196                .breakpoints
197                .insert(breakpoint_id.clone(), breakpoint);
198        }
199
200        println!("🔴 Added breakpoint: {}", breakpoint_id);
201        Ok(breakpoint_id)
202    }
203
204    /// Remove a breakpoint
205    pub fn remove_breakpoint(&mut self, breakpoint_id: &str) -> Result<(), DebuggerError> {
206        let current_session = self.current_session.lock().unwrap();
207        let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
208
209        let mut sessions = self.sessions.lock().unwrap();
210        if let Some(session) = sessions.get_mut(session_id) {
211            session
212                .breakpoints
213                .remove(breakpoint_id)
214                .ok_or_else(|| DebuggerError::BreakpointNotFound(breakpoint_id.to_string()))?;
215        }
216
217        println!("⭕ Removed breakpoint: {}", breakpoint_id);
218        Ok(())
219    }
220
221    /// Execute debug command
222    pub fn execute_command(&mut self, command: DebugCommand) -> Result<(), DebuggerError> {
223        let current_session = self.current_session.lock().unwrap();
224        let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
225
226        // Broadcast command to debug runtime
227        let _ = self.command_sender.send(command.clone());
228
229        // Update session state based on command
230        let new_state = {
231            let mut sessions = self.sessions.lock().unwrap();
232            if let Some(session) = sessions.get_mut(session_id) {
233                session.state = match command {
234                    DebugCommand::Continue => ExecutionState::Running,
235                    DebugCommand::Stop => ExecutionState::Stopped,
236                    DebugCommand::StepOver | DebugCommand::StepInto | DebugCommand::StepOut => {
237                        ExecutionState::Paused {
238                            location: "next_instruction".to_string(),
239                            reason: "step_command".to_string(),
240                        }
241                    }
242                    DebugCommand::Restart => {
243                        session.call_stack.clear();
244                        ExecutionState::Running
245                    }
246                };
247                session.state.clone()
248            } else {
249                ExecutionState::Error {
250                    message: "Session not found".to_string(),
251                }
252            }
253        };
254
255        // Broadcast state change
256        let _ = self.state_sender.send(new_state);
257
258        println!("🎮 Executed debug command: {:?}", command);
259        Ok(())
260    }
261
262    /// Check if execution should pause at current location
263    pub fn should_pause(&self, location: &str, context: &HashMap<String, String>) -> bool {
264        let current_session = self.current_session.lock().unwrap();
265        if let Some(session_id) = current_session.as_ref() {
266            let sessions = self.sessions.lock().unwrap();
267            if let Some(session) = sessions.get(session_id) {
268                for breakpoint in session.breakpoints.values() {
269                    if !breakpoint.enabled {
270                        continue;
271                    }
272
273                    let location_matches = match &breakpoint.location {
274                        BreakpointLocation::Function { name } => location.contains(name),
275                        BreakpointLocation::Line { file, line: _ } => location.contains(file),
276                        BreakpointLocation::Render { chart_type } => {
277                            location.contains("render")
278                                && context.get("chart_type") == Some(chart_type)
279                        }
280                        BreakpointLocation::DataProcessing { operation } => {
281                            location.contains("data") && context.get("operation") == Some(operation)
282                        }
283                        BreakpointLocation::Performance { threshold_ms } => {
284                            if let Some(duration_str) = context.get("duration_ms") {
285                                if let Ok(duration_ms) = duration_str.parse::<u64>() {
286                                    return duration_ms > *threshold_ms;
287                                }
288                            }
289                            false
290                        }
291                    };
292
293                    if location_matches {
294                        if let Some(condition) = &breakpoint.condition {
295                            if self.evaluate_condition(condition, context) {
296                                return true;
297                            }
298                        } else {
299                            return true;
300                        }
301                    }
302                }
303            }
304        }
305        false
306    }
307
308    /// Inspect variable state
309    pub fn inspect_variables(&self, scope: &str) -> Result<Vec<VariableInfo>, DebuggerError> {
310        // Mock implementation - in real debugger would inspect actual runtime state
311        let mut variables = Vec::new();
312
313        match scope {
314            "chart" => {
315                variables.push(VariableInfo {
316                    name: "spec".to_string(),
317                    value: "ChartSpec { mark: Line, ... }".to_string(),
318                    type_name: "ChartSpec".to_string(),
319                    children: vec![VariableInfo {
320                        name: "mark".to_string(),
321                        value: "Line { stroke_width: 2.0 }".to_string(),
322                        type_name: "MarkType".to_string(),
323                        children: Vec::new(),
324                        memory_address: Some("0x7fff5fbff890".to_string()),
325                    }],
326                    memory_address: Some("0x7fff5fbff880".to_string()),
327                });
328            }
329            "data" => {
330                variables.push(VariableInfo {
331                    name: "dataframe".to_string(),
332                    value: "DataFrame { shape: (1000, 5) }".to_string(),
333                    type_name: "polars::DataFrame".to_string(),
334                    children: Vec::new(),
335                    memory_address: Some("0x7fff5fbff8a0".to_string()),
336                });
337            }
338            _ => {
339                return Err(DebuggerError::StateInspectionFailed(format!(
340                    "Unknown scope: {}",
341                    scope
342                )));
343            }
344        }
345
346        Ok(variables)
347    }
348
349    /// Get current call stack
350    pub fn get_call_stack(&self) -> Result<Vec<StackFrame>, DebuggerError> {
351        let current_session = self.current_session.lock().unwrap();
352        let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
353
354        let sessions = self.sessions.lock().unwrap();
355        if let Some(session) = sessions.get(session_id) {
356            Ok(session.call_stack.clone())
357        } else {
358            Ok(Vec::new())
359        }
360    }
361
362    /// Create enhanced error message with context and suggestions
363    pub fn enhance_error(&self, error: &str, location: Option<&str>) -> EnhancedError {
364        if !self.enhanced_errors {
365            return EnhancedError {
366                original_error: error.to_string(),
367                error_type: "Unknown".to_string(),
368                location: location.map(|s| s.to_string()),
369                context: HashMap::new(),
370                suggestions: Vec::new(),
371                related_documentation: Vec::new(),
372                severity: ErrorSeverity::Error,
373            };
374        }
375
376        let (error_type, suggestions, docs, severity) = self.analyze_error(error);
377        let context = self.gather_error_context(error, location);
378
379        EnhancedError {
380            original_error: error.to_string(),
381            error_type,
382            location: location.map(|s| s.to_string()),
383            context,
384            suggestions,
385            related_documentation: docs,
386            severity,
387        }
388    }
389
390    /// Analyze error and provide suggestions
391    fn analyze_error(&self, error: &str) -> (String, Vec<String>, Vec<String>, ErrorSeverity) {
392        let error_lower = error.to_lowercase();
393
394        if error_lower.contains("webgpu") {
395            (
396                "WebGPU Error".to_string(),
397                vec![
398                    "Check if WebGPU is supported in current browser".to_string(),
399                    "Verify GPU drivers are up to date".to_string(),
400                    "Consider enabling WebGL2 fallback".to_string(),
401                ],
402                vec![
403                    "https://docs.helios.dev/webgpu-support".to_string(),
404                    "https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API".to_string(),
405                ],
406                ErrorSeverity::Error,
407            )
408        } else if error_lower.contains("polars") || error_lower.contains("dataframe") {
409            (
410                "Data Processing Error".to_string(),
411                vec![
412                    "Verify data format and schema compatibility".to_string(),
413                    "Check for null values or invalid data types".to_string(),
414                    "Consider using lazy evaluation for large datasets".to_string(),
415                ],
416                vec![
417                    "https://docs.helios.dev/data-processing".to_string(),
418                    "https://docs.pola.rs/".to_string(),
419                ],
420                ErrorSeverity::Error,
421            )
422        } else if error_lower.contains("leptos") || error_lower.contains("signal") {
423            (
424                "Reactive System Error".to_string(),
425                vec![
426                    "Ensure reactive updates are properly tracked".to_string(),
427                    "Check for signal disposal and cleanup".to_string(),
428                    "Verify component lifecycle management".to_string(),
429                ],
430                vec![
431                    "https://docs.helios.dev/reactive-system".to_string(),
432                    "https://leptos.dev/".to_string(),
433                ],
434                ErrorSeverity::Warning,
435            )
436        } else if error_lower.contains("performance") || error_lower.contains("timeout") {
437            (
438                "Performance Issue".to_string(),
439                vec![
440                    "Enable performance profiling to identify bottlenecks".to_string(),
441                    "Consider reducing data complexity or visualization detail".to_string(),
442                    "Check for memory leaks or excessive allocations".to_string(),
443                ],
444                vec!["https://docs.helios.dev/performance-optimization".to_string()],
445                ErrorSeverity::Warning,
446            )
447        } else {
448            (
449                "General Error".to_string(),
450                vec![
451                    "Check the documentation for similar issues".to_string(),
452                    "Enable debug logging for more details".to_string(),
453                    "Consider filing an issue with reproduction steps".to_string(),
454                ],
455                vec![
456                    "https://docs.helios.dev/troubleshooting".to_string(),
457                    "https://github.com/helios-viz/helios/issues".to_string(),
458                ],
459                ErrorSeverity::Error,
460            )
461        }
462    }
463
464    /// Gather error context information
465    fn gather_error_context(&self, error: &str, location: Option<&str>) -> HashMap<String, String> {
466        let mut context = HashMap::new();
467
468        context.insert("timestamp".to_string(), chrono::Utc::now().to_rfc3339());
469
470        if let Some(loc) = location {
471            context.insert("location".to_string(), loc.to_string());
472        }
473
474        context.insert(
475            "browser_info".to_string(),
476            "Chrome/120.0 (mock)".to_string(),
477        );
478
479        context.insert("webgpu_supported".to_string(), "true".to_string());
480
481        context.insert("memory_usage".to_string(), "45MB".to_string());
482
483        // Add error-specific context
484        if error.to_lowercase().contains("webgpu") {
485            context.insert("webgpu_adapter".to_string(), "Default adapter".to_string());
486        }
487
488        context
489    }
490
491    /// Evaluate breakpoint condition
492    fn evaluate_condition(&self, condition: &str, context: &HashMap<String, String>) -> bool {
493        // Simple condition evaluation - in real implementation would use proper parser
494        if condition.contains("==") {
495            let parts: Vec<&str> = condition.split("==").collect();
496            if parts.len() == 2 {
497                let left = parts[0].trim();
498                let right = parts[1].trim().trim_matches('"');
499                return context.get(left) == Some(&right.to_string());
500            }
501        }
502
503        if condition.contains(">") {
504            let parts: Vec<&str> = condition.split(">").collect();
505            if parts.len() == 2 {
506                let left = parts[0].trim();
507                let right = parts[1].trim();
508                if let (Some(left_val), Ok(right_val)) = (context.get(left), right.parse::<f64>()) {
509                    if let Ok(left_num) = left_val.parse::<f64>() {
510                        return left_num > right_val;
511                    }
512                }
513            }
514        }
515
516        // Default to true for simple conditions
517        true
518    }
519
520    /// Subscribe to debug commands
521    pub fn subscribe_to_commands(&self) -> broadcast::Receiver<DebugCommand> {
522        self.command_sender.subscribe()
523    }
524
525    /// Subscribe to execution state changes
526    pub fn subscribe_to_state(&self) -> broadcast::Receiver<ExecutionState> {
527        self.state_sender.subscribe()
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[tokio::test]
536    async fn test_debug_session_lifecycle() {
537        let mut debugger = InteractiveDebugger::new();
538
539        // Start session
540        let session_id = debugger.start_session("test_debug").unwrap();
541        assert!(!session_id.is_empty());
542
543        // Add breakpoint
544        let bp_id = debugger
545            .add_breakpoint(
546                BreakpointLocation::Function {
547                    name: "render".to_string(),
548                },
549                None,
550            )
551            .unwrap();
552        assert!(!bp_id.is_empty());
553
554        // Execute command
555        debugger.execute_command(DebugCommand::StepOver).unwrap();
556
557        // Remove breakpoint
558        debugger.remove_breakpoint(&bp_id).unwrap();
559    }
560
561    #[test]
562    fn test_breakpoint_evaluation() {
563        let debugger = InteractiveDebugger::new();
564        let mut context = HashMap::new();
565        context.insert("chart_type".to_string(), "Line".to_string());
566
567        // Should not pause with empty breakpoints
568        assert!(!debugger.should_pause("render_line_chart", &context));
569    }
570
571    #[test]
572    fn test_error_enhancement() {
573        let debugger = InteractiveDebugger::new();
574        let error = "WebGPU adapter not found";
575        let enhanced = debugger.enhance_error(error, Some("render.rs:45"));
576
577        assert_eq!(enhanced.error_type, "WebGPU Error");
578        assert!(!enhanced.suggestions.is_empty());
579        assert!(!enhanced.related_documentation.is_empty());
580        assert!(matches!(enhanced.severity, ErrorSeverity::Error));
581    }
582
583    #[test]
584    fn test_condition_evaluation() {
585        let debugger = InteractiveDebugger::new();
586        let mut context = HashMap::new();
587        context.insert("duration_ms".to_string(), "150".to_string());
588
589        // Test numeric comparison
590        assert!(debugger.evaluate_condition("duration_ms > 100", &context));
591        assert!(!debugger.evaluate_condition("duration_ms > 200", &context));
592
593        // Test string equality
594        context.insert("type".to_string(), "LineChart".to_string());
595        assert!(debugger.evaluate_condition("type == \"LineChart\"", &context));
596    }
597
598    #[test]
599    fn test_variable_inspection() {
600        let debugger = InteractiveDebugger::new();
601
602        let chart_vars = debugger.inspect_variables("chart").unwrap();
603        assert!(!chart_vars.is_empty());
604        assert_eq!(chart_vars[0].name, "spec");
605        assert_eq!(chart_vars[0].type_name, "ChartSpec");
606    }
607}