zshrs 0.13.42

The first JIT-compiled Unix shell — bytecode VM, Cranelift JIT, worker pool, AOP intercept, Rkyv caching
Documentation
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! Shared test infrastructure for serializing tests that touch
//! crate-level global mutable state (errflag, paramtab, lex
//! buffers, zcontext stacks, paramtab_hashed_storage, ShellExecutor
//! singletons, etc.).
//!
//! Many ported subsystems intentionally mirror C's file-static
//! globals via `OnceLock<Mutex<…>>` / `AtomicI32`. Tests against
//! those subsystems share the same singletons. Parallel cargo
//! test execution races on read-modify-write patterns even when
//! each individual test cleans up afterwards, because two tests
//! observe each other's mid-test state.
//!
//! Pattern: tests that touch global state call
//! `let _g = crate::test_util::global_state_lock();` at entry.
//! The MutexGuard is held for the test's lifetime, serializing
//! against every other test that does the same — purely-functional
//! tests still run in parallel.
//!
//! Mutex-poisoning is recovered automatically so a single panicking
//! test doesn't break the whole suite.

use std::sync::{Mutex, MutexGuard, OnceLock};

fn lock() -> &'static Mutex<()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

/// Acquire the crate-wide test mutex. Tests that touch global
/// state (errflag, paramtab, lex buffers, zcontext, ShellExecutor,
/// etc.) hold this guard for their duration to serialize against
/// other stateful tests.
pub fn global_state_lock() -> MutexGuard<'static, ()> {
    let g = match lock().lock() {
        Ok(g) => g,
        Err(poisoned) => poisoned.into_inner(),
    };
    // Seed the option table with zsh's own defaults. A test binary
    // never runs `parseargs`/`setupvals`, so `OPTS_LIVE` starts EMPTY
    // and every `isset(X)` reports false — including options zsh
    // enables by default (UNSET, EXECOPT, PROMPTPERCENT, CASEMATCH…).
    // That made `let ZL_X=5` fail with "parameter not set" (NO_UNSET
    // semantics) whenever no earlier test had happened to populate the
    // table as a side effect. `emulate("zsh", fully)` is the canonical
    // populator (Src/options.c:533 → installemulation c:523), and
    // running it per-test also rolls back whatever options the previous
    // test flipped.
    crate::ported::options::emulate("zsh", true);
    // `assignstrvalue` (and downstream `setsparam`/`setiparam`/etc.)
    // bails out at the top with `if unset(EXECOPT) return;`. The
    // option default is OFF in test builds, so without enabling it
    // here every test that calls a param-setter sees a no-op write.
    // Real shells enable EXECOPT at init via `parseargs` (init.c:404).
    crate::ported::options::opt_state_set("exec", true);
    // `promptpercent` and `promptbang` are zsh-default-ON (Src/options.c
    // default_opts[] — both enabled in PROMPTPERCENT/PROMPTBANG default
    // state). The Rust port reads them via `isset(PROMPTPERCENT)` /
    // `isset(PROMPTBANG)` in the prompt expander; without this the
    // option table comes up clean and every `%X`/`!`-style expansion
    // is silently disabled.
    crate::ported::options::opt_state_set("promptpercent", true);
    crate::ported::options::opt_state_set("promptbang", true);
    // `inittyptab` (Src/utils.c:4155) populates the global character
    // classification table (`typtab[]`) — IIDENT, IALNUM, IDIGIT, ISEP,
    // IWORD, etc. C calls it once at startup in `setupvals` (init.c) —
    // without it `zistype('i', IIDENT)` returns false and `itype_end`
    // can't parse identifiers, so `fetchvalue("intvar", …)` returns
    // None even when the param exists in paramtab. Mirror the C startup
    // call so any test that touches the param/lex pipelines sees a
    // fully initialised table.
    crate::ported::utils::inittyptab();
    // Clear `errflag` so a previous test that errored doesn't leak its
    // ERRFLAG_ERROR / ERRFLAG_INT bits into this test's lex/parse/math
    // pipeline. `errflag` is a process-wide `AtomicI32` (utils.rs); any
    // test that calls `zerr()` sets it, and the next test sees a
    // non-zero value at every "if errflag != 0 return LEXERR" gate
    // (lex.rs:1064, parse.rs, math.rs, etc.). C zsh resets it at the
    // top of every `loop` iteration in `init.c::zsh_main`; mirror that
    // here so each test starts with a clean error state.
    crate::ported::utils::errflag.store(0, std::sync::atomic::Ordering::Relaxed);
    // Reset options that other tests temporarily flip. The lock
    // serialises but doesn't restore on panic — a test that sets
    // `octalzeroes=true` and panics before its restore leaves the
    // option ON for every subsequent test. Force OFF here so each test
    // starts from a deterministic default. Add other "test-toggled"
    // options here as they surface as cross-test interference.
    crate::ported::options::opt_state_set("octalzeroes", false);
    crate::ported::options::opt_state_set("cbases", false);
    // ksharrays flips array subscript base from 1 (zsh default) to 0
    // (ksh emulation). Other tests temporarily enable it and the lock
    // doesn't roll back on panic. Force OFF so `(( arr[2]=N ))` writes
    // the 2nd (1-indexed) element as zsh expects.
    crate::ported::options::opt_state_set("ksharrays", false);
    // `casematch` defaults ON in real zsh (verified via `/bin/zsh -fc
    // 'echo $options[casematch]'` → "on"). Without it stamped here,
    // `zcond_regex_match` and other case-sensitivity-gated paths see
    // the Rust-default `false` and silently swap to case-insensitive,
    // breaking the `[[ abc =~ ABC ]] → no-match` pin.
    crate::ported::options::opt_state_set("casematch", true);
    // Reset emulation to EMULATE_ZSH so `init_builtins` (called by
    // builtin tests) doesn't disable the `repeat` reswd in reswdtab
    // for every subsequent test that tries to parse `repeat N do …`.
    crate::ported::options::emulation.store(
        crate::ported::zsh_h::EMULATE_ZSH,
        std::sync::atomic::Ordering::Relaxed,
    );
    // Re-enable `repeat` in reswdtab — once a prior test ran
    // init_builtins under non-zsh emulation, the disable persists in
    // the process-wide table. tab.enable is the C `disablenode(hn, 1)`
    // equivalent (Src/hashtable.c::sethashnode_disable_state).
    if let Ok(mut tab) = crate::ported::hashtable::reswdtab_lock().write() {
        tab.enable("repeat");
    }
    load_test_terminal();
    detach_test_terminal();
    restore_test_locale();
    g
}

