Skip to main content

zsh/extensions/
cow_map.rs

1//! `cow_map` — copy-on-write `HashMap` wrapper for cheap subshell
2//! snapshot/restore.
3//!
4//! !!! WARNING: RUST-ONLY HELPER !!!
5//! C zsh has no counterpart. `$( … )` and `( … )` are forks there
6//! (`c:Src/exec.c:4783` `getoutput` → `entersubsh`), so the child gets
7//! the parent's whole address space by page-table copy and the kernel
8//! does the copy-on-write. zshrs runs both forms IN PROCESS and has to
9//! snapshot the mutable globals by hand, which turned "enter a
10//! substitution" into O(total shell state). This type is the userspace
11//! stand-in for the page-table trick: sharing until someone writes.
12//!
13//! Every read goes through `Deref` and touches the shared map. Every
14//! `&mut` method goes through `DerefMut`, which calls `Arc::make_mut` —
15//! a no-op when the map is unshared (the ordinary case), and a single
16//! deep copy the first time a subshell body writes while the parent
17//! still holds a snapshot. After that copy the subshell owns its map
18//! and the parent's snapshot is frozen at the pre-write contents, which
19//! is exactly what the fork gave C.
20//!
21//! `clone()` is deliberately NOT a deep copy — it is the snapshot
22//! operation, so it must stay O(1). Independence is still guaranteed:
23//! whichever side writes first is the one that pays for the split.
24
25use std::collections::HashMap;
26use std::hash::Hash;
27use std::ops::{Deref, DerefMut};
28use std::sync::Arc;
29
30/// Copy-on-write associative store used for subshell snapshot/restore.
31///
32/// Drop-in for `HashMap<K, V>` at read and write call sites via
33/// `Deref`/`DerefMut`; the difference is only visible in the cost of
34/// `clone()`.
35#[derive(Debug)]
36pub struct CowHashMap<K, V> {
37    inner: Arc<HashMap<K, V>>,
38}
39
40impl<K, V> CowHashMap<K, V> {
41    /// Empty map, sharing nothing.
42    pub fn new() -> Self {
43        Self {
44            inner: Arc::new(HashMap::new()),
45        }
46    }
47
48    /// True while another handle (a live subshell snapshot) shares this
49    /// map, i.e. while the next write will pay for a deep copy.
50    pub fn is_shared(&self) -> bool {
51        Arc::strong_count(&self.inner) > 1
52    }
53}
54
55impl<K, V> Default for CowHashMap<K, V> {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61/// O(1) — a refcount bump, NOT a deep copy. This is the snapshot
62/// operation; see the module docs.
63impl<K, V> Clone for CowHashMap<K, V> {
64    fn clone(&self) -> Self {
65        Self {
66            inner: Arc::clone(&self.inner),
67        }
68    }
69}
70
71impl<K, V> Deref for CowHashMap<K, V> {
72    type Target = HashMap<K, V>;
73
74    fn deref(&self) -> &HashMap<K, V> {
75        &self.inner
76    }
77}
78
79impl<K: Clone + Eq + Hash, V: Clone> DerefMut for CowHashMap<K, V> {
80    /// Splits the map away from any snapshot sharing it, then hands out
81    /// the `&mut`. A caller that takes `&mut` only to read still pays
82    /// the split — that is a cost bug, never a correctness one.
83    fn deref_mut(&mut self) -> &mut HashMap<K, V> {
84        Arc::make_mut(&mut self.inner)
85    }
86}
87
88impl<K, V> From<HashMap<K, V>> for CowHashMap<K, V> {
89    fn from(map: HashMap<K, V>) -> Self {
90        Self {
91            inner: Arc::new(map),
92        }
93    }
94}
95
96impl<K: Clone + Eq + Hash, V: Clone> FromIterator<(K, V)> for CowHashMap<K, V> {
97    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
98        Self::from(HashMap::from_iter(iter))
99    }
100}
101
102impl<'a, K, V> IntoIterator for &'a CowHashMap<K, V> {
103    type Item = (&'a K, &'a V);
104    type IntoIter = std::collections::hash_map::Iter<'a, K, V>;
105
106    fn into_iter(self) -> Self::IntoIter {
107        self.inner.iter()
108    }
109}
110
111impl<K: Eq + Hash, V: PartialEq> PartialEq for CowHashMap<K, V> {
112    fn eq(&self, other: &Self) -> bool {
113        // Sharing the same allocation is equality without a walk.
114        Arc::ptr_eq(&self.inner, &other.inner) || *self.inner == *other.inner
115    }
116}
117
118/// Copy-on-write box around any cloneable store — the same sharing
119/// discipline as [`CowHashMap`], for the tables that are not a plain
120/// `HashMap` (the `hashtable_nodes` bucket arrays behind `aliastab`,
121/// `sufaliastab`, `cmdnamtab`, `reswdtab` and `nameddirtab`).
122///
123/// Reads go through `Deref` and touch the shared value. Every `&mut`
124/// access goes through `DerefMut` → `Arc::make_mut`, so the first write
125/// while a snapshot is alive pays one deep copy and every later write is
126/// free. `clone()` is the snapshot operation and stays O(1).
127#[derive(Debug, Default)]
128pub struct CowArc<T> {
129    inner: Arc<T>,
130}
131
132impl<T> CowArc<T> {
133    /// Wrap `value`, sharing nothing.
134    pub fn new(value: T) -> Self {
135        Self {
136            inner: Arc::new(value),
137        }
138    }
139
140    /// True while another handle (a live subshell snapshot) shares the
141    /// value, i.e. while the next write will pay for a deep copy.
142    pub fn is_shared(&self) -> bool {
143        Arc::strong_count(&self.inner) > 1
144    }
145
146    /// True when both handles point at the same allocation — a snapshot
147    /// that no write has split away from its source.
148    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
149        Arc::ptr_eq(&a.inner, &b.inner)
150    }
151}
152
153/// O(1) — a refcount bump, NOT a deep copy. This is the snapshot
154/// operation; see the module docs.
155impl<T> Clone for CowArc<T> {
156    fn clone(&self) -> Self {
157        Self {
158            inner: Arc::clone(&self.inner),
159        }
160    }
161}
162
163impl<T> Deref for CowArc<T> {
164    type Target = T;
165
166    fn deref(&self) -> &T {
167        &self.inner
168    }
169}
170
171impl<T: Clone> DerefMut for CowArc<T> {
172    /// Splits the value away from any snapshot sharing it, then hands out
173    /// the `&mut`. A caller that takes `&mut` only to read still pays the
174    /// split — a cost bug, never a correctness one.
175    fn deref_mut(&mut self) -> &mut T {
176        Arc::make_mut(&mut self.inner)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    /// `CowArc` keeps the `CowHashMap` contract for an arbitrary value:
185    /// sharing until the first write, and the snapshot frozen at the
186    /// pre-write contents.
187    #[test]
188    fn cow_arc_snapshot_is_shared_until_a_write_splits_it() {
189        let mut live: CowArc<Vec<String>> = CowArc::new(vec!["outer".into()]);
190        let snap = live.clone();
191        assert!(CowArc::ptr_eq(&live, &snap), "clone must share, not copy");
192
193        // A read never splits.
194        assert_eq!(live.len(), 1);
195        assert!(CowArc::ptr_eq(&live, &snap));
196
197        live.push("inner".into());
198        assert!(!CowArc::ptr_eq(&live, &snap), "the write must split");
199        assert_eq!(*snap, vec!["outer".to_string()]);
200
201        live = snap;
202        assert_eq!(*live, vec!["outer".to_string()]);
203        assert!(!live.is_shared());
204    }
205
206    /// The whole point: a snapshot must not walk the map, and must not
207    /// see writes made after it was taken.
208    #[test]
209    fn snapshot_is_shared_until_a_write_splits_it() {
210        let mut live: CowHashMap<String, Vec<String>> = CowHashMap::new();
211        live.insert("_comps".into(), vec!["git".into()]);
212
213        let snap = live.clone();
214        assert!(live.is_shared(), "clone must share, not copy");
215
216        // Subshell body writes: the split happens here, not at clone.
217        live.insert("added_in_subshell".into(), vec![]);
218        assert!(!live.is_shared(), "make_mut must have split the map");
219
220        assert!(snap.get("added_in_subshell").is_none());
221        assert_eq!(snap.get("_comps").map(Vec::len), Some(1));
222        assert_eq!(live.len(), 2);
223    }
224
225    /// Restoring the snapshot must undo the subshell's writes, which is
226    /// what `$( … )` does at its tail.
227    #[test]
228    fn restoring_a_snapshot_drops_the_subshell_writes() {
229        let mut live: CowHashMap<String, String> = CowHashMap::new();
230        live.insert("keep".into(), "outer".into());
231        let snap = live.clone();
232
233        live.insert("keep".into(), "inner".into());
234        live.insert("leaked".into(), "inner".into());
235        live.remove("nothing");
236
237        live = snap;
238        assert_eq!(live.get("keep").map(String::as_str), Some("outer"));
239        assert!(live.get("leaked").is_none());
240    }
241
242    /// Mutating through the snapshot handle must not reach back into the
243    /// live map either — the split is symmetric.
244    #[test]
245    fn writing_through_the_snapshot_does_not_reach_the_live_map() {
246        let mut live: CowHashMap<String, String> = CowHashMap::new();
247        live.insert("k".into(), "live".into());
248        let mut snap = live.clone();
249
250        snap.insert("k".into(), "snap".into());
251
252        assert_eq!(live.get("k").map(String::as_str), Some("live"));
253        assert_eq!(snap.get("k").map(String::as_str), Some("snap"));
254    }
255}