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}