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}