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}