1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//! `async_precmd` hook — run precmd-style functions on a POOL WORKER THREAD so
//! they never block prompt rendering.
//!
//! zsh's `precmd` hooks run synchronously before the prompt paints, so a slow
//! hook stalls the prompt. `async_precmd` is a new lifecycle hook (no zsh
//! equivalent): its functions run on the shared worker pool AFTER the prompt is
//! built/rendered, writing their results into the shared, `RwLock`-synchronized
//! global param table. The prompt reads whatever is currently there and never
//! waits — a slow segment simply updates a prompt or two later.
//!
//! Registration mirrors zsh's hook arrays:
//! * a function literally named `async_precmd`, and/or
//! * members of the `async_precmd_functions` array.
//!
//! Built on the Phase-1 [`crate::vm_helper::ShellExecutor::new_worker`]
//! lightweight worker executor. No isolation is needed here: `async_precmd`
//! WANTS its `typeset -g` writes to land in the shared table.
//!
//! ## The batch never overlaps shell code on the shell thread
//!
//! A hook is a shell FUNCTION, and running one moves state C keeps for a
//! single thread of execution: `locallevel` (`Src/params.c:5837`
//! `locallevel++` in `startparamscope`, `:5856` `locallevel--` in
//! `endparamscope`) and the level-stamped entries of the one `paramtab`,
//! which `scanendscope` (`Src/params.c:5904-5907`, `if (pm->level >
//! locallevel)`) restores or deletes on every scope exit. The worker's
//! pushes and pops land on the same counter and the same table as the
//! shell thread's.
//!
//! The batch is fired from `preprompt()`, and the first thing the shell
//! thread does after the prompt paints is usually run a widget: a
//! shell-function widget brackets its body with `startparamscope();
//! makezleparams(0); … endparamscope();` (`Src/Zle/zle_main.c:1533-1540`),
//! and `makezleparams` stamps `$BUFFER` and its family with the scope level
//! (`Src/Zle/zle_params.c:206`). A worker scope exit that lands inside that
//! window deletes the widget's `$BUFFER`, and the write-back in
//! `zle_param_sync::sync_from_paramtab` then copies the now-empty value
//! into the editor: every character typed so far on the line disappears.
//! Measured with a self-insert wrapper (`zle -N self-insert f; f() { zle
//! .self-insert }`, the shape zpwr's `zpwrSelfInsert` has) and a hook that
//! runs for half a second: most typed lines lost a prefix, and a trace
//! showed the worker's `endparamscope` removing `BUFFER` at the widget's
//! level immediately before the editor was overwritten with "".
//! A top-level `typeset X=1` typed during the batch was stamped with the
//! worker's level and vanished when the hook returned.
//!
//! So shell code on the shell thread waits for a batch that is already
//! running ([`quiesce`]), and withdraws one the pool has not started yet.
//! Plain typing is not held up: builtin widgets never open a scope. The
//! first scope usually opens after the prompt is on screen; a
//! `zle-line-init` widget is the exception, because this port's `zleread`
//! runs it before its first `zrefresh` (C paints first, c:1353, then calls
//! the hook, c:1357), so a running batch delays that paint by the hook's
//! runtime.
use ;
use ;
/// The session's shared worker pool, published by `ShellExecutor::new()` at
/// startup. `preprompt()` runs BETWEEN commands where the thread_local
/// `CURRENT_EXECUTOR` is not set, so `try_with_executor` returns `None` there —
/// this global handle reaches the pool without an executor context.
static SESSION_POOL: = new;
/// Publish the session worker pool. Called once from `ShellExecutor::new()`.
/// The session pool, for callers with no executor context (the p10k
/// render runs from `preprompt()`, between commands).
/// Where the current batch is. Debounce: while it is not [`IDLE`], the next
/// prompt skips its round rather than pile up overlapping runs of the same
/// hooks.
static BATCH: AtomicU8 = new;
/// No batch in flight.
const IDLE: u8 = 0;
/// Submitted to the pool, not started. [`quiesce`] may withdraw it.
const QUEUED: u8 = 1;
/// A worker is running the hooks. [`quiesce`] waits for it.
const RUNNING: u8 = 2;
/// Paired with [`BATCH_DONE`]: the worker flips `BATCH` back to [`IDLE`]
/// while holding it, so a waiter that checked `BATCH` under the same lock
/// cannot miss the wake-up.
static BATCH_LOCK: = new;
static BATCH_DONE: Condvar = new;
/// Returns the batch to [`IDLE`] and wakes [`quiesce`] however the worker
/// leaves the hooks — including by unwinding out of a panicking one, which
/// would otherwise leave the shell thread waiting forever.
;
/// Make the shell thread safe to run shell code: wait for a batch a worker
/// is running, or withdraw one that is only queued (its hooks run on the
/// next prompt instead). Returns at once when no batch is in flight, and on
/// any thread but the shell's — a worker must never wait on its own batch.
///
/// Called where the shell thread opens a parameter scope
/// (`utils::inc_locallevel`, every function, widget, hook and trap call)
/// and before it executes an accepted line at top level (`init::loop_`).
/// After it returns no batch can start until the next `preprompt()`, since
/// that is the only place one is fired.
/// Collect the registered `async_precmd` hook function names: the function
/// literally named `async_precmd` (if defined) followed by every member of the
/// `async_precmd_functions` array, in order.
///
/// The hook functions must live in the shared global `shfunctab` for a worker
/// to run them — which is the case for functions defined by SOURCED config
/// (.zshrc / plugins), the normal way hooks are registered. (A function TYPED
/// at the interactive prompt currently takes a compile path that registers it
/// only in the per-executor table, so a worker can't see it — not the intended
/// registration route for a hook.)
/// Fire the `async_precmd` hooks on a worker thread. Called from `preprompt()`
/// AFTER precmd + prompt render, so the prompt is already on screen. Returns
/// immediately (non-blocking): it submits ONE closure to the shared worker pool
/// and lets it run in the background. Debounced via [`BATCH`].