source_lang/map.rs
1//! The source map: many sources laid out across one global position space.
2
3use alloc::boxed::Box;
4use alloc::vec::Vec;
5
6use span_lang::{BytePos, Span};
7
8use crate::{SourceFile, SourceId, SourceMapError};
9
10/// A collection of sources laid out end to end in a single global position space.
11///
12/// A `SourceMap` is the multi-file coordinate layer of a front-end. Each source
13/// added to it gets a stable [`SourceId`] and a non-overlapping range in one
14/// shared position space, so a single global [`BytePos`] names a point across the
15/// whole project. [`locate`](SourceMap::locate) maps such a position back to its
16/// `(SourceId, local offset)` — the inverse of the layout — which is how a
17/// diagnostic rendered against a global span knows *which file* to point at.
18///
19/// # Layout
20///
21/// Sources are placed in the order they are added: the first occupies
22/// `0..len₀`, the next `len₀..len₀ + len₁`, and so on. Because the bases only
23/// increase, the internal list is always sorted by start offset, so a lookup is a
24/// binary search over it — `O(log files)` — with no separate index to maintain.
25/// The whole space is 32 bits wide, the same envelope a single
26/// [`BytePos`] addresses, so the combined length of every source is capped at
27/// `u32::MAX`; overrunning it is the [`SpaceExhausted`] error, never a silent
28/// wrap into a neighbour's range.
29///
30/// [`SpaceExhausted`]: SourceMapError::SpaceExhausted
31///
32/// # Examples
33///
34/// ```
35/// use source_lang::{BytePos, SourceMap};
36///
37/// let mut map = SourceMap::new();
38/// let main = map.add("main.rs", "fn main() {}").expect("fits"); // global 0..12
39/// let util = map.add("util.rs", "fn helper() {}").expect("fits"); // global 12..26
40///
41/// // A global position resolves to the file it lands in and the local offset.
42/// let (id, local) = map.locate(BytePos::new(13)).expect("inside util.rs");
43/// assert_eq!(id, util);
44/// assert_eq!(local, BytePos::new(1)); // 13 - 12
45/// assert_eq!(map.source(id).unwrap().name(), "util.rs");
46///
47/// // Position 0 is the very start of the first file.
48/// assert_eq!(map.locate(BytePos::new(0)).unwrap().0, main);
49///
50/// // Anything past the last byte belongs to no file.
51/// assert_eq!(map.locate(BytePos::new(26)), None);
52/// ```
53#[derive(Clone, Debug, Default)]
54pub struct SourceMap {
55 /// Sources in insertion order; always sorted by `span().start()` because
56 /// each new base is the previous high-water mark.
57 files: Vec<SourceFile>,
58 /// The next free global offset — the exclusive end of the last source's
59 /// range, and the base the next source will be placed at.
60 next_base: u32,
61}
62
63impl SourceMap {
64 /// Creates an empty map whose global position space starts at `0`.
65 ///
66 /// # Examples
67 ///
68 /// ```
69 /// use source_lang::SourceMap;
70 ///
71 /// let map = SourceMap::new();
72 /// assert!(map.is_empty());
73 /// ```
74 #[inline]
75 #[must_use]
76 pub const fn new() -> Self {
77 Self {
78 files: Vec::new(),
79 next_base: 0,
80 }
81 }
82
83 /// Creates an empty map with room for `capacity` sources preallocated.
84 ///
85 /// A hint only: it sizes the internal list so that adding up to `capacity`
86 /// sources does not reallocate, which matters when the source count is known
87 /// up front. The global position space still starts empty.
88 ///
89 /// # Examples
90 ///
91 /// ```
92 /// use source_lang::SourceMap;
93 ///
94 /// let mut map = SourceMap::with_capacity(2);
95 /// map.add("a", "x").expect("fits");
96 /// map.add("b", "y").expect("fits");
97 /// assert_eq!(map.len(), 2);
98 /// ```
99 #[inline]
100 #[must_use]
101 pub fn with_capacity(capacity: usize) -> Self {
102 Self {
103 files: Vec::with_capacity(capacity),
104 next_base: 0,
105 }
106 }
107
108 /// Adds a source under `name` with the given `text`, returning its
109 /// [`SourceId`].
110 ///
111 /// The source is appended after every existing one: it takes the range
112 /// `next..next + text.len()` where `next` is the current end of the global
113 /// space. Both `name` and `text` are taken by value (anything that converts
114 /// into a `Box<str>` — a `String` or a `&str`), so the map owns the text and
115 /// callers can borrow it back for the life of the map.
116 ///
117 /// Adding an empty `text` is allowed: it yields a valid id whose source has a
118 /// zero-width span and does not advance the global space, so it can never be
119 /// the target of a [`locate`](SourceMap::locate).
120 ///
121 /// # Errors
122 ///
123 /// Returns [`SourceMapError::SpaceExhausted`] if `text` does not fit in the
124 /// bytes left in the 32-bit global space, or if the map already holds the
125 /// maximum number of sources. The map is left unchanged, so the failure is
126 /// recoverable.
127 ///
128 /// # Examples
129 ///
130 /// ```
131 /// use source_lang::SourceMap;
132 ///
133 /// let mut map = SourceMap::new();
134 /// let id = map.add("config.toml", "name = \"demo\"").expect("fits");
135 /// assert_eq!(map.source(id).unwrap().text(), "name = \"demo\"");
136 ///
137 /// // A String works just as well as a &str.
138 /// let owned = String::from("generated");
139 /// let _ = map.add("out.txt", owned).expect("fits");
140 /// ```
141 pub fn add(
142 &mut self,
143 name: impl Into<Box<str>>,
144 text: impl Into<Box<str>>,
145 ) -> Result<SourceId, SourceMapError> {
146 let text = text.into();
147 let needed = text.len() as u64;
148 let base = self.next_base;
149 // `base` never exceeds `u32::MAX`, so this is the bytes still free.
150 let available = u64::from(u32::MAX - base);
151
152 // The source must fit in the remaining bytes, and the map must have an
153 // unused id left to mint. Both are checked before anything is mutated.
154 let index = u32::try_from(self.files.len());
155 let (len, index) = match (needed <= available, index) {
156 // `needed <= available <= u32::MAX`, so the narrowing is lossless.
157 (true, Ok(index)) => (needed as u32, index),
158 _ => return Err(SourceMapError::SpaceExhausted { needed, available }),
159 };
160
161 // `base + len <= base + available == u32::MAX`, so this cannot overflow.
162 let end = base + len;
163 let span = Span::new(base, end);
164 let id = SourceId::from_index(index);
165 self.files.push(SourceFile::new(name.into(), text, span));
166 self.next_base = end;
167 Ok(id)
168 }
169
170 /// Resolves a global position to the source it falls in and the local offset
171 /// within that source.
172 ///
173 /// The returned [`BytePos`] is `pos` minus the source's base, i.e. the offset
174 /// into [`SourceFile::text`]. Resolution is a binary search over the sources'
175 /// start offsets, so it is `O(log files)` and borrows the located source
176 /// rather than copying it.
177 ///
178 /// Returns `None` when `pos` belongs to no source: past the end of the last
179 /// one, or — since a zero-width source contains no position — at the exact
180 /// offset of an empty source. The membership is half-open: a source covering
181 /// `start..end` contains `start` but not `end`, so the boundary between two
182 /// adjacent sources resolves to the second, never to both.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// use source_lang::{BytePos, SourceMap};
188 ///
189 /// let mut map = SourceMap::new();
190 /// let a = map.add("a", "abc").expect("fits"); // 0..3
191 /// let b = map.add("b", "de").expect("fits"); // 3..5
192 ///
193 /// assert_eq!(map.locate(BytePos::new(2)), Some((a, BytePos::new(2))));
194 /// // The shared boundary at 3 is the start of `b`, not the end of `a`.
195 /// assert_eq!(map.locate(BytePos::new(3)), Some((b, BytePos::new(0))));
196 /// assert_eq!(map.locate(BytePos::new(5)), None);
197 /// ```
198 #[must_use]
199 pub fn locate(&self, pos: BytePos) -> Option<(SourceId, BytePos)> {
200 let at = pos.to_u32();
201
202 // The list is sorted by start offset, so the last source whose range
203 // begins at or before `at` is the only one that can contain it.
204 let after = self
205 .files
206 .partition_point(|f| f.span().start().to_u32() <= at);
207 let index = after.checked_sub(1)?;
208 let file = &self.files[index];
209
210 if file.span().contains(pos) {
211 let local = at - file.span().start().to_u32();
212 // `index < files.len() <= u32::MAX`, so the cast is lossless.
213 Some((SourceId::from_index(index as u32), BytePos::new(local)))
214 } else {
215 None
216 }
217 }
218
219 /// Borrows the source named by `id`, or `None` if the id is not from this map.
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// use source_lang::SourceMap;
225 ///
226 /// let mut map = SourceMap::new();
227 /// let id = map.add("readme.md", "# title").expect("fits");
228 /// assert_eq!(map.source(id).unwrap().name(), "readme.md");
229 /// ```
230 #[inline]
231 #[must_use]
232 pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
233 self.files.get(id.to_u32() as usize)
234 }
235
236 /// Returns the number of sources in the map.
237 ///
238 /// # Examples
239 ///
240 /// ```
241 /// use source_lang::SourceMap;
242 ///
243 /// let mut map = SourceMap::new();
244 /// assert_eq!(map.len(), 0);
245 /// map.add("a", "x").expect("fits");
246 /// assert_eq!(map.len(), 1);
247 /// ```
248 #[inline]
249 #[must_use]
250 pub fn len(&self) -> usize {
251 self.files.len()
252 }
253
254 /// Returns `true` if the map holds no sources.
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// use source_lang::SourceMap;
260 ///
261 /// let mut map = SourceMap::new();
262 /// assert!(map.is_empty());
263 /// map.add("a", "x").expect("fits");
264 /// assert!(!map.is_empty());
265 /// ```
266 #[inline]
267 #[must_use]
268 pub fn is_empty(&self) -> bool {
269 self.files.is_empty()
270 }
271
272 /// Iterates over the sources in insertion order, pairing each with its id.
273 ///
274 /// The order is also id order (`0`, `1`, …) and global-offset order, so the
275 /// iterator walks the global position space from start to end. Useful for
276 /// listing the loaded files or building a side table keyed by `SourceId`.
277 ///
278 /// # Examples
279 ///
280 /// ```
281 /// use source_lang::SourceMap;
282 ///
283 /// let mut map = SourceMap::new();
284 /// map.add("a.txt", "one").expect("fits");
285 /// map.add("b.txt", "two").expect("fits");
286 ///
287 /// let names: Vec<_> = map.iter().map(|(_, f)| f.name()).collect();
288 /// assert_eq!(names, ["a.txt", "b.txt"]);
289 /// ```
290 pub fn iter(&self) -> impl ExactSizeIterator<Item = (SourceId, &SourceFile)> + '_ {
291 self.files
292 .iter()
293 .enumerate()
294 // `i < files.len() <= u32::MAX`, so the cast is lossless.
295 .map(|(i, file)| (SourceId::from_index(i as u32), file))
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 extern crate alloc;
302 use alloc::format;
303 use alloc::vec::Vec;
304
305 use super::*;
306
307 #[test]
308 fn test_add_assigns_sequential_stable_ids() {
309 let mut map = SourceMap::new();
310 let a = map.add("a", "x").expect("fits");
311 let b = map.add("b", "yy").expect("fits");
312 let c = map.add("c", "zzz").expect("fits");
313 assert_eq!((a.to_u32(), b.to_u32(), c.to_u32()), (0, 1, 2));
314 // Earlier ids still resolve to their original source after later adds.
315 assert_eq!(map.source(a).unwrap().name(), "a");
316 assert_eq!(map.source(b).unwrap().name(), "b");
317 }
318
319 #[test]
320 fn test_layout_is_contiguous_and_non_overlapping() {
321 let mut map = SourceMap::new();
322 map.add("a", "abc").expect("fits"); // 0..3
323 map.add("b", "de").expect("fits"); // 3..5
324 map.add("c", "fghi").expect("fits"); // 5..9
325 let spans: Vec<_> = map.iter().map(|(_, f)| f.span()).collect();
326 assert_eq!(spans[0], Span::new(0, 3));
327 assert_eq!(spans[1], Span::new(3, 5));
328 assert_eq!(spans[2], Span::new(5, 9));
329 }
330
331 #[test]
332 fn test_locate_at_boundaries_zero_one_and_past_end() {
333 let mut map = SourceMap::new();
334 let a = map.add("a", "abc").expect("fits"); // 0..3
335 let b = map.add("b", "de").expect("fits"); // 3..5
336 assert_eq!(map.locate(BytePos::new(0)), Some((a, BytePos::new(0))));
337 assert_eq!(map.locate(BytePos::new(2)), Some((a, BytePos::new(2))));
338 // The boundary belongs to the second file.
339 assert_eq!(map.locate(BytePos::new(3)), Some((b, BytePos::new(0))));
340 assert_eq!(map.locate(BytePos::new(4)), Some((b, BytePos::new(1))));
341 // End of the space and beyond are unmapped.
342 assert_eq!(map.locate(BytePos::new(5)), None);
343 assert_eq!(map.locate(BytePos::new(6)), None);
344 }
345
346 #[test]
347 fn test_locate_on_empty_map_is_none() {
348 let map = SourceMap::new();
349 assert_eq!(map.locate(BytePos::new(0)), None);
350 }
351
352 #[test]
353 fn test_empty_source_does_not_advance_space_and_is_unlocatable() {
354 let mut map = SourceMap::new();
355 let a = map.add("a", "ab").expect("fits"); // 0..2
356 let empty = map.add("empty", "").expect("fits"); // 2..2
357 let b = map.add("b", "cd").expect("fits"); // 2..4
358
359 assert!(map.source(empty).unwrap().span().is_empty());
360 // Position 2 is the start of `b`, never the zero-width `empty`.
361 assert_eq!(map.locate(BytePos::new(2)), Some((b, BytePos::new(0))));
362 assert_eq!(map.locate(BytePos::new(1)), Some((a, BytePos::new(1))));
363 }
364
365 #[test]
366 fn test_source_rejects_foreign_id() {
367 let mut map = SourceMap::new();
368 let _ = map.add("a", "x").expect("fits");
369 let mut other = SourceMap::new();
370 let foreign = other.add("b", "y").expect("fits");
371 // `foreign` has index 0, which exists here too, so cross-map ids are not
372 // distinguishable by value — but an out-of-range index is rejected.
373 let beyond = other.add("c", "z").expect("fits");
374 assert!(map.source(beyond).is_none());
375 assert!(map.source(foreign).is_some());
376 }
377
378 #[test]
379 fn test_add_at_space_boundary_accepts_exact_fit() {
380 let mut map = SourceMap::new();
381 // Drive the high-water mark to four bytes below the ceiling without
382 // allocating gigabytes; the field is private to this module's tests.
383 map.next_base = u32::MAX - 4;
384 let id = map.add("edge", "abcd").expect("exactly fills the space");
385 assert_eq!(
386 map.source(id).unwrap().span(),
387 Span::new(u32::MAX - 4, u32::MAX)
388 );
389 assert_eq!(map.next_base, u32::MAX);
390 }
391
392 #[test]
393 fn test_add_past_space_boundary_is_rejected() {
394 let mut map = SourceMap::new();
395 map.next_base = u32::MAX - 4;
396 let err = map.add("edge", "abcde").expect_err("one byte too many");
397 assert_eq!(
398 err,
399 SourceMapError::SpaceExhausted {
400 needed: 5,
401 available: 4,
402 },
403 );
404 // The map is unchanged after a rejected add.
405 assert!(map.is_empty());
406 assert_eq!(map.next_base, u32::MAX - 4);
407 }
408
409 #[test]
410 fn test_add_empty_source_at_full_space_still_succeeds() {
411 let mut map = SourceMap::new();
412 map.next_base = u32::MAX;
413 // Zero bytes fit even when no space remains; one byte does not.
414 let empty = map.add("nothing", "").expect("zero bytes always fit");
415 assert!(map.source(empty).unwrap().span().is_empty());
416 assert!(map.add("one", "x").is_err());
417 }
418
419 #[test]
420 fn test_iter_reports_exact_len_and_pairs_ids_in_order() {
421 let mut map = SourceMap::new();
422 for i in 0..5 {
423 map.add(format!("f{i}"), "..").expect("fits");
424 }
425 let mut iter = map.iter();
426 assert_eq!(iter.len(), 5);
427 let collected: Vec<_> = iter.by_ref().map(|(id, _)| id.to_u32()).collect();
428 assert_eq!(collected, [0, 1, 2, 3, 4]);
429 }
430}