Skip to main content

wolf_span/
lib.rs

1//! Source files, positions and ranges.
2//!
3//! Every file the compiler reads is stored once in a [`SourceMap`] and identified by a [`FileId`].
4//! Positions inside a file are byte offsets ([`Span`]). Line and column numbers are computed only
5//! when a diagnostic is shown; columns count Unicode scalar values (spec chapter 1, section 1).
6
7use std::ops::Range;
8
9/// Identifies a file inside a [`SourceMap`].
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub struct FileId(u32);
12
13impl FileId {
14    /// The id of the first file of a map. Useful as a placeholder in constants.
15    pub const ZERO: FileId = FileId(0);
16
17    /// The index of the file in its [`SourceMap`].
18    pub fn index(self) -> usize {
19        self.0 as usize
20    }
21}
22
23/// A range of bytes in one file. `start` is inclusive, `end` is exclusive.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct Span {
26    /// The file the range belongs to.
27    pub file: FileId,
28    /// Offset of the first byte.
29    pub start: u32,
30    /// Offset one past the last byte.
31    pub end: u32,
32}
33
34impl Span {
35    /// Creates a span. If `end < start`, the span is empty at `start`.
36    pub fn new(file: FileId, start: u32, end: u32) -> Span {
37        Span {
38            file,
39            start,
40            end: end.max(start),
41        }
42    }
43
44    /// The byte range of the span, for slicing the source text.
45    pub fn range(self) -> Range<usize> {
46        self.start as usize..self.end as usize
47    }
48
49    /// Length in bytes.
50    pub fn len(self) -> u32 {
51        self.end - self.start
52    }
53
54    /// Whether the span covers no bytes.
55    pub fn is_empty(self) -> bool {
56        self.start == self.end
57    }
58
59    /// The smallest span covering both `self` and `other`. Both must be in the same file;
60    /// otherwise `self` is returned.
61    pub fn to(self, other: Span) -> Span {
62        if self.file != other.file {
63            return self;
64        }
65        Span::new(
66            self.file,
67            self.start.min(other.start),
68            self.end.max(other.end),
69        )
70    }
71}
72
73/// A one-based line and column. The column counts Unicode scalar values.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct LineCol {
76    /// Line number, starting at 1.
77    pub line: u32,
78    /// Column number, starting at 1, in Unicode scalar values.
79    pub column: u32,
80}
81
82/// The largest file the compiler accepts, in bytes. Offsets are stored as `u32`.
83pub const MAX_FILE_SIZE: usize = u32::MAX as usize;
84
85/// One source file: its display name and its text.
86#[derive(Debug)]
87pub struct SourceFile {
88    name: String,
89    text: String,
90    line_starts: Vec<u32>,
91}
92
93impl SourceFile {
94    fn new(name: String, text: String) -> SourceFile {
95        let mut line_starts = vec![0];
96        for (offset, byte) in text.bytes().enumerate() {
97            if byte == b'\n' {
98                line_starts.push(to_u32(offset + 1));
99            }
100        }
101        SourceFile {
102            name,
103            text,
104            line_starts,
105        }
106    }
107
108    /// The name shown in diagnostics, usually a path relative to the working directory.
109    pub fn name(&self) -> &str {
110        &self.name
111    }
112
113    /// The full text of the file.
114    pub fn text(&self) -> &str {
115        &self.text
116    }
117
118    /// The zero-based index of the line containing `offset`.
119    pub fn line_index(&self, offset: u32) -> usize {
120        match self.line_starts.binary_search(&offset) {
121            Ok(line) => line,
122            Err(next) => next.saturating_sub(1),
123        }
124    }
125
126    /// The byte range of the zero-based line `index`, including its line terminator.
127    /// Returns `None` if there is no such line.
128    pub fn line_range(&self, index: usize) -> Option<Range<usize>> {
129        let start = *self.line_starts.get(index)? as usize;
130        let end = match self.line_starts.get(index + 1) {
131            Some(next) => *next as usize,
132            None => self.text.len(),
133        };
134        Some(start..end)
135    }
136
137    /// Line and column of `offset`. An offset inside a multi-byte character, or past the end,
138    /// is clamped to the nearest valid position before it.
139    pub fn line_col(&self, offset: u32) -> LineCol {
140        let line = self.line_index(offset);
141        let line_start = self.line_starts.get(line).copied().unwrap_or(0) as usize;
142        let offset = (offset as usize).min(self.text.len()).max(line_start);
143        let prefix = self.text.get(line_start..offset).unwrap_or("");
144        LineCol {
145            line: to_u32(line + 1),
146            column: to_u32(prefix.chars().count() + 1),
147        }
148    }
149}
150
151/// All source files of one compilation.
152#[derive(Debug, Default)]
153pub struct SourceMap {
154    files: Vec<SourceFile>,
155}
156
157impl SourceMap {
158    /// Creates an empty map.
159    pub fn new() -> SourceMap {
160        SourceMap::default()
161    }
162
163    /// Adds a file and returns its id. The caller must ensure `text.len() <= MAX_FILE_SIZE`;
164    /// longer text is truncated at a character boundary.
165    pub fn add(&mut self, name: impl Into<String>, mut text: String) -> FileId {
166        if text.len() > MAX_FILE_SIZE {
167            let mut cut = MAX_FILE_SIZE;
168            while !text.is_char_boundary(cut) {
169                cut -= 1;
170            }
171            text.truncate(cut);
172        }
173        let id = FileId(to_u32(self.files.len()));
174        self.files.push(SourceFile::new(name.into(), text));
175        id
176    }
177
178    /// The file with the given id, if it exists.
179    pub fn get(&self, id: FileId) -> Option<&SourceFile> {
180        self.files.get(id.index())
181    }
182
183    /// Iterates over all files with their ids.
184    pub fn iter(&self) -> impl Iterator<Item = (FileId, &SourceFile)> {
185        self.files
186            .iter()
187            .enumerate()
188            .map(|(index, file)| (FileId(to_u32(index)), file))
189    }
190}
191
192/// Converts a length or offset to `u32`, saturating. Files are limited to [`MAX_FILE_SIZE`],
193/// so saturation never happens for valid offsets.
194fn to_u32(value: usize) -> u32 {
195    u32::try_from(value).unwrap_or(u32::MAX)
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    fn file(text: &str) -> SourceFile {
203        SourceFile::new("test.wolf".to_string(), text.to_string())
204    }
205
206    #[test]
207    fn line_col_counts_scalar_values() {
208        let f = file("ab\nпривет x\n");
209        assert_eq!(f.line_col(0), LineCol { line: 1, column: 1 });
210        assert_eq!(f.line_col(3), LineCol { line: 2, column: 1 });
211        // "привет " is 13 bytes, 7 scalar values
212        assert_eq!(f.line_col(3 + 13), LineCol { line: 2, column: 8 });
213    }
214
215    #[test]
216    fn line_col_at_end_of_file() {
217        let f = file("a\nb");
218        assert_eq!(f.line_col(3), LineCol { line: 2, column: 2 });
219        assert_eq!(f.line_col(100), LineCol { line: 2, column: 2 });
220    }
221
222    #[test]
223    fn line_col_inside_a_character_is_clamped() {
224        let f = file("я");
225        assert_eq!(f.line_col(1).line, 1);
226    }
227
228    #[test]
229    fn line_ranges_include_terminators() {
230        let f = file("a\r\nbc\n");
231        assert_eq!(f.line_range(0), Some(0..3));
232        assert_eq!(f.line_range(1), Some(3..6));
233        assert_eq!(f.line_range(2), Some(6..6));
234        assert_eq!(f.line_range(3), None);
235    }
236
237    #[test]
238    fn span_to_merges() {
239        let mut map = SourceMap::new();
240        let id = map.add("a", "hello".to_string());
241        let a = Span::new(id, 1, 2);
242        let b = Span::new(id, 3, 5);
243        assert_eq!(a.to(b), Span::new(id, 1, 5));
244        assert_eq!(Span::new(id, 4, 2).len(), 0);
245    }
246}