/// Load the terminal the capability-reading tests measure in, once per test
/// binary.
///
/// `%B` is `tcstr[TCBOLDFACEBEG]`, `echoti bold` reads the terminfo entry and
/// `gettermcap co` reads the termcap one; all three come from `init_term()`,
/// which fills them from the entry `$TERM` names. A test binary runs none of
/// zsh's startup, and a CI runner has no tty and no `TERM` at all — so every
/// capability came back empty there and `%B` expanded to nothing, while a
/// developer machine's own terminal made the same assertions pass. The terminal
/// is a fact about the environment, so the harness states it rather than
/// inheriting whatever the shell that started `cargo test` was using.
///
/// Three names, in order, because a minimal image carries only the older ones:
/// each of `xterm-256color`, `xterm` and `vt100` gives the capabilities these
/// tests pin (measured: the same `\e[1m`, `\e[4m` and `\e[0m` from all three).
/// If none of them loads there is no terminfo database at all, and the tests
/// that read a capability fail on that, which is what a missing database
/// deserves.
fn load_test_terminal() {
    static LOADED: OnceLock<()> = OnceLock::new();
    LOADED.get_or_init(|| {
        for term in ["xterm-256color", "xterm", "vt100"] {
            crate::ported::params::setsparam("TERM", term);
            if crate::ported::init::init_term() == 1 {
                // `zsh/terminfo` asks the ENVIRONMENT rather than the parameter
                // table — `setupterm(NULL)` is ncurses' own lookup, and the port
                // keeps it — so the environment has to say what the table says or
                // `${terminfo[bold]}` answers for a terminal nobody selected.
                //
                // SAFETY: once per process, from inside the crate-wide test lock,
                // before any test has read the variable.
                unsafe { std::env::set_var("TERM", term) };
                break;
            }
        }
    });
}

