ruchy_notebook/error/
stack_trace.rs

1use serde::{Deserialize, Serialize};
2use crate::vm::OpCode;
3
4/// Stack trace for debugging VM execution
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct StackTrace {
7    pub frames: Vec<StackFrame>,
8}
9
10/// A single frame in the stack trace
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct StackFrame {
13    pub function_name: Option<String>,
14    pub instruction_pointer: usize,
15    pub opcode: Option<OpCode>,
16    pub source_location: Option<SourceLocation>,
17    pub locals: Vec<(String, String)>, // name -> value representation
18}
19
20/// Source location information
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SourceLocation {
23    pub file: Option<String>,
24    pub line: usize,
25    pub column: usize,
26    pub source_line: Option<String>,
27}
28
29impl StackTrace {
30    /// Create a new empty stack trace
31    pub fn new() -> Self {
32        Self {
33            frames: Vec::new(),
34        }
35    }
36    
37    /// Add a frame to the stack trace
38    pub fn push_frame(&mut self, frame: StackFrame) {
39        self.frames.push(frame);
40    }
41    
42    /// Remove the top frame
43    pub fn pop_frame(&mut self) -> Option<StackFrame> {
44        self.frames.pop()
45    }
46    
47    /// Get the current frame (top of stack)
48    pub fn current_frame(&self) -> Option<&StackFrame> {
49        self.frames.last()
50    }
51    
52    /// Format the stack trace for display
53    pub fn format(&self) -> String {
54        if self.frames.is_empty() {
55            return "No stack trace available".to_string();
56        }
57        
58        let mut result = String::from("Stack trace:\n");
59        
60        for (i, frame) in self.frames.iter().rev().enumerate() {
61            result.push_str(&format!("  {}: ", i));
62            
63            if let Some(name) = &frame.function_name {
64                result.push_str(&format!("in function '{}'", name));
65            } else {
66                result.push_str("in <anonymous>");
67            }
68            
69            if let Some(loc) = &frame.source_location {
70                result.push_str(&format!(" at line {}", loc.line));
71                if let Some(file) = &loc.file {
72                    result.push_str(&format!(" in {}", file));
73                }
74                
75                if let Some(source_line) = &loc.source_line {
76                    result.push_str(&format!("\n    {}", source_line));
77                    if loc.column > 0 {
78                        result.push_str(&format!("\n    {}^", " ".repeat(loc.column - 1)));
79                    }
80                }
81            }
82            
83            if let Some(opcode) = &frame.opcode {
84                result.push_str(&format!(" [instruction: {:?}]", opcode));
85            }
86            
87            result.push('\n');
88        }
89        
90        result
91    }
92    
93    /// Get the deepest source location
94    pub fn deepest_location(&self) -> Option<&SourceLocation> {
95        self.frames.iter()
96            .filter_map(|f| f.source_location.as_ref())
97            .last()
98    }
99    
100    /// Clear the stack trace
101    pub fn clear(&mut self) {
102        self.frames.clear();
103    }
104}
105
106impl StackFrame {
107    /// Create a new stack frame
108    pub fn new(instruction_pointer: usize) -> Self {
109        Self {
110            function_name: None,
111            instruction_pointer,
112            opcode: None,
113            source_location: None,
114            locals: Vec::new(),
115        }
116    }
117    
118    /// Set the function name
119    pub fn with_function(mut self, name: impl Into<String>) -> Self {
120        self.function_name = Some(name.into());
121        self
122    }
123    
124    /// Set the opcode
125    pub fn with_opcode(mut self, opcode: OpCode) -> Self {
126        self.opcode = Some(opcode);
127        self
128    }
129    
130    /// Set the source location
131    pub fn with_location(mut self, location: SourceLocation) -> Self {
132        self.source_location = Some(location);
133        self
134    }
135    
136    /// Add a local variable
137    pub fn add_local(&mut self, name: impl Into<String>, value: impl Into<String>) {
138        self.locals.push((name.into(), value.into()));
139    }
140    
141    /// Get local variables as formatted string
142    pub fn format_locals(&self) -> String {
143        if self.locals.is_empty() {
144            return "No local variables".to_string();
145        }
146        
147        let mut result = String::from("Local variables:\n");
148        for (name, value) in &self.locals {
149            result.push_str(&format!("  {}: {}\n", name, value));
150        }
151        result
152    }
153}
154
155impl SourceLocation {
156    /// Create a new source location
157    pub fn new(line: usize, column: usize) -> Self {
158        Self {
159            file: None,
160            line,
161            column,
162            source_line: None,
163        }
164    }
165    
166    /// Set the file name
167    pub fn with_file(mut self, file: impl Into<String>) -> Self {
168        self.file = Some(file.into());
169        self
170    }
171    
172    /// Set the source line content
173    pub fn with_source_line(mut self, source_line: impl Into<String>) -> Self {
174        self.source_line = Some(source_line.into());
175        self
176    }
177}
178
179impl Default for StackTrace {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185/// Helper trait for objects that can capture stack traces
186pub trait StackCapture {
187    /// Capture the current stack state
188    fn capture_stack(&self) -> StackTrace;
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    
195    #[test]
196    fn test_stack_trace_creation() {
197        let mut trace = StackTrace::new();
198        assert!(trace.frames.is_empty());
199        
200        let frame = StackFrame::new(100)
201            .with_function("main")
202            .with_opcode(OpCode::Add)
203            .with_location(SourceLocation::new(10, 5).with_file("test.ruchy"));
204        
205        trace.push_frame(frame);
206        assert_eq!(trace.frames.len(), 1);
207        assert_eq!(trace.current_frame().unwrap().instruction_pointer, 100);
208    }
209    
210    #[test]
211    fn test_stack_trace_formatting() {
212        let mut trace = StackTrace::new();
213        
214        let frame1 = StackFrame::new(0)
215            .with_function("helper")
216            .with_location(
217                SourceLocation::new(5, 10)
218                    .with_file("helper.ruchy")
219                    .with_source_line("let x = a + b")
220            );
221        
222        let frame2 = StackFrame::new(50)
223            .with_function("main")
224            .with_opcode(OpCode::Call)
225            .with_location(SourceLocation::new(20, 1).with_file("main.ruchy"));
226        
227        trace.push_frame(frame1);
228        trace.push_frame(frame2);
229        
230        let formatted = trace.format();
231        assert!(formatted.contains("Stack trace:"));
232        assert!(formatted.contains("function 'main'"));
233        assert!(formatted.contains("function 'helper'"));
234        assert!(formatted.contains("line 20"));
235        assert!(formatted.contains("line 5"));
236    }
237    
238    #[test]
239    fn test_local_variables() {
240        let mut frame = StackFrame::new(10);
241        frame.add_local("x", "42");
242        frame.add_local("name", "\"Alice\"");
243        
244        let formatted = frame.format_locals();
245        assert!(formatted.contains("x: 42"));
246        assert!(formatted.contains("name: \"Alice\""));
247    }
248    
249    #[test]
250    fn test_deepest_location() {
251        let mut trace = StackTrace::new();
252        
253        let frame1 = StackFrame::new(0)
254            .with_location(SourceLocation::new(1, 1));
255        let frame2 = StackFrame::new(10);  // No location
256        let frame3 = StackFrame::new(20)
257            .with_location(SourceLocation::new(15, 3));
258        
259        trace.push_frame(frame1);
260        trace.push_frame(frame2);
261        trace.push_frame(frame3);
262        
263        let deepest = trace.deepest_location().unwrap();
264        assert_eq!(deepest.line, 15);
265        assert_eq!(deepest.column, 3);
266    }
267}