Skip to main content

praxis_source/
file.rs

1//! Source files and the [`SourceMap`] that interns them.
2//!
3//! A [`SourceMap`] is the compiler's registry of loaded source files. It owns
4//! the file text, mints opaque [`FileId`] handles, and precomputes each file's
5//! [`LineMap`]. Files are append-only once interned — source snapshots (§13.1)
6//! must remain stable for the lifetime of a compilation session.
7
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, RwLock};
10
11use crate::line_map::LineMap;
12use crate::span::FileSpan;
13
14/// An opaque handle to one interned source file.
15///
16/// `FileId`s are only ever minted by [`SourceMap::intern`]; they are pure
17/// identity tokens and must never be constructed by hand. The field is private,
18/// which keeps the construction surface closed across crate boundaries.
19#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct FileId(u32);
21
22impl FileId {
23    /// The synthetic file id used for diagnostics that are not tied to any real
24    /// source location (for example a CLI usage error). It sits at the top of
25    /// the id space: [`SourceMap::intern`] mints from 0 upward and panics
26    /// rather than reach it.
27    pub const SYNTHETIC: FileId = FileId(u32::MAX);
28
29    #[inline]
30    pub const fn to_u32(self) -> u32 {
31        self.0
32    }
33}
34
35impl std::fmt::Debug for FileId {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        if *self == Self::SYNTHETIC {
38            write!(f, "FileId(<synthetic>)")
39        } else {
40            write!(f, "FileId({})", self.0)
41        }
42    }
43}
44
45/// One loaded source file: its id, the path it was loaded from, the full text,
46/// and a precomputed line table.
47#[derive(Clone)]
48pub struct SourceFile {
49    id: FileId,
50    path: PathBuf,
51    text: String,
52    line_map: LineMap,
53}
54
55impl SourceFile {
56    #[inline]
57    pub fn id(&self) -> FileId {
58        self.id
59    }
60
61    #[inline]
62    pub fn path(&self) -> &Path {
63        &self.path
64    }
65
66    #[inline]
67    pub fn text(&self) -> &str {
68        &self.text
69    }
70
71    #[inline]
72    pub fn line_map(&self) -> &LineMap {
73        &self.line_map
74    }
75
76    /// A [`FileSpan`] covering the whole file.
77    pub fn full_span(&self) -> FileSpan {
78        FileSpan::new(self.id, crate::span::Span::new(0, self.text.len() as u32))
79    }
80}
81
82/// The compiler's registry of source files.
83///
84/// Interning is append-only: once a file has an id its text and path are fixed
85/// for the lifetime of the map, which keeps diagnostics and snapshots stable.
86/// The map is internally synchronized so it can be shared across threads (the
87/// LSP, for example, reads source from a background query thread).
88#[derive(Default)]
89pub struct SourceMap {
90    /// Each file is behind an `Arc`, so its address does not depend on the
91    /// `Vec`'s capacity. That is what lets [`SourceMap::get`] hand out a view
92    /// that outlives the read guard without extending a borrow into storage a
93    /// later `intern` may reallocate.
94    files: RwLock<Vec<Arc<SourceFile>>>,
95}
96
97impl SourceMap {
98    /// Create an empty source map.
99    pub fn new() -> SourceMap {
100        SourceMap::default()
101    }
102
103    /// Intern a source file under the given path, returning its id.
104    ///
105    /// Each call mints a fresh id even for a repeated path: a later load of the
106    /// same path is treated as a distinct snapshot. This matches the §13.1
107    /// "revisioned source" model — the LSP holds several revisions of one file
108    /// simultaneously.
109    pub fn intern(&self, path: impl Into<PathBuf>, text: impl Into<String>) -> FileId {
110        let path = path.into();
111        let text = text.into();
112        let line_map = LineMap::new(&text);
113
114        let mut files = self.files.write().unwrap();
115        // SYNTHETIC is reserved; a non-pathological program cannot exhaust u32
116        // ids, but if it ever does we'd rather panic loudly than alias SYNTHETIC.
117        let id = u32::try_from(files.len()).expect("more than 2^32 source files");
118        assert!(id != FileId::SYNTHETIC.to_u32(), "file id space exhausted");
119
120        files.push(Arc::new(SourceFile {
121            id: FileId(id),
122            path,
123            text,
124            line_map,
125        }));
126        FileId(id)
127    }
128
129    /// The number of interned files.
130    pub fn len(&self) -> usize {
131        self.files.read().unwrap().len()
132    }
133
134    /// True if no files have been interned.
135    pub fn is_empty(&self) -> bool {
136        self.files.read().unwrap().is_empty()
137    }
138
139    /// Fetch a file by id. Returns `None` for unknown or synthetic ids.
140    ///
141    /// The returned view shares ownership of the file, so it stays valid across
142    /// later `intern` calls (which reallocate the `Vec`) and across threads.
143    pub fn get(&self, id: FileId) -> Option<FileView> {
144        if id == FileId::SYNTHETIC {
145            return None;
146        }
147        let files = self.files.read().unwrap();
148        let file = Arc::clone(files.get(id.to_u32() as usize)?);
149        Some(FileView { file })
150    }
151}
152
153/// A shared view of an interned source file.
154///
155/// It holds a reference count rather than a borrow, so it is independent of the
156/// map's internal storage and of the lock: interning more files, or dropping
157/// the map itself, cannot invalidate a live view.
158#[derive(Clone)]
159pub struct FileView {
160    file: Arc<SourceFile>,
161}
162
163impl std::ops::Deref for FileView {
164    type Target = SourceFile;
165    fn deref(&self) -> &SourceFile {
166        &self.file
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn intern_assigns_sequential_ids() {
176        let map = SourceMap::new();
177        let a = map.intern("a.px", "first");
178        let b = map.intern("b.px", "second");
179        assert_eq!(a.to_u32(), 0);
180        assert_eq!(b.to_u32(), 1);
181        assert_eq!(map.len(), 2);
182    }
183
184    #[test]
185    fn intern_same_path_yields_distinct_ids() {
186        let map = SourceMap::new();
187        let first = map.intern("dup.px", "one");
188        let second = map.intern("dup.px", "two");
189        assert_ne!(first, second, "each intern is a distinct snapshot");
190    }
191
192    #[test]
193    fn get_returns_interned_file() {
194        let map = SourceMap::new();
195        let id = map.intern("day.px", "out(1)\n");
196        let view = map.get(id).expect("file was just interned");
197        assert_eq!(view.path(), Path::new("day.px"));
198        assert_eq!(view.text(), "out(1)\n");
199        assert_eq!(view.id(), id);
200    }
201
202    #[test]
203    fn synthetic_and_unknown_ids_return_none() {
204        let map = SourceMap::new();
205        assert!(map.get(FileId::SYNTHETIC).is_none());
206        assert!(map.get(FileId(0)).is_none()); // no files interned
207    }
208
209    #[test]
210    fn full_span_covers_whole_file() {
211        let map = SourceMap::new();
212        let id = map.intern("f.px", "abc");
213        let view = map.get(id).unwrap();
214        let span = view.full_span();
215        assert_eq!(span.file, id);
216        assert_eq!(span.span.start().to_u32(), 0);
217        assert_eq!(span.span.end().to_u32(), 3);
218    }
219
220    #[test]
221    fn empty_map_reports_empty() {
222        let map = SourceMap::new();
223        assert!(map.is_empty());
224        assert_eq!(map.len(), 0);
225    }
226
227    /// A live view must survive a reallocating `intern`. Violating this is UB
228    /// rather than a wrong answer, so only Miri would fail on it; it is in the
229    /// ordinary suite because shared ownership is observable without Miri.
230    #[test]
231    fn regression_file_view_remains_valid_when_more_files_are_interned() {
232        let map = SourceMap::new();
233        let first = map.intern("first.px", "stable");
234        let view = map.get(first).expect("first file exists");
235
236        // Force the backing Vec through several reallocations while `view`
237        // remains live: the `Arc` is what keeps the file's address stable
238        // through them.
239        for i in 0..4_096 {
240            map.intern(format!("later-{i}.px"), format!("revision {i}"));
241        }
242
243        assert_eq!(view.text(), "stable");
244        assert_eq!(view.path(), Path::new("first.px"));
245    }
246}