/// Detach the test binary from any controlling terminal and point its stdin
/// at `/dev/null`, once per process.
///
/// Unit tests that drive the shell's input layer (`inputline` → `shingetline`
/// → `read(SHIN)`, `SHIN` = 0) read the process's real fd 0. Under a terminal
/// that is the tty, and a test binary in a background process group (a CI
/// runner's job, or `cargo test &`) gets SIGTTIN on that read: the WHOLE
/// process stops, libtest included, and the run goes silent until the job
/// timeout. Reproduced on macOS by running the lib tests in a background group
/// under a pty: stopped after ~130 s with
/// `ported::input::tests::inputline_deterministic_on_idle_state` in `read`
/// (`input.rs:250`). With no tty (the ubuntu runner) the same read sees EOF.
/// Redirecting fd 0 is not enough on its own: other tests reach the terminal
/// through `/dev/tty` (`SHTTY`) and `tcsetattr`, which raise SIGTTOU the same
/// way (the same reproduction then stopped at ~200 s instead). So the harness
/// gives up the controlling terminal — `TIOCNOTTY` from a process that is not
/// the session leader only drops the association — after which no job-control
/// stop can reach the process, `/dev/tty` cannot be opened, and the binary runs
/// in the environment the ubuntu runner already provides. Input and the
/// terminal are facts about the environment, so the harness states them, as
/// `load_test_terminal` does for `TERM`; tests that need a terminal open their
/// own pty, and tests that need data on fd 0 dup a pipe onto it and restore it.
fn detach_test_terminal() {
    static DONE: OnceLock<()> = OnceLock::new();
    DONE.get_or_init(|| unsafe {
        let fd = libc::open(c"/dev/null".as_ptr(), libc::O_RDONLY);
        if fd > 0 {
            libc::dup2(fd, 0);
            libc::close(fd);
        }
        let tty = libc::open(c"/dev/tty".as_ptr(), libc::O_RDWR | libc::O_NOCTTY);
        if tty >= 0 {
            if libc::getsid(0) != libc::getpid() {
                libc::ioctl(tty, libc::TIOCNOTTY as _);
            }
            libc::close(tty);
        }
    });
}

/// Put the process locale back to the one the environment selects, before
/// every stateful test.
///
/// The locale is process-wide C library state, and tests that pin `LC_ALL=C`
/// behaviour (`params.rs`, `computil.rs`, `compcore.rs`, `zle_params.rs`)
/// call `setlocale` and do not all put it back — a test that panics before its
/// restore never does. The next test then runs in `C`: under it a UTF-8 value
/// is sliced and case-mapped per byte, e.g.
/// `paramsubst_zsh_corpus_multibyte_slice_first_three` got
/// `"t\u{83}ã\u{83}\u{89}"` for `${ZMSL[1,3]}` of `ténébreux`, failing on CI and
/// locally in a full run while passing alone. zsh selects its locale once at
/// startup (c:Src/init.c:1208 `setlocale(LC_ALL, "")`); the harness does the
/// same once and restores that choice per test, like the option reset above.
fn restore_test_locale() {
    static STARTUP: OnceLock<std::ffi::CString> = OnceLock::new();
    let name = STARTUP.get_or_init(|| unsafe {
        let p = libc::setlocale(libc::LC_ALL, c"".as_ptr());
        if p.is_null() {
            c"C".to_owned()
        } else {
            std::ffi::CStr::from_ptr(p).to_owned()
        }
    });
    unsafe { libc::setlocale(libc::LC_ALL, name.as_ptr()) };
}

