Skip to main content

aura_lang/
source.rs

1//! The source owner (SPEC §1.2): every `&'a str` in the lexer/AST/Value borrows from here.
2//! An append-only arena: entries are never removed or mutated.
3
4use std::cell::RefCell;
5
6use crate::span::SourceId;
7
8/// Every source text, kept alive as long as the cache is, so the zero-copy
9/// pipeline can hand out `&str` slices into it.
10///
11/// The texts are held as raw pointers from `Box::into_raw`, which is load-bearing
12/// and was arrived at the hard way — Miri rejected the two obvious alternatives:
13///
14/// * Storing `Box<str>` and taking a reference before pushing. `Vec::push` *moves*
15///   the box, and moving one re-asserts unique ownership of its contents, which
16///   invalidates every slice already handed out. Reallocation moves them again.
17///   `Box` carries a genuine `noalias` guarantee that LLVM exploits, so this is
18///   not a technicality.
19/// * Storing `&'static str` from `Box::leak`. Handing out slices is then safe, but
20///   `Drop` has to free them, and reconstructing a `Box` from a *shared* reference
21///   deallocates through provenance that never granted write access.
22///
23/// A raw pointer has no aliasing guarantee to violate: moving it inside the `Vec`
24/// retags nothing, shared references derived from it stay valid, and `Drop` frees
25/// through the same provenance it was created with. Checked under both Stacked
26/// Borrows and Tree Borrows; see `references_survive_later_insertions_and_reallocation`.
27#[derive(Default)]
28pub struct SourceCache {
29    files: RefCell<Vec<(String, *mut str)>>,
30}
31
32impl SourceCache {
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Registers a source and returns a slice with the cache's lifetime.
38    ///
39    /// The slice is returned bounded by `&self`, so no caller can outlive the
40    /// arena that owns it.
41    pub fn add(&self, name: String, text: String) -> (SourceId, &str) {
42        let ptr: *mut str = Box::into_raw(text.into_boxed_str());
43        let mut files = self.files.borrow_mut();
44        files.push((name, ptr));
45        let id = (files.len() - 1) as SourceId;
46        // SAFETY: `ptr` owns a live allocation from `Box::into_raw`. Nothing moves
47        // or frees the string data before `Drop`, and the arena never removes or
48        // mutates an entry, so the slice is valid for as long as `&self`.
49        (id, unsafe { &*ptr })
50    }
51
52    pub fn name(&self, id: SourceId) -> Option<String> {
53        self.files.borrow().get(id as usize).map(|(n, _)| n.clone())
54    }
55
56    pub fn text(&self, id: SourceId) -> Option<&str> {
57        // The pointer is copied out before the borrow ends, so nothing borrowed
58        // from the `RefCell` escapes it.
59        let ptr = *self.files.borrow().get(id as usize).map(|(_, p)| p)?;
60        // SAFETY: see `add`.
61        Some(unsafe { &*ptr })
62    }
63}
64
65impl Drop for SourceCache {
66    fn drop(&mut self) {
67        for (_, ptr) in self.files.borrow_mut().drain(..) {
68            // SAFETY: each pointer came from `Box::into_raw` in `add`, is owned by
69            // this arena alone, and is reconstructed exactly once. Every slice
70            // handed out was bounded by `&self`, so none outlives this.
71            unsafe { drop(Box::from_raw(ptr)) };
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    /// The arena hands out references that outlive the `RefCell` borrow they were
81    /// derived from, which is the whole point and also the only reason this file
82    /// contains `unsafe`. Run under Miri (`cargo +nightly miri test -p aura-lang
83    /// source::`) to check that against the aliasing model rather than against a
84    /// comment.
85    #[test]
86    fn references_survive_later_insertions_and_reallocation() {
87        let cache = SourceCache::new();
88        let (first_id, first) = cache.add(
89            "a.aura".into(),
90            "port: 8080
91"
92            .into(),
93        );
94
95        // Enough pushes to force the Vec to reallocate several times. The Box
96        // contents must not move with it.
97        let mut held = vec![first];
98        for i in 0..64 {
99            let (_, text) = cache.add(
100                format!("f{i}.aura"),
101                format!(
102                    "x: {i}
103"
104                ),
105            );
106            held.push(text);
107        }
108
109        // The first slice is still readable, and still says what it said.
110        assert_eq!(
111            first,
112            "port: 8080
113"
114        );
115        assert_eq!(
116            cache.text(first_id),
117            Some(
118                "port: 8080
119"
120            )
121        );
122        for (i, text) in held.iter().skip(1).enumerate() {
123            assert_eq!(
124                *text,
125                format!(
126                    "x: {i}
127"
128                )
129            );
130        }
131
132        // A slice fetched through `text()` outlives the `Ref` guard inside it.
133        let borrowed = cache.text(first_id).unwrap();
134        let _ = cache.add(
135            "later.aura".into(),
136            "y: 1
137"
138            .into(),
139        );
140        assert_eq!(
141            borrowed,
142            "port: 8080
143"
144        );
145    }
146
147    #[test]
148    fn an_unknown_id_is_none_rather_than_a_panic() {
149        let cache = SourceCache::new();
150        assert_eq!(cache.text(7), None);
151        assert_eq!(cache.name(7), None);
152    }
153}