Skip to main content

rucc_diag/
source.rs

1//! The source map: which file a [`BytePos`] lands in, and where in that file.
2//!
3//! Design: `spec/03-architecture.md`, and `spec/05-preprocessor.md` section 5.2 for the
4//! coordinate space a token span lives in.
5//!
6//! Every file in a translation unit gets a range of one flat coordinate space, so a [`Span`]
7//! is two integers and comparing two of them does not need to know which file each came from.
8//! That is what keeps a token at sixteen bytes with a header nest twelve deep, and it is why
9//! the expander can join the span of a macro argument with the span of the call site without
10//! a special case.
11//!
12//! The price is that turning an offset back into a file, a line and a column is a search
13//! rather than a field read. It is paid only when a diagnostic is rendered, which happens for
14//! a handful of positions out of the millions the lexer produces, so the line table for a
15//! file is built the first time somebody asks about that file and never for a file nobody
16//! asks about.
17//!
18//! ```
19//! use rucc_diag::SourceMap;
20//!
21//! let mut map = SourceMap::new();
22//! let file = map.add("hello.c", b"int main(void)\n{\n    return 0;\n}\n".to_vec()).unwrap();
23//! let brace = map.file(file).start + 15;
24//! let loc = map.lookup(brace).unwrap();
25//! assert_eq!(loc.line, 2);
26//! assert_eq!(loc.column, 1);
27//! assert_eq!(map.render_position(brace), "hello.c:2:1");
28//! ```
29
30use std::fmt;
31use std::sync::{Arc, OnceLock};
32
33use crate::{BytePos, Span};
34
35/// The contents of a file, shared rather than copied.
36///
37/// A trait object rather than a `Vec`, so that the memory mapped input in
38/// `spec/05-preprocessor.md` section 5.2 can be handed over as it is, and shared so that a
39/// header included twice, or served twice out of the header cache, is held once.
40///
41/// It is a type of its own rather than a bare `Arc` so that it can have a `Debug` that says
42/// how long a file is instead of printing it. A `{:#?}` of anything holding one of these
43/// should not dump the whole of `stdio.h` into a test failure.
44#[derive(Clone)]
45pub struct SourceBytes(Arc<dyn AsRef<[u8]> + Send + Sync>);
46
47impl SourceBytes {
48    /// Takes ownership of anything that is a slice of bytes.
49    pub fn new(bytes: impl AsRef<[u8]> + Send + Sync + 'static) -> SourceBytes {
50        SourceBytes(Arc::new(bytes))
51    }
52
53    /// The bytes.
54    #[inline]
55    pub fn as_slice(&self) -> &[u8] {
56        (*self.0).as_ref()
57    }
58}
59
60impl fmt::Debug for SourceBytes {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "SourceBytes({} bytes)", self.as_slice().len())
63    }
64}
65
66impl AsRef<[u8]> for SourceBytes {
67    #[inline]
68    fn as_ref(&self) -> &[u8] {
69        self.as_slice()
70    }
71}
72
73impl std::ops::Deref for SourceBytes {
74    type Target = [u8];
75
76    #[inline]
77    fn deref(&self) -> &[u8] {
78        self.as_slice()
79    }
80}
81
82/// A file in the source map.
83///
84/// Only meaningful against the map that issued it. A `FileId` is an index, so passing one to
85/// a different map is a bug the type system does not catch, which is fine because there is
86/// one map per compilation and it lives on the session.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
88pub struct FileId(u32);
89
90impl FileId {
91    /// The index of this file in the map, for a caller keeping a side table.
92    #[inline]
93    pub const fn index(self) -> usize {
94        self.0 as usize
95    }
96}
97
98/// A position resolved back to a human coordinate.
99///
100/// Lines and columns both count from one, because that is what every editor, every other
101/// compiler and every user expects, and an off by one here is the kind of bug that survives
102/// for years because nobody quite trusts their own arithmetic enough to file it.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct Loc {
105    /// Which file.
106    pub file: FileId,
107    /// The line, counting from one.
108    pub line: u32,
109    /// The column in bytes, counting from one.
110    ///
111    /// Bytes rather than characters or display columns. A tab counts as one and a multibyte
112    /// character counts as its encoded length, which is what a caret line drawn from the same
113    /// bytes needs. `-ftabstop` and the width of a CJK character are the renderer's problem;
114    /// what belongs here is the offset into the line.
115    pub column: u32,
116}
117
118/// The bytes of one file, plus where they sit in the flat space.
119///
120/// Contents are held behind a trait object rather than as a `Vec`, so that the memory mapped
121/// input in `spec/05-preprocessor.md` section 5.2 can be handed over as it is instead of
122/// being copied into one. Reading the bytes goes through one virtual call, which is fine
123/// because it happens once per file in the lexer and once per rendered diagnostic, never in
124/// a loop.
125pub struct SourceFile {
126    /// This file's own id, so that anything holding a `&SourceFile` can name it.
127    pub id: FileId,
128    /// The name to print in a diagnostic, which is the path as the user wrote it rather than
129    /// a canonical one. Somebody who typed `-I../include` wants to read `../include/foo.h`.
130    pub name: String,
131    /// First byte of this file in the flat space.
132    pub start: BytePos,
133    /// One past this file's last byte.
134    pub end: BytePos,
135    /// The `#include` that pulled this file in, or `None` for a file named on the command
136    /// line. This is what "in file included from" is printed from.
137    pub included_from: Option<Span>,
138    bytes: SourceBytes,
139    /// Absolute offset of the first byte of each line. Built on first use, because most files
140    /// in a build are never the subject of a diagnostic.
141    lines: OnceLock<Vec<BytePos>>,
142}
143
144impl fmt::Debug for SourceFile {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        // The bytes are deliberately not printed. A `{:#?}` of a session should not dump the
147        // whole of `stdio.h` into a test failure.
148        f.debug_struct("SourceFile")
149            .field("id", &self.id)
150            .field("name", &self.name)
151            .field("start", &self.start)
152            .field("end", &self.end)
153            .field("included_from", &self.included_from)
154            .finish()
155    }
156}
157
158impl SourceFile {
159    /// The file's contents.
160    #[inline]
161    pub fn bytes(&self) -> &[u8] {
162        self.bytes.as_slice()
163    }
164
165    /// The file's contents, shared.
166    #[inline]
167    pub fn shared_bytes(&self) -> SourceBytes {
168        self.bytes.clone()
169    }
170
171    /// Length in bytes.
172    #[inline]
173    pub fn len(&self) -> u32 {
174        self.end - self.start
175    }
176
177    /// Whether the file is empty.
178    #[inline]
179    pub fn is_empty(&self) -> bool {
180        self.start == self.end
181    }
182
183    /// Whether `pos` falls in this file.
184    ///
185    /// The end position is included, so a diagnostic about something missing at the end of a
186    /// file still names the file rather than falling into the gap after it.
187    #[inline]
188    pub fn contains(&self, pos: BytePos) -> bool {
189        self.start <= pos && pos <= self.end
190    }
191
192    /// How many lines the file has, counting a trailing newline as ending the last line
193    /// rather than starting another. An empty file has one line, which is empty.
194    pub fn line_count(&self) -> u32 {
195        u32::try_from(self.lines().len()).unwrap_or(u32::MAX)
196    }
197
198    /// The bytes of line `line`, counting from one, without its line terminator.
199    ///
200    /// `None` if the file has no such line.
201    pub fn line_bytes(&self, line: u32) -> Option<&[u8]> {
202        let lines = self.lines();
203        let index = usize::try_from(line.checked_sub(1)?).ok()?;
204        let from = *lines.get(index)? - self.start;
205        let to = lines.get(index + 1).map_or(self.len(), |next| *next - self.start);
206        let text = self.bytes().get(from as usize..to as usize)?;
207        // Strip the terminator rather than the last byte, so that a file with CRLF endings
208        // does not put a carriage return in the middle of a rendered caret line.
209        let text = text.strip_suffix(b"\n").unwrap_or(text);
210        Some(text.strip_suffix(b"\r").unwrap_or(text))
211    }
212
213    /// The line and column of `pos`, or `None` if `pos` is not in this file.
214    pub fn position(&self, pos: BytePos) -> Option<Loc> {
215        let (line, begin) = self.line_of(pos)?;
216        Some(Loc { file: self.id, line, column: pos - begin + 1 })
217    }
218
219    /// The span covering the line `pos` is on, including its terminator.
220    pub fn line_span(&self, pos: BytePos) -> Option<Span> {
221        let (line, begin) = self.line_of(pos)?;
222        let end = self.lines().get(line as usize).copied().unwrap_or(self.end);
223        Some(Span::new(begin, end))
224    }
225
226    /// The one-based line `pos` is on, and where that line starts.
227    fn line_of(&self, pos: BytePos) -> Option<(u32, BytePos)> {
228        if !self.contains(pos) {
229            return None;
230        }
231        let lines = self.lines();
232        // `partition_point` gives the number of line starts at or before `pos`, which is the
233        // one-based line number, and is never zero because the first entry is the file start.
234        let line = lines.partition_point(|&start| start <= pos);
235        let begin = lines.get(line.saturating_sub(1)).copied().unwrap_or(self.start);
236        Some((u32::try_from(line).unwrap_or(u32::MAX), begin))
237    }
238
239    /// The line start table, built on first use.
240    fn lines(&self) -> &[BytePos] {
241        self.lines.get_or_init(|| {
242            let bytes = self.bytes();
243            // Twenty four bytes a line is roughly what C source averages. Getting this wrong
244            // costs a reallocation, not a correctness problem.
245            let mut starts = Vec::with_capacity(bytes.len() / 24 + 1);
246            starts.push(self.start);
247            for (at, _) in bytes.iter().enumerate().filter(|&(_, &b)| b == b'\n') {
248                let next = self.start + u32::try_from(at).unwrap_or(u32::MAX - 1) + 1;
249                // A newline as the very last byte ends the last line, it does not open an
250                // empty one. Every other newline opens a line, including one followed
251                // immediately by another newline.
252                if next < self.end {
253                    starts.push(next);
254                }
255            }
256            starts
257        })
258    }
259}
260
261/// The flat coordinate space is full.
262///
263/// Reaching this needs four gigabytes of source in one translation unit, counting every
264/// header once per time it is included. It is reported rather than ignored because the
265/// alternative is spans that silently point at the wrong file.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct SourceMapFull;
268
269impl fmt::Display for SourceMapFull {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        f.write_str("the translation unit does not fit in the four gigabyte source map")
272    }
273}
274
275impl std::error::Error for SourceMapFull {}
276
277/// Every file of one translation unit, laid end to end.
278#[derive(Debug, Default)]
279pub struct SourceMap {
280    files: Vec<SourceFile>,
281    next: BytePos,
282}
283
284impl SourceMap {
285    /// An empty map.
286    pub fn new() -> SourceMap {
287        SourceMap::default()
288    }
289
290    /// Adds a file named on the command line.
291    ///
292    /// # Errors
293    ///
294    /// [`SourceMapFull`] if the file does not fit in what is left of the coordinate space.
295    pub fn add(
296        &mut self,
297        name: impl Into<String>,
298        bytes: impl AsRef<[u8]> + Send + Sync + 'static,
299    ) -> Result<FileId, SourceMapFull> {
300        self.push(name.into(), SourceBytes::new(bytes), None)
301    }
302
303    /// Adds a file whose contents are already shared.
304    ///
305    /// This is the entry point the file system abstraction uses, because it hands out bytes
306    /// it may also be holding in a cache.
307    ///
308    /// # Errors
309    ///
310    /// [`SourceMapFull`] if the file does not fit in what is left of the coordinate space.
311    pub fn add_shared(
312        &mut self,
313        name: impl Into<String>,
314        bytes: SourceBytes,
315        included_from: Option<Span>,
316    ) -> Result<FileId, SourceMapFull> {
317        self.push(name.into(), bytes, included_from)
318    }
319
320    /// Adds a file reached through the `#include` at `from`.
321    ///
322    /// # Errors
323    ///
324    /// [`SourceMapFull`] if the file does not fit in what is left of the coordinate space.
325    pub fn add_included(
326        &mut self,
327        name: impl Into<String>,
328        bytes: impl AsRef<[u8]> + Send + Sync + 'static,
329        from: Span,
330    ) -> Result<FileId, SourceMapFull> {
331        self.push(name.into(), SourceBytes::new(bytes), Some(from))
332    }
333
334    fn push(
335        &mut self,
336        name: String,
337        bytes: SourceBytes,
338        included_from: Option<Span>,
339    ) -> Result<FileId, SourceMapFull> {
340        let len = u32::try_from(bytes.as_slice().len()).map_err(|_| SourceMapFull)?;
341        let start = self.next;
342        let end = start.checked_add(len).ok_or(SourceMapFull)?;
343        // One byte of padding after every file, so that the position one past the end of a
344        // file is still that file's and not the first byte of the next one. Without it a
345        // diagnostic about a missing `}` at the end of a header names whatever came after it.
346        // `BytePos::MAX` is `Span::DUMMY` and belongs to nobody, so the space stops one short.
347        self.next = end.checked_add(1).filter(|&n| n < BytePos::MAX).ok_or(SourceMapFull)?;
348        let id = FileId(u32::try_from(self.files.len()).map_err(|_| SourceMapFull)?);
349        self.files.push(SourceFile {
350            id,
351            name,
352            start,
353            end,
354            included_from,
355            bytes,
356            lines: OnceLock::new(),
357        });
358        Ok(id)
359    }
360
361    /// Every file, in the order they were added.
362    pub fn files(&self) -> &[SourceFile] {
363        &self.files
364    }
365
366    /// The file `id` names.
367    ///
368    /// # Panics
369    ///
370    /// Panics if `id` came from a different map. There is one map per compilation, on the
371    /// session, so this is a programming error rather than something a caller handles.
372    pub fn file(&self, id: FileId) -> &SourceFile {
373        &self.files[id.index()]
374    }
375
376    /// Which file `pos` is in.
377    pub fn lookup_file(&self, pos: BytePos) -> Option<FileId> {
378        if pos == BytePos::MAX {
379            return None;
380        }
381        // Files are laid out in increasing order and never overlap, so the candidate is the
382        // last one starting at or before `pos`. It is a candidate rather than the answer
383        // because `pos` may be in the padding byte after that file.
384        let at = self.files.partition_point(|f| f.start <= pos);
385        let file = self.files.get(at.checked_sub(1)?)?;
386        file.contains(pos).then_some(file.id)
387    }
388
389    /// The file, line and column of `pos`.
390    pub fn lookup(&self, pos: BytePos) -> Option<Loc> {
391        self.file(self.lookup_file(pos)?).position(pos)
392    }
393
394    /// `name:line:column` for `pos`, or `<unknown>` for a position in no file.
395    ///
396    /// This is the prefix of a rendered diagnostic and the form every editor already knows
397    /// how to jump to.
398    pub fn render_position(&self, pos: BytePos) -> String {
399        match self.lookup(pos) {
400            Some(loc) => format!("{}:{}:{}", self.file(loc.file).name, loc.line, loc.column),
401            None => "<unknown>".to_owned(),
402        }
403    }
404
405    /// The chain of `#include` directives that led to `pos`, innermost first.
406    ///
407    /// Empty for a position in a file named on the command line. This is what the "in file
408    /// included from" block of a diagnostic is printed from, and reading it out of the map
409    /// rather than out of a stack the preprocessor keeps means it is still available long
410    /// after preprocessing has finished.
411    pub fn include_stack(&self, pos: BytePos) -> Vec<Span> {
412        let mut stack = Vec::new();
413        let mut at = self.lookup_file(pos);
414        while let Some(file) = at {
415            let Some(from) = self.file(file).included_from else { break };
416            stack.push(from);
417            at = self.lookup_file(from.lo);
418            // A file is always added after the one that includes it, so the walk terminates.
419            // A map built by hand in a test could say otherwise, and an infinite loop inside
420            // the diagnostic renderer is a bad way to find that out.
421            if stack.len() > self.files.len() {
422                break;
423            }
424        }
425        stack
426    }
427
428    /// How much of the coordinate space is used, which is where the next file will start.
429    pub fn used(&self) -> BytePos {
430        self.next
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    fn map_with(files: &[(&str, &str)]) -> (SourceMap, Vec<FileId>) {
439        let mut map = SourceMap::new();
440        let ids = files
441            .iter()
442            .map(|(name, text)| map.add(*name, text.as_bytes().to_vec()).unwrap())
443            .collect();
444        (map, ids)
445    }
446
447    #[test]
448    fn the_first_file_starts_at_zero_and_the_next_one_after_a_gap() {
449        let (map, ids) = map_with(&[("a.c", "ab"), ("b.c", "cd")]);
450        assert_eq!(map.file(ids[0]).start, 0);
451        assert_eq!(map.file(ids[0]).end, 2);
452        assert_eq!(map.file(ids[1]).start, 3);
453        assert_eq!(map.used(), 6);
454    }
455
456    #[test]
457    fn the_position_after_a_file_belongs_to_that_file_and_not_the_next() {
458        let (map, ids) = map_with(&[("a.c", "ab"), ("b.c", "cd")]);
459        assert_eq!(map.lookup_file(2), Some(ids[0]));
460        assert_eq!(map.lookup_file(3), Some(ids[1]));
461    }
462
463    #[test]
464    fn a_position_in_the_gap_is_in_no_file() {
465        let mut map = SourceMap::new();
466        map.add("a.c", b"ab".to_vec()).unwrap();
467        // Offset 2 is the end of `a.c`, and offset 3 would be the next file, which does not
468        // exist, so nothing is there.
469        assert_eq!(map.lookup_file(3), None);
470        assert_eq!(map.render_position(3), "<unknown>");
471    }
472
473    #[test]
474    fn a_dummy_span_resolves_to_nothing() {
475        let (map, _) = map_with(&[("a.c", "ab")]);
476        assert_eq!(map.lookup(Span::DUMMY.lo), None);
477        assert_eq!(map.lookup_file(BytePos::MAX), None);
478    }
479
480    #[test]
481    fn lines_and_columns_count_from_one() {
482        let (map, ids) = map_with(&[("a.c", "one\ntwo\nthree\n")]);
483        let start = map.file(ids[0]).start;
484        assert_eq!(map.lookup(start).unwrap(), Loc { file: ids[0], line: 1, column: 1 });
485        assert_eq!(map.lookup(start + 4).unwrap(), Loc { file: ids[0], line: 2, column: 1 });
486        assert_eq!(map.lookup(start + 6).unwrap(), Loc { file: ids[0], line: 2, column: 3 });
487        assert_eq!(map.render_position(start + 8), "a.c:3:1");
488    }
489
490    #[test]
491    fn a_trailing_newline_does_not_open_a_line() {
492        let (map, ids) = map_with(&[("a.c", "one\ntwo\n"), ("b.c", "one\ntwo")]);
493        assert_eq!(map.file(ids[0]).line_count(), 2);
494        assert_eq!(map.file(ids[1]).line_count(), 2);
495    }
496
497    #[test]
498    fn a_blank_line_is_a_line() {
499        let (map, ids) = map_with(&[("a.c", "one\n\nthree\n")]);
500        let file = map.file(ids[0]);
501        assert_eq!(file.line_count(), 3);
502        assert_eq!(file.line_bytes(2), Some(&b""[..]));
503        assert_eq!(file.line_bytes(3), Some(&b"three"[..]));
504        assert_eq!(file.line_bytes(4), None);
505        assert_eq!(file.line_bytes(0), None);
506    }
507
508    #[test]
509    fn a_carriage_return_is_not_part_of_the_line() {
510        let (map, ids) = map_with(&[("a.c", "one\r\ntwo\r\n")]);
511        let file = map.file(ids[0]);
512        assert_eq!(file.line_bytes(1), Some(&b"one"[..]));
513        assert_eq!(file.line_bytes(2), Some(&b"two"[..]));
514    }
515
516    #[test]
517    fn an_empty_file_has_one_position_and_no_lines_to_read() {
518        let (map, ids) = map_with(&[("a.c", "")]);
519        let file = map.file(ids[0]);
520        assert!(file.is_empty());
521        assert_eq!(map.lookup(file.start).unwrap().line, 1);
522        assert_eq!(file.line_bytes(1), Some(&b""[..]));
523        assert_eq!(file.line_bytes(2), None);
524    }
525
526    #[test]
527    fn a_line_span_covers_the_terminator() {
528        let (map, ids) = map_with(&[("a.c", "one\ntwo\n")]);
529        let file = map.file(ids[0]);
530        assert_eq!(file.line_span(file.start + 1), Some(Span::new(0, 4)));
531        assert_eq!(file.line_span(file.start + 5), Some(Span::new(4, 8)));
532    }
533
534    #[test]
535    fn the_include_stack_runs_from_the_innermost_out() {
536        let mut map = SourceMap::new();
537        let main = map.add("main.c", b"#include <a.h>\n".to_vec()).unwrap();
538        let outer = Span::new(map.file(main).start, map.file(main).start + 14);
539        let a = map.add_included("a.h", b"#include <b.h>\n".to_vec(), outer).unwrap();
540        let inner = Span::new(map.file(a).start, map.file(a).start + 14);
541        let b = map.add_included("b.h", b"int x;\n".to_vec(), inner).unwrap();
542        let stack = map.include_stack(map.file(b).start);
543        assert_eq!(stack, vec![inner, outer]);
544        assert_eq!(map.lookup(stack[0].lo).unwrap().file, a);
545        assert_eq!(map.lookup(stack[1].lo).unwrap().file, main);
546        assert!(map.include_stack(outer.lo).is_empty());
547    }
548
549    #[test]
550    fn a_file_that_does_not_fit_is_refused_rather_than_wrapped() {
551        let mut map = SourceMap::new();
552        map.add("a.c", b"x".to_vec()).unwrap();
553        // The map is then asked for everything that is left plus the padding it always adds,
554        // which is one byte more than the space holds.
555        map.next = BytePos::MAX - 2;
556        assert_eq!(map.add("b.c", b"xx".to_vec()), Err(SourceMapFull));
557        assert_eq!(map.files().len(), 1);
558    }
559
560    #[test]
561    fn contents_can_be_anything_that_is_a_slice_of_bytes() {
562        // What a memory mapped file will look like when it arrives: not a `Vec`, not a
563        // `String`, just something that hands out a slice.
564        struct Mapped(&'static [u8]);
565        impl AsRef<[u8]> for Mapped {
566            fn as_ref(&self) -> &[u8] {
567                self.0
568            }
569        }
570        let mut map = SourceMap::new();
571        let id = map.add("a.c", Mapped(b"int x;\n")).unwrap();
572        assert_eq!(map.file(id).bytes(), b"int x;\n");
573        assert_eq!(map.file(id).line_count(), 1);
574    }
575}