/// Reset the completion-machinery globals a `compadd`-driven completion
/// unit test depends on, so its result reflects the test's own inputs
/// rather than whatever the previous test in the same binary left behind.
///
/// [`global_state_lock`] serialises stateful tests but restores nothing,
/// and the completion subsystem is almost entirely process-wide state:
///
/// * `$PREFIX` / `$SUFFIX` / `$IPREFIX` / `$ISUFFIX`. `addmatches`
///   re-seeds the `compprefix` / `compsuffix` / `compiprefix` /
///   `compisuffix` globals from those parameters whenever `incompfunc`
///   is set (`src/ported/zle/compcore.rs:4334-4347`) and then matches
///   every `CAF_MATCH` candidate against them
///   (`compcore.rs:4360-4373`, `Src/Zle/compcore.c:2253-2300`). A dozen
///   completion tests park a non-empty word there and never clear it
///   (`_absolute_command_paths.rs:168` `"ls"`, `_ldap_filters.rs:267`
///   `"-x"`, `_debbugs_bugnumber.rs:142` `"notabug"`, …); after any of
///   them EVERY candidate a later `compadd` offers fails to match, so
///   `compadd` returns 1 and `_all_labels` / `_wanted` report "no
///   matches" for a tag set that was registered perfectly well.
/// * `comptags[]`, which `bin_comptags` indexes by `locallevel`
///   (`Src/Zle/computil.c:3782` "Array of tag-set infos. Index is the
///   locallevel", ported at `computil.rs:6886`), plus `locallevel`
///   itself — nothing unwinds it when a test panics out of a
///   `doshfunc`-shaped port.
/// * the `zstyle` table, which `_hosts` / `_domains` / `_completers` /
///   `_call_program` all consult for their candidate lists.
/// * the three process-wide `compadd` shadows `_approximate` and
///   `_complete_help` install (`src/ported/zle/complete.rs:975-982`,
///   `:1043-1048`).
///
/// Every half of this is the boot/teardown entry point zsh itself uses,
/// not a bespoke reset: `Src/Zle/complete.c:1788 finish_` zsfree's each
/// `comp*` string global, `Src/Zle/computil.c:5124 setup_` zeroes
/// `comptags[]` and `lasttaglevel`, and `zstyle -d` with no pattern is
/// `zstyletab->emptytable` (`Src/Modules/zutil.c:639-640`).
pub fn reset_completion_state() {
    use std::sync::atomic::Ordering;
    // c:Src/Zle/complete.c:1788 — clears compprefix/compsuffix/compiprefix/
    // compisuffix/compqiprefix/compqisuffix/compquote/compqstack/complist
    // and compwords.
    let _ = crate::ported::zle::complete::finish_(std::ptr::null());
    // c:Src/Zle/computil.c:5124 — `memset(comptags, 0, sizeof(comptags))`
    // plus `lasttaglevel = 0`.
    let _ = crate::ported::zle::computil::setup_();
    // The globals cleared above are re-seeded FROM these parameters at
    // compcore.rs:4334-4347, so clearing only the globals would leave the
    // stale word to come straight back. `curcontext` selects every style
    // context; `expl`, `_comp_tags`, `_tags_level`, `_next_tags_not` and
    // `_sort_tags` are the bookkeeping parameters `_tags` / `_all_labels`
    // read (`_all_labels.rs:291-323`, `_tags.rs:157`, `_tags.rs:213`).
    for p in [
        "PREFIX",
        "SUFFIX",
        "IPREFIX",
        "ISUFFIX",
        "QIPREFIX",
        "QISUFFIX",
        "curcontext",
        "expl",
        "_comp_tags",
        "_tags_level",
        "_next_tags_not",
        "_sort_tags",
    ] {
        crate::ported::params::unsetparam(p);
    }
    // c:Src/params.c:54 `locallevel` — the `comptags[]` index.
    crate::ported::params::locallevel.store(0, Ordering::Relaxed);
    // c:Src/Zle/complete.c:41 `compignored` — `$compstate[ignored]`, the
    // gate `_ignored` fires on.
    crate::ported::zle::complete::COMPIGNORED.store(0, Ordering::Relaxed);
    // complete.rs:975-982 / :983-1002 / :1043-1048 — the shell-function,
    // argv-rewrite and trace shadows over `compadd`. All three make the
    // builtin return 1 (or swallow the call) for reasons that have
    // nothing to do with the caller under test.
    crate::ported::zle::complete::set_compadd_trace(false);
    crate::ported::zle::complete::clear_compadd_prefix_injector();
    if let Ok(mut g) = crate::ported::zle::complete::COMPADD_ARGV_SHADOW.lock() {
        *g = None;
    }
    // c:Src/Modules/zutil.c:639-640 — `zstyle -d` with no pattern is
    // `zstyletab->emptytable`, i.e. drop every style.
    let ops = crate::ported::zsh_h::options {
        ind: [0u8; crate::ported::zsh_h::MAX_OPS],
        args: Vec::new(),
        argscount: 0,
        argsalloc: 0,
    };
    let _ = crate::ported::modules::zutil::bin_zstyle("zstyle", &["-d".to_string()], &ops, 0);
}

