Skip to main content

zsh/extensions/
thread_shell_state.rs

1//! Per-thread storage for the shell state C keeps per THREAD OF EXECUTION.
2//!
3//! **zshrs-original infrastructure — no `Src/*.c` counterpart.** It is the
4//! generalisation of [`crate::errflag_cell`], and it exists for the same
5//! reason: zshrs replaced a `fork(2)` with a thread.
6//!
7//! C zsh runs on one thread. `scriptname` (`Src/init.c`, the diagnostic
8//! prefix `zwarning` reads at `Src/utils.c:147`), `funcstack`
9//! (`Src/exec.c:340`, behind `$funcstack` / `$functrace` /
10//! `$funcfiletrace` / `$funcsourcetrace`), `zsh_eval_context`
11//! (`zsh.export:355`, behind `$ZSH_EVAL_CONTEXT`) and `locallevel`
12//! (`Src/params.c:54`) are all plain globals that describe WHERE THE ONE
13//! THREAD CURRENTLY IS. Background work in C is a forked child, so the
14//! child's pushes onto those globals are invisible to the parent.
15//!
16//! zshrs runs some of that work on the worker pool ([`crate::worker`])
17//! instead — `async_precmd` (`crate::async_precmd`) hands hook FUNCTIONS to
18//! a worker, and `compsys::in_editor` drives the completer tree on a
19//! dedicated thread. A hook function moves every one of those globals while
20//! it runs, and the shell thread reads the moved value:
21//!
22//! ```text
23//! async_precmd_functions=(spin_hook)   # spin_hook is running on a worker
24//! % done                               # typed at the prompt
25//! spin_hook:4: parse error near `done'     <- zshrs, before this module
26//! zsh: parse error near `done'             <- zsh
27//! ```
28//!
29//! The user is pointed at a function that has nothing to do with what they
30//! typed, which is worse than no information at all. `quiesce`
31//! (`crate::async_precmd::quiesce`) closes the window where the shell
32//! thread EXECUTES shell code during a batch, but the line above is
33//! diagnosed by the LEXER, which runs before the `quiesce()` in
34//! `init::loop_` — and `quiesce` is a serialisation, not a fix for the
35//! sharing.
36//!
37//! Restoring the C model is the fix: the shell thread keeps the
38//! process-wide value, every other thread gets its own. A thread that runs
39//! shell code off the shell thread IS a forked child in the C design, so
40//! where it is stays with it.
41//!
42//! Two shapes live here:
43//!
44//! * [`ThreadMutex`] for the stack-shaped state (`scriptname`,
45//!   `scriptfilename`, `funcstack`, `zsh_eval_context`). It mirrors
46//!   `Mutex`'s `lock()` / `clear_poison()` so the ~50 existing
47//!   `FUNCSTACK.lock()` call sites read exactly as they did before.
48//! * [`LocalLevelCell`] for `locallevel`, which cannot simply become
49//!   per-thread — see its own docs.
50
51use std::cell::Cell;
52use std::collections::HashMap;
53use std::sync::atomic::{AtomicI32, Ordering};
54use std::sync::{LockResult, Mutex, MutexGuard};
55use std::thread::ThreadId;
56
57thread_local! {
58    /// Whether this thread is the shell thread, computed once per thread.
59    /// Same test as [`crate::errflag_cell`] and
60    /// `crate::async_precmd::quiesce`: the shell runs on the process's
61    /// initial thread, which Rust names `"main"`; pool workers are spawned
62    /// without a name or as `"zshrs-worker-N"`.
63    static IS_SHELL: bool = std::thread::current().name() == Some("main");
64
65    /// This thread's own `locallevel` — see [`LocalLevelCell`].
66    static OWN_LOCALLEVEL: Cell<i32> = const { Cell::new(0) };
67}
68
69/// True on the shell thread, false on every worker.
70///
71/// !!! WARNING: RUST-ONLY !!! C has one thread and needs no such test.
72///
73/// Sharp edge, same as `errflag_cell`'s: a thread deliberately named
74/// `"main"` by `thread::Builder` would be misclassified as the shell.
75/// Nothing in this tree does that.
76#[inline]
77pub fn is_shell_thread() -> bool {
78    IS_SHELL.with(|s| *s)
79}
80
81/// A `Mutex` whose contents are the shell thread's on the shell thread and
82/// this thread's own everywhere else.
83///
84/// !!! WARNING: RUST-ONLY !!! No counterpart in `Src/*.c` — C gets this
85/// split from `fork(2)`.
86///
87/// The API is deliberately `Mutex`'s, down to the `LockResult`, so a
88/// global can be re-typed from `Mutex<T>` to `ThreadMutex<T>` without
89/// touching its readers. The per-thread halves are leaked `Mutex<T>`s so
90/// both branches can hand back the same `MutexGuard<'static, T>` type; the
91/// leak is bounded by (threads that ever ran shell code) × (cells), which
92/// is the small fixed worker pool.
93///
94/// A worker's half starts EMPTY rather than as a copy of the shell
95/// thread's. That is the `fork()` a C zsh would do from `preprompt()`,
96/// where the shell is at top level with nothing on any of these stacks;
97/// it is not a copy of an arbitrary mid-function state, because no site
98/// that runs shell code off the shell thread is reached from inside one.
99pub struct ThreadMutex<T: 'static> {
100    /// The shell thread's value — the process-wide one, as before.
101    shell: Mutex<T>,
102    /// Builds a fresh value for a thread's first use of this cell.
103    seed: fn() -> T,
104    /// One leaked `Mutex<T>` per non-shell thread that has touched this
105    /// cell. `Option` so the map can be built in a `const fn`.
106    others: Mutex<Option<HashMap<ThreadId, &'static Mutex<T>>>>,
107}
108
109impl<T: 'static> ThreadMutex<T> {
110    /// `shell` is the shell thread's initial value; `seed` builds a
111    /// non-shell thread's. `const` so the consumer stays a plain `static`.
112    pub const fn new(shell: T, seed: fn() -> T) -> Self {
113        ThreadMutex {
114            shell: Mutex::new(shell),
115            seed,
116            others: Mutex::new(None),
117        }
118    }
119
120    /// Mirrors `Mutex::lock`.
121    #[inline]
122    pub fn lock(&'static self) -> LockResult<MutexGuard<'static, T>> {
123        if is_shell_thread() {
124            self.shell.lock()
125        } else {
126            self.own().lock()
127        }
128    }
129
130    /// Mirrors `Mutex::try_lock`.
131    #[inline]
132    pub fn try_lock(&'static self) -> std::sync::TryLockResult<MutexGuard<'static, T>> {
133        if is_shell_thread() {
134            self.shell.try_lock()
135        } else {
136            self.own().try_lock()
137        }
138    }
139
140    /// Mirrors `Mutex::clear_poison`.
141    pub fn clear_poison(&'static self) {
142        if is_shell_thread() {
143            self.shell.clear_poison();
144        } else {
145            self.own().clear_poison();
146        }
147    }
148
149    /// This thread's half, created on first use.
150    fn own(&'static self) -> &'static Mutex<T> {
151        let id = std::thread::current().id();
152        let mut guard = self.others.lock().unwrap_or_else(|e| e.into_inner());
153        let map = guard.get_or_insert_with(HashMap::new);
154        if let Some(slot) = map.get(&id) {
155            return slot;
156        }
157        let slot: &'static Mutex<T> = Box::leak(Box::new(Mutex::new((self.seed)())));
158        map.insert(id, slot);
159        slot
160    }
161}
162
163/// `locallevel` storage: shared for parameter scoping, per-thread for
164/// diagnostics.
165///
166/// !!! WARNING: RUST-ONLY !!! C has one `int locallevel`
167/// (`Src/params.c:54`) serving both jobs, because with one thread they are
168/// the same number.
169///
170/// zshrs cannot simply make it per-thread. `locallevel` is the level a
171/// parameter is STAMPED with (`Src/params.c:5837` `locallevel++` in
172/// `startparamscope`) and the level `scanendscope` compares every entry
173/// against on scope exit (`Src/params.c:5904-5907`, `if (pm->level >
174/// locallevel)`) — and the param table those stamps live in is one shared
175/// table that `async_precmd` hooks deliberately write into. Give each
176/// thread its own counter and two threads both stamp level 1 in the same
177/// table, so one thread's `endparamscope` deletes the other's locals: a
178/// strictly worse version of the `$BUFFER` loss `quiesce` was added for.
179/// So [`load`](Self::load) — every parameter-scoping reader — keeps
180/// returning the shared value, unchanged.
181///
182/// What a worker must NOT contaminate is the other job: the
183/// "am I inside a function" test the error formatters make
184/// (`Src/utils.c:150` `if (unset(SHINSTDIN) || locallevel)`, `:301` in
185/// `zerrmsg`). A hook running on a worker raised the shared counter, and
186/// an error the user caused at top level came out formatted as if it had
187/// happened inside a function — with a line number and a function-name
188/// prefix zsh does not print. [`here`](Self::here) answers that question
189/// for the CALLING thread, and the formatters read it.
190///
191/// Every mutation goes through this type, so the per-thread depth cannot
192/// drift away from the sites that move the shared counter: the four
193/// methods below are the entire mutation surface of `params::locallevel`.
194pub struct LocalLevelCell {
195    /// The stamp shared with the one param table. Read by every scoping
196    /// site exactly as the `AtomicI32` it replaced was.
197    shared: AtomicI32,
198}
199
200impl LocalLevelCell {
201    /// A top-level counter. `const` so `locallevel` stays a plain `static`.
202    pub const fn new() -> Self {
203        LocalLevelCell {
204            shared: AtomicI32::new(0),
205        }
206    }
207
208    /// Mirrors `AtomicI32::load` — the SHARED counter, for param scoping.
209    #[inline]
210    pub fn load(&self, order: Ordering) -> i32 {
211        self.shared.load(order)
212    }
213
214    /// Mirrors `AtomicI32::store`. Sites that save and restore the whole
215    /// counter (`computil`'s `comparguments` bracket, `param_private`'s
216    /// hoist) mean "this thread is now at level v", so the thread's own
217    /// depth follows.
218    #[inline]
219    pub fn store(&self, val: i32, order: Ordering) {
220        OWN_LOCALLEVEL.with(|d| d.set(val));
221        self.shared.store(val, order);
222    }
223
224    /// Mirrors `AtomicI32::fetch_add`, returning the previous SHARED value.
225    #[inline]
226    pub fn fetch_add(&self, val: i32, order: Ordering) -> i32 {
227        OWN_LOCALLEVEL.with(|d| d.set(d.get() + val));
228        self.shared.fetch_add(val, order)
229    }
230
231    /// Mirrors `AtomicI32::fetch_sub`, returning the previous SHARED value.
232    #[inline]
233    pub fn fetch_sub(&self, val: i32, order: Ordering) -> i32 {
234        OWN_LOCALLEVEL.with(|d| d.set(d.get() - val));
235        self.shared.fetch_sub(val, order)
236    }
237
238    /// The calling thread's OWN function-scope depth — what C's
239    /// `locallevel` would read if this thread were the only one, which is
240    /// what the error formatters need. Clamped at zero: a thread whose
241    /// first act is `endparamscope` (a scope opened before it existed)
242    /// would otherwise report a negative depth.
243    #[inline]
244    pub fn here(&self) -> i32 {
245        OWN_LOCALLEVEL.with(|d| d.get()).max(0)
246    }
247}
248
249impl Default for LocalLevelCell {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    /// The regression this module exists for, in miniature: a worker
260    /// inside a hook function must not be able to move the shell thread's
261    /// view of where the shell is.
262    #[test]
263    fn a_workers_frames_do_not_reach_the_shell_threads_view() {
264        static STACK: ThreadMutex<Vec<String>> = ThreadMutex::new(Vec::new(), Vec::new);
265
266        STACK.lock().unwrap().push("shell-frame".to_string());
267
268        std::thread::spawn(|| {
269            assert!(
270                STACK.lock().unwrap().is_empty(),
271                "a worker starts with its own stack, as a forked child would"
272            );
273            STACK.lock().unwrap().push("hook".to_string());
274        })
275        .join()
276        .unwrap();
277
278        let seen = STACK.lock().unwrap().clone();
279        assert_eq!(
280            seen,
281            vec!["shell-frame".to_string()],
282            "the worker's frame leaked into the shell thread's stack"
283        );
284    }
285
286    /// `load()` must stay shared — the param table's level stamps depend
287    /// on it — while `here()` reports only the calling thread's depth.
288    #[test]
289    fn locallevel_shares_the_stamp_and_splits_the_depth() {
290        static LL: LocalLevelCell = LocalLevelCell::new();
291        LL.store(0, Ordering::Relaxed);
292
293        std::thread::spawn(|| {
294            LL.fetch_add(1, Ordering::Relaxed); // the hook's function scope
295            assert_eq!(LL.here(), 1, "the worker is one scope deep");
296        })
297        .join()
298        .unwrap();
299
300        assert_eq!(
301            LL.load(Ordering::Relaxed),
302            1,
303            "the shared stamp must still carry the worker's scope"
304        );
305        assert_eq!(
306            LL.here(),
307            0,
308            "this thread is at top level and its diagnostics must say so"
309        );
310    }
311}