Skip to main content

pipa/compiler/
location.rs

1use std::fmt;
2
3#[derive(Debug, Clone, Default)]
4pub struct SourceLocation {
5    pub filename: String,
6    pub line: u32,
7    pub column: u32,
8}
9
10impl SourceLocation {
11    pub fn new(filename: impl Into<String>, line: u32, column: u32) -> Self {
12        Self {
13            filename: filename.into(),
14            line,
15            column,
16        }
17    }
18
19    pub fn unknown() -> Self {
20        Self {
21            filename: "<anonymous>".to_string(),
22            line: 0,
23            column: 0,
24        }
25    }
26}
27
28impl fmt::Display for SourceLocation {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "{}:{}:{}", self.filename, self.line, self.column)
31    }
32}
33
34pub type LineNumberEntry = (u32, u32);
35
36#[derive(Debug, Clone, Default)]
37pub struct LineNumberTable {
38    entries: Vec<LineNumberEntry>,
39}
40
41impl LineNumberTable {
42    pub fn new() -> Self {
43        Self {
44            entries: Vec::new(),
45        }
46    }
47
48    pub fn add_entry(&mut self, instruction_offset: u32, line_number: u32) {
49        if let Some(pos) = self
50            .entries
51            .binary_search_by(|(off, _)| off.cmp(&instruction_offset))
52            .ok()
53        {
54            self.entries[pos] = (instruction_offset, line_number);
55        } else {
56            let pos = self
57                .entries
58                .partition_point(|(off, _)| *off < instruction_offset);
59            self.entries.insert(pos, (instruction_offset, line_number));
60        }
61    }
62
63    pub fn lookup_line(&self, instruction_offset: u32) -> Option<u32> {
64        match self
65            .entries
66            .binary_search_by(|(off, _)| off.cmp(&instruction_offset))
67        {
68            Ok(idx) => Some(self.entries[idx].1),
69            Err(idx) if idx > 0 => Some(self.entries[idx - 1].1),
70            _ => None,
71        }
72    }
73
74    pub fn entries(&self) -> &[LineNumberEntry] {
75        &self.entries
76    }
77}
78
79#[derive(Debug, Clone)]
80pub struct FrameInfo {
81    pub function_name: String,
82    pub location: SourceLocation,
83}
84
85impl FrameInfo {
86    pub fn new(function_name: impl Into<String>, location: SourceLocation) -> Self {
87        Self {
88            function_name: function_name.into(),
89            location,
90        }
91    }
92}
93
94pub struct TracebackFormatter;
95
96impl TracebackFormatter {
97    pub fn format(error_message: &str, frames: &[FrameInfo], error_type: Option<&str>) -> String {
98        let mut result = String::new();
99
100        if let Some(etype) = error_type {
101            result.push_str(&format!("Uncaught {}: {}", etype, error_message));
102        } else {
103            result.push_str(&format!("Uncaught: {}", error_message));
104        }
105        result.push('\n');
106
107        for frame in frames.iter().rev() {
108            result.push_str(&format!(
109                "    at {} ({}:{}:{})\n",
110                frame.function_name,
111                frame.location.filename,
112                frame.location.line,
113                frame.location.column
114            ));
115        }
116
117        result
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_line_number_table() {
127        let mut table = LineNumberTable::new();
128
129        table.add_entry(0, 1);
130        table.add_entry(10, 2);
131        table.add_entry(25, 5);
132
133        assert_eq!(table.lookup_line(0), Some(1));
134        assert_eq!(table.lookup_line(5), Some(1));
135        assert_eq!(table.lookup_line(10), Some(2));
136        assert_eq!(table.lookup_line(20), Some(2));
137        assert_eq!(table.lookup_line(25), Some(5));
138        assert_eq!(table.lookup_line(100), Some(5));
139    }
140
141    #[test]
142    fn test_traceback_formatter() {
143        let frames = vec![
144            FrameInfo::new("inner", SourceLocation::new("test.js", 3, 5)),
145            FrameInfo::new("outer", SourceLocation::new("test.js", 7, 2)),
146            FrameInfo::new("global", SourceLocation::new("test.js", 10, 1)),
147        ];
148
149        let output = TracebackFormatter::format("x is not a function", &frames, Some("TypeError"));
150
151        assert!(output.contains("Uncaught TypeError: x is not a function"));
152        assert!(output.contains("at inner (test.js:3:5)"));
153        assert!(output.contains("at outer (test.js:7:2)"));
154        assert!(output.contains("at global (test.js:10:1)"));
155    }
156}