Skip to main content

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}