/// Install one `zstyle` for the duration of a test, the way a user would
/// write it on the command line: `zstyle <context> <style> <value…>`
/// (`Src/Modules/zutil.c:606-616` — the no-flag arm is `setstyle`).
///
/// Completion functions that shell out through `_call_program` take the
/// command line from the `command` style
/// (`Completion/Base/Utility/_call_program:26`, ported at
/// `_call_program.rs:74-101`), so this is the upstream-sanctioned way to
/// give such a port a fixed candidate list instead of whatever the host
/// machine's `ifconfig` / `global` happens to print.
pub fn set_test_zstyle(context: &str, style: &str, value: &str) {
    let ops = crate::ported::zsh_h::options {
        ind: [0u8; crate::ported::zsh_h::MAX_OPS],
        args: Vec::new(),
        argscount: 0,
        argsalloc: 0,
    };
    let _ = crate::ported::modules::zutil::bin_zstyle(
        "zstyle",
        &[context.to_string(), style.to_string(), value.to_string()],
        &ops,
        0,
    );
}

/// Put the shell on a truecolor terminal for the duration of a test, with
/// zsh/nearcolor unloaded; restores `$.term.extensions` on drop.
///
/// `match_colour` (c:Src/prompt.c:1988-1996) emits a `#RRGGBB` colour as
/// 24-bit only when no `get_color_attr` hook answers. zsh/nearcolor is that
/// hook, and with `$.term.extensions` not listing `truecolor` the C code loads
/// it on the spot (c:1990-1992), so the colour is quantized to the 256-colour
/// palette. Both are process-wide state another test may have left behind.
/// Measured with the master oracle:
///     zsh -fc 'print -rP "%F{#f00}x"'                         → \e[38;5;196m
///     zsh -fc '.term.extensions=(truecolor); print -rP …'      → \e[38;2;255;0;0m
///     zsh -fc 'zmodload zsh/nearcolor; .term.extensions=(truecolor); …' → \e[38;5;196m
/// A test pinning the 24-bit branch states both preconditions with this.
pub struct TruecolorTerminal {
    saved: Option<Vec<String>>,
}

pub fn truecolor_terminal() -> TruecolorTerminal {
    let saved = crate::ported::params::getaparam(".term.extensions");
    let _ = crate::ported::params::setaparam(".term.extensions", vec!["truecolor".to_string()]);
    let mut tab = crate::ported::module::MODULESTAB.lock().unwrap();
    if crate::ported::module::module_loaded(&tab, "zsh/nearcolor") != 0 {
        crate::ported::module::unload_named_module(&mut tab, "zsh/nearcolor", "zmodload", 1);
    }
    TruecolorTerminal { saved }
}

impl Drop for TruecolorTerminal {
    fn drop(&mut self) {
        match self.saved.take() {
            Some(v) => {
                let _ = crate::ported::params::setaparam(".term.extensions", v);
            }
            None => {
                crate::ported::params::unsetparam(".term.extensions");
            }
        }
    }
}

/// A live completion scope for a test: what `callcompfunc` opens around a
/// completion function (`Src/Zle/compcore.c:814-818` — `startparamscope();
/// makecompparams(); comp_setunset(…)`). `$PREFIX`, `$words`, `$CURRENT`,
/// the other `comprparams[]` params and the `$compstate` hash exist only
/// inside it, as gsu views of the `complete.c` globals. Dropping the guard
/// ends the scope (`endparamscope()`).
pub struct CompScope;

/// Open a [`CompScope`] with every `$compstate` key set, as a completion
/// function sees them after `comp_setunset(CP_ALLREALS, 0, CP_ALLKEYS, 0)`.
pub fn comp_scope() -> CompScope {
    use crate::ported::zle::comp_h::{CP_ALLKEYS, CP_ALLREALS, CP_KEYPARAMS, CP_REALPARAMS};
    use crate::ported::zle::complete::{comp_setunset, compkpms, comprpms, makecompparams};
    *comprpms.lock().unwrap() = Some(vec![None; CP_REALPARAMS as usize]);
    *compkpms.lock().unwrap() = Some(vec![None; CP_KEYPARAMS as usize]);
    let mut scope = crate::ported::params::newparamtable(1, "scope").unwrap();
    crate::ported::params::startparamscope(&mut scope);
    makecompparams();
    comp_setunset(CP_ALLREALS as i32, 0, CP_ALLKEYS as i32, 0);
    CompScope
}

impl Drop for CompScope {
    fn drop(&mut self) {
        crate::ported::params::endparamscope();
        *crate::ported::zle::complete::comprpms.lock().unwrap() = None;
        *crate::ported::zle::complete::compkpms.lock().unwrap() = None;
    }
}