span/
source.rs

1//! Source file representation
2
3use core::marker::PhantomData;
4use std::{fs, io};
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7
8use crate::{FilePosition, Span};
9
10/// The filename of a [`SourceFile`]
11///
12/// It can either be a filesystem path, or an anonymous source (used on testing)
13#[derive(Clone)]
14pub enum FileName {
15    Path(PathBuf),
16    Stdin,
17    Annon,
18}
19
20impl<T: Into<PathBuf>> From<T> for FileName {
21    fn from(value: T) -> Self {
22        FileName::Path(value.into())
23    }
24}
25
26#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
27pub struct FileId(usize);
28
29impl FileId {
30    pub fn from_offset(off: usize) -> Self { Self(off) }
31}
32
33/// A source file for the compiler
34#[derive(Clone)]
35pub struct SourceFile {
36    pub fname: FileName,
37    pub contents: Rc<str>,
38    pub offset: usize,
39    /* Private field to avoid external modules
40     * from building their own SourceFiles */
41    _marker: PhantomData<()>,
42}
43
44impl SourceFile {
45
46    /// Gets a string slice with the filename of this `SourceFile`
47    ///
48    /// This method returns an `Option` because the [`FileName::Path`] variant
49    /// stores a [`PathBuf`], which can contain non-utf8 filenames.
50    ///
51    /// It the conversion [to string](std::path::Path::to_str) fails, returns None
52    pub fn filename(&self) -> Option<&str> {
53        match &self.fname {
54            FileName::Path(path) => path.to_str(),
55            FileName::Stdin => Some("/dev/stdin"),
56            FileName::Annon => Some("<annon>"),
57        }
58    }
59
60    /// Returns the length of this `SourceFile`
61    ///
62    /// This is the same as `self.contents.len()`
63    pub fn len(&self) -> usize { self.contents.len() }
64
65    /// Returns true if this `SourceFile` is empty
66    ///
67    /// This is the same as `self.contents.is_empty()`
68    pub fn is_empty(&self) -> bool { self.contents.is_empty() }
69
70    /// Returns a tuple with the contents and offset of this file
71    ///
72    /// # Example
73    /// ```
74    /// use span::source::{SourceMap, SourceFile};
75    ///
76    /// let mut sm = SourceMap::new();
77    ///
78    /// let input1 = "Hello world!";
79    /// let (contents, offset) = sm.add_file_annon(input1.into()).into_parts();
80    /// assert_eq!(&*contents, input1);
81    /// assert_eq!(offset, 0);
82    ///
83    /// let input2 = "Second file";
84    /// let (contents, offset) = sm.add_file_annon(input2.into()).into_parts();
85    /// assert_eq!(&*contents, input2);
86    /// assert_eq!(offset, input1.len());
87    /// ```
88    pub fn into_parts(&self) -> (Rc<str>, usize) {
89        (Rc::clone(&self.contents), self.offset)
90    }
91
92    /// Returns the absolute offset of this file inside it's [`SourceMap`]
93    ///
94    /// # Example
95    /// ```
96    /// use span::source::{SourceMap, SourceFile};
97    ///
98    /// let mut sm = SourceMap::new();
99    ///
100    /// let input1 = "Hello world!";
101    /// let offset1 = sm.add_file_annon(input1.into()).offset();
102    /// assert_eq!(offset1, 0);
103    ///
104    /// let input2 = "Second file";
105    /// let offset2 = sm.add_file_annon(input2.into()).offset();
106    /// assert_eq!(offset2, input1.len());
107    /// ```
108    pub const fn offset(&self) -> usize { self.offset }
109
110    pub fn contains_span(&self, span: &Span) -> bool {
111        span.offset >= self.offset &&
112        span.offset + span.len <= self.offset + self.len()
113    }
114
115    /// Slices the given span
116    ///
117    /// **NOTE**: `span` must be contained in this `SourceFile`
118    #[must_use]
119    #[inline]
120    pub fn slice(&self, span: &Span) -> &str {
121        debug_assert!(self.contains_span(span));
122        span.slice(self.offset, &self.contents)
123    }
124
125    /// Returns the file position of this span inside `self`
126    ///
127    /// **NOTE**: `span` must be contained in this `SourceFile`
128    #[must_use]
129    #[inline]
130    pub fn file_position(&self, span: &Span) -> FilePosition {
131        span.file_position(self.offset, &self.contents)
132    }
133
134    pub const fn id(&self) -> FileId { FileId(self.offset) }
135
136    pub fn path(&self) -> Option<&Path> {
137        match &self.fname {
138            FileName::Path(pbuf) => Some(pbuf),
139            FileName::Stdin |
140            FileName::Annon => None,
141        }
142    }
143}
144
145/// A storage for source files.
146///
147/// # Example
148/// ```
149/// use span::{Span, source::{FileName, SourceMap, SourceFile}};
150///
151/// fn process_file(f: &SourceFile) -> Span {
152///     Span {
153///         offset: 2 + f.offset(),
154///         len: 3,
155///     }
156/// }
157///
158/// let mut source = SourceMap::default();
159///
160/// let f = source.add_file(FileName::Annon, "ABCDEFG".into());
161/// let span1 = process_file(f);
162///
163/// let f = source.add_file(FileName::Annon, "DEFGHIJK".into());
164/// let span2 = process_file(f);
165///
166/// let slice1 = source.slice(&span1);
167/// let slice2 = source.slice(&span2);
168///
169/// assert_eq!(slice1, Some("CDE"));
170/// assert_eq!(slice2, Some("FGH"));
171/// ```
172#[derive(Default)]
173pub struct SourceMap {
174    files: Vec<SourceFile>,
175}
176
177impl SourceMap {
178    /// Creates a new empty `SourceMap`
179    pub const fn new() -> Self {
180        Self { files: Vec::new() }
181    }
182
183    /// Adds a new [`SourceFile`] to the `SourceMap`
184    ///
185    /// Returns a reference to the newly created file
186    pub fn add_file(&mut self, fname: FileName, contents: Rc<str>) -> &SourceFile {
187        #[allow(clippy::cast_possible_truncation)]
188        let offset = match self.files.last() {
189            Some(file) => file.offset + file.len(),
190            None => 0
191        };
192        let file = SourceFile { fname, contents, offset, _marker: PhantomData };
193        self.files.push(file);
194        self.files.last().unwrap()
195    }
196
197    /// Reads the given `path` and creates a [`SourceFile`] with it's contents
198    pub fn add_file_fs(&mut self, path: PathBuf) -> io::Result<&SourceFile> {
199        let text = fs::read_to_string(&path)?;
200        Ok(self.add_file(FileName::Path(path), text.into()))
201    }
202
203    /// Adds a new annonymous [`SourceFile`] (without a filename) to this `SourceMap`
204    #[inline]
205    pub fn add_file_annon(&mut self, contents: Rc<str>) -> &SourceFile {
206        self.add_file(FileName::Annon, contents)
207    }
208
209    /// Gets the corresponding [`SourceFile`] for the given `span`
210    ///
211    /// # Example
212    /// ```
213    /// use span::{Span, source::{SourceMap, FileName}};
214    ///
215    /// let mut sm = SourceMap::new();
216    /// let (input1, offset1) =
217    ///     sm.add_file(
218    ///         FileName::from("file1.txt"),
219    ///         "Hello world!".into()
220    ///     )
221    ///     .into_parts();
222    /// let (input2, offset2) =
223    ///     sm.add_file(
224    ///         FileName::from("file2.txt"),
225    ///         "Another file :)".into()
226    ///     )
227    ///     .into_parts();
228    ///
229    /// let span1 = Span {
230    ///     offset: 3 + offset1,
231    ///     len: 3,
232    /// };
233    ///
234    /// let span2 = Span {
235    ///     offset: 1 + offset2,
236    ///     len: 4,
237    /// };
238    ///
239    /// let file1 = sm.get_file_of_span(&span1).unwrap();
240    /// assert_eq!(file1.filename().unwrap(), "file1.txt");
241    ///
242    /// let file2 = sm.get_file_of_span(&span2).unwrap();
243    /// assert_eq!(file2.filename().unwrap(), "file2.txt");
244    /// ```
245    pub fn get_file_of_span(&self, span: &Span) -> Option<&SourceFile> {
246        self.files.iter().find(|file| file.contains_span(span))
247    }
248
249    /// Returns the file for a given offset
250    pub fn get_file_for_offset(&self, offset: usize) -> Option<&SourceFile> {
251        self.files.iter().find(|file| file.offset == offset)
252    }
253
254    pub fn get_file_for_id(&self, id: &FileId) -> Option<&SourceFile> {
255        self.get_file_for_offset(id.0)
256    }
257
258    /// Returns base offset for a [`Span`]. This is: the offset of the
259    /// file it belongs to.
260    ///
261    /// If no file is found, returns 0
262    pub fn get_base_offset_of_span(&self, span: &Span) -> Option<usize> {
263        self.get_file_of_span(span).map(|f| f.offset)
264    }
265
266    /// Slices the given [`Span`]
267    ///
268    /// This function will compute the [`SourceFile`] for the span
269    /// and slice it
270    pub fn slice(&self, span: &Span) -> Option<&str> {
271        self.get_file_of_span(span).map(|file| file.slice(span))
272    }
273
274    /// Returns the [`FilePosition`] for this [`Span`]
275    ///
276    /// This function will compute the [`SourceFile`] for the span
277    /// and slice it
278    pub fn file_position(&self, span: &Span) -> Option<FilePosition> {
279        self.get_file_of_span(span).map(|file| file.file_position(span))
280    }
281
282    pub fn files(&self) -> &[SourceFile] { &self.files }
283
284    /// Returns true if this is a valid span, this is, if it finds
285    /// a matching [`SourceFile`] for it
286    pub fn is_valid(&self, span: &Span) -> bool {
287        self.get_file_of_span(span).is_some()
288    }
289}
290
291#[cfg(test)]
292mod test {
293    use super::*;
294
295    #[test]
296    fn spans() {
297         let mut sm = SourceMap::new();
298         let (_, offset1) =
299             sm.add_file(
300                 FileName::from("file1.txt"),
301                 "Hello world!".into()
302             )
303             .into_parts();
304         let (_, offset2) =
305             sm.add_file(
306                 FileName::from("file2.txt"),
307                 "Another file :)".into()
308             )
309             .into_parts();
310
311         let span1 = Span {
312             offset: 3 + offset1,
313             len: 3,
314         };
315
316         let slice1 = sm.slice(&span1);
317         let pos1 = sm.file_position(&span1);
318         assert_eq!(slice1, Some("lo "));
319         assert_eq!(pos1, Some(FilePosition {
320             start_line: 1,
321             start_col: 3,
322             end_line: 1,
323             end_col: 6,
324         }));
325
326         let span2 = Span {
327             offset: 1 + offset2,
328             len: 4,
329         };
330
331         let slice2 = sm.slice(&span2);
332         let pos2 = sm.file_position(&span2);
333         assert_eq!(slice2, Some("noth"));
334         assert_eq!(pos2, Some(FilePosition {
335             start_line: 1,
336             start_col: 1,
337             end_line: 1,
338             end_col: 5,
339         }));
340
341         let bad_span1 = Span {
342             offset: 9999,
343             len: 4,
344         };
345
346         let bad_span2 = Span {
347             offset: 0,
348             len: 182,
349         };
350
351         assert!(sm.slice(&bad_span1).is_none());
352         assert!(sm.file_position(&bad_span1).is_none());
353         assert!(sm.slice(&bad_span2).is_none());
354         assert!(sm.file_position(&bad_span2).is_none());
355    }
356}