Skip to main content

sbpf_assembler/preprocessor/
source_map.rs

1use std::ops::Range;
2
3/// Opaque handle into the file registry
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub struct FileId(pub(crate) u32);
6
7impl FileId {
8    /// Get the numeric index for this file ID (for use as a map key)
9    pub fn index(self) -> u32 {
10        self.0
11    }
12}
13
14/// Entry for a loaded file
15#[derive(Debug, Clone)]
16struct FileEntry {
17    path: String,
18    content: String,
19}
20
21/// Registry of all source files encountered during preprocessing
22#[derive(Debug, Clone, Default)]
23pub struct FileRegistry {
24    files: Vec<FileEntry>,
25}
26
27impl FileRegistry {
28    pub fn new() -> Self {
29        Self { files: Vec::new() }
30    }
31
32    /// Register a file and return its ID
33    pub fn add(&mut self, path: &str, content: String) -> FileId {
34        let id = FileId(self.files.len() as u32);
35        self.files.push(FileEntry {
36            path: path.to_string(),
37            content,
38        });
39        id
40    }
41
42    /// Get the display path for a file
43    pub fn path(&self, id: FileId) -> &str {
44        &self.files[id.0 as usize].path
45    }
46
47    /// Get the original content of a file
48    pub fn content(&self, id: FileId) -> &str {
49        &self.files[id.0 as usize].content
50    }
51
52    /// Get all registered file IDs
53    pub fn file_ids(&self) -> impl Iterator<Item = FileId> {
54        (0..self.files.len()).map(|i| FileId(i as u32))
55    }
56
57    /// Compute the byte offset of the start of a 1-based line number in a file's content.
58    /// Returns the byte offset, or 0 if the line is out of range.
59    pub fn line_byte_offset(&self, id: FileId, line: u32) -> usize {
60        let content = self.content(id);
61        if line <= 1 {
62            return 0;
63        }
64        let target = (line - 1) as usize;
65        let mut current_line = 0;
66        for (i, ch) in content.char_indices() {
67            if ch == '\n' {
68                current_line += 1;
69                if current_line == target {
70                    return i + 1;
71                }
72            }
73        }
74        // Line beyond end of file -- clamp to end
75        content.len()
76    }
77
78    /// Get the length of a 1-based line in a file (excluding newline).
79    pub fn line_length(&self, id: FileId, line: u32) -> usize {
80        let content = self.content(id);
81        let start = self.line_byte_offset(id, line);
82        let rest = &content[start..];
83        rest.find('\n').unwrap_or(rest.len())
84    }
85}
86
87/// Information about a macro expansion in the call stack
88#[derive(Debug, Clone)]
89pub struct MacroExpansionInfo {
90    pub macro_name: String,
91    pub invocation_origin: SourceOrigin,
92    pub depth: u32,
93}
94
95/// Where a line of expanded output originally came from
96#[derive(Debug, Clone)]
97pub struct SourceOrigin {
98    pub file_id: FileId,
99    /// 1-based line number in the original file
100    pub line: u32,
101    /// If this line was produced by a macro expansion, the chain of expansions
102    pub macro_expansion: Option<Box<MacroExpansionInfo>>,
103}
104
105impl SourceOrigin {
106    pub fn new(file_id: FileId, line: u32) -> Self {
107        Self {
108            file_id,
109            line,
110            macro_expansion: None,
111        }
112    }
113
114    pub fn with_macro_expansion(
115        file_id: FileId,
116        line: u32,
117        macro_name: String,
118        invocation_origin: SourceOrigin,
119        depth: u32,
120    ) -> Self {
121        Self {
122            file_id,
123            line,
124            macro_expansion: Some(Box::new(MacroExpansionInfo {
125                macro_name,
126                invocation_origin,
127                depth,
128            })),
129        }
130    }
131}
132
133/// Maps byte offsets in expanded text back to original source locations.
134///
135/// The source map records one `SourceOrigin` per line of expanded output.
136/// To resolve a byte offset from pest, we convert it to a line number
137/// (by counting newlines), then look up the origin for that line.
138#[derive(Debug, Clone)]
139pub struct SourceMap {
140    pub file_registry: FileRegistry,
141    line_origins: Vec<SourceOrigin>,
142}
143
144impl SourceMap {
145    pub fn new(file_registry: FileRegistry, line_origins: Vec<SourceOrigin>) -> Self {
146        Self {
147            file_registry,
148            line_origins,
149        }
150    }
151
152    /// Resolve a byte offset in the expanded source to its original location
153    pub fn resolve(&self, byte_offset: usize, expanded_source: &str) -> &SourceOrigin {
154        let line_index = byte_offset_to_line(byte_offset, expanded_source);
155        let clamped = line_index.min(self.line_origins.len().saturating_sub(1));
156        &self.line_origins[clamped]
157    }
158
159    /// Remap a span (byte range) from expanded source to an original origin.
160    /// Uses the start of the span for resolution.
161    pub fn resolve_span(&self, span: &Range<usize>, expanded_source: &str) -> &SourceOrigin {
162        self.resolve(span.start, expanded_source)
163    }
164
165    /// Get the number of tracked output lines
166    pub fn len(&self) -> usize {
167        self.line_origins.len()
168    }
169
170    pub fn is_empty(&self) -> bool {
171        self.line_origins.is_empty()
172    }
173
174    /// Format a human-readable location string for diagnostics
175    pub fn format_location(&self, origin: &SourceOrigin) -> String {
176        let file_path = self.file_registry.path(origin.file_id);
177        let mut location = format!("{}:{}", file_path, origin.line);
178
179        if let Some(ref expansion) = origin.macro_expansion {
180            location.push_str(&format!(
181                " (in expansion of macro '{}'",
182                expansion.macro_name
183            ));
184            let mut current = &expansion.invocation_origin;
185            loop {
186                let inv_path = self.file_registry.path(current.file_id);
187                location.push_str(&format!(", invoked at {}:{}", inv_path, current.line));
188                if let Some(ref inner) = current.macro_expansion {
189                    location.push_str(&format!(", in expansion of macro '{}'", inner.macro_name));
190                    current = &inner.invocation_origin;
191                } else {
192                    break;
193                }
194            }
195            location.push(')');
196        }
197
198        location
199    }
200}
201
202/// Convert a byte offset to a 0-based line index by counting newlines
203fn byte_offset_to_line(byte_offset: usize, source: &str) -> usize {
204    let clamped = byte_offset.min(source.len());
205    source[..clamped].matches('\n').count()
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_file_registry() {
214        let mut reg = FileRegistry::new();
215        let id1 = reg.add("main.s", "line1\nline2".to_string());
216        let id2 = reg.add("utils.s", "helper".to_string());
217
218        assert_eq!(reg.path(id1), "main.s");
219        assert_eq!(reg.path(id2), "utils.s");
220        assert_eq!(reg.content(id1), "line1\nline2");
221    }
222
223    #[test]
224    fn test_byte_offset_to_line() {
225        let source = "line0\nline1\nline2\n";
226        assert_eq!(byte_offset_to_line(0, source), 0);
227        assert_eq!(byte_offset_to_line(3, source), 0);
228        assert_eq!(byte_offset_to_line(6, source), 1);
229        assert_eq!(byte_offset_to_line(12, source), 2);
230        // Past end clamps
231        assert_eq!(byte_offset_to_line(999, source), 3);
232    }
233
234    #[test]
235    fn test_source_map_resolve() {
236        let mut reg = FileRegistry::new();
237        let file_id = reg.add("test.s", "original content".to_string());
238
239        let origins = vec![
240            SourceOrigin::new(file_id, 1),
241            SourceOrigin::new(file_id, 5),
242            SourceOrigin::new(file_id, 10),
243        ];
244
245        let expanded = "first line\nsecond line\nthird line\n";
246        let map = SourceMap::new(reg, origins);
247
248        // Offset in first line -> origin line 1
249        let origin = map.resolve(3, expanded);
250        assert_eq!(origin.line, 1);
251
252        // Offset in second line -> origin line 5
253        let origin = map.resolve(11, expanded);
254        assert_eq!(origin.line, 5);
255
256        // Offset in third line -> origin line 10
257        let origin = map.resolve(23, expanded);
258        assert_eq!(origin.line, 10);
259    }
260
261    #[test]
262    fn test_format_location_simple() {
263        let mut reg = FileRegistry::new();
264        let file_id = reg.add("main.s", String::new());
265        let origin = SourceOrigin::new(file_id, 42);
266        let map = SourceMap::new(reg, vec![]);
267
268        assert_eq!(map.format_location(&origin), "main.s:42");
269    }
270
271    #[test]
272    fn test_format_location_with_macro() {
273        let mut reg = FileRegistry::new();
274        let file_id = reg.add("macros.s", String::new());
275        let main_id = reg.add("main.s", String::new());
276
277        let origin = SourceOrigin::with_macro_expansion(
278            file_id,
279            3,
280            "MY_MACRO".to_string(),
281            SourceOrigin::new(main_id, 15),
282            1,
283        );
284        let map = SourceMap::new(reg, vec![]);
285
286        assert_eq!(
287            map.format_location(&origin),
288            "macros.s:3 (in expansion of macro 'MY_MACRO', invoked at main.s:15)"
289        );
290    }
291}