sbpf_assembler/preprocessor/
source_map.rs1use std::ops::Range;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub struct FileId(pub(crate) u32);
6
7impl FileId {
8 pub fn index(self) -> u32 {
10 self.0
11 }
12}
13
14#[derive(Debug, Clone)]
16struct FileEntry {
17 path: String,
18 content: String,
19}
20
21#[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 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 pub fn path(&self, id: FileId) -> &str {
44 &self.files[id.0 as usize].path
45 }
46
47 pub fn content(&self, id: FileId) -> &str {
49 &self.files[id.0 as usize].content
50 }
51
52 pub fn file_ids(&self) -> impl Iterator<Item = FileId> {
54 (0..self.files.len()).map(|i| FileId(i as u32))
55 }
56
57 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 content.len()
76 }
77
78 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#[derive(Debug, Clone)]
89pub struct MacroExpansionInfo {
90 pub macro_name: String,
91 pub invocation_origin: SourceOrigin,
92 pub depth: u32,
93}
94
95#[derive(Debug, Clone)]
97pub struct SourceOrigin {
98 pub file_id: FileId,
99 pub line: u32,
101 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#[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 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 pub fn resolve_span(&self, span: &Range<usize>, expanded_source: &str) -> &SourceOrigin {
162 self.resolve(span.start, expanded_source)
163 }
164
165 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 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
202fn 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 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 let origin = map.resolve(3, expanded);
250 assert_eq!(origin.line, 1);
251
252 let origin = map.resolve(11, expanded);
254 assert_eq!(origin.line, 5);
255
256 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}