ruchy_notebook/error/
stack_trace.rs1use serde::{Deserialize, Serialize};
2use crate::vm::OpCode;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct StackTrace {
7 pub frames: Vec<StackFrame>,
8}
9
10#[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)>, }
19
20#[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 pub fn new() -> Self {
32 Self {
33 frames: Vec::new(),
34 }
35 }
36
37 pub fn push_frame(&mut self, frame: StackFrame) {
39 self.frames.push(frame);
40 }
41
42 pub fn pop_frame(&mut self) -> Option<StackFrame> {
44 self.frames.pop()
45 }
46
47 pub fn current_frame(&self) -> Option<&StackFrame> {
49 self.frames.last()
50 }
51
52 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 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 pub fn clear(&mut self) {
102 self.frames.clear();
103 }
104}
105
106impl StackFrame {
107 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 pub fn with_function(mut self, name: impl Into<String>) -> Self {
120 self.function_name = Some(name.into());
121 self
122 }
123
124 pub fn with_opcode(mut self, opcode: OpCode) -> Self {
126 self.opcode = Some(opcode);
127 self
128 }
129
130 pub fn with_location(mut self, location: SourceLocation) -> Self {
132 self.source_location = Some(location);
133 self
134 }
135
136 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 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 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 pub fn with_file(mut self, file: impl Into<String>) -> Self {
168 self.file = Some(file.into());
169 self
170 }
171
172 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
185pub trait StackCapture {
187 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); 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}