Skip to main content

dune_netlist/
span.rs

1//! Source positions for diagnostics.
2//!
3//! A deck may pull in other files through `.include`, so a [`Span`]
4//! names a file (by [`FileId`] into the [`SourceMap`]) plus a byte
5//! range in that file's text. Line/column resolution is done lazily,
6//! only when a diagnostic is rendered.
7
8use std::path::{Path, PathBuf};
9
10/// Index of a file inside a [`SourceMap`].
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct FileId(pub(crate) u32);
13
14/// A byte range inside one source file.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct Span {
17    pub file: FileId,
18    pub start: u32,
19    pub end: u32,
20}
21
22impl Span {
23    pub(crate) fn new(file: FileId, start: usize, end: usize) -> Self {
24        // Files larger than 4 GiB are rejected at load time, so the
25        // casts cannot truncate.
26        Span {
27            file,
28            start: start as u32,
29            end: end as u32,
30        }
31    }
32
33    /// A span covering both `self` and `other` when they are in the
34    /// same file; otherwise `self` (good enough for diagnostics).
35    pub fn to(self, other: Span) -> Span {
36        if self.file == other.file {
37            Span {
38                file: self.file,
39                start: self.start.min(other.start),
40                end: self.end.max(other.end),
41            }
42        } else {
43            self
44        }
45    }
46}
47
48/// One loaded source file.
49#[derive(Debug)]
50pub struct SourceFile {
51    pub name: PathBuf,
52    pub text: String,
53    /// Byte offset of the start of each line, computed on demand.
54    line_starts: std::cell::OnceCell<Vec<u32>>,
55}
56
57/// All files that took part in a parse (root deck + includes).
58#[derive(Debug, Default)]
59pub struct SourceMap {
60    files: Vec<SourceFile>,
61}
62
63/// A resolved human-readable position.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct Location {
66    pub file: PathBuf,
67    /// 1-based.
68    pub line: u32,
69    /// 1-based byte column.
70    pub column: u32,
71}
72
73impl SourceMap {
74    pub fn new() -> Self {
75        SourceMap::default()
76    }
77
78    /// Register a file's text; its bytes are addressed by the returned
79    /// [`FileId`]. Fails for texts of 4 GiB or more (spans are `u32`).
80    pub(crate) fn add(&mut self, name: impl Into<PathBuf>, text: String) -> Option<FileId> {
81        if text.len() >= u32::MAX as usize || self.files.len() >= u32::MAX as usize {
82            return None;
83        }
84        let id = FileId(self.files.len() as u32);
85        self.files.push(SourceFile {
86            name: name.into(),
87            text,
88            line_starts: std::cell::OnceCell::new(),
89        });
90        Some(id)
91    }
92
93    pub fn file(&self, id: FileId) -> &SourceFile {
94        &self.files[id.0 as usize]
95    }
96
97    /// The source text a span points at.
98    pub fn snippet(&self, span: Span) -> &str {
99        let text = &self.file(span.file).text;
100        text.get(span.start as usize..span.end as usize)
101            .unwrap_or("")
102    }
103
104    /// Resolve a span's start to file/line/column.
105    pub fn locate(&self, span: Span) -> Location {
106        let file = self.file(span.file);
107        let starts = file.line_starts.get_or_init(|| {
108            let mut v = vec![0u32];
109            for (i, b) in file.text.bytes().enumerate() {
110                if b == b'\n' {
111                    v.push(i as u32 + 1);
112                }
113            }
114            v
115        });
116        let line = match starts.binary_search(&span.start) {
117            Ok(i) => i,
118            Err(i) => i - 1,
119        };
120        Location {
121            file: file.name.clone(),
122            line: line as u32 + 1,
123            column: span.start - starts[line] + 1,
124        }
125    }
126}
127
128impl SourceFile {
129    pub fn path(&self) -> &Path {
130        &self.name
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn locate_lines_and_columns() {
140        let mut map = SourceMap::new();
141        let id = map.add("t.cir", "title\nr1 1 0 1k\n.end\n".into()).unwrap();
142        let loc = map.locate(Span::new(id, 6, 8));
143        assert_eq!(loc.line, 2);
144        assert_eq!(loc.column, 1);
145        let loc = map.locate(Span::new(id, 9, 10));
146        assert_eq!(loc.line, 2);
147        assert_eq!(loc.column, 4);
148        assert_eq!(map.snippet(Span::new(id, 6, 8)), "r1");
149    }
150
151    #[test]
152    fn locate_at_file_start_and_end() {
153        let mut map = SourceMap::new();
154        let id = map.add("t.cir", "a\nb".into()).unwrap();
155        assert_eq!(map.locate(Span::new(id, 0, 1)).line, 1);
156        let loc = map.locate(Span::new(id, 2, 3));
157        assert_eq!((loc.line, loc.column), (2, 1));
158    }
159}