zerodds_idl/preprocessor/
source_map.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct FileId(pub u32);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct SourceLocation {
25 pub file_id: FileId,
26 pub byte_offset: usize,
27}
28
29#[derive(Debug, Clone, Default)]
31pub struct SourceMap {
32 files: Vec<String>,
33 segments: Vec<Segment>,
34}
35
36#[derive(Debug, Clone, Copy)]
37struct Segment {
38 output_start: usize,
39 length: usize,
40 file_id: FileId,
41 original_offset: usize,
42}
43
44impl SourceMap {
45 #[must_use]
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn add_file(&mut self, name: &str) -> FileId {
53 if let Some(idx) = self.files.iter().position(|f| f == name) {
54 return FileId(idx as u32);
55 }
56 let id = FileId(self.files.len() as u32);
57 self.files.push(name.to_string());
58 id
59 }
60
61 pub fn record_segment(
65 &mut self,
66 output_start: usize,
67 length: usize,
68 file_id: FileId,
69 original_offset: usize,
70 ) {
71 if length == 0 {
72 return;
73 }
74 self.segments.push(Segment {
75 output_start,
76 length,
77 file_id,
78 original_offset,
79 });
80 }
81
82 #[must_use]
87 pub fn lookup(&self, output_pos: usize) -> Option<SourceLocation> {
88 for s in &self.segments {
89 if output_pos >= s.output_start && output_pos < s.output_start + s.length {
90 let delta = output_pos - s.output_start;
91 return Some(SourceLocation {
92 file_id: s.file_id,
93 byte_offset: s.original_offset + delta,
94 });
95 }
96 }
97 None
98 }
99
100 #[must_use]
102 pub fn file_name(&self, id: FileId) -> Option<&str> {
103 self.files.get(id.0 as usize).map(String::as_str)
104 }
105
106 #[must_use]
108 pub fn segment_count(&self) -> usize {
109 self.segments.len()
110 }
111
112 #[must_use]
114 pub fn file_count(&self) -> usize {
115 self.files.len()
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 #![allow(clippy::expect_used)]
122 use super::*;
123
124 #[test]
125 fn add_file_returns_stable_id_for_same_name() {
126 let mut m = SourceMap::new();
127 let a = m.add_file("foo.idl");
128 let b = m.add_file("foo.idl");
129 assert_eq!(a, b);
130 }
131
132 #[test]
133 fn add_file_returns_distinct_ids_for_different_names() {
134 let mut m = SourceMap::new();
135 let a = m.add_file("a.idl");
136 let b = m.add_file("b.idl");
137 assert_ne!(a, b);
138 }
139
140 #[test]
141 fn lookup_returns_none_for_empty_map() {
142 let m = SourceMap::new();
143 assert!(m.lookup(0).is_none());
144 }
145
146 #[test]
147 fn lookup_finds_position_in_recorded_segment() {
148 let mut m = SourceMap::new();
149 let id = m.add_file("a.idl");
150 m.record_segment(10, 5, id, 100);
151 let loc = m.lookup(12).expect("in segment");
152 assert_eq!(loc.file_id, id);
153 assert_eq!(loc.byte_offset, 102);
154 }
155
156 #[test]
157 fn lookup_beyond_last_segment_is_none() {
158 let mut m = SourceMap::new();
159 let id = m.add_file("a.idl");
160 m.record_segment(0, 5, id, 0);
161 assert!(m.lookup(100).is_none());
162 }
163
164 #[test]
165 fn record_segment_with_zero_length_is_noop() {
166 let mut m = SourceMap::new();
167 let id = m.add_file("a.idl");
168 m.record_segment(0, 0, id, 0);
169 assert_eq!(m.segment_count(), 0);
170 }
171
172 #[test]
173 fn file_name_resolves_back() {
174 let mut m = SourceMap::new();
175 let id = m.add_file("foo.idl");
176 assert_eq!(m.file_name(id), Some("foo.idl"));
177 }
178
179 #[test]
180 fn file_count_tracks_unique_files() {
181 let mut m = SourceMap::new();
182 m.add_file("a");
183 m.add_file("b");
184 m.add_file("a"); assert_eq!(m.file_count(), 2);
186 }
187
188 #[test]
189 fn lookup_works_across_multiple_segments() {
190 let mut m = SourceMap::new();
191 let a = m.add_file("a");
192 let b = m.add_file("b");
193 m.record_segment(0, 10, a, 0);
194 m.record_segment(10, 20, b, 50);
195 let l1 = m.lookup(5).expect("in a");
196 assert_eq!(l1.file_id, a);
197 assert_eq!(l1.byte_offset, 5);
198 let l2 = m.lookup(15).expect("in b");
199 assert_eq!(l2.file_id, b);
200 assert_eq!(l2.byte_offset, 55);
201 }
202}