Skip to main content

zsh/ported/
init.rs

1//! init.c - main loop and initialization routines
2//!
3//! Port of Src/init.c
4
5use std::sync::atomic::{AtomicI32, AtomicU32, AtomicUsize, Ordering};
6use std::sync::Mutex;
7
8use crate::ported::builtin::{realexit, LASTVAL, RETFLAG, STOPMSG};
9use crate::ported::context::zcontext_restore;
10use crate::ported::hist::{curhist, curline, hbegin, hend, hist_ring, histlinect, stophist};
11use crate::ported::lex::{set_tok, tok, ENDINPUT};
12use crate::ported::mem::popheap;
13use crate::ported::options::{dosetopt, emulation};
14use crate::ported::params::{getsparam, TERMFLAGS};
15use crate::ported::signals::{
16    dotrap, install_handler, intr, queue_signals, signal_ignore, sigtrapped, unqueue_signals,
17};
18use crate::ported::signals_h::dont_queue_signals;
19use crate::ported::text::getpermtext;
20use crate::ported::utils::{callhookfunc, errflag, movefd, unmeta, ERRFLAG_ERROR};
21use crate::ported::zsh_h::{
22    eprog, hookdef, interact, islogin, isset, jobbing, CONTINUEONERROR, EMULATE_KSH,
23    EMULATE_SH, GLOBALRCS, HISTBEEP, HISTIGNOREDUPS, HIST_DUP, HIST_TMPSTORE, HOOKF_ALL,
24    HOOK_SUFFIX, HUP, IGNOREEOF, INTERACTIVE, LEXERR, PRIVILEGED, RCS, SHINSTDIN, SINGLECOMMAND,
25    TERM_BAD, TERM_NOUP, TERM_UNKNOWN, ZEXIT_NORMAL, ZLE_CMD_POSTEXEC, ZLE_CMD_PREEXEC,
26};
27// =========================================================================
28// File-scope globals from init.c
29// =========================================================================
30
31/// Port of `int noexitct` from Src/init.c:44.
32pub static noexitct: AtomicI32 = AtomicI32::new(0); // c:44
33
34// buffer for $_ and its length                                              // c:46
35
36/// Port of `char *zunderscore` from Src/init.c:49.
37pub static zunderscore: Mutex<String> = Mutex::new(String::new()); // c:49
38
39/// Port of `size_t underscorelen` from Src/init.c:52.
40pub static underscorelen: AtomicUsize = AtomicUsize::new(0); // c:52
41
42/// Port of `int underscoreused` from Src/init.c:55.
43pub static underscoreused: AtomicI32 = AtomicI32::new(0); // c:55
44
45// what level of sourcing we are at                                          // c:57
46
47/// Port of `int sourcelevel` from Src/init.c:60.
48pub static sourcelevel: AtomicI32 = AtomicI32::new(0); // c:60
49
50// the shell tty fd                                                          // c:62
51
52/// Port of `mod_export int SHTTY` from Src/init.c:65.
53pub static SHTTY: AtomicI32 = AtomicI32::new(-1); // c:65
54
55// the FILE attached to the shell tty                                        // c:67
56// `mod_export FILE *shout;` (c:70). zshrs writes the tty through
57// `crate::shout` on SHTTY rather than a stdio stream, so this holds only
58// what C tests the pointer for: 0 is NULL, otherwise the fd the stream
59// was opened on, plus one (`shout = stderr` stores 3).
60pub static shout: Mutex<usize> = Mutex::new(0); // c:70
61
62// termcap strings                                                           // c:72
63
64/// Port of `mod_export char *tcstr[TC_COUNT]` from Src/init.c:75.
65pub static tcstr: Mutex<[String; 39]> = Mutex::new([
66    // c:75
67    String::new(),
68    String::new(),
69    String::new(),
70    String::new(),
71    String::new(),
72    String::new(),
73    String::new(),
74    String::new(),
75    String::new(),
76    String::new(),
77    String::new(),
78    String::new(),
79    String::new(),
80    String::new(),
81    String::new(),
82    String::new(),
83    String::new(),
84    String::new(),
85    String::new(),
86    String::new(),
87    String::new(),
88    String::new(),
89    String::new(),
90    String::new(),
91    String::new(),
92    String::new(),
93    String::new(),
94    String::new(),
95    String::new(),
96    String::new(),
97    String::new(),
98    String::new(),
99    String::new(),
100    String::new(),
101    String::new(),
102    String::new(),
103    String::new(),
104    String::new(),
105    String::new(),
106]);
107
108// lengths of each termcap string                                            // c:78
109
110/// Port of `mod_export int tclen[TC_COUNT]` from Src/init.c:81.
111pub static tclen: Mutex<[i32; 39]> = Mutex::new([0; 39]); // c:81
112
113// Values of the li, co and am entries                                       // c:82
114
115/// Port of `int tclines` from Src/init.c:85.
116pub static tclines: AtomicI32 = AtomicI32::new(0); // c:85
117
118/// Port of `int tccolumns` from Src/init.c:85.
119pub static tccolumns: AtomicI32 = AtomicI32::new(0); // c:85
120
121/// Port of `mod_export int hasam` from Src/init.c:87.
122pub static hasam: AtomicI32 = AtomicI32::new(0); // c:87
123
124/// Port of `int hasxn` from Src/init.c:89.
125pub static hasxn: AtomicI32 = AtomicI32::new(0); // c:89
126
127// Value of the Co (max_colors) entry: may not be set                        // c:91
128
129/// Port of `mod_export int tccolours` from Src/init.c:94.
130pub static tccolours: AtomicI32 = AtomicI32::new(0); // c:94
131
132// SIGCHLD mask                                                              // c:96
133// `mod_export sigset_t sigchld_mask;` — owned by signals layer.             // c:99
134
135/// Port of `struct hookdef zshhooks[]` from `Src/init.c:101-106`:
136/// ```c
137/// struct hookdef zshhooks[] = {
138///     HOOKDEF("exit", NULL, HOOKF_ALL),
139///     HOOKDEF("before_trap", NULL, HOOKF_ALL),
140///     HOOKDEF("after_trap", NULL, HOOKF_ALL),
141///     HOOKDEF("get_color_attr", NULL, HOOKF_ALL),
142/// };
143/// ```
144/// Stored as `AtomicPtr<hookdef>` holding the base pointer of a
145/// heap-leaked `[hookdef; 4]` so that `addhookdefs(NULL, zshhooks,
146/// 4)` at `setupvals()` (c:1085) can walk the array via `h++` exactly
147/// as the C call does. The leak is intentional — C's static-storage
148/// `zshhooks[]` has program lifetime, and the registered hookdef
149/// pointers must stay valid for any later `runhookdef` dispatch.
150pub static zshhooks: once_cell::sync::Lazy<
151    // c:101
152    std::sync::atomic::AtomicPtr<hookdef>,
153> = once_cell::sync::Lazy::new(|| {
154    let arr: Box<[hookdef; 4]> = Box::new([
155        hookdef {
156            // c:102
157            next: std::ptr::null_mut(),
158            name: "exit".to_string(),
159            def: None,
160            flags: HOOKF_ALL,
161            funcs: std::ptr::null_mut(),
162        },
163        hookdef {
164            // c:103
165            next: std::ptr::null_mut(),
166            name: "before_trap".to_string(),
167            def: None,
168            flags: HOOKF_ALL,
169            funcs: std::ptr::null_mut(),
170        },
171        hookdef {
172            // c:104
173            next: std::ptr::null_mut(),
174            name: "after_trap".to_string(),
175            def: None,
176            flags: HOOKF_ALL,
177            funcs: std::ptr::null_mut(),
178        },
179        hookdef {
180            // c:105
181            next: std::ptr::null_mut(),
182            name: "get_color_attr".to_string(),
183            def: None,
184            flags: HOOKF_ALL,
185            funcs: std::ptr::null_mut(),
186        },
187    ]);
188    let base = Box::into_raw(arr) as *mut hookdef;
189    std::sync::atomic::AtomicPtr::new(base)
190});
191
192/// !!! RUST-ONLY — NO C COUNTERPART !!!
193/// C registers `zshhooks` exactly once, from setupvals (c:1085). zshrs
194/// has two entry routes to that step: the `-c` route never runs
195/// setupvals, so ShellExecutor::new performs it; the stdin / script /
196/// interactive route runs ShellExecutor::new AND then zsh_main →
197/// setupvals. Both go through this guard so the second attempt does not
198/// re-add the same static hookdefs, which addhookdefs reports as
199/// `name clash when adding hook` (module.c:889).
200pub static ZSHHOOKS_ADDED: std::sync::Once = std::sync::Once::new();
201
202// original argv[0]. This is already metafied                                // c:258
203
204/// Port of `static char *argv0` from Src/init.c:259.
205static argv0: Mutex<String> = Mutex::new(String::new()); // c:259
206
207/// Port of `mod_export ZleEntryPoint zle_entry_ptr` from Src/init.c:1730.
208/// Stored as a usize representing a fn pointer (0 == NULL).
209pub static zle_entry_ptr: AtomicUsize = AtomicUsize::new(0); // c:1730
210
211/// Port of `mod_export int zle_load_state` from Src/init.c:1739.
212pub static zle_load_state: AtomicI32 = AtomicI32::new(0); // c:1739
213
214/// Port of `mod_export CompctlReadFn compctlreadptr` from Src/init.c:1831.
215pub static compctlreadptr: AtomicUsize = AtomicUsize::new(0); // c:1831
216
217/// Port of `mod_export int use_exit_printed` from Src/init.c:1846.
218pub static use_exit_printed: AtomicI32 = AtomicI32::new(0); // c:1846
219
220// =========================================================================
221// Static arrays from init.c
222// =========================================================================
223
224/// Port of `static char *tccapnams[TC_COUNT]` from Src/init.c:747.
225const tccapnams: [&str; 39] = [
226    // c:747
227    "cl", "le", "LE", "nd", "RI", "up", "UP", "do", "DO", "dc", "DC", "ic", "IC", "cd", "ce", "al",
228    "dl", "ta", "md", "mh", "so", "us", "ZH", "me", "se", "ue", "ZR", "ch", "ku", "kd", "kl", "kr",
229    "sc", "rc", "bc", "AF", "AB", "vi", "ve",
230];
231
232/// Bookkeeping for C's `2` option value (Src/init.c:365-372): INTERACTIVE,
233/// MONITOR and HASHDIRS start at 2, "on by default", so that `opts[X] &= 1`
234/// and `if (opts[X] == 2)` can tell a default from an explicit `-i`/`-m`.
235/// Option storage here is boolean; a set bit means the option still holds its
236/// default-on sentinel. Bit 0 INTERACTIVE, bit 1 MONITOR, bit 2 HASHDIRS.
237static OPT_DEFAULT_ON: AtomicU32 = AtomicU32::new(0);
238
239/// Clears the default-on sentinel of INTERACTIVE/MONITOR/HASHDIRS once
240/// `parseopts` sets the option explicitly (C stores 0 or 1 over the 2).
241macro_rules! note_explicit_opt {
242    ($optno:expr) => {
243        match ($optno as i32).abs() {
244            crate::ported::zsh_h::INTERACTIVE => {
245                OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst);
246            }
247            crate::ported::zsh_h::MONITOR => {
248                OPT_DEFAULT_ON.fetch_and(!2, Ordering::SeqCst);
249            }
250            crate::ported::zsh_h::HASHDIRS => {
251                OPT_DEFAULT_ON.fetch_and(!4, Ordering::SeqCst);
252            }
253            _ => {}
254        }
255    };
256}
257
258/// Port of `static void parseargs(...)` from Src/init.c:263.
259fn parseargs(
260    zsh_name: &str,
261    argv: &mut Vec<String>, // c:263
262    runscript: &mut Option<String>,
263    cmdptr: &mut Option<String>,
264) {
265    let mut idx: usize = 0; // c:265
266    let flags: i32 = 1; /* PARSEARGS_TOPLEVEL */
267    // c:267
268    let flags = if argv.first().map(|s| s.starts_with('-')).unwrap_or(false)
269    // c:268-269
270    {
271        flags | 2 /* PARSEARGS_LOGIN */
272    } else {
273        flags
274    };
275
276    // c:282 — `argv0 = argzero = posixzero = *argv++;` — all THREE are
277    // seeded from the kernel-supplied argv[0]. Only `argv0` was; `argzero`
278    // and `posixzero` stayed unset for an interactive shell, so `$0` read
279    // empty at the prompt where zsh reads `/bin/zsh`, and the empty value
280    // cascaded into `doshfunc`'s funcstack push: with `argzero` NULL,
281    // `funcsave->argv0` (c:6011) is None, so the outermost frame's
282    // `fstack.caller` fell through to a re-read of `argzero` — which
283    // doshfunc had just overwritten with the callee's own name (c:5986) —
284    // and `$functrace[-1]` reported `_main_complete:2` instead of
285    // `/bin/zsh:2`. The same None also skipped the c:6116 restore
286    // (`if let Some(saved) = funcsave_argv0`), leaking the completer's name
287    // into the interactive shell's `$0` after every TAB.
288    // A `-c` name argument (c:299) and a runscript (c:1402) both overwrite
289    // this later, exactly as in C.
290    *argv0.lock().unwrap() = argv[idx].clone(); // c:282
291    crate::ported::utils::set_argzero(Some(argv[idx].clone())); // c:282
292    crate::ported::utils::set_posixzero(Some(argv[idx].clone())); // c:282
293    idx += 1;
294    // SHIN = 0;                                                             // c:272
295
296    // parseopts(zsh_name, &argv, opts, cmdptr, NULL, flags)                 // c:280
297    let _ = parseopts(zsh_name, argv, &mut idx, cmdptr, flags);
298
299    // c:291-292 — `if (opts[SHINSTDIN]) opts[USEZLE] = opts[USEZLE] &&
300    // isatty(0);`. SHINSTDIN starts unset here (set below), so this is
301    // normally a no-op; honor it for an explicitly-set SHINSTDIN.
302    if isset(SHINSTDIN) {
303        let usezle = isset(crate::ported::zsh_h::USEZLE) && unsafe { libc::isatty(0) != 0 };
304        // USEZLE's canonical option name is `zle` (zsh_h.rs:4145
305        // `opt_name(USEZLE) == "zle"`); `isset(USEZLE)` reads the `zle`
306        // key, so the write MUST use `zle` too. The prior `"usezle"` key
307        // was never read → this `opts[USEZLE] = opts[USEZLE] && isatty(0)`
308        // downgrade silently did nothing.
309        crate::ported::options::opt_state_set("zle", usezle); // c:292
310    }
311
312    // c:294 — `paramlist = znewlinklist();`
313    let mut paramlist: Vec<String> = Vec::new();
314    if idx < argv.len() {
315        // c:295 — there's a non-option argument.
316        // c:296 — `if (unset(SHINSTDIN))` — a positional arg is the
317        // script to run (or $0 under -c) ONLY when not already forced to
318        // read stdin.
319        if !isset(SHINSTDIN) {
320            // c:297 — `posixzero = *argv;`
321            crate::ported::utils::set_posixzero(Some(argv[idx].clone()));
322            if cmdptr.is_some() {
323                // c:299 — `argzero = *argv;` ($0 under -c)
324                crate::ported::utils::set_argzero(Some(argv[idx].clone()));
325            } else {
326                // c:301 — `*runscript = *argv;`
327                *runscript = Some(argv[idx].clone());
328            }
329            // c:302 — `opts[INTERACTIVE] &= 1;`. A script source makes the
330            // shell non-interactive unless `-i` forced it on.
331            if OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst) & 1 != 0 {
332                crate::ported::options::opt_state_set("interactive", false);
333            }
334            idx += 1;
335        }
336        // c:305-306 — remaining args become positional parameters.
337        while idx < argv.len() {
338            paramlist.push(argv[idx].clone());
339            idx += 1;
340        }
341    } else if cmdptr.is_none() {
342        // c:307-308 — `else if (!*cmdptr) opts[SHINSTDIN] = 1;` — no
343        // script and no `-c`: read commands from stdin.
344        crate::ported::options::opt_state_set("shinstdin", true);
345    }
346    // c:309-310 — `if (isset(SINGLECOMMAND)) opts[INTERACTIVE] &= 1;`
347    if isset(SINGLECOMMAND) && OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst) & 1 != 0 {
348        crate::ported::options::opt_state_set("interactive", false);
349    }
350    // c:311 — `opts[INTERACTIVE] = !!opts[INTERACTIVE];` is a no-op for
351    // the bool port.
352    // c:312-315 — `if (opts[MONITOR] == 2) opts[MONITOR] = opts[INTERACTIVE];`
353    // and the same for HASHDIRS: only a still-default value follows INTERACTIVE.
354    let interactive = isset(INTERACTIVE);
355    let still_default = OPT_DEFAULT_ON.load(Ordering::SeqCst);
356    if still_default & 2 != 0 {
357        crate::ported::options::opt_state_set("monitor", interactive);
358    }
359    if still_default & 4 != 0 {
360        crate::ported::options::opt_state_set("hashdirs", interactive);
361    }
362    // c:316 — `pparams = paramlist;`
363    if !paramlist.is_empty() {
364        if let Ok(mut p) = crate::ported::builtin::PPARAMS.lock() {
365            *p = paramlist;
366        }
367    }
368}
369
370/// Port of `static void parseopts_insert(...)` from Src/init.c:328.
371///
372/// Insert into list in order of pointer value.
373fn parseopts_insert(optlist: &mut Vec<usize>, base: usize, optno: i32) {
374    // c:328
375    let ptr = base + (if optno < 0 { -optno } else { optno }) as usize; // c:348
376    for (i, &node) in optlist.iter().enumerate() {
377        // c:348
378        if ptr < node {
379            // c:348
380            optlist.insert(i, ptr); // c:348
381            return; // c:348
382        }
383    }
384    optlist.push(ptr); // c:348
385}
386
387/// zshrs-only long flags that take a following word as their argument.
388/// They are handled by the binary front-end; `parseopts` only needs to
389/// skip BOTH words so the argument is not mistaken for a script operand.
390const ZSHRS_LONG_FLAGS_WITH_ARG: &[&str] = &["emulate", "rcfile", "init-file", "docs", "out"];
391
392/// Port of `mod_export int parseopts(...)` from Src/init.c:390.
393/// Rust idiom replacement: index-walk over `argv` Vec covers the C
394/// argv pointer-advance; the `emulate_required` / `toplevel` state
395/// tracking mirrors the C source's local flags. The long-option
396/// table lookups happen against the shared options.rs registry.
397pub fn parseopts(
398    _nam: &str,
399    argv: &mut Vec<String>,
400    idx: &mut usize, // c:390
401    cmdp: &mut Option<String>,
402    flags: i32,
403) -> i32 {
404    let toplevel = (flags & 1) != 0; // c:396
405    let mut emulate_required = toplevel; // c:397
406    *cmdp = None; // c:400
407
408    while *idx < argv.len() {
409        // c:418
410        let arg = argv[*idx].clone();
411        if !(arg.starts_with('-') || arg.starts_with('+')) {
412            break;
413        }
414        if arg == "--version" {
415            // c:434
416            println!("zshrs (C-port)"); // c:435-436
417            if toplevel {
418                std::process::exit(0);
419            } // c:437
420        }
421        if arg == "--help" {
422            // c:439
423            printhelp(); // c:440
424            if toplevel {
425                std::process::exit(0);
426            } // c:441
427        }
428        if arg == "-c" {
429            // c:470
430            if emulate_required {
431                // c:471
432                parseopts_setemulate(_nam, flags); // c:472
433                emulate_required = false; // c:473
434            }
435            *idx += 1;
436            *cmdp = argv.get(*idx).cloned(); // c:476
437            // c:495 — `new_opts[INTERACTIVE] &= 1;`: a default-on INTERACTIVE is
438            // cleared by `-c`, an explicit `-i` is kept.
439            if OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst) & 1 != 0 {
440                crate::ported::options::opt_state_set("interactive", false);
441            }
442            *idx += 1;
443            continue;
444        }
445        // c:Src/init.c:478-490 — `-o NAME` / `+o NAME`: a long-name
446        // option whose name follows (`-o nullglob`). `optlookup` resolves
447        // the name; `dosetopt` applies it with the sign as the on/off
448        // action (`-` sets, `+` unsets).
449        if arg == "-o" || arg == "+o" {
450            if emulate_required {
451                parseopts_setemulate(_nam, flags); // c:481-483
452                emulate_required = false;
453            }
454            let action = arg.starts_with('-'); // c:420
455            *idx += 1;
456            if let Some(name) = argv.get(*idx).cloned() {
457                let optno = crate::ported::options::optlookup(&name); // c:493
458                if optno != crate::ported::zsh_h::OPT_INVALID {
459                    // c:501 — dosetopt(optno, action, toplevel, new_opts)
460                    crate::ported::options::dosetopt(optno, action as i32, toplevel as i32);
461                    note_explicit_opt!(optno);
462                }
463            }
464            *idx += 1;
465            continue;
466        }
467        // c:Src/init.c:425-429 — the pseudo-option `--` ends option
468        // processing; everything after it is an operand even if it looks
469        // like a flag.
470        if arg == "--" {
471            *idx += 1;
472            break;
473        }
474        // c:Src/init.c:432-460 — GNU-style long options. C rewrites `-`
475        // to `_` in the name and falls into the shared `longoptions:`
476        // label, so `--no-rcs` is exactly `-o no_rcs`.
477        //
478        // zshrs-specific long flags (`--zsh`, `--bash`, `--dap`, …) are
479        // NOT zsh options; the bin's front-end consumes them before
480        // zsh_main and c:492's `optlookup` would reject them with "no
481        // such option", so an unrecognised `--NAME` is still skipped
482        // here. That is the deliberate divergence. Skipping ALL of them
483        // was too broad: a long flag that IS a real option never reached
484        // the option table, so `zshrs --no-rcs` left RCS set and an
485        // interactive shell still sourced .zshenv/.zshrc, while the
486        // equivalent `-f` and `-o norcs` both suppressed them (the rc
487        // gate is `isset(RCS)` at init.rs:1546/1568).
488        if let Some(long) = arg.strip_prefix("--") {
489            let name = long.replace('-', "_"); // c:456-459
490            let optno = crate::ported::options::optlookup(&name); // c:492
491            if optno != crate::ported::zsh_h::OPT_INVALID {
492                if emulate_required {
493                    parseopts_setemulate(_nam, flags); // c:488-490
494                    emulate_required = false;
495                }
496                // c:498 — `dosetopt(optno, action, toplevel, new_opts)`.
497                // `action` is true for a `-`-introduced word (c:420);
498                // optlookup answers a NEGATIVE optno for a `no…`
499                // spelling and dosetopt inverts on that, which is what
500                // the `-o` arm above already relies on.
501                crate::ported::options::dosetopt(optno, 1, toplevel as i32);
502                note_explicit_opt!(optno);
503            }
504            // A zshrs-only long flag. Those that take a following word
505            // must consume it too, or the word is mistaken for the
506            // script operand and the shell tries to RUN the user's
507            // `--rcfile` argument.
508            *idx += 1;
509            if ZSHRS_LONG_FLAGS_WITH_ARG.contains(&&arg[2..]) {
510                *idx += 1;
511            }
512            continue;
513        }
514        // c:Src/init.c:516-534 — a cluster of single option letters
515        // (`-x`, `-v`, `-xv`, `+x`). Each char maps via `optlookupc` to a
516        // (possibly negated, for inverted-sense letters like `f`) option
517        // number; `dosetopt` applies it with the sign as the action.
518        // OPT_INVALID letters are skipped rather than erroring as C does
519        // at c:517 — zshrs accepts some non-zsh single-dash flags the C
520        // table doesn't know.
521        if emulate_required {
522            parseopts_setemulate(_nam, flags); // c:516-519
523            emulate_required = false;
524        }
525        let action = arg.starts_with('-'); // c:420
526        for c in arg[1..].chars() {
527            let optno = crate::ported::options::optlookupc(c); // c:520
528            if optno != crate::ported::zsh_h::OPT_INVALID {
529                // c:526 — dosetopt(optno, action, toplevel, new_opts)
530                crate::ported::options::dosetopt(optno, action as i32, toplevel as i32);
531                note_explicit_opt!(optno);
532            }
533        }
534        *idx += 1;
535    }
536    if emulate_required {
537        // c:557
538        parseopts_setemulate(_nam, flags); // c:557
539    }
540    0 // c:557
541}
542
543/// Port of `static void printhelp(void)` from Src/init.c:557.
544fn printhelp() {
545    // c:557
546    let argz = argv0.lock().unwrap().clone(); // c:557
547    println!("Usage: {} [<options>] [<argument> ...]", argz); // c:559
548    println!(); // c:560
549    println!("Special options:"); // c:560
550    println!("  --help     show this message, then exit"); // c:561
551    println!("  --version  show zsh version number, then exit"); // c:562
552    println!("  -b         end option processing, like --"); // c:564
553    println!("  -c         take first argument as a command to execute"); // c:577
554    println!("  -o OPTION  set an option by name (see below)"); // c:577
555    println!(); // c:577
556    println!("Normal options are named.  An option may be turned on by"); // c:577
557    println!("`-o OPTION', `--OPTION', `+o no_OPTION' or `+-no-OPTION'.  An"); // c:577
558    println!("option may be turned off by `-o no_OPTION', `--no-OPTION',"); // c:577
559    println!("`+o OPTION' or `+-OPTION'.  Options are listed below only in"); // c:577
560    println!("`--OPTION' or `--no-OPTION' form."); // c:577
561                                                   // printoptionlist();                                                    // c:577
562}
563
564/// Port of `mod_export void init_io(char *cmd)` from Src/init.c:577.
565pub fn init_io(_cmd: Option<&str>) {
566    // c:577
567    // stdout, stderr fully buffered                                         // c:577
568    // setvbuf(stdout, outbuf, _IOFBF, BUFSIZ); setvbuf(stderr, ...)         // c:587-591
569    // (Rust's stdout/stderr are line/block buffered by default)
570
571    // Close any existing shout                                              // c:605-614
572    *shout.lock().unwrap() = 0;
573    if SHTTY.load(Ordering::SeqCst) != -1 {
574        // c:615
575        // c:616 — `zclose(SHTTY);` — fdtable-aware close. SHTTY was
576        // registered as FDT_INTERNAL by movefd at one of the open
577        // sites below (c:627 ttyname/O_RDWR open, c:658 dup(0), c:662
578        // dup(1), c:668 /dev/tty open — all routed through movefd
579        // which sets fdtable[fd] = FDT_INTERNAL at utils.rs:2243).
580        // Prior port used raw libc::close which skipped the
581        // fdtable_set(fd, FDT_UNUSED) clear that zclose does at
582        // utils.rs:2402. Same leak shape as random.rs finish_
583        // (b3107b5a46), tcp.rs tcp_close (9b4dae375a), and
584        // zpty.rs deleteptycmd (c37083d09f) — stale FDT_INTERNAL
585        // marker survives the close → kernel-reused fd inherits the
586        // module-owned classification → closem(FDT_UNUSED, 0) calls
587        // from sibling builtins skip closing it.
588        let _ = crate::ported::utils::zclose(SHTTY.load(Ordering::SeqCst));
589        SHTTY.store(-1, Ordering::SeqCst); // c:617
590    }
591
592    // xtrerr = stderr;                                                      // c:621
593
594    // Make sure the tty is opened read/write.                               // c:623
595    //
596    // C uses `if (isatty(0))` — the truthy test, NOT a strict
597    // `== 1`. POSIX guarantees isatty returns 0 on false but
598    // "non-zero" on true with the exact value implementation-
599    // defined. macOS/glibc/musl all return 1 today but the
600    // strict `== 1` check is brittle — match C's truthy test
601    // so a future libc that returns any non-zero value still
602    // hits the SHTTY-open path.
603    // ttystrname is the canonical "name of the controlling tty" global
604    // (init.c declares it; clone.c reads it for $TTY after fork). The
605    // C `init_io` keeps it in lockstep with `SHTTY`: every open-site
606    // and every dup-fallback either replaces it with `ttyname(fd)` of
607    // the new SHTTY or resets to "" / "/dev/tty" on failure. Mirror
608    // every store here so $TTY isn't empty after init.
609    let set_ttystrname = |s: String| {
610        // c:625 zsfree(ttystrname); ttystrname = ztrdup(...)
611        *crate::ported::modules::clone::ttystrname.lock().unwrap() = s;
612    };
613    #[cfg(unix)]
614    let ttyname_of = |fd: i32| -> Option<String> {
615        unsafe {
616            let p = libc::ttyname(fd);
617            if p.is_null() {
618                None
619            } else {
620                std::ffi::CStr::from_ptr(p)
621                    .to_str()
622                    .ok()
623                    .map(|s| s.to_string())
624            }
625        }
626    };
627    #[cfg(unix)]
628    unsafe {
629        if libc::isatty(0) != 0 {
630            // c:624
631            let name_ptr = libc::ttyname(0); // c:626
632            if !name_ptr.is_null() {
633                let name = std::ffi::CStr::from_ptr(name_ptr);
634                let cstr = std::ffi::CString::new(name.to_bytes()).unwrap();
635                // c:626 ttystrname = ztrdup(ttyname(0))
636                set_ttystrname(name.to_string_lossy().into_owned());
637                let fd = libc::open(
638                    cstr.as_ptr(), // c:627
639                    libc::O_RDWR | libc::O_NOCTTY,
640                );
641                SHTTY.store(movefd(fd), Ordering::SeqCst);
642            }
643            if SHTTY.load(Ordering::SeqCst) == -1 {
644                // c:658
645                SHTTY.store(movefd(libc::dup(0)), Ordering::SeqCst);
646                // c:659
647            }
648        }
649        if SHTTY.load(Ordering::SeqCst) == -1 && libc::isatty(1) != 0 {
650            // c:662
651            SHTTY.store(movefd(libc::dup(1)), Ordering::SeqCst);
652            // c:663
653            // c:664-665 — zsfree(ttystrname); ttystrname = ztrdup(ttyname(1));
654            if let Some(n) = ttyname_of(1) {
655                set_ttystrname(n);
656            }
657        }
658        if SHTTY.load(Ordering::SeqCst) == -1 {
659            // c:667
660            let dev_tty = std::ffi::CString::new("/dev/tty").unwrap();
661            let fd = libc::open(dev_tty.as_ptr(), libc::O_RDWR | libc::O_NOCTTY); // c:668
662            SHTTY.store(movefd(fd), Ordering::SeqCst);
663            if SHTTY.load(Ordering::SeqCst) != -1 {
664                // c:669-670 — ttystrname = ztrdup(ttyname(SHTTY));
665                if let Some(n) = ttyname_of(SHTTY.load(Ordering::SeqCst)) {
666                    set_ttystrname(n);
667                }
668            }
669        }
670        if SHTTY.load(Ordering::SeqCst) == -1 {
671            // c:672-674 — failed all opens: ttystrname = "";
672            set_ttystrname(String::new());
673        } else {
674            // c:675
675            let fdflags = libc::fcntl(SHTTY.load(Ordering::SeqCst), libc::F_GETFD, 0); // c:677
676            if fdflags != -1 {
677                // c:678
678                libc::fcntl(
679                    SHTTY.load(Ordering::SeqCst),
680                    libc::F_SETFD, // c:680
681                    fdflags | libc::FD_CLOEXEC,
682                );
683            }
684            // c:683-684 — if (!ttystrname) ttystrname = ztrdup("/dev/tty");
685            // Rust mirrors NULL-check via empty-string check on the Mutex.
686            let mut guard = crate::ported::modules::clone::ttystrname.lock().unwrap();
687            if guard.is_empty() {
688                *guard = "/dev/tty".to_string();
689            }
690        }
691    }
692
693    // c:689-694 — set up terminal output only for an interactive shell;
694    // disable the line editor (USEZLE → the special `zle` option) when the
695    // shell isn't interactive, or is interactive without a real tty.
696    if interact() {
697        // c:689
698        init_shout(); // c:690
699                      // c:691-692 — `if (!SHTTY || !shout) opts[USEZLE] = 0;`. zshrs has
700                      // no `shout` FILE* (it writes the tty via SHTTY / fd 2), so the C
701                      // `!shout` arm collapses into the SHTTY check. The option's
702                      // canonical name is `zle` (options.rs:92), NOT `usezle`.
703        if SHTTY.load(Ordering::SeqCst) == -1 {
704            crate::ported::options::opt_state_set("zle", false); // c:692 opts[USEZLE]=0
705        }
706    } else {
707        // c:693-694 — `} else opts[USEZLE] = 0;`
708        crate::ported::options::opt_state_set("zle", false); // c:694
709    }
710
711    // c:699 — `mypid = (zlong)getpid();` — no zshrs global; getpid() is
712    // called where needed (acquire_pgrp computes it locally).
713    // c:700-707 — if interactive, make sure the shell is in the foreground
714    // and is the process-group leader. Gating on MONITOR + the one-shot
715    // `!origpgrp` guard matches C exactly; the prior port called
716    // acquire_pgrp unconditionally and never recorded origpgrp, so
717    // release_pgrp at exit had no group to hand the tty back to.
718    if isset(crate::ported::zsh_h::MONITOR) {
719        // c:700
720        if SHTTY.load(Ordering::SeqCst) == -1 {
721            // c:701
722            crate::ported::options::opt_state_set("monitor", false); // c:702
723        } else {
724            // c:703 — `else if (!origpgrp)`: only acquire the first time.
725            let origpgrp_recorded = *crate::ported::jobs::ORIGPGRP
726                .get_or_init(|| std::sync::Mutex::new(0))
727                .lock()
728                .unwrap()
729                != 0;
730            if !origpgrp_recorded {
731                // c:704 — `origpgrp = GETPGRP();`
732                *crate::ported::jobs::ORIGPGRP
733                    .get_or_init(|| std::sync::Mutex::new(0))
734                    .lock()
735                    .unwrap() = unsafe { libc::getpgrp() };
736                // c:705 — `acquire_pgrp();` (might also clear opts[MONITOR]).
737                let _ = crate::ported::jobs::acquire_pgrp();
738            }
739        }
740    }
741}
742
743/// Port of `mod_export void init_shout(void)` from Src/init.c:728.
744pub fn init_shout() {
745    // c:728
746    let fd = SHTTY.load(Ordering::SeqCst);
747    if fd == -1 {
748        // c:735-739 — "Since we're interactive, it's nice to have
749        // somewhere to write."
750        *shout.lock().unwrap() = 2 + 1; // c:738 shout = stderr
751        return; // c:739
752    }
753    // c:747-752 — `shout = fdopen(SHTTY, "w")` + setvbuf; the buffering
754    // lives in `crate::shout`.
755    *shout.lock().unwrap() = fd as usize + 1; // c:748
756    if let Some(ti) = crate::ported::utils::gettyinfo() {
757        // c:754 gettyinfo(&shttyinfo)
758        *crate::ported::utils::SHTTYINFO.lock().unwrap() = Some(ti);
759    }
760}
761
762/// Port of `mod_export char *tccap_get_name(int cap)` from Src/init.c:756.
763pub fn tccap_get_name(cap: usize) -> &'static str {
764    // c:756
765    if cap >= 39
766    /* TC_COUNT */
767    {
768        // c:771
769        return ""; // c:771
770    }
771    tccapnams[cap] // c:771
772}
773
774/// Port of `mod_export int init_term(void)` from Src/init.c:771.
775///
776/// Reads `$TERM` from the param table, and on a recognised term
777/// populates `tcstr[]`/`tclen[]` from the system termcap/terminfo
778/// DB via `tgetent` + `tgetstr` over `tccapnams[]`, exactly like C.
779/// This is the substrate every `tcmultout` / `tc_leftcurs` /
780/// `tsetcap` call site reads.
781///
782/// ncurses is already linked (build.rs `rustc-link-lib=ncurses`, the
783/// same lib the zsh/terminfo module's `tigetstr` externs use), so
784/// the termcap-emulation entry points are available. The previous
785/// Rust body hardcoded ANSI/VT100 escapes — under `TERM=screen-*` /
786/// `tmux-*` that diverged from zsh on standout (`so` is `\e[3m`
787/// there, not the hardcoded `\e[7m`).
788pub fn init_term() -> i32 {
789    // c:766 — the termcap emulation entry points. These used to be an
790    // `extern "C"` block resolved against ncurses; they are now
791    // `crate::terminfo_db`, which reads the compiled terminfo database
792    // directly. That removed the last reason the binary linked a C
793    // terminal library, and with it `libtinfo.so.6` as an install
794    // dependency on Ubuntu. Verified identical to ncurses over the whole
795    // reference database — 2819 entries x 497 capabilities.
796    use crate::terminfo_db::{tgetent, tgetflag, tgetnum, tgetstr};
797    use crate::ported::zsh_h::{
798        TCBACKSPACE, TCCLEARSCREEN, TCDOWN, TCFAINTBEG, TCITALICSBEG, TCITALICSEND, TCLEFT,
799        TCRESTRCURSOR, TCSAVECURSOR, TCUP, TC_COUNT,
800    };
801
802    // c:776-779 — `if (!*term) { termflags |= TERM_UNKNOWN; return 0; }`
803    let term = getsparam("TERM").unwrap_or_default();
804    if term.is_empty() {
805        TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::SeqCst);
806        return 0;
807    }
808
809    // c:782-783 — `if (!strcmp(term, "emacs")) opts[USEZLE] = 0;`
810    // C proceeds to tgetent afterwards; zshrs keeps the USEZLE-off
811    // via the option layer.
812    if term == "emacs" {
813        crate::ported::options::opt_state_set("zle", false); // c:783 opts[USEZLE] = 0
814    }
815
816    // An embedded NUL can never name a terminfo entry.
817    if term.contains('\0') {
818        TERMFLAGS.fetch_or(TERM_BAD, Ordering::SeqCst);
819        return 0;
820    }
821    // c:785-797 — `if (tgetent(termbuf, term) != TGETENT_SUCCESS) {
822    //   zerr(...); errflag &= ~ERRFLAG_ERROR; termflags |= TERM_BAD;
823    //   return 0; }`. ncurses tgetent accepts NULL (the
824    //   TGETENT_ACCEPTS_NULL arm at c:786-787).
825    let ent = tgetent(&term);
826    if ent != 1 {
827        // c:791 — `if (interact) zerr("can't find terminal definition
828        // for %s", term);` — interact gate keeps -fc scripts quiet.
829        if crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE) {
830            crate::ported::utils::zerr(&format!("can't find terminal definition for {}", term));
831            // c:792
832        }
833        crate::ported::utils::errflag
834            .fetch_and(!crate::ported::utils::ERRFLAG_ERROR, Ordering::Relaxed); // c:793
835        TERMFLAGS.fetch_or(TERM_BAD, Ordering::SeqCst); // c:794
836        return 0; // c:795
837    }
838
839    // c:801-802 — `termflags &= ~TERM_BAD; termflags &= ~TERM_UNKNOWN;`
840    TERMFLAGS.fetch_and(!(TERM_BAD | TERM_UNKNOWN), Ordering::SeqCst);
841
842    // c:803-815 — `for (t0 = 0; t0 != TC_COUNT; t0++) { ... tgetstr
843    // (tccapnams[t0], &pp) ... }` — fetch every capability string.
844    {
845        let mut s = tcstr.lock().unwrap();
846        let mut l = tclen.lock().unwrap();
847        // c:798 `char tbuf[1024]` — element type must be c_char: it is
848        // i8 on macOS/x86_64-linux but u8 on aarch64-linux.
849        for t0 in 0..TC_COUNT as usize {
850            match tgetstr(tccapnams[t0]) {
851                // c:811-813 — dup the cap string + record length.
852                Some(bytes) => {
853                    s[t0] = String::from_utf8_lossy(&bytes).into_owned();
854                    l[t0] = bytes.len() as i32;
855                }
856                // c:809 — `tcstr[t0] = NULL, tclen[t0] = 0;`
857                None => {
858                    s[t0] = String::new();
859                    l[t0] = 0;
860                }
861            }
862        }
863    }
864
865    // c:817-818 — automargin / newline-glitch flags.
866    let flag = tgetflag;
867    let num = tgetnum;
868    hasam.store(flag("am"), Ordering::SeqCst); // c:818 `hasam = tgetflag("am");`
869    hasxn.store(flag("xn"), Ordering::SeqCst); // c:819 `hasxn = tgetflag("xn");`
870    tclines.store(num("li"), Ordering::SeqCst); // c:821 `tclines = tgetnum("li");`
871    tccolumns.store(num("co"), Ordering::SeqCst); // c:822 `tccolumns = tgetnum("co");`
872    tccolours.store(num("Co"), Ordering::SeqCst); // c:823 `tccolours = tgetnum("Co");`
873
874    // Post-fetch fixups — all operate on tcstr/tclen under one lock.
875    {
876        let mut s = tcstr.lock().unwrap();
877        let mut l = tclen.lock().unwrap();
878        let can = |l: &[i32; 39], cap: i32| l[cap as usize] != 0; // tccan()
879
880        // c:825-833 — no cursor-up cap → single-line mode (TERM_NOUP).
881        if can(&l, TCUP) {
882            TERMFLAGS.fetch_and(!TERM_NOUP, Ordering::SeqCst); // c:829
883        } else {
884            s[TCUP as usize] = String::new(); // c:831-832
885            l[TCUP as usize] = 0;
886            TERMFLAGS.fetch_or(TERM_NOUP, Ordering::SeqCst); // c:833
887        }
888
889        // c:836-840 — most termcaps don't define "bc"; default `\b`.
890        if !can(&l, TCBACKSPACE) {
891            s[TCBACKSPACE as usize] = "\u{8}".to_string(); // c:838
892            l[TCBACKSPACE as usize] = 1; // c:839
893        }
894
895        // c:843-847 — no cursor-left cap → use backspace.
896        if !can(&l, TCLEFT) {
897            s[TCLEFT as usize] = s[TCBACKSPACE as usize].clone(); // c:845
898            l[TCLEFT as usize] = l[TCBACKSPACE as usize]; // c:846
899        }
900
901        // c:849-853 — save-cursor without restore-cursor is useless.
902        if can(&l, TCSAVECURSOR) && !can(&l, TCRESTRCURSOR) {
903            l[TCSAVECURSOR as usize] = 0; // c:850
904            s[TCSAVECURSOR as usize] = String::new(); // c:851-852
905        }
906
907        // c:856-860 — if the down cap is `\n`, don't use it.
908        if can(&l, TCDOWN) && s[TCDOWN as usize].starts_with('\n') {
909            l[TCDOWN as usize] = 0; // c:857
910            s[TCDOWN as usize] = String::new(); // c:858-859
911        }
912
913        // c:863-867 — no clear cap → ^L.
914        if !can(&l, TCCLEARSCREEN) {
915            s[TCCLEARSCREEN as usize] = "\u{c}".to_string(); // c:865
916            l[TCCLEARSCREEN as usize] = 1; // c:866
917        }
918
919        // c:868 — `rprompt_indent = 1;` lives in the params layer
920        // (rprompt_indent_unsetfn keeps it there); no-op here.
921
922        // c:876-884 — no italics caps → CSI 3 m / CSI 23 m.
923        if !can(&l, TCITALICSBEG) {
924            s[TCITALICSBEG as usize] = "\x1b[3m".to_string(); // c:878
925            l[TCITALICSBEG as usize] = 4; // c:879
926        }
927        if !can(&l, TCITALICSEND) {
928            s[TCITALICSEND as usize] = "\x1b[23m".to_string(); // c:882
929            l[TCITALICSEND as usize] = 5; // c:883
930        }
931        // c:885-888 — no faint cap → CSI 2 m.
932        if !can(&l, TCFAINTBEG) {
933            s[TCFAINTBEG as usize] = "\x1b[2m".to_string(); // c:887
934            l[TCFAINTBEG as usize] = 4; // c:888
935        }
936    }
937
938    1 // c:890 `return 1;`
939}
940
941/// Port of `static char *getmypath(const char *name, const char *cwd)` from Src/init.c:909.
942pub fn getmypath(name: Option<&str>, cwd: Option<&str>) -> Option<String> {
943    // c:909
944    #[cfg(target_os = "macos")]
945    unsafe {
946        // c:914
947        let mut buf = vec![0u8; libc::PATH_MAX as usize]; // c:918
948        let mut n: u32 = libc::PATH_MAX as u32; // c:916
949        let ret = libc::_NSGetExecutablePath(
950            buf.as_mut_ptr() as *mut i8, // c:919
951            &mut n,
952        );
953        if ret < 0 {
954            // c:919
955            buf.resize(n as usize, 0); // c:921
956            let ret2 = libc::_NSGetExecutablePath(buf.as_mut_ptr() as *mut i8, &mut n); // c:922
957            if ret2 == 0 {
958                let s = std::ffi::CStr::from_ptr(buf.as_ptr() as *const i8);
959                let lossy = s.to_string_lossy().into_owned();
960                if !lossy.is_empty() {
961                    return Some(lossy);
962                }
963            }
964        } else if ret == 0 {
965            // c:924
966            let s = std::ffi::CStr::from_ptr(buf.as_ptr() as *const i8);
967            let lossy = s.to_string_lossy().into_owned();
968            if !lossy.is_empty() {
969                return Some(lossy);
970            } // c:925
971        }
972    }
973    #[cfg(target_os = "linux")]
974    {
975        if let Ok(p) = std::fs::read_link("/proc/self/exe") {
976            // c:946
977            return Some(p.to_string_lossy().into_owned()); // c:949
978        }
979    }
980
981    let name = name?; // c:956-957
982    let name = if name.starts_with('-') {
983        &name[1..]
984    } else {
985        name
986    }; // c:958-959
987    let namelen = name.len(); // c:960
988    if namelen == 0 {
989        return None;
990    } // c:960-961
991    if name.ends_with('/') {
992        return None;
993    } // c:963-964
994    if name.starts_with('/') {
995        // c:965
996        return Some(name.to_string()); // c:967
997    }
998    if name.contains('/') {
999        // c:969
1000        let cwd = cwd?; // c:971-972
1001        return Some(format!("{}/{}", cwd, name)); // c:974
1002    }
1003    let path = std::env::var("PATH").ok()?; // c:984
1004    if path.is_empty() {
1005        return None;
1006    } // c:985-986
1007    for dir in path.split(':') {
1008        // c:990-1000
1009        let candidate = if dir.is_empty() {
1010            std::path::PathBuf::from(name)
1011        } else {
1012            std::path::PathBuf::from(format!("{}/{}", dir, name))
1013        };
1014        if let Ok(real) = std::fs::canonicalize(&candidate) {
1015            // c:1014
1016            if real.is_file() {
1017                return Some(real.to_string_lossy().into_owned());
1018            }
1019        }
1020    }
1021    None // c:1014
1022}
1023
1024/// Bootstrap `module_path` / `MODULE_PATH` per `Src/init.c:1176`:
1025/// `module_path = mkarray(ztrdup(MODULE_DIR))` plus the matching
1026/// PM_TIED scalar from `Src/params.c:404` IPDEF8.
1027///
1028/// MODULE_DIR is a build-time `#define` from `Src/zshpaths.h`
1029/// (e.g. `/opt/homebrew/Cellar/zsh/5.9.1/lib`) resolved during
1030/// zsh's `./configure`. zshrs ships modules statically linked so
1031/// there is no equivalent build-time anchor. Probe the running
1032/// system's zsh once (cached via OnceLock) so `${module_path[1]}`
1033/// / `${(j.:.)module_path}` / `${#module_path}` agree between
1034/// `zshrs --zsh` and the system zsh that parity tests compare
1035/// against. Falls back to an empty array when no system zsh is
1036/// installed (this also makes paramtab carry an empty
1037/// `module_path` array rather than no entry at all, matching the
1038/// `mkarray(NULL)` shape the C code produces for an empty
1039/// MODULE_DIR build).
1040///
1041/// Called from `setupvals` (the canonical c:1014 entry point) and
1042/// from `ShellExecutor::new` (the bin entry that skips full
1043/// `setupvals` per the init_bltinmods comment at vm_helper.rs).
1044///
1045/// **Extension** — no direct C analog. The C source inlines this
1046/// at `Src/init.c:1176` inside `setupvals`. Allowlisted in
1047/// `tests/data/fake_fn_allowlist.txt` so ShellExecutor can call
1048/// just the module_path-init subset of setupvals without invoking
1049/// the full body (which would conflict with the bin entry's
1050/// piecewise paramtab init).
1051pub fn module_path_init() {
1052    use crate::ported::params::*;
1053    use crate::ported::zsh_h::*;
1054    static MODULE_DIR_CACHE: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
1055    let module_dir: Vec<String> = MODULE_DIR_CACHE
1056        .get_or_init(|| {
1057            // c:Src/init.c:1176 — `module_path = mkarray(ztrdup(MODULE_DIR))`,
1058            // where `MODULE_DIR` is the C build's configure-time install
1059            // prefix: the directory holding the `.so`/`.bundle` files that
1060            // `try_load_module` (c:Src/module.c:1583) dlopens.
1061            //
1062            // zshrs's module directory is ITS OWN — `$ZSHRS_HOME/modules`,
1063            // default `~/.zshrs/modules`, the single-directory rule every
1064            // other zshrs artifact follows. It is emphatically NOT any zsh C
1065            // installation's `lib`: zshrs links its modules statically (the
1066            // port's `try_load_module` answers from `module_linked`, never
1067            // from a path search) and its runtime-loadable modules are
1068            // `znative` cdylibs reached through `zmodload -R`, which takes an
1069            // explicit path. Pointing this parameter at a foreign zsh's
1070            // bundle directory named files zshrs can never load, made the
1071            // value depend on which zsh happened to be installed, and — in
1072            // the shape this replaces — spent 7.3 ms of every single startup
1073            // forking `zsh -fc 'print -r ${module_path[1]}'` to ask.
1074            //
1075            // Consequence, stated plainly: `$module_path` does not match the
1076            // system zsh's, the same way `$0` does not (both are properties
1077            // of the running binary's own installation).
1078            let home = std::env::var_os("ZSHRS_HOME")
1079                .map(std::path::PathBuf::from)
1080                .or_else(|| {
1081                    std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".zshrs"))
1082                });
1083            match home {
1084                Some(h) => vec![h.join("modules").to_string_lossy().into_owned()],
1085                // No $HOME and no $ZSHRS_HOME: C leaves `module_path` a
1086                // one-element array whatever MODULE_DIR was, so keep the
1087                // shape rather than inventing a path.
1088                None => vec![String::new()],
1089            }
1090        })
1091        .clone();
1092    let scalar_join = module_dir.join(":");
1093    let mut tab = paramtab().write().unwrap();
1094    // c:Src/init.c:1176 — module_path = PM_ARRAY tied to MODULE_PATH.
1095    let mp = Box::new(param {
1096        node: hashnode {
1097            next: None,
1098            nam: "module_path".to_string(),
1099            flags: (PM_ARRAY | PM_SPECIAL | PM_TIED) as i32,
1100        },
1101        u_data: 0,
1102        u_tied: None,
1103        u_arr: Some(module_dir),
1104        u_str: None,
1105        u_val: 0,
1106        u_dval: 0.0,
1107        u_hash: None,
1108        gsu_s: None,
1109        gsu_i: None,
1110        gsu_f: None,
1111        gsu_a: None,
1112        gsu_h: None,
1113        base: 0,
1114        width: 0,
1115        env: None,
1116        ename: Some("MODULE_PATH".to_string()),
1117        old: None,
1118        level: 0,
1119    });
1120    tab.insert("module_path".to_string(), mp);
1121    // c:Src/params.c:404 IPDEF8 — PM_TIED scalar mirroring
1122    // module_path with `:` join.
1123    let mps = Box::new(param {
1124        node: hashnode {
1125            next: None,
1126            nam: "MODULE_PATH".to_string(),
1127            flags: (PM_SCALAR | PM_SPECIAL | PM_TIED | PM_DONTIMPORT) as i32,
1128        },
1129        u_data: 0,
1130        u_tied: None,
1131        u_arr: None,
1132        u_str: Some(scalar_join),
1133        u_val: 0,
1134        u_dval: 0.0,
1135        u_hash: None,
1136        gsu_s: None,
1137        gsu_i: None,
1138        gsu_f: None,
1139        gsu_a: None,
1140        gsu_h: None,
1141        base: 0,
1142        width: 0,
1143        env: None,
1144        ename: Some("module_path".to_string()),
1145        old: None,
1146        level: 0,
1147    });
1148    tab.insert("MODULE_PATH".to_string(), mps);
1149}
1150
1151/// Port of `void setupvals(char *cmd, char *runscript, char *zsh_name)` from Src/init.c:1014.
1152///
1153/// Initialize lots of global variables and hash tables.                     // c:1014
1154pub fn setupvals(cmd: Option<&str>, runscript: Option<&str>, zsh_name: &str) {
1155    // c:1014
1156    let mut close_fds = [0i32; 10]; // c:1043
1157    let mut tmppipe = [-1i32; 2]; // c:1043
1158
1159    // Workaround NIS grabbing fd's 0-9                                      // c:1045
1160    #[cfg(unix)]
1161    unsafe {
1162        if libc::pipe(tmppipe.as_mut_ptr()) == 0 {
1163            // c:1053
1164            let mut i: i32 = -1; // c:1060
1165            while i < 9 {
1166                // c:1061
1167                let j: i32;
1168                if i < tmppipe[0] {
1169                    // c:1063
1170                    j = tmppipe[0]; // c:1064
1171                } else if i < tmppipe[1] {
1172                    // c:1065
1173                    j = tmppipe[1]; // c:1066
1174                } else {
1175                    j = libc::dup(0); // c:1068
1176                    if j == -1 {
1177                        break;
1178                    } // c:1069-1070
1179                }
1180                if j < 10 {
1181                    // c:1072
1182                    close_fds[j as usize] = 1; // c:1073
1183                } else {
1184                    libc::close(j); // c:1075
1185                }
1186                if i < j {
1187                    i = j;
1188                } // c:1076-1077
1189            }
1190            if i < tmppipe[0] {
1191                libc::close(tmppipe[0]);
1192            } // c:1079-1080
1193            if i < tmppipe[1] {
1194                libc::close(tmppipe[1]);
1195            } // c:1081-1082
1196        }
1197    }
1198
1199    // c:1085 — `(void)addhookdefs(NULL, zshhooks, sizeof(zshhooks)/sizeof(*zshhooks));`
1200    // Registers the four well-known hookdefs (exit, before_trap,
1201    // after_trap, get_color_attr) into the global `hooktab` chain.
1202    ZSHHOOKS_ADDED.call_once(|| {
1203        let base = zshhooks.load(std::sync::atomic::Ordering::SeqCst);
1204        let _ = crate::ported::module::addhookdefs(std::ptr::null(), base, 4);
1205    });
1206    // In C the `zsh/zle` module's boot_ (zle_main.c:2301) registers the
1207    // before_trap/after_trap hookfuncs AND the comphooks[] hookdefs
1208    // (insert_match, menu_start, compctl_make, compctl_cleanup,
1209    // comp_list_matches with def=ilistmatches). zshrs static-links ZLE, and
1210    // `zmodload zsh/zle` routes to features_, not boot_, so that
1211    // registration never happened — leaving `comp_list_matches` unregistered
1212    // so `list_matches` fell straight to the plain uncolored `ilistmatches`
1213    // and `zmodload zsh/complist`'s `addhookfunc` had no hookdef to attach
1214    // `complistmatches` to (every list-colors/group-colors style dropped).
1215    // Run the ZLE module boot once here at startup; addhookdef's duplicate
1216    // guard makes a later real boot_ idempotent.
1217    let _ = crate::ported::zle::zle_main::boot_(std::ptr::null());
1218    // C's zsh/complete module boot_ (complete.c:1758) attaches the funcs to
1219    // the ZLE hookdefs registered just above: complete/before_complete/
1220    // after_complete/list_matches/invalidate_list. Like zle_main::boot_ it
1221    // never ran (zmodload routes to features_), so after_complete's hook had
1222    // no func and menu-completion's menu_start (→ domenuselect) never fired.
1223    let _ = crate::ported::zle::complete::boot_(std::ptr::null());
1224    // init_eprog();                                                         // c:1087
1225    // zero_mnumber.type = MN_INTEGER; zero_mnumber.u.l = 0;                 // c:1089-1090
1226
1227    // noeval = 0;                                                           // c:1092
1228    // curhist = 0; histsiz = DEFAULT_HISTSIZE; inithist();                  // c:1093-1095
1229    let _ = crate::ported::hist::inithist();
1230    // cmdstack = zalloc(CMDSTACKSZ); cmdsp = 0;                             // c:1097-1098
1231    // bangchar = '!'; hashchar = '#'; hatchar = '^';                        // c:1100-1102
1232    // termflags = TERM_UNKNOWN;                                             // c:1103
1233    TERMFLAGS.store(TERM_UNKNOWN, Ordering::SeqCst);
1234    // curjob = prevjob = coprocin = coprocout = -1;                         // c:1104
1235    // c:1121 — `zgettime_monotonic_if_available(&shtimer);  /* init
1236    // $SECONDS */`. C stamps `shtimer` at STARTUP. zshrs's analog
1237    // (`params::shtimer_lock()`) is a lazily-initialised OnceLock, so
1238    // without this it got stamped on its FIRST READ instead — making every
1239    // elapsed-since-shell-start measurement come out ~0. Visible in bare
1240    // `time`, whose real-time column is `now - shtimer`
1241    // (c:Src/jobs.c:1961 `dtime_ts(&dtimespec, &shtimer, &now)`): it
1242    // printed `0.000 total` and a nonsense `1684500% cpu`. Stamped with the
1243    // same wall-clock basis the consumers use (`SystemTime` since the
1244    // epoch), not CLOCK_MONOTONIC, so the subtraction below stays valid.
1245    // TOUCH, not assign: the lock's initialiser stamps "now" on first
1246    // access, so touching it primes it exactly once. Re-assigning here
1247    // would RE-stamp it on the `zsh_main` path, resetting $SECONDS after
1248    // the process-entry stamp in `bins/zshrs.rs::main` already ran.
1249    let _ = crate::ported::params::shtimer_lock(); // c:1121
1250                                                   // srand((unsigned)(shtimer.tv_sec + shtimer.tv_nsec));                  // c:1122
1251    #[cfg(unix)]
1252    unsafe {
1253        let mut ts: libc::timespec = std::mem::zeroed();
1254        libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts);
1255        libc::srand((ts.tv_sec as u32).wrapping_add(ts.tv_nsec as u32));
1256    }
1257
1258    // Set default path                                                      // c:1108
1259    // path = ["/bin", "/usr/bin", "/usr/ucb", "/usr/local/bin", NULL];      // c:1109-1114
1260    std::env::set_var(
1261        "PATH", // c:1109
1262        std::env::var("PATH")
1263            .unwrap_or_else(|_| "/bin:/usr/bin:/usr/ucb:/usr/local/bin".to_string()),
1264    );
1265
1266    // cdpath, manpath, fignore = mkarray(NULL)                              // c:1116-1118
1267    // fpath = ...                                                           // c:1132-1172
1268    // mailpath, psvar = mkarray(...)                                        // c:1174-1175
1269    // modulestab = newmoduletable(17, "modules");                           // c:1177
1270    // linkedmodules = znewlinklist();                                       // c:1178
1271
1272    module_path_init();
1273
1274    // Set default prompts                                                   // c:1180
1275    // prompt, prompt2, prompt3, prompt4, sprompt = ztrdup(...)              // c:1181-1194
1276
1277    // ifs = EMULATION(KSH|SH) ? DEFAULT_IFS_SH : DEFAULT_IFS                // c:1196-1197
1278    // wordchars = ztrdup(DEFAULT_WORDCHARS); postedit = ztrdup("");         // c:1198-1199
1279
1280    // If _ is set in environment then initialize our $_ by copying it      // c:1200
1281    let underscore_val = std::env::var("_").unwrap_or_default(); // c:1201
1282    let metafied = crate::ported::utils::metafy(&underscore_val); // c:1202
1283    *zunderscore.lock().unwrap() = metafied.clone(); // c:1202
1284    underscoreused.store((metafied.len() + 1) as i32, Ordering::SeqCst); // c:1203
1285    let ulen = (metafied.len() + 1 + 31) & !31; // c:1204
1286    underscorelen.store(ulen, Ordering::SeqCst); // c:1205
1287                                                 // zunderscore = zrealloc(zunderscore, underscorelen);                   // c:1205
1288
1289    // zoptarg = ""; zoptind = 1;                                            // c:1207-1208
1290
1291    crate::ported::params::ppid.store(unsafe { libc::getppid() } as i64, Ordering::Relaxed); // c:1226
1292    crate::ported::params::mypid.store(unsafe { libc::getpid() } as i64, Ordering::Relaxed); // c:1227
1293    // term = ztrdup("");                                                    // c:1212
1294
1295    // nullcmd = "cat"; readnullcmd = DEFAULT_READNULLCMD;                   // c:1214-1215
1296
1297    // cached_uid = getuid();                                                // c:1219
1298    // pswd = getpwuid(cached_uid); home = pswd->pw_dir; ...                 // c:1222-1234
1299    if let Ok(home) = std::env::var("HOME") {
1300        // c:1225
1301        let _ = home;
1302    }
1303
1304    // PWD/OLDPWD initialization                                             // c:1236-1259
1305    let cwd = crate::ported::compat::zgetcwd(); // c:1252
1306    std::env::set_var("PWD", &cwd); // c:1253
1307    if std::env::var("OLDPWD").is_err() {
1308        // c:1255-1257
1309        std::env::set_var("OLDPWD", &cwd); // c:1256
1310    }
1311
1312    // c:1261 — `inittyptab();`. This is the FIRST call C makes, and it runs
1313    // here for a reason: `parseargs` has already set INTERACTIVE/SHINSTDIN, so
1314    // the one-shot `ZTF_INIT` latch inside (utils.c:4160-4164) sees the real
1315    // values and can raise `ZTF_INTERACT`. That bit is the head of the chain
1316    // c:4257 → `ZTF_BANGCHAR` → c:4291 → `bangchar` is `ISPECIAL`.
1317    //
1318    // zshrs reaches `inittyptab` EARLIER than C does — `ShellExecutor::new`
1319    // (vm_helper.rs, C's `createparamtable`, which C runs at c:1286, AFTER
1320    // this) and `lex_init` both seed the type table defensively because their
1321    // callers need `isident`/`iblank` working. Those calls happen before
1322    // options are parsed, so the latch was being decided with
1323    // `interact=false, shinstdin=false` (measured) and `ZTF_INTERACT` could
1324    // never be set — `!` was not a special character in ANY interactive
1325    // zshrs, and `${(q)v}` on `v='a!b'` gave `a!b` where zsh gives `a\!b`.
1326    //
1327    // Re-arm the latch so it is decided HERE, at C's own call site, with C's
1328    // inputs. The observable semantics are C's: computed once, from
1329    // `interact && isset(SHINSTDIN)`, and never revisited afterwards.
1330    crate::ported::ztype_h::TYPTAB_FLAGS.fetch_and(
1331        !crate::ported::ztype_h::ZTF_INIT,
1332        std::sync::atomic::Ordering::Relaxed,
1333    );
1334    crate::ported::utils::inittyptab(); // c:1261
1335    crate::ported::lex::initlextabs(); // c:1262
1336
1337    crate::ported::hashtable::createreswdtable(); // c:1264
1338    crate::ported::hashtable::createaliastables(); // c:1265
1339    crate::ported::hashtable::createcmdnamtable(); // c:1266
1340    crate::ported::hashtable::createshfunctable(); // c:1267
1341    let _ = crate::ported::builtin::createbuiltintable(); // c:1268
1342    crate::ported::hashnameddir::createnameddirtable(); // c:1269
1343    crate::ported::params::createparamtable(); // c:1270
1344
1345    // c:Src/init.c:1274-1276 — `#ifdef TIOCGWINSZ / adjustwinsize(0);`, the
1346    // first thing after createparamtable. Probes the tty via TIOCGWINSZ and
1347    // publishes the geometry to $COLUMNS/$LINES.
1348    //
1349    // zshrs never made this call, so it never asked the tty: $COLUMNS/$LINES
1350    // read 0 in every shell with a terminal (zsh reports 97/24 in a 97x24 pty).
1351    // Everything width-derived was then computed against 0 — prompt `%<<`
1352    // truncation, select lists, completion listing widths.
1353    //
1354    // Ordering is C's and it is load-bearing in both directions:
1355    //   - It must follow init_io (c:1908), which is what sets SHTTY; while
1356    //     SHTTY is -1 adjustwinsize early-returns (c:1900-1901) and does
1357    //     nothing. That is also the correct behaviour with no terminal at all:
1358    //     $COLUMNS keeps whatever the environment supplied, or 0.
1359    //   - It must follow the environ import (createparamtable, c:1270) because
1360    //     the tty geometry OVERRIDES an inherited COLUMNS: a 97-column terminal
1361    //     reports 97 even when COLUMNS=10 was exported in. (C reaches that via
1362    //     the c:1906-1907 "Signal missed while a job owned the tty?" promotion
1363    //     of from=0 to from=1, which makes adjustcolumns take the signalled
1364    //     path and overwrite zterm_columns with ws_col.)
1365    //
1366    // Note SHTTY does not require stdin/stdout to be a terminal: init_io's last
1367    // resort is `open("/dev/tty")` (c:667-670), so a piped-but-still-attached
1368    // shell gets the real width too — `zsh -fc 'print $COLUMNS | cat'` in a
1369    // 97-column terminal prints 97.
1370    let _ = crate::ported::utils::adjustwinsize(0); // c:1276
1371
1372    // c:Src/init.c:1180-1194 — default prompts. C sets the `prompt`/
1373    // `prompt2`/`prompt3`/`prompt4`/`sprompt` globals (which IPDEF7 binds
1374    // to PS1/PS2/PS3/PS4/SPROMPT) BEFORE createparamtable; zshrs creates
1375    // those special scalars empty, so set the defaults here, right after.
1376    //   - prompt/prompt2 are gated on INTERACTIVE (c:1181): a
1377    //     non-interactive shell keeps empty PS1/PS2. zshrs's INTERACTIVE
1378    //     is isatty-based and is NOT downgraded for `-c`/script, so also
1379    //     require reading from stdin (no `-c`, no runscript) — the
1380    //     SHINSTDIN condition — to match zsh's "interactive" gate.
1381    //   - prompt3/prompt4/sprompt are set unconditionally (c:1191-1194).
1382    // Each default only applies when the param is still empty, so an
1383    // env-imported value (e.g. `PS4` exported) wins, mirroring C where
1384    // importenv overrides the compiled defaults.
1385    {
1386        let ksh_sh = crate::ported::zsh_h::EMULATION(EMULATE_KSH | EMULATE_SH);
1387        let set_default = |name: &str, val: &str| {
1388            // C sets these defaults at c:1196-1206, i.e. BEFORE
1389            // `createparamtable()` (c:1270) walks `environ` and assigns
1390            // over them (c:919-924 `assignsparam(iname, ..,
1391            // ASSPM_ENV_IMPORT)`), so an inherited value ALWAYS wins —
1392            // including an inherited EMPTY one. zshrs runs the import
1393            // first and the defaults second, so "already set" has to be
1394            // decided the way C decides it: was the name in the
1395            // environment? Testing `getsparam().is_empty()` conflated
1396            // "unset" with "exported as empty" and re-seeded the default
1397            // over an explicit `export PS1=` — the exact idiom
1398            // Test/W02jobs.ztst's `zpty_start` uses to silence the prompt,
1399            // which is why every zpty chunk's expected output was prefixed
1400            // with a live `%m%# ` prompt.
1401            if std::env::var_os(name).is_some() {
1402                return;
1403            }
1404            if getsparam(name).map_or(true, |v| v.is_empty()) {
1405                crate::ported::params::setsparam(name, val);
1406            }
1407        };
1408        // c:1181 — interactive shell reading from stdin gets the
1409        // hostname prompt; ksh/sh emulation gets the bare `$`/`#`.
1410        if isset(INTERACTIVE) && cmd.is_none() && runscript.is_none() {
1411            if ksh_sh {
1412                set_default(
1413                    "PS1",
1414                    if crate::ported::utils::privasserted() {
1415                        "# "
1416                    } else {
1417                        "$ "
1418                    },
1419                ); // c:1185
1420                set_default("PS2", "> "); // c:1186
1421            } else {
1422                set_default("PS1", "%m%# "); // c:1188
1423                set_default("PS2", "%_> "); // c:1189
1424            }
1425        }
1426        set_default("PS3", "?# "); // c:1191
1427        set_default("PS4", if ksh_sh { "+ " } else { "+%N:%i> " }); // c:1192-1193
1428        set_default("SPROMPT", "zsh: correct '%R' to '%r' [nyae]? "); // c:1194
1429    }
1430
1431    // condtab = NULL; wrappers = NULL;                                      // c:1272-1273
1432
1433    crate::ported::utils::adjustwinsize(0); // c:1276
1434
1435    // getrlimit loop                                                        // c:1286-1289
1436
1437    // breaks = contflag = loops = ancestor_loops = 0;  (c:1328; statics start at 0)
1438    // lastmailcheck = zmonotime(NULL);                                      // c:1293
1439    // locallevel = sourcelevel = 0;                                         // c:1294
1440    sourcelevel.store(0, Ordering::SeqCst); // c:1294
1441                                            // sfcontext = SFC_NONE; trap_return = 0;                                // c:1295-1296
1442                                            // trap_state = TRAP_STATE_INACTIVE;                                     // c:1297
1443                                            // noerrexit = NOERREXIT_EXIT|RETURN|SIGNAL;                             // c:1298
1444                                            crate::ported::exec::nohistsave.store(1, Ordering::SeqCst); // c:1332
1445                                            // dirstack = znewlinklist(); bufstack = znewlinklist();                 // c:1300-1301
1446                                            // hsubl = hsubr = NULL; lastpid = 0;                                    // c:1302-1303
1447
1448    // get_usage();                                                          // c:1305
1449
1450    // Close fd's we opened to block 0-9                                     // c:1307
1451    #[cfg(unix)]
1452    for i in 0..10 {
1453        // c:1308
1454        if close_fds[i] != 0 {
1455            // c:1309
1456            unsafe {
1457                libc::close(i as i32);
1458            } // c:1310
1459        }
1460    }
1461
1462    crate::ported::prompt::set_default_colour_sequences(); // c:1313
1463
1464    // c:1315-1326 — `/* ZSH_EXEPATH */`: the absolute path of the running
1465    // executable, from argv[0] resolved against the start-up cwd. zshrs
1466    // keeps C's `pwd` global in $PWD.
1467    {
1468        let exename = argv0.lock().unwrap().clone(); // c:1318
1469        let cwd = crate::ported::params::getsparam("PWD"); // c:1319
1470        if let Some(mypath) = getmypath(Some(&exename), cwd.as_deref()) {
1471            // c:1320-1323
1472            crate::ported::params::setsparam("ZSH_EXEPATH", &mypath); // c:1324
1473        }
1474    }
1475
1476    // These are PARAMETERS (`setsparam`), not environment entries. The port
1477    // used `std::env::set_var`, which put ZSH_EXEPATH / ZSH_SCRIPT /
1478    // ZSH_EXECUTION_STRING / ZSH_NAME into the environ every external
1479    // command inherits; under zsh a child sees none of them.
1480    if let Some(cmd) = cmd {
1481        // c:1182
1482        crate::ported::params::setsparam("ZSH_EXECUTION_STRING", cmd); // c:1183
1483    }
1484    if let Some(rs) = runscript {
1485        // c:1184
1486        crate::ported::params::setsparam("ZSH_SCRIPT", rs); // c:1185
1487    }
1488    // c:1186 — `setsparam("ZSH_NAME", ztrdup(zsh_name));`. Not repeated
1489    // here: ShellExecutor::new (vm_helper.rs, `setsparam("ZSH_NAME", "zsh")`)
1490    // has already created the param with the literal "zsh", and writing the
1491    // basename of argv[0] ("zshrs") over it would change $ZSH_NAME.
1492    let _ = zsh_name;
1493}
1494
1495/// Port of `static void setupshin(char *runscript)` from Src/init.c:1340.
1496fn setupshin(runscript: Option<&str>) {
1497    // c:1340
1498    if let Some(script) = runscript {
1499        // c:1340
1500        let funmeta = unmeta(script); // c:1346
1501        let mut sfname: Option<String> = None; // c:1343
1502        if std::path::Path::new(&funmeta).is_file() {
1503            // c:1350-1352
1504            sfname = Some(script.to_string()); // c:1353
1505        } else if crate::ported::zsh_h::isset(crate::ported::zsh_h::PATHSCRIPT)
1506            && !script.contains('/')
1507        {
1508            // c:1354-1360 — with PATHSCRIPT, search $PATH when the script
1509            // name has no directory part.
1510            sfname = crate::ported::utils::pathprog(script)
1511                .map(|p| p.to_string_lossy().into_owned()); // c:1359
1512        }
1513        if sfname.is_none() {
1514            // c:1361
1515            crate::ported::utils::zerr(&format!(
1516                // c:1364
1517                "can't open input file: {}",
1518                script
1519            ));
1520            std::process::exit(127); // c:1365
1521        }
1522    }
1523    // lineno = 1;                                                           // c:1394
1524    // shinbufalloc();                                                       // c:1394
1525}
1526
1527/// Port of `void init_signals(void)` from Src/init.c:1394.
1528pub fn init_signals() {
1529    // c:1394
1530    // c:1398-1399 — `sigtrapped = hcalloc(TRAPCOUNT * sizeof(int));`
1531    // and `siglists = hcalloc(TRAPCOUNT * sizeof(Eprog));`. Both tables
1532    // are statics in signals.rs, sized at their declaration.
1533
1534    // c:1401-1406 — `if (interact) { signal_setmask(signal_mask(0));
1535    // for (i=0; i<NSIG; ++i) signal_default(i); }`. Reset to default
1536    // dispositions on every signal at startup so a parent's stale
1537    // handlers don't leak in.
1538    #[cfg(unix)]
1539    if interact() {
1540        let empty = crate::ported::signals::signal_mask(0);
1541        let _ = crate::ported::signals::signal_setmask(&empty);
1542        // c:1404 — `for (i=0; i<NSIG; ++i) signal_default(i);`. NSIG
1543        // is `<signal.h>`-provided in C; libc-rs doesn't re-export it
1544        // directly. `signals_h::SIGCOUNT` is the canonical port of
1545        // NSIG-1 (Linux=64, macOS=31).
1546        //
1547        // The previous Rust port used `1..64i32` — hardcoded, missing
1548        // signal 64 on Linux (where NSIG=65) AND iterating PAST the
1549        // valid range on macOS (where signals 32..63 don't exist).
1550        // Use `1..=SIGCOUNT` so the loop iterates exactly NSIG-1
1551        // signals on each platform, skipping signal 0 (C iterates
1552        // it but signal_default(0) is implementation-defined).
1553        for i in 1..=crate::ported::signals_h::SIGCOUNT {
1554            let _ = crate::ported::signals::signal_default(i);
1555        }
1556    }
1557
1558    // c:1407 — `sigchld_mask = signal_mask(SIGCHLD);`. The mask is a pure
1559    // function of SIGCHLD, so `child_block`/`child_unblock` (signals_h.rs)
1560    // derive it with `signal_mask` at each use instead of reading a cached copy.
1561
1562    intr(); // c:1409
1563
1564    #[cfg(unix)]
1565    {
1566        // c:1444-1445 — detect a parent-installed SIG_IGN on SIGQUIT and
1567        // record it as an ignored trap. The body lives in
1568        // `extensions::startup_signals` because `bins/zshrs.rs`'s `-c` and
1569        // script-file dispatch bypass this function entirely and need the
1570        // same two lines; keeping ONE implementation avoids the two paths
1571        // drifting apart.
1572        crate::startup_signals::record_inherited_sigquit_ignore();
1573        // c:1414-1416 — `#ifndef QDEBUG signal_ignore(SIGQUIT)`.
1574        signal_ignore(libc::SIGQUIT);
1575
1576        // c:1418-1421 — SIGHUP: if parent installed SIG_IGN, clear
1577        // the HUP option; otherwise install our handler.
1578        if signal_ignore(libc::SIGHUP) == libc::SIG_IGN {
1579            dosetopt(HUP, 0, 0); // c:1419
1580        } else {
1581            install_handler(libc::SIGHUP); // c:1421
1582        }
1583        install_handler(libc::SIGCHLD); // c:1422
1584        #[cfg(not(target_os = "haiku"))]
1585        {
1586            install_handler(libc::SIGWINCH); // c:1424
1587                                            // c:1425 — `winch_block(); /* See utils.c:preprompt() */`
1588                                            //
1589                                            // The standing block is the whole delivery policy for
1590                                            // SIGWINCH: from here on the handler runs ONLY inside an
1591                                            // explicit unblock window — `preprompt` (c:Src/utils.c:1540),
1592                                            // `raw_getbyte`'s poll/read (c:Src/Zle/zle_main.c:588/850,
1593                                            // ported at zle/zle_main.rs:504/648/757) and `readoutput`
1594                                            // (c:Src/input.c:277). C's remaining unblocks
1595                                            // (c:Src/exec.c:533/571/576/586/590/632) are not windows at
1596                                            // all: they sit in the forked child immediately before
1597                                            // `execve`, and only decide the mask the new program
1598                                            // inherits. Everywhere else — a widget body, and
1599                                            // so the whole of completion — a resize stays PENDING and
1600                                            // `zterm_columns`/`zterm_lines` hold the geometry the
1601                                            // current redraw started with.
1602                                            //
1603                                            // Without it the block existed only as a SIDE EFFECT of
1604                                            // `raw_getbyte` re-blocking after each read
1605                                            // (zle_main.rs:507/654/762): every unblock happened to be
1606                                            // paired, so the mask happened to be right. An UNPAIRED
1607                                            // unblock then leaves SIGWINCH live until the next
1608                                            // `raw_getbyte` re-block — i.e. for the rest of the
1609                                            // current command, a completion included.
1610                                            // `zexecve_recover` had four of them (vm_helper.rs,
1611                                            // c:565/570/580/584/626, child-side in C).
1612                                            //
1613                                            // Whenever the handler does run inside a widget,
1614                                            // `adjustwinsize(1)` lands in the middle of `calclist` and
1615                                            // display strings built against 80 columns get counted
1616                                            // against the new 60. Measured that way, `git <TAB>`
1617                                            // across a 24x80 -> 24x60 resize asks "see all 164
1618                                            // possibilities (251 lines)?" for 164 one-line matches
1619                                            // where zsh asks 153/153.
1620            crate::ported::signals_h::winch_block(); // c:1425
1621        }
1622
1623        // c:1427-1431 — interactive-only handlers.
1624        if interact() {
1625            install_handler(libc::SIGPIPE); // c:1428
1626            install_handler(libc::SIGALRM); // c:1429
1627            signal_ignore(libc::SIGTERM); // c:1430
1628        }
1629
1630        // c:1432-1436 — `if (jobbing)` job-control signal ignores.
1631        if jobbing() {
1632            signal_ignore(libc::SIGTTOU); // c:1433
1633            signal_ignore(libc::SIGTSTP); // c:1434
1634            signal_ignore(libc::SIGTTIN); // c:1435
1635        }
1636    }
1637}
1638
1639/// Port of `void run_init_scripts(void)` from Src/init.c:1445.
1640/// Runs the standard zsh init scripts (or KSH/SH-compat scripts when
1641/// emulating). Each guard mirrors the C predicate set: islogin, RCS,
1642/// GLOBALRCS, PRIVILEGED, INTERACTIVE.
1643pub fn run_init_scripts() {
1644    // c:1445
1645
1646    // c:1447 — noerrexit = NOERREXIT_EXIT | NOERREXIT_RETURN | NOERREXIT_SIGNAL;
1647    crate::ported::exec::noerrexit.store(
1648        crate::ported::zsh_h::NOERREXIT_EXIT
1649            | crate::ported::zsh_h::NOERREXIT_RETURN
1650            | crate::ported::zsh_h::NOERREXIT_SIGNAL,
1651        Ordering::SeqCst,
1652    );
1653
1654    // c:1449 — if (EMULATION(EMULATE_KSH|EMULATE_SH)) { ... }
1655    let emul = emulation.load(Ordering::SeqCst);
1656    let is_posix = (emul & (EMULATE_KSH | EMULATE_SH) as i32) != 0;
1657
1658    let is_login = islogin();
1659    let interact = isset(INTERACTIVE);
1660    let privileged = isset(PRIVILEGED);
1661
1662    // ZSHRS-ONLY, no C counterpart. C models two startup-file sets — zsh's
1663    // and one lumped Bourne one — because `emulate sh`/`ksh` is all it
1664    // offers. zshrs ships drop-ins for eight shells, and each reads its
1665    // own files: `--bash` wants `~/.bashrc` and the `~/.bash_profile`
1666    // chain, `--ksh` wants `$ENV` defaulted to `~/.kshrc`, `--mksh`
1667    // `~/.mkshrc`, `--csh` `~/.cshrc` + `~/.login`. Routed through
1668    // `extensions::emulation_startup`, which owns that table. Only a
1669    // selected DROP-IN takes this path; a runtime `emulate sh` keeps the
1670    // faithful branches below.
1671    if crate::extensions::emulation_startup::overrides_zsh_startup() {
1672        crate::extensions::emulation_startup::run_init_scripts();
1673        crate::ported::exec::noerrexit.store(0, Ordering::SeqCst); // c:1549
1674        crate::ported::exec::nohistsave.store(0, Ordering::SeqCst); // c:1550
1675        return;
1676    }
1677
1678    if is_posix {
1679        // c:1450-1451 — if (islogin) source("/etc/profile");
1680        if is_login {
1681            let _ = source("/etc/profile");
1682        }
1683        if !privileged {
1684            // c:1452
1685            // c:1454 — if (islogin) sourcehome(".profile");
1686            if is_login {
1687                sourcehome(".profile");
1688            }
1689            // c:1456-1468 — if (interact) { … getsparam("ENV"); source(s); }
1690            if interact {
1691                if let Some(s) = getsparam("ENV") {
1692                    let _ = source(&s);
1693                }
1694            }
1695        } else {
1696            // c:1470 — source("/etc/suid_profile");
1697            let _ = source("/etc/suid_profile");
1698        }
1699    } else {
1700        // !!! RUST-ONLY — no C counterpart. A `zshrs-recorder` shard
1701        // (~/.zshrs/images/*-recorder.rkyv) holds the end-state of this
1702        // whole chain; with `[shell].skip_configs` not "off", replay it
1703        // and source NONE of the files below, /etc/zshenv included.
1704        // `-f` (RCS unset), PRIVILEGED and the `--zsh` parity mode
1705        // (`emulating()`) keep the faithful C chain. A shard that
1706        // applies zero rows falls through to sourcing.
1707        if isset(RCS)
1708            && !privileged
1709            && !crate::extensions::emulation_startup::emulating()
1710            && crate::canonical_apply::replay_startup()
1711        {
1712            crate::ported::exec::noerrexit.store(0, Ordering::SeqCst); // c:1549
1713            crate::ported::exec::nohistsave.store(0, Ordering::SeqCst); // c:1550
1714            return;
1715        }
1716        // c:1473 — source(GLOBAL_ZSHENV);
1717        let _ = source(&crate::extensions::global_rc::global_rc_path(
1718            crate::ported::config_h::GLOBAL_ZSHENV,
1719        ));
1720
1721        // c:1476-1490 — if (isset(RCS) && unset(PRIVILEGED))
1722        //                  { newuser-probe; sourcehome(".zshenv"); }
1723        if isset(RCS) && !privileged {
1724            sourcehome(".zshenv");
1725        }
1726        // c:1491-1498 — if (islogin) { GLOBAL_ZPROFILE? + .zprofile }
1727        if is_login {
1728            if isset(RCS) && isset(GLOBALRCS) {
1729                let _ = source(&crate::extensions::global_rc::global_rc_path(
1730                    crate::ported::config_h::GLOBAL_ZPROFILE,
1731                ));
1732            }
1733            if isset(RCS) && !privileged {
1734                sourcehome(".zprofile");
1735            }
1736        }
1737        // c:1499-1506 — if (interact) { GLOBAL_ZSHRC? + .zshrc }
1738        if interact {
1739            if isset(RCS) && isset(GLOBALRCS) {
1740                let _ = source(&crate::extensions::global_rc::global_rc_path(
1741                    crate::ported::config_h::GLOBAL_ZSHRC,
1742                ));
1743            }
1744            if isset(RCS) && !privileged {
1745                sourcehome(".zshrc");
1746            }
1747        }
1748        // c:1507-1514 — if (islogin) { GLOBAL_ZLOGIN? + .zlogin }
1749        if is_login {
1750            if isset(RCS) && isset(GLOBALRCS) {
1751                let _ = source(&crate::extensions::global_rc::global_rc_path(
1752                    crate::ported::config_h::GLOBAL_ZLOGIN,
1753                ));
1754            }
1755            if isset(RCS) && !privileged {
1756                sourcehome(".zlogin");
1757            }
1758        }
1759    }
1760    crate::ported::exec::noerrexit.store(0, Ordering::SeqCst); // c:1549
1761    crate::ported::exec::nohistsave.store(0, Ordering::SeqCst); // c:1550
1762}
1763
1764/// Port of `void init_misc(char *cmd, char *zsh_name)` from Src/init.c:1524.
1765pub fn init_misc(cmd: Option<&str>, zsh_name: &str) {
1766    // c:1524
1767    if zsh_name.starts_with('r') {
1768        // c:1524
1769        crate::ported::utils::zerrnam(
1770            zsh_name, // c:1527
1771            "no support for restricted mode",
1772        );
1773        std::process::exit(1); // c:1528
1774    }
1775    if let Some(cmdstr) = cmd {
1776        // c:1530
1777        // if (SHIN >= 10) close(SHIN);                                      // c:1531-1532
1778        // SHIN = movefd(open("/dev/null", O_RDONLY|O_NOCTTY));              // c:1551
1779        // shinbufreset();                                                   // c:1551
1780        // execstring(cmd, 0, 1, "cmdarg");                                  // c:1551
1781        let _ = cmdstr;
1782        // stopmsg = 1; zexit(...);                                          // c:1551-1537
1783        std::process::exit(0);
1784    }
1785
1786    // c:1573-1574 — `if (interact && isset(RCS))
1787    //                    readhistfile(NULL, 0, HFILE_USE_OPTIONS);`
1788    // — THE startup read of $HISTFILE into the history ring. Without
1789    // it an interactive shell starts with empty history (up-arrow /
1790    // fc -l see nothing from previous sessions).
1791    if crate::ported::zsh_h::interact() && isset(RCS) {
1792        crate::ported::hist::readhistfile(None, 0, crate::ported::zsh_h::HFILE_USE_OPTIONS as i32);
1793        // c:1574
1794    }
1795}
1796
1797/// Port of `mod_export enum source_return source(char *s)` from Src/init.c:1551.
1798///
1799/// Body dispatches through `with_executor` to the fusevm bytecode
1800/// pipeline (`execute_script_zsh_pipeline`), matching the path
1801/// `bins/zshrs.rs::source_from_memory` takes. `scriptname` /
1802/// `scriptfilename` / `sourcelevel` are saved+restored around the
1803/// nested execution per the C save/restore at c:1572-1670.
1804pub fn source(s: &str) -> i32 {
1805    // c:1551
1806    let us = unmeta(s); // c:1551
1807    let path = std::path::Path::new(&us);
1808    // c:1565-1568 — `if (!s || (!(prog = try_source_file(us)) &&
1809    // (tempfd = ... open(us, ...)) == -1)) return SOURCE_NOT_FOUND;`
1810    // — a loadable `<file>.zwc` rescues a missing/unreadable plain
1811    // file; only BOTH failing is NOT_FOUND. The compiled prog itself
1812    // is fetched below (the Rust port defers it to the contents
1813    // read so the state save/restore stays in one place).
1814    let zwc_prog = crate::ported::parse::try_source_file(&us);
1815    if zwc_prog.is_none() && !path.exists() {
1816        return 1; /* SOURCE_NOT_FOUND */
1817    }
1818
1819    // c:1571-1581 — save shell state.
1820    let old_scriptname = crate::ported::utils::scriptname_get(); // c:1573
1821    let old_scriptfilename = crate::ported::utils::scriptfilename_get(); // c:1574
1822    crate::ported::utils::set_scriptname(Some(us.clone())); // c:1572
1823    crate::ported::utils::set_scriptfilename(Some(us.clone()));
1824
1825    // c:1627-1630 — `if (isset(SOURCETRACE)) { printprompt4();
1826    //     fprintf(xtrerr ? xtrerr : stderr, "<sourcetrace>\n"); }`
1827    if isset(crate::ported::zsh_h::SOURCETRACE) {
1828        // c:1621 — `lineno = 1;` precedes the trace, so PS4 `%i` reads 1.
1829        // The file's own statements set it again as they run; put the
1830        // caller's value back for the frame push below.
1831        let oldlineno = crate::ported::params::getsparam("LINENO")
1832            .and_then(|s| s.parse::<i64>().ok())
1833            .unwrap_or(0);
1834        crate::fusevm_bridge::set_lineno_impl(1); // c:1621
1835        crate::ported::utils::printprompt4(); // c:1628
1836        crate::fusevm_bridge::xtrerr_fputs("<sourcetrace>\n"); // c:1629
1837        crate::fusevm_bridge::xtrerr_flush();
1838        crate::fusevm_bridge::set_lineno_impl(oldlineno);
1839    }
1840
1841    sourcelevel.fetch_add(1, Ordering::SeqCst); // c:1606
1842
1843    // c:1610-1618 — push an FS_SOURCE funcstack frame so `$funcstack`,
1844    // `$functrace` and `$funcfiletrace` include the sourced file (the
1845    // readers at parameter.rs walk FUNCSTACK and special-case
1846    // `tp == FS_SOURCE`). FUNCSTACK is a Vec stack: the last element is
1847    // the top, so `prev` is left None (the index encodes the link, same
1848    // convention as the FS_FUNC push in exec.rs::doshfunc:5821).
1849    let oldlineno = crate::ported::lex::lineno() as i64; // c:1576 oldlineno = lineno
1850    {
1851        // c:1612-1613 — caller: the current funcstack top, else the
1852        // previous script filename, else "zsh".
1853        let caller = {
1854            let stk = crate::ported::modules::parameter::FUNCSTACK
1855                .lock()
1856                .unwrap_or_else(|e| e.into_inner());
1857            stk.last().map(|f| f.name.clone())
1858        }
1859        .or_else(|| old_scriptfilename.clone())
1860        .or_else(|| Some("zsh".to_string()));
1861        let frame = crate::ported::zsh_h::funcstack {
1862            prev: None,                          // c:1617 (Vec-stack index encodes link)
1863            name: us.clone(),                    // c:1611 fstack.name = scriptfilename
1864            filename: Some(us.clone()),          // c:1616
1865            caller,                              // c:1612
1866            flineno: 0,                          // c:1614
1867            lineno: oldlineno,                   // c:1615
1868            tp: crate::ported::zsh_h::FS_SOURCE, // c:1618
1869        };
1870        crate::ported::modules::parameter::FUNCSTACK
1871            .lock()
1872            .unwrap_or_else(|e| e.into_inner())
1873            .push(frame); // c:1618 funcstack = &fstack
1874    }
1875
1876    // c:1618-1642 — parse-and-execute loop. Route through the
1877    // fusevm executor for the actual parse+exec; if no executor
1878    // context (out-of-band call), fall back to the partial
1879    // read-for-side-effects path so errors still surface.
1880    //
1881    // c:1566 — `try_source_file(us)` runs FIRST: a sibling
1882    // `<file>.zwc` newer than the file (or `s` itself being a
1883    // `.zwc`) supplies the compiled wordcode instead of the plain
1884    // read. Bridge wordcode → text via getpermtext (same as the
1885    // `.zwc` autoload path in exec.rs::loadautofn).
1886    let from_zwc = zwc_prog.is_some();
1887    let contents = match zwc_prog {
1888        Some(prog) => Ok(crate::ported::text::getpermtext(Box::new(prog), None, 0)),
1889        // c:1566/1626 — source() reads the file as raw bytes; a
1890        // non-UTF-8 byte is metafied, not an error (Src/utils.c:4856).
1891        None => crate::script_bytes::read_script_file(path),
1892    };
1893    if let Ok(body) = contents {
1894        // c:Src/jobs.c:1878-1884 — zsh runs the sourced list through
1895        // execpline, whose per-pipeline `initjob()` caps recursion at
1896        // MAX_MAXJOBS: `zerr("job table full or recursion limit exceeded")`
1897        // then bails. The fusevm pipeline path below doesn't allocate a job
1898        // per pipeline, so without this guard runaway `. self`-style
1899        // recursion (invisible to FUNCNEST, which counts FS_FUNC frames
1900        // only) overflowed the 256 MB main-thread stack → uncatchable
1901        // SIGBUS. Reproduce the ceiling: total FUNCSTACK depth is the proxy
1902        // for zsh's concurrently-held job slots. At/over the ceiling, raise
1903        // the zsh-identical error (zerr sets ERRFLAG_ERROR so the outer
1904        // sourced lists unwind) and refuse the deeper body — the FS_SOURCE
1905        // frame just pushed is popped normally below.
1906        let over_limit = crate::ported::modules::parameter::FUNCSTACK
1907            .lock()
1908            .map(|s| s.len())
1909            .unwrap_or(0)
1910            >= crate::ported::jobs::MAX_MAXJOBS;
1911        if over_limit {
1912            crate::ported::utils::zerr("job table full or recursion limit exceeded");
1913            crate::ported::builtin::LASTVAL.store(1, Ordering::Relaxed);
1914        } else if from_zwc {
1915            // c:1621 — `execode(prog, 1, 0, "filecode")`. The deparse of an
1916            // already-compiled program is re-lexed here, so it needs the
1917            // lexer pinned to the spelling `untokenize` writes; see
1918            // ShellExecutor::execute_zwc_program.
1919            let _ = crate::fusevm_bridge::execute_zwc_program(&body);
1920        } else {
1921            let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
1922        }
1923    }
1924
1925    sourcelevel.fetch_sub(1, Ordering::SeqCst); // c:1644
1926
1927    // c:1664 — `funcstack = fstack.prev;` — pop our FS_SOURCE frame.
1928    crate::ported::modules::parameter::FUNCSTACK
1929        .lock()
1930        .unwrap_or_else(|e| e.into_inner())
1931        .pop();
1932
1933    // c:1646-1670 — restore shell state.
1934    crate::ported::utils::set_scriptname(old_scriptname);
1935    crate::ported::utils::set_scriptfilename(old_scriptfilename);
1936    0 /* SOURCE_OK */ // c:1679
1937}
1938
1939/// Port of `void sourcehome(char *s)` from Src/init.c:1679.
1940pub fn sourcehome(s: &str) {
1941    // c:1679
1942    queue_signals(); // c:1679
1943    let emul = emulation.load(Ordering::SeqCst);
1944    let is_posix = (emul & 6) != 0;
1945    // c:1684 — `h = is_posix ? getsparam("HOME") : (getsparam("ZDOTDIR")
1946    //                                                 ?: getsparam("HOME"))`
1947    //          paramtab read; was OS env.
1948    let h = if is_posix {
1949        getsparam("HOME")
1950    } else {
1951        getsparam("ZDOTDIR").or_else(|| getsparam("HOME"))
1952    };
1953    let h = match h {
1954        // c:1685-1689
1955        Some(h) => h,
1956        None => {
1957            unqueue_signals();
1958            return;
1959        }
1960    };
1961    let buf = format!("{}/{}", h, s); // c:1713
1962    unqueue_signals(); // c:1713
1963    source(&buf); // c:1713
1964}
1965
1966/// Port of `void init_bltinmods(void)` from Src/init.c:1703.
1967pub fn init_bltinmods() {
1968    // c:1703
1969    // #include "bltinmods.list"                                             // c:1705
1970    // load_module("zsh/main", NULL, 0);                                     // c:1706
1971    //
1972    // C's bltinmods.list is autotools-generated and contains a
1973    // `register_module(name, setup_, ...)` line per statically-linked
1974    // module — which seeds MODULESTAB and runs each module's setup_
1975    // (and later boot_) entry points. Our equivalent is the
1976    // register_builtin_modules() call inside `modulestab::new()`, which
1977    // is reached the first time MODULESTAB is observed. Force that
1978    // observation here so default-loaded modules
1979    // (zsh/parameter, zsh/main, zsh/watch, …) register their
1980    // builtins/params/preprompt hooks before user code runs.
1981    drop(crate::ported::module::MODULESTAB.lock().unwrap());
1982}
1983
1984/// Port of `mod_export void noop_function(void)` from Src/init.c:1713.
1985pub fn noop_function() { // c:1713
1986                         /* do nothing */                                                         // c:1720
1987}
1988
1989/// Port of `mod_export void noop_function_int(int nothing)` from Src/init.c:1720.
1990pub fn noop_function_int(_nothing: i32) { // c:1720
1991                                          /* do nothing */                                                         // c:1720
1992}
1993
1994/// Once-guard for the `zleentry` module autoload (c:1755 `case 0:`).
1995///
1996/// In C the guard IS `zle_load_state == 0`: ZLE is dlopened lazily by
1997/// the first `zleentry` call, and that same call loads `zsh/compctl`
1998/// alongside it (c:1764-1765). zshrs links ZLE in statically and
1999/// initialises it earlier, in `zsh_main`, which already sets
2000/// `zle_load_state = 1` — so the C gate can never fire at the first ZLE
2001/// read. This flag restores the one-shot semantics for the
2002/// module-registration half of c:1755-1773, which still has to happen
2003/// at the first zleentry-equivalent call and not before.
2004///
2005/// The observable effect of that half: an interactive shell has
2006/// `zsh/zle`, `zsh/complete` (a `zsh/compctl` dependency) and
2007/// `zsh/compctl` marked loaded from its first line read onwards, while
2008/// `-c` / script runs — which never reach a ZLE read — leave all three
2009/// merely `autoloaded`. `_default` (Completion/Zsh/Context/_default)
2010/// branches on `zmodload -e zsh/compctl` to decide whether the legacy
2011/// `compcall` engine is available, so this is load-bearing for
2012/// completion parity, not just for `zmodload` listings.
2013pub static zle_modules_loaded: std::sync::atomic::AtomicBool =
2014    std::sync::atomic::AtomicBool::new(false);
2015
2016/// Port of `mod_export char *zleentry(...)` from Src/init.c:1743.
2017pub fn zleentry(cmd: i32) -> Option<String> {
2018    // c:1743
2019    let mut cmd = cmd;
2020    match zle_load_state.load(Ordering::SeqCst) {
2021        // c:1755
2022        0 => {
2023            // c:1756
2024            // c:1761-1762 — `if (cmd != ZLE_CMD_TRASH &&
2025            //   cmd != ZLE_CMD_RESET_PROMPT && cmd != ZLE_CMD_REFRESH)`
2026            if cmd != crate::ported::zsh_h::ZLE_CMD_TRASH
2027                && cmd != crate::ported::zsh_h::ZLE_CMD_RESET_PROMPT
2028                && cmd != crate::ported::zsh_h::ZLE_CMD_REFRESH
2029            {
2030                // c:1764 — `if (load_module("zsh/zle", NULL, 0) != 1)`
2031                // then c:1765 `(void)load_module("zsh/compctl", NULL, 0)`.
2032                // `zsh/compctl` pulls in `zsh/complete` through the
2033                // compctl.mdd moddeps edge seeded in
2034                // register_builtin_modules.
2035                if !zle_modules_loaded.swap(true, Ordering::SeqCst) {
2036                    let mut tab = crate::ported::module::MODULESTAB.lock().unwrap();
2037                    if tab.load_module("zsh/zle", None, false) != 1 {
2038                        tab.load_module("zsh/compctl", None, false); // c:1765
2039                    }
2040                }
2041                zle_load_state.store(2, Ordering::SeqCst); // c:1770
2042            }
2043        }
2044        1 => {
2045            // c:1776
2046            // c:1777 — `ret = zle_entry_ptr(cmd, ap);`. The pointer is
2047            // set at Src/Zle/zle_main.c:2248 to `zle_main_entry`, so a
2048            // LOADED zle dispatches the command there; the fallback
2049            // switch below is skipped (`cmd = -1`).
2050            //
2051            // This call was MISSING: the arm set `cmd = -1` and returned
2052            // None, making `zleentry(ZLE_CMD_TRASH)` a silent no-op. C
2053            // relies on it in `zwarning` (Src/utils.c:144-145 `if
2054            // (isatty(2)) zleentry(ZLE_CMD_TRASH);`) to park the cursor
2055            // past the ZLE display before a diagnostic lands on stderr
2056            // and to set `resetneeded` so the editor line is repainted
2057            // afterwards. Without it, a warning emitted mid-ZLE (e.g.
2058            // `compdescribe`'s "invalid argument" during `pr<TAB>`) was
2059            // written INTO the prompt line and the completion list was
2060            // then drawn without the prompt being redrawn — 5 rows off
2061            // zsh in the comptab parity grid.
2062            //
2063            // Rust deviation (no C counterpart): C's `zleentry` is
2064            // variadic, so it forwards every command's `va_list`
2065            // straight through. The Rust signature carries no varargs,
2066            // so only the ARGLESS commands can be reconstructed here;
2067            // the arg-carrying ones (READ / GET_LINE / ADD_TO_LINE /
2068            // SET_KEYMAP / GET_KEY / SET_HIST_LINE) are called through
2069            // their own typed paths and still fall through unchanged.
2070            use crate::ported::zle::zle_main::zle_main_entry_args;
2071            let mut args = match cmd {
2072                x if x == crate::ported::zsh_h::ZLE_CMD_TRASH => {
2073                    Some(zle_main_entry_args::Trash) // c:2152
2074                }
2075                x if x == crate::ported::zsh_h::ZLE_CMD_RESET_PROMPT => {
2076                    Some(zle_main_entry_args::ResetPrompt) // c:2156
2077                }
2078                x if x == crate::ported::zsh_h::ZLE_CMD_REFRESH => {
2079                    Some(zle_main_entry_args::Refresh) // c:2160
2080                }
2081                x if x == crate::ported::zsh_h::ZLE_CMD_PREEXEC => {
2082                    Some(zle_main_entry_args::Preexec) // c:2187
2083                }
2084                x if x == crate::ported::zsh_h::ZLE_CMD_POSTEXEC => {
2085                    Some(zle_main_entry_args::Postexec) // c:2191
2086                }
2087                x if x == crate::ported::zsh_h::ZLE_CMD_CHPWD => {
2088                    Some(zle_main_entry_args::Chpwd) // c:2195
2089                }
2090                _ => None,
2091            };
2092            let ret = args
2093                .as_mut()
2094                .and_then(|a| crate::ported::zle::zle_main::zle_main_entry(cmd, a)); // c:1777
2095            cmd = -1; // c:1779
2096            if ret.is_some() {
2097                return ret; // c:1777
2098            }
2099        }
2100        2 => { /* fallback */ } // c:1782
2101        _ => {}
2102    }
2103    match cmd {
2104        // c:1788
2105        // ZLE_CMD_READ                                                      // c:1796
2106        4 => {
2107            // c:1796
2108            let _line = String::new(); // c:1808
2109            return Some(String::new());
2110        }
2111        // ZLE_CMD_GET_LINE                                                  // c:1812
2112        5 => {
2113            // c:1812
2114            return Some(String::new()); // c:1835
2115        }
2116        _ => {}
2117    }
2118    None // c:1855
2119}
2120
2121/// Port of `mod_export int fallback_compctlread(...)` from Src/init.c:1835.
2122pub fn fallback_compctlread(name: &str) -> i32 {
2123    // c:1835
2124    crate::ported::utils::zwarnnam(
2125        name, // c:1855
2126        "no loaded module provides read for completion context",
2127    );
2128    1 // c:1855
2129}
2130
2131/// Port of `mod_export int zsh_main(int argc, char **argv)` from Src/init.c:1855.
2132pub fn zsh_main(_argc: i32, argv: &[String]) -> i32 {
2133    // c:1855
2134    #[cfg(unix)]
2135    unsafe {
2136        let empty = std::ffi::CString::new("").unwrap();
2137        libc::setlocale(libc::LC_ALL, empty.as_ptr()); // c:1861
2138    }
2139    // The process locale is now the environment's, so consume the lazy
2140    // fallback that library consumers (unit tests, embedded entry points)
2141    // rely on. Doing it here means it can never fire later and undo an
2142    // `LC_*` parameter assignment made from an rc file.
2143    let _ = *crate::ported::utils::MB_LOCALE_READY;
2144
2145    // init_jobs(argv, environ);                                             // c:1864
2146    let env: Vec<String> = std::env::vars()
2147        .map(|(k, v)| format!("{}={}", k, v))
2148        .collect();
2149    let _ = crate::ported::jobs::init_jobs(argv, &env);
2150
2151    // typtab[...] |= IMETA;                                                 // c:1871-1875
2152
2153    // Metafy each argv (already strings in Rust)                            // c:1877
2154
2155    let mut zsh_name = argv.first().cloned().unwrap_or_default(); // c:1879
2156    loop {
2157        // c:1880
2158        let arg0 = zsh_name.clone(); // c:1881
2159        zsh_name = match arg0.rfind('/') {
2160            // c:1882
2161            None => arg0.clone(),                 // c:1883
2162            Some(i) => arg0[i + 1..].to_string(), // c:1885
2163        };
2164        if zsh_name.starts_with('-') {
2165            // c:1886
2166            zsh_name = zsh_name[1..].to_string(); // c:1887
2167        }
2168        if zsh_name == "su" {
2169            // c:1888
2170            if let Ok(sh) = std::env::var("SHELL") {
2171                // c:1889
2172                if !sh.is_empty() && arg0 != sh {
2173                    // c:1890
2174                    zsh_name = sh; // c:1891
2175                    continue; // c:1892
2176                }
2177            }
2178            break; // c:1893
2179        }
2180        break; // c:1895
2181    }
2182
2183    // fdtable_size = zopenmax(); fdtable[0..2] = FDT_EXTERNAL;              // c:1898-1900
2184    let _ = crate::ported::compat::zopenmax();
2185    // c:1900 — `fdtable[0] = fdtable[1] = fdtable[2] = FDT_EXTERNAL;`.
2186    // stdin/stdout/stderr belong to whoever invoked the shell, so they
2187    // are marked as somebody else's: c:Src/exec.c:3892-3894 lets `>&N` /
2188    // `<&N` duplicate an `FDT_EXTERNAL` descriptor and refuses every
2189    // other classified one. The port called `zopenmax()` and stopped,
2190    // leaving all three reading `FDT_UNUSED`.
2191    for fd in 0..=2 {
2192        crate::ported::utils::check_fd_table(fd);
2193        crate::ported::utils::fdtable_set(fd, crate::ported::zsh_h::FDT_EXTERNAL);
2194    }
2195
2196    crate::ported::options::createoptiontable(); // c:1902
2197
2198    // parseargs(zsh_name, argv, &runscript, &cmd);                          // c:1905
2199    let mut argv_v = argv.to_vec();
2200    let mut runscript: Option<String> = None; // c:1857
2201    let mut cmd: Option<String> = None; // c:1858
2202    parseargs(&zsh_name, &mut argv_v, &mut runscript, &mut cmd);
2203
2204    SHTTY.store(-1, Ordering::SeqCst); // c:1907
2205    init_io(cmd.as_deref()); // c:1908
2206    crate::startup_trace::mark("init_io");
2207    setupvals(cmd.as_deref(), runscript.as_deref(), &zsh_name); // c:1909
2208    // c:Src/params.c:893-988 — createparamtable imports `environ` and, for
2209    // an IPDEF8 PM_TIED colon-array, installs BOTH sides: the scalar and
2210    // the array split on ':'. `setupvals` above re-seeds the specials from
2211    // the static table, which leaves each tied array EMPTY; without the
2212    // split, an interactive shell reached its first prompt with
2213    //     typeset -aT FPATH fpath=(  )
2214    // while `$FPATH` still held the full value.
2215    //
2216    // `path` masked this: `PATH` is always exported, so a later env import
2217    // refilled it. zsh never exports `FPATH`, so nothing refilled that one,
2218    // and `-c` never runs this path at all -- which is why only an
2219    // interactive shell lost `$fpath`. A `.zshrc` doing the standard
2220    // `fpath=( mydir $fpath )` then appended to nothing and the shell kept
2221    // only what the rc file added.
2222    for (arr, scalar) in crate::ported::params::TIED_COLON_ARRAYS {
2223        let empty = crate::ported::params::getaparam(arr)
2224            .map(|v| v.is_empty())
2225            .unwrap_or(true);
2226        if !empty {
2227            continue;
2228        }
2229        let joined = crate::ported::params::getsparam(scalar).unwrap_or_default();
2230        if joined.is_empty() {
2231            continue;
2232        }
2233        let split: Vec<String> = joined.split(':').map(str::to_string).collect();
2234        crate::ported::params::setaparam(arr, split);
2235    }
2236    // The bundled tree has to survive the re-seed too, and the split above
2237    // does not restore it: with an INHERITED FPATH the array comes back
2238    // non-empty (the env import refilled it), so the split is skipped --
2239    // and the entry `ShellExecutor::new` appended lives only in the param
2240    // it just overwrote. The result was that `~/.zshrs/functions` was
2241    // present with FPATH unset and missing with FPATH set, in interactive
2242    // shells only. Append it here, last, exactly as the constructor does.
2243    if let Some(d) = crate::bundled_functions::functions_dir() {
2244        if d.is_dir() {
2245            let dir = d.to_string_lossy().into_owned();
2246            let mut arr = crate::ported::params::getaparam("fpath").unwrap_or_default();
2247            if !arr.iter().any(|e| *e == dir) {
2248                arr.push(dir);
2249                crate::ported::params::setaparam("fpath", arr);
2250            }
2251        }
2252    }
2253    crate::startup_trace::mark("setupvals");
2254
2255    init_signals(); // c:1911
2256    crate::startup_trace::mark("init_signals");
2257    init_bltinmods(); // c:1912
2258    crate::startup_trace::mark("init_bltinmods");
2259    crate::ported::builtin::init_builtins(); // c:1913
2260    crate::startup_trace::mark("init_builtins");
2261
2262    // `setupvals` above is what opens the shell's own long-lived files —
2263    // the log, the history database, the plugin cache — and none of them
2264    // arrives through `movefd`, so none of them registered itself the way
2265    // c:Src/utils.c:2007-2010 does. Sweep once here, after those opens
2266    // and before a single line of user code runs, so the gates that read
2267    // the fdtable (c:Src/exec.c:3830-3835 for `exec N>&-`,
2268    // c:Src/exec.c:3884-3897 for `>&N` / `<&N`) see the shell's own
2269    // descriptors as the shell's.
2270    crate::lowfd::register_internal_fds();
2271
2272    // Initialize the ZLE line editor for interactive sessions BEFORE the
2273    // rc files are sourced, so a user's `bindkey` in .zshrc/.zshenv
2274    // layers onto the populated emacs/main keymap instead of being wiped
2275    // by a later default_bindings(). C loads `zsh/zle` during init
2276    // (init.c via the module system); zshrs links ZLE statically, so the
2277    // equivalent setup — register the built-in widgets (init_thingies),
2278    // create the keymap name table, and build + select the default
2279    // keymaps (default_bindings sets curkeymap="main") — runs here once.
2280    // Guarded by zle_load_state so it never re-runs and clobbers user
2281    // bindings. Gated on `interact` so non-interactive `-c` shells skip
2282    // ZLE entirely (they read via shingetline, not the editor).
2283    //
2284    // Gated on the SAME predicate C uses to decide whether a line is read
2285    // through the editor at all — `interact && isset(SHINSTDIN) && SHTTY
2286    // != -1 && isset(USEZLE)` (c:Src/input.c:385). In C the zsh/zle module
2287    // is dlopened lazily by that read, so a shell started with `+Z`
2288    // (NO_ZLE, as Test/W02jobs.ztst and W03jobparameters.ztst launch it:
2289    // `zsh -fiV +Z`) never loads it and `zle_load_state` stays 0.
2290    // zshrs links ZLE in statically and initialises it eagerly, and the
2291    // gate here was `interact()` alone — so `+Z` still set
2292    // `zle_load_state = 1`. That flag is what arms the shell-integration
2293    // markers: `init.c:227/235` fire `zleentry(ZLE_CMD_PREEXEC /
2294    // POSTEXEC)` only when it is 1, and those write the OSC 133;C / 133;D
2295    // "start/end of output" sequences (Src/Zle/termquery.c:759-765
2296    // `mark_output`). With `+Z` honoured they are silent, as in zsh.
2297    // `zle_load_state` is still set lazily on the first real ZLE read
2298    // (input.rs, the `use_zle` branch), so `setopt zle` from an rc file
2299    // re-arms the editor exactly like C's lazy `load_module`.
2300    if interact()
2301        && isset(SHINSTDIN)
2302        && SHTTY.load(Ordering::SeqCst) != -1
2303        && isset(crate::ported::zsh_h::USEZLE)
2304        && zle_load_state.load(Ordering::SeqCst) == 0
2305    {
2306        crate::ported::zle::zle_thingy::init_thingies();
2307        crate::ported::zle::zle_keymap::createkeymapnamtab();
2308        crate::ported::zle::zle_keymap::default_bindings();
2309        zle_load_state.store(1, Ordering::SeqCst); // c:1739 — ZLE loaded
2310    }
2311
2312    // c:1914 — run_init_scripts() sources .zshenv/.zshrc/.zlogin via
2313    // source(). That runs BEFORE the loop's first execode establishes a
2314    // VM execution context, so the sourced bodies must execute under an
2315    // explicit SESSION_EXECUTOR context or they silently no-op (rc files
2316    // ignored). Scope it to JUST this call — the loop below enters its
2317    // own per-command context, and a broad/global executor scope would
2318    // re-enter on nested command substitution and hang.
2319    crate::fusevm_bridge::with_session_context(run_init_scripts); // c:1914
2320    crate::startup_trace::mark("run_init_scripts");
2321    setupshin(runscript.as_deref()); // c:1915
2322    crate::startup_trace::mark("setupshin");
2323    init_misc(cmd.as_deref(), &zsh_name); // c:1916
2324    crate::startup_trace::mark("init_misc");
2325
2326    loop {
2327        // c:1918
2328        let mut errexit = 0; // c:1924
2329                             // maybeshrinkjobtab();                                              // c:1925
2330                             // c:1927-1935 — `do { retflag = 0; loop(1,0); if (errflag &&
2331                             // !interact && !isset(CONTINUEONERROR)) { errexit = 1; break; } }
2332                             // while (tok != ENDINPUT && (tok != LEXERR || isset(SHINSTDIN)));`
2333        loop {
2334            // c:1927 do
2335            RETFLAG.store(0, Ordering::SeqCst); // c:1929 retflag = 0
2336            let _ = r#loop(1, 0); // c:1930
2337            if errflag.load(Ordering::SeqCst) != 0 && !interact() && !isset(CONTINUEONERROR) {
2338                // c:1931
2339                errexit = 1; // c:1932
2340                break; // c:1933
2341            }
2342            let tok_v = tok(); // c:1935
2343            if !(tok_v != ENDINPUT && (tok_v != LEXERR || isset(SHINSTDIN))) {
2344                break; // c:1935 while-cond false → exit do-while
2345            }
2346        }
2347        if tok() == LEXERR || errexit != 0 {
2348            // c:1936
2349            // c:1938-1939 — `if (!lastval) lastval = 1;` (fatal error → nonzero)
2350            if LASTVAL.load(Ordering::SeqCst) == 0 {
2351                LASTVAL.store(1, Ordering::SeqCst);
2352            }
2353            STOPMSG.store(1, Ordering::SeqCst); // c:1940 stopmsg = 1
2354            crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL);
2355            // c:1941
2356        }
2357        if !(isset(IGNOREEOF) && interact()) {
2358            // c:1943
2359            // c:1944-1947 — interactive "logout\n"/"exit\n" echo is `#if 0`'d
2360            // out in the C source, so nothing to port there.
2361            crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL); // c:1948
2362            continue; // c:1949 — only reached if zexit DEFERRED (running jobs)
2363        }
2364        noexitct.fetch_add(1, Ordering::SeqCst); // c:1951 noexitct++
2365        if noexitct.load(Ordering::SeqCst) >= 10 {
2366            // c:1952
2367            STOPMSG.store(1, Ordering::SeqCst); // c:1953 stopmsg = 1
2368            crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL);
2369            // c:1954
2370        }
2371        // c:1961-1963 — IGNOREEOF interactive nudge.
2372        if use_exit_printed.load(Ordering::SeqCst) == 0 {
2373            crate::ported::utils::zerrnam(
2374                "zsh",
2375                if !islogin() {
2376                    "use 'exit' to exit."
2377                } else {
2378                    "use 'logout' to logout."
2379                },
2380            );
2381        }
2382    }
2383}
2384
2385// =========================================================================
2386// Functions from init.c
2387// =========================================================================
2388
2389/// Port of `enum loop_return loop(int toplevel, int justonce)` from Src/init.c:113.
2390///
2391/// Keep executing lists until EOF found.                                    // c:109
2392///
2393/// ```c
2394/// enum loop_return
2395/// loop(int toplevel, int justonce)
2396/// {
2397///     Eprog prog;
2398///     int err, non_empty = 0;
2399///     queue_signals();
2400///     pushheap();
2401///     if (!toplevel)
2402///         zcontext_save();
2403///     for (;;) {
2404///         freeheap();
2405///         if (stophist == 3) hend(NULL);
2406///         hbegin(1);
2407///         if (isset(SHINSTDIN)) {
2408///             setblock_stdin();
2409///             if (interact && toplevel) { ... preprompt() ... }
2410///         }
2411///         use_exit_printed = 0;
2412///         intr();
2413///         lexinit();
2414///         if (!(prog = parse_event(ENDINPUT))) { ... }
2415///         if (hend(prog)) { ... preexec ... execode ... }
2416///         if (ferror(stderr)) { ... }
2417///         if (subsh) realexit();
2418///         if (((!interact || sourcelevel) && errflag) || retflag) break;
2419///         if (isset(SINGLECOMMAND) && toplevel) { dotrap(SIGEXIT); realexit(); }
2420///         if (justonce) break;
2421///     }
2422///     err = errflag;
2423///     if (!toplevel) zcontext_restore();
2424///     popheap();
2425///     unqueue_signals();
2426///     if (err) return LOOP_ERROR;
2427///     if (!non_empty) return LOOP_EMPTY;
2428///     return LOOP_OK;
2429/// }
2430/// ```
2431pub fn r#loop(toplevel: i32, justonce: i32) -> i32 {
2432    // c:113
2433
2434    // c:114 — `int subsh = subsh;` — read the global at exec.rs:160
2435    // (port of `int subsh;` from Src/exec.c). Non-zero when current
2436    // shell is a forked subshell (set by entersubsh c:1083).
2437    let subsh: i32 = crate::ported::exec::subsh.load(Ordering::Relaxed);
2438
2439    let mut prog: Option<crate::ported::parse::ZshProgram>; // c:115
2440    let err: i32; // c:116
2441    let mut non_empty: i32 = 0; // c:116
2442
2443    queue_signals(); // c:118
2444    crate::ported::mem::pushheap(); // c:119
2445    if toplevel == 0 {
2446        // c:120 !toplevel
2447        crate::ported::context::zcontext_save(); // c:121
2448    }
2449    // One HISTORY UNIT per accepted editor line. C's par_event recurses
2450    // until ENDINPUT (parse.c:635), so a pasted multi-command buffer parses
2451    // into ONE prog and hend stores ONE entry ("cmd1\ncmd2"). zshrs's
2452    // single-event interleave (parse_event returns per logical command; see
2453    // parse.rs:766-786) would cycle hbegin/hend per command and SPLIT the
2454    // pasted line into separate history entries — up-arrow then recalled
2455    // only the last command. While the previous accepted line still has
2456    // unconsumed input buffered, keep the history entry OPEN: skip the
2457    // hend/hbegin cycle (and preprompt) until the line is drained.
2458    let mut hist_open_across_events = false;
2459    loop {
2460        // c:122
2461        crate::ported::mem::freeheap(); // c:123
2462        if !hist_open_across_events {
2463            if stophist.load(Ordering::SeqCst) == 3 {
2464                // c:124 stophist == 3
2465                hend(None); // c:125 hend(NULL)
2466            }
2467            // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2468            // A `@` line run by stryke from `inputline` reached the lexer as
2469            // an empty line; its event is closed now, so record its text
2470            // before the next event takes a number.
2471            crate::stryke_history_flush();
2472            hbegin(1); // c:126 hbegin(1)
2473        }
2474        if isset(SHINSTDIN) && !hist_open_across_events {
2475            // c:127 isset(SHINSTDIN)
2476            crate::ported::utils::setblock_stdin(); // c:128
2477            if isset(INTERACTIVE) && toplevel != 0 {
2478                // c:129 interact && toplevel
2479                let hstop = stophist.load(Ordering::SeqCst); // c:130
2480                stophist.store(3, Ordering::SeqCst); // c:131
2481                                                     // c:146-148 — drain any signals queued during the previous read
2482                                                     // WITHOUT changing the queueing nesting level: snapshot the level,
2483                                                     // dont_queue_signals() (whose run_queued_signals() reaps bg-job
2484                                                     // SIGCHLDs that were queued while queueing_enabled stayed at its
2485                                                     // baseline 1 across the ZLE read), then restore the level. This
2486                                                     // port previously jumped straight to preprompt(), so every queued
2487                                                     // SIGCHLD went undrained → children never reaped (zombies) and the
2488                                                     // shell hung at the prompt. Port of Src/init.c:146-148.
2489                let q = crate::ported::signals_h::queue_signal_level();
2490                dont_queue_signals();
2491                crate::ported::signals_h::restore_queue_signals(q);
2492                // c:133-138 — reset errflag for preprompt
2493                errflag.store(0, Ordering::SeqCst); // c:139 errflag = 0
2494                crate::ported::utils::preprompt(); // c:140
2495                if stophist.load(Ordering::SeqCst) != 3 {
2496                    // c:141
2497                    hbegin(1); // c:142
2498                } else {
2499                    // c:143
2500                    stophist.store(hstop, Ordering::SeqCst); // c:144
2501                }
2502                // c:146-149 — reset errflag again
2503                errflag.store(0, Ordering::SeqCst); // c:150
2504            }
2505        }
2506        use_exit_printed.store(0, Ordering::SeqCst); // c:153
2507        intr(); // c:154
2508        crate::ported::lex::lexinit(); // c:155
2509        // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2510        // C's parse_event builds the event's wordcode Eprog, and the preexec
2511        // call below renders `$2`/`$3` from it (c:210-211 getjobtext /
2512        // getpermtext). zshrs's parse_event returns the fusevm AST, which the
2513        // text renderers cannot walk, so record the event's source as the
2514        // lexer consumes it (the same echo buffer funcdef bodies use) and
2515        // compile it to an Eprog only if a preexec hook is present.
2516        let event_mark = (toplevel != 0).then(crate::funcdef_capture::body_mark_begin);
2517        prog = crate::ported::parse::parse_event(ENDINPUT as i32); // c:156
2518        let event_src = event_mark.and_then(crate::funcdef_capture::event_text);
2519        if prog.is_none() {
2520            // c:156
2521            hend(None); // c:158
2522                        // A parse failure closes any deferred same-line history unit —
2523                        // the next iteration must run its own hbegin again.
2524            hist_open_across_events = false;
2525            // c:159-161 — break on clean EOF / non-toplevel LEXERR / justonce
2526            let tok_v = tok(); // c:159 tok
2527            let errflag_v = errflag.load(Ordering::SeqCst);
2528            let lexerr_break = tok_v == LEXERR && (!isset(SHINSTDIN) || toplevel == 0);
2529            let endinput_break = tok_v == ENDINPUT && errflag_v == 0;
2530            if endinput_break || lexerr_break || justonce != 0 {
2531                // c:159-161
2532                break; // c:162
2533            }
2534            if crate::ported::hist::exit_pending.load(Ordering::SeqCst) {
2535                // c:163 exit_pending
2536                STOPMSG.store(1, Ordering::SeqCst); // c:169 stopmsg = 1
2537                crate::ported::builtin::zexit(
2538                    // c:170 zexit(exit_val, ZEXIT_NORMAL)
2539                    crate::ported::builtin::EXIT_VAL.load(Ordering::SeqCst),
2540                    ZEXIT_NORMAL,
2541                );
2542            }
2543            if tok_v == LEXERR && LASTVAL.load(Ordering::SeqCst) == 0
2544            // c:172
2545            {
2546                LASTVAL.store(1, Ordering::SeqCst); // c:173 lastval = 1
2547            }
2548            continue; // c:174
2549        }
2550        let prog_inner = prog.take().unwrap();
2551        // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2552        // The accepted line is about to run at top level, where commands
2553        // stamp `locallevel` without opening a scope (a bare `typeset X=1`
2554        // takes the current level). An `async_precmd` batch fired by the
2555        // preprompt() above may still be running hook functions on a worker,
2556        // with that counter raised by its own scope — the typeset would then
2557        // belong to the worker's scope and vanish when the hook returned.
2558        // C runs precmd hooks synchronously (c:140 preprompt), so nothing
2559        // else can hold a scope here; wait for the batch, or withdraw it.
2560        crate::async_precmd::quiesce();
2561        // c:176 — `if (hend(prog))`: passing the program in commits the
2562        // history line. zshrs's `hend` takes Option<&[u8]>; sentinel
2563        // non-empty slice signals "program present" to the commit path.
2564        //
2565        // Same-accepted-line continuation (see hist_open_across_events at
2566        // the loop head): while the input buffer still holds unconsumed
2567        // chars of the line being edited, DEFER hend — chline keeps
2568        // accumulating so the whole pasted buffer commits as one entry,
2569        // exactly as C's to-ENDINPUT par_event produces.
2570        hist_open_across_events = isset(INTERACTIVE)
2571            && toplevel != 0
2572            && crate::ported::input::inbufct.with(|c| c.get()) > 0;
2573        let hend_ret = if hist_open_across_events {
2574            1 // execute; the history commit happens when the line drains
2575        } else {
2576            let prog_bytes: Vec<u8> = format!("{:?}", prog_inner).into_bytes();
2577            hend(Some(&prog_bytes)) // c:176
2578        };
2579        if hend_ret != 0 {
2580            let _toksav = tok(); // c:177
2581            non_empty = 1; // c:179
2582                           // c:180-215 — preexec hook + ZLE_CMD_PREEXEC.
2583            if toplevel != 0 {
2584                // c:180-215 — the preexec hook, shared with the script-file
2585                // event loop (vm_helper::run_events_per_command).
2586                crate::vm_helper::run_preexec_hook(event_src.as_deref());
2587            }
2588            if toplevel != 0                                                 // c:216
2589                && zle_load_state.load(Ordering::SeqCst) == 1
2590            {
2591                let _ = zleentry(
2592                    // c:217 ZLE_CMD_PREEXEC
2593                    ZLE_CMD_PREEXEC,
2594                );
2595            }
2596            if STOPMSG.load(Ordering::SeqCst) != 0 {
2597                // c:218
2598                STOPMSG.fetch_sub(1, Ordering::SeqCst); // c:219
2599            }
2600            // c:220 — `execode(prog, 0, 0, toplevel ? "toplevel" : "file");`
2601            let exec_label = if toplevel != 0 { "toplevel" } else { "file" };
2602            // Save the lexer's line counter across execution. Each
2603            // statement's `SET_LINENO` (c:Src/exec.c execpline WC_PIPE_LINENO)
2604            // overwrites `lineno` to that statement's line while running;
2605            // C's execlist brackets this with `oldlineno = lineno` /
2606            // `lineno = oldlineno` (exec.c:28/292) so the value is restored
2607            // on exit. Without it, the faithful single-event loop (which
2608            // interleaves parse + execode) had the NEXT event parse from the
2609            // just-executed statement's line — `$LINENO` froze at 1 across a
2610            // piped/interactive script instead of advancing 1,2,3.
2611            let saved_lex_lineno = crate::ported::lex::LEX_LINENO.with(|c| c.get());
2612            crate::ported::exec::execode(&prog_inner, 0, 0, exec_label); // c:220
2613            crate::ported::lex::LEX_LINENO.with(|c| c.set(saved_lex_lineno));
2614            // c:Src/exec.c:1611-1618 — errexit (`set -e`) fires a shell
2615            // exit FROM execlist (`if (errexit) { errflag = 0; if
2616            // (sigtrapped[SIGEXIT]) dotrap(SIGEXIT); realexit(); }`).
2617            // zshrs's fusevm can't realexit mid-VM, so BUILTIN_ERREXIT_CHECK
2618            // defers it via EXIT_PENDING + a jump to chunk-end. Whole-program
2619            // mode then skips the rest of the single chunk via that jump, but
2620            // the faithful single-event loop runs each event as its OWN chunk
2621            // — the jump only ends the failed event, and without honoring the
2622            // pending exit here the NEXT event would still run. Mirror C's
2623            // execlist realexit at the execode boundary: zexit fires the EXIT
2624            // trap (its trap-body dispatch reaches the session executor) and
2625            // realexits with EXIT_VAL. Guarded to top level + no subshell so
2626            // a deferred subshell/cmdsub exit still propagates via its own
2627            // unwind (fusevm_bridge) instead of killing the parent here.
2628            if toplevel != 0
2629                && crate::ported::builtin::SUBSHELL_DEPTH.load(Ordering::SeqCst) == 0
2630                && crate::ported::builtin::EXIT_PENDING.load(Ordering::SeqCst) != 0
2631            {
2632                crate::ported::builtin::zexit(
2633                    crate::ported::builtin::EXIT_VAL.load(Ordering::SeqCst),
2634                    ZEXIT_NORMAL,
2635                );
2636            }
2637            // c:221 — `tok = toksav;` restore
2638            set_tok(_toksav);
2639            if toplevel != 0 {
2640                // c:222
2641                noexitct.store(0, Ordering::SeqCst); // c:223
2642                if zle_load_state.load(Ordering::SeqCst) == 1 {
2643                    // c:224
2644                    let _ = zleentry(
2645                        // c:225 ZLE_CMD_POSTEXEC
2646                        ZLE_CMD_POSTEXEC,
2647                    );
2648                }
2649            }
2650        }
2651        // c:228-231 — `if (ferror(stderr))` write-error path. Mirror as a
2652        // best-effort flush; Rust panic on broken pipe handled upstream.
2653        // c:232 — `if (subsh) realexit();`
2654        if subsh != 0 {
2655            // c:232
2656            realexit(); // c:233
2657        }
2658        // c:234 — `if (((!interact || sourcelevel) && errflag) || retflag) break;`
2659        let errflag_v = errflag.load(Ordering::SeqCst);
2660        let interact_v = isset(INTERACTIVE);
2661        let srclvl = sourcelevel.load(Ordering::SeqCst);
2662        let retflag_v = RETFLAG.load(Ordering::SeqCst);
2663        if ((!interact_v || srclvl != 0) && errflag_v != 0) || retflag_v != 0 {
2664            // c:234
2665            break; // c:235
2666        }
2667        if isset(SINGLECOMMAND) && toplevel != 0 {
2668            // c:236
2669            dont_queue_signals(); // c:237
2670                                  // c:238 — sigtrapped[SIGEXIT] != 0 → dotrap(SIGEXIT)
2671            let tr = sigtrapped.lock().unwrap();
2672            let sigexit = libc::SIGINT as usize; // SIGEXIT = 0 (zsh-internal)
2673            if tr.get(sigexit).copied().unwrap_or(0) != 0 {
2674                // c:238
2675                drop(tr);
2676                let _ = dotrap(0 /* SIGEXIT */); // c:239
2677            }
2678            realexit(); // c:240
2679        }
2680        if justonce != 0 {
2681            // c:242
2682            break; // c:243
2683        }
2684        let _ = histlinect.load(Ordering::SeqCst); // silence unused alias
2685    }
2686    err = errflag.load(Ordering::SeqCst); // c:245 err = errflag
2687    if toplevel == 0 {
2688        // c:246 !toplevel
2689        zcontext_restore(); // c:247
2690    }
2691    popheap(); // c:248
2692    unqueue_signals(); // c:249
2693
2694    if err != 0 {
2695        return 2; /* LOOP_ERROR */
2696    } // c:251-252
2697    if non_empty == 0 {
2698        return 1; /* LOOP_EMPTY */
2699    } // c:253-254
2700    0 /* LOOP_OK */ // c:255
2701}
2702
2703/// Port of `static void parseopts_setemulate(...)` from Src/init.c:348.
2704fn parseopts_setemulate(nam: &str, flags: i32) {
2705    // c:350 — `emulate(nam, 1, &emulation, opts);` initialises most
2706    // options. Strip leading `-` (login-shell marker) and any path
2707    // components so the name passed to `emulate` is just the basename
2708    // (`ksh` / `sh` / `csh` / `zsh`).
2709    let bare = nam.trim_start_matches('-');
2710    let basename = std::path::Path::new(bare)
2711        .file_name()
2712        .and_then(|n| n.to_str())
2713        .unwrap_or(bare);
2714    // ZSHRS-ONLY. In C, `argv[0]` IS the personality selector, so
2715    // re-deriving the emulation from it here is free. zshrs adds `--MODE`
2716    // flags C has no equivalent of, and `argv[0]` is always the zshrs
2717    // binary — so this call used to reset a `--ksh` / `--sh` / `--dash`
2718    // shell back to zsh emulation on the interactive path, which reaches
2719    // `zsh_main` (the `-c` and script-file paths in bins/zshrs.rs do not).
2720    // `zshrs --sh -i` ran with `shwordsplit` OFF as a result. The
2721    // CLI-selected personality wins when there is one.
2722    let name = crate::extensions::emulation_startup::selected_emulate_name().unwrap_or(basename);
2723    crate::ported::options::emulate(name, true); // c:350
2724    // ZSHRS-ONLY. `emulate()` resets the option table wholesale, so the
2725    // drop-in's own deltas (bash's brace expansion, `$BASH_REMATCH`, the
2726    // prompt options) have to be re-applied on top of the preset. Without
2727    // this they survived only on the `-c` / script paths, which do not
2728    // reach this function — an interactive `--bash`, i.e. a login shell,
2729    // ran without any of them.
2730    crate::extensions::emulation_startup::apply_personality_option_deltas();
2731
2732    // c:351 — `opts[LOGINSHELL] = ((flags & PARSEARGS_LOGIN) != 0);`
2733    const PARSEARGS_LOGIN: i32 = 2;
2734    crate::ported::options::opt_state_set("loginshell", (flags & PARSEARGS_LOGIN) != 0);
2735
2736    // c:352 — `opts[PRIVILEGED] = (getuid() != geteuid() || …);`
2737    let priv_on = unsafe { libc::getuid() != libc::geteuid() || libc::getgid() != libc::getegid() };
2738    crate::ported::options::opt_state_set("privileged", priv_on);
2739
2740    // c:361 — `opts[INTERACTIVE] = isatty(0) ? 2 : 0;`. The "2"
2741    // sentinel means "default-on, may be downgraded by SHINSTDIN
2742    // handling below". Rust port stores as bool — we only retain
2743    // the on/off distinction; the downgrade logic at c:end-of-fn
2744    // is a no-op once we collapse to bool.
2745    let interactive_default = unsafe { libc::isatty(0) != 0 };
2746    crate::ported::options::opt_state_set("interactive", interactive_default);
2747    // MONITOR and HASHDIRS start at 2 as well (c:366-367).
2748    OPT_DEFAULT_ON.store(i32::from(interactive_default) as u32 | 2 | 4, Ordering::SeqCst);
2749
2750    // c:366-368 — `opts[MONITOR] = 2; opts[HASHDIRS] = 2; opts[USEZLE] = 1;`
2751    crate::ported::options::opt_state_set("monitor", true);
2752    crate::ported::options::opt_state_set("hashdirs", true);
2753    // USEZLE's canonical name is `zle` (zsh_h.rs:4145); the prior
2754    // `"usezle"` key was never read by `isset(USEZLE)` → this default was a
2755    // silent no-op. init_io (c:691-694) downgrades it for non-tty shells.
2756    crate::ported::options::opt_state_set("zle", true); // c:368 opts[USEZLE]=1
2757                                                        // c:369-370 — `opts[SHINSTDIN] = 0; opts[SINGLECOMMAND] = 0;`
2758    crate::ported::options::opt_state_set("shinstdin", false);
2759    crate::ported::options::opt_state_set("singlecommand", false);
2760}
2761
2762#[cfg(test)]
2763mod tests {
2764    use super::*;
2765
2766    /// source() pushes an FS_SOURCE funcstack frame (init.c:1610-1618)
2767    /// and pops it on exit (c:1664). Verify the push is balanced (no
2768    /// leak) and that a readable file returns SOURCE_OK.
2769    #[test]
2770    fn source_funcstack_push_is_balanced() {
2771        use crate::ported::modules::parameter::FUNCSTACK;
2772        let depth_before = FUNCSTACK.lock().unwrap().len();
2773
2774        let mut path = std::env::temp_dir();
2775        path.push(format!("zshrs_source_balance_{}.zsh", std::process::id()));
2776        std::fs::write(&path, ": # no-op sourced file\n").unwrap();
2777
2778        let rc = source(path.to_str().unwrap());
2779
2780        let depth_after = FUNCSTACK.lock().unwrap().len();
2781        std::fs::remove_file(&path).ok();
2782
2783        assert_eq!(rc, 0, "source of a readable file returns SOURCE_OK");
2784        assert_eq!(
2785            depth_before, depth_after,
2786            "FS_SOURCE push must be matched by a pop (no funcstack leak)"
2787        );
2788
2789        // A non-existent file is SOURCE_NOT_FOUND and pushes nothing.
2790        let missing = "/nonexistent/zshrs_source_xyz_should_not_exist";
2791        assert_ne!(source(missing), 0, "missing file returns SOURCE_NOT_FOUND");
2792        assert_eq!(
2793            FUNCSTACK.lock().unwrap().len(),
2794            depth_before,
2795            "NOT_FOUND path must not touch the funcstack"
2796        );
2797    }
2798
2799    /// `fallback_compctlread` is the dispatch target when the zle
2800    /// module isn't loaded — `compctl -K read` paths need SOMETHING
2801    /// callable. C returns 1 + zwarnnam to signal "no provider". A
2802    /// regression returning 0 would make completers believe a read
2803    /// happened when nothing did (silent data corruption in completion).
2804    #[test]
2805    fn fallback_compctlread_signals_failure() {
2806        let _g = crate::test_util::global_state_lock();
2807        assert_eq!(fallback_compctlread("test"), 1);
2808    }
2809
2810    /// c:418 — `parseopts` advances `idx` past every consumed option.
2811    /// `-c CMD` consumes 2 slots and stores CMD in cmdp. Regression
2812    /// that doesn't advance idx would loop forever or skip args.
2813    #[test]
2814    fn parseopts_dash_c_captures_command_string() {
2815        let _g = crate::test_util::global_state_lock();
2816        let mut argv = vec!["-c".to_string(), "echo hi".to_string()];
2817        let mut idx = 0usize;
2818        let mut cmd: Option<String> = None;
2819        let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2820        assert_eq!(r, 0);
2821        assert_eq!(
2822            cmd.as_deref(),
2823            Some("echo hi"),
2824            "-c must capture the next arg as the command"
2825        );
2826        assert_eq!(idx, 2, "idx must advance past both -c AND its arg");
2827    }
2828
2829    /// c:Src/init.c:456-460 — a GNU-style `--LONG` option is rewritten
2830    /// (`-` becomes `_`) and applied through optlookup + dosetopt, so
2831    /// `--no-rcs` is exactly `-o no_rcs`.
2832    ///
2833    /// This arm used to skip EVERY `--NAME`, so a long-spelled option
2834    /// never reached the option table: `zshrs --no-rcs -i` left RCS set
2835    /// and still sourced .zshenv/.zshrc (the gate is `isset(RCS)` in
2836    /// `run_init_scripts`), while `-f` and `-o norcs` both suppressed
2837    /// them. Non-interactive `-c` masked it because nothing reads .zshrc
2838    /// there.
2839    #[test]
2840    fn parseopts_long_option_reaches_the_option_table() {
2841        let _g = crate::test_util::global_state_lock();
2842        for spelling in ["--no-rcs", "--norcs", "--no_rcs"] {
2843            crate::ported::options::opt_state_set("rcs", true);
2844            assert!(isset(RCS), "precondition for {}: RCS starts on", spelling);
2845            let mut argv = vec![spelling.to_string()];
2846            let mut idx = 0usize;
2847            let mut cmd: Option<String> = None;
2848            let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 1);
2849            assert_eq!(r, 0, "{} must parse cleanly", spelling);
2850            assert!(!isset(RCS), "{} must clear RCS, like -f does", spelling);
2851            assert_eq!(idx, 1, "{} consumes exactly one slot", spelling);
2852        }
2853        crate::ported::options::opt_state_set("rcs", true);
2854    }
2855
2856    /// The deliberate divergence from c:492 must hold: zshrs-only long
2857    /// flags (`--zsh`, `--dap`, …) are NOT zsh options, are consumed by
2858    /// the bin front-end, and must be skipped here rather than rejected
2859    /// with "no such option". Guards the fix above from over-reaching.
2860    #[test]
2861    fn parseopts_skips_zshrs_specific_long_flags() {
2862        let _g = crate::test_util::global_state_lock();
2863        for flag in ["--zsh", "--bash", "--zsh-compat", "--dap"] {
2864            let mut argv = vec![flag.to_string()];
2865            let mut idx = 0usize;
2866            let mut cmd: Option<String> = None;
2867            let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 1);
2868            assert_eq!(r, 0, "{} must not be rejected", flag);
2869            assert_eq!(idx, 1, "{} must still be consumed", flag);
2870        }
2871    }
2872
2873    /// c:418 — `parseopts` stops at the first non-option positional.
2874    /// Regression that keeps walking past `--` or first bareword would
2875    /// silently consume the script name as an option.
2876    #[test]
2877    fn parseopts_stops_at_first_positional() {
2878        let _g = crate::test_util::global_state_lock();
2879        let mut argv = vec!["script.sh".to_string(), "arg1".to_string()];
2880        let mut idx = 0usize;
2881        let mut cmd: Option<String> = None;
2882        parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2883        assert_eq!(idx, 0, "must stop AT the first bareword (no advance)");
2884        assert!(cmd.is_none(), "no -c → cmd stays None");
2885    }
2886
2887    /// c:418 — `parseopts` on empty argv exits cleanly with idx=0
2888    /// and cmd=None. Regression panicking on empty would crash
2889    /// startup with weird args.
2890    #[test]
2891    fn parseopts_empty_argv_is_safe() {
2892        let _g = crate::test_util::global_state_lock();
2893        let mut argv: Vec<String> = Vec::new();
2894        let mut idx = 0usize;
2895        let mut cmd: Option<String> = None;
2896        assert_eq!(parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0), 0);
2897        assert_eq!(idx, 0);
2898        assert!(cmd.is_none());
2899    }
2900
2901    /// c:756 — `tccap_get_name` indexes into the terminal-capability
2902    /// name array. Out-of-range MUST NOT panic — it returns "" so
2903    /// downstream tigetstr calls fail-soft. Regression panicking
2904    /// would crash the prompt subsystem on tiny terminals.
2905    #[test]
2906    fn tccap_get_name_out_of_range_returns_empty() {
2907        let _g = crate::test_util::global_state_lock();
2908        // index 9999 is well past the cap table (~39 entries per
2909        // init.c:75). Must return safely.
2910        let n = tccap_get_name(9999);
2911        assert!(
2912            n.is_empty(),
2913            "out-of-range index must return empty (got {n:?})"
2914        );
2915    }
2916
2917    /// c:418 — `parseopts` consumes `-x` (xtrace) as a flag without
2918    /// an argument. Pin the no-arg-required path; a regression that
2919    /// always reads `argv[idx+1]` would OOB-index when -x is the
2920    /// last arg.
2921    #[test]
2922    fn parseopts_dash_x_is_single_slot_flag() {
2923        let _g = crate::test_util::global_state_lock();
2924        let mut argv = vec!["-x".to_string()];
2925        let mut idx = 0usize;
2926        let mut cmd: Option<String> = None;
2927        let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2928        assert_eq!(r, 0);
2929        assert_eq!(idx, 1, "-x consumes exactly 1 arg slot");
2930        assert!(cmd.is_none(), "-x must NOT capture a command");
2931    }
2932
2933    /// c:418 — `parseopts` -c with NO following argument. The
2934    /// contract is "no valid command captured" so the caller can
2935    /// surface a usage error.
2936    #[test]
2937    fn parseopts_dash_c_without_argument_does_not_capture_command() {
2938        let _g = crate::test_util::global_state_lock();
2939        let mut argv = vec!["-c".to_string()];
2940        let mut idx = 0usize;
2941        let mut cmd: Option<String> = None;
2942        let _ = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2943        assert!(
2944            cmd.is_none() || cmd.as_deref() == Some(""),
2945            "-c without arg must NOT capture a usable command"
2946        );
2947    }
2948
2949    /// c:418 — `parseopts` consumes multiple flags in sequence.
2950    /// Pin the per-arg loop so a regen that early-exits after the
2951    /// first flag silently drops -v.
2952    #[test]
2953    fn parseopts_consumes_multiple_flags() {
2954        let _g = crate::test_util::global_state_lock();
2955        let mut argv = vec!["-x".to_string(), "-v".to_string()];
2956        let mut idx = 0usize;
2957        let mut cmd: Option<String> = None;
2958        let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2959        assert_eq!(r, 0);
2960        assert_eq!(idx, 2, "both flags must be consumed");
2961    }
2962
2963    /// c:756 — `tccap_get_name(0)` returns the first cap name. The
2964    /// table is non-empty per init.c:75; index 0 must be a valid
2965    /// stem. Pin the boundary so a regen that adds an "off-by-one
2966    /// slot 0" mistake gets caught.
2967    #[test]
2968    fn tccap_get_name_index_zero_is_nonempty() {
2969        let _g = crate::test_util::global_state_lock();
2970        let n = tccap_get_name(0);
2971        assert!(
2972            !n.is_empty(),
2973            "index 0 must yield a real cap name; got {:?}",
2974            n
2975        );
2976        for c in n.chars() {
2977            assert!(
2978                c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_',
2979                "cap name {:?} contains non-termcap char {:?}",
2980                n,
2981                c
2982            );
2983        }
2984    }
2985
2986    /// c:1713 — `noop_function` MUST be safe to call without panics.
2987    /// C body is empty; the Rust port mirrors it. Re-entry test
2988    /// catches a regen that adds stateful side-effects.
2989    #[test]
2990    fn noop_function_is_safe_to_call_multiple_times() {
2991        let _g = crate::test_util::global_state_lock();
2992        noop_function();
2993        noop_function();
2994        noop_function();
2995    }
2996
2997    /// c:1720 — `noop_function_int(n)` ignores its argument across
2998    /// the full i32 range. Pin no-side-effect contract.
2999    #[test]
3000    fn noop_function_int_ignores_argument() {
3001        let _g = crate::test_util::global_state_lock();
3002        noop_function_int(0);
3003        noop_function_int(42);
3004        noop_function_int(-1);
3005        noop_function_int(i32::MAX);
3006        noop_function_int(i32::MIN);
3007    }
3008
3009    /// c:1743 — `zleentry(cmd)` for an unknown cmd code returns
3010    /// None (matching the C "no zle" branch). Every call site
3011    /// assumes None means "no ZLE active".
3012    #[test]
3013    fn zleentry_unknown_command_returns_none() {
3014        let _g = crate::test_util::global_state_lock();
3015        assert!(
3016            zleentry(99999).is_none(),
3017            "unknown zle command must return None, not a default string"
3018        );
3019    }
3020
3021    /// c:1551 — `source("")` on empty path must NOT segfault. The
3022    /// C source opens("") which fails with ENOENT, returning non-0.
3023    #[test]
3024    fn source_empty_path_returns_nonzero() {
3025        let _g = crate::test_util::global_state_lock();
3026        let r = source("");
3027        assert_ne!(r, 0, "source of empty path must report failure");
3028    }
3029
3030    // ─── zsh-corpus pins for init helpers ───────────────────────────
3031
3032    /// `tccap_get_name(0)` returns a non-empty cap name.
3033    #[test]
3034    fn init_corpus_tccap_index_zero_nonempty() {
3035        let _g = crate::test_util::global_state_lock();
3036        let name = tccap_get_name(0);
3037        assert!(!name.is_empty(), "cap[0] has a name, got {name:?}");
3038    }
3039
3040    /// `tccap_get_name` for index TC_COUNT or higher returns "".
3041    #[test]
3042    fn init_corpus_tccap_out_of_range_returns_empty() {
3043        let _g = crate::test_util::global_state_lock();
3044        assert_eq!(tccap_get_name(39), "", "TC_COUNT = empty");
3045        assert_eq!(tccap_get_name(999), "", "way out of range = empty");
3046    }
3047
3048    /// All cap indexes 0..TC_COUNT return non-empty distinct names.
3049    #[test]
3050    fn init_corpus_tccap_indexes_all_named() {
3051        let _g = crate::test_util::global_state_lock();
3052        for i in 0..39 {
3053            let n = tccap_get_name(i);
3054            assert!(!n.is_empty(), "cap {i} should have a name");
3055        }
3056    }
3057
3058    /// `source("/nonexistent/path/zshrs_test_xyz")` returns non-zero.
3059    #[test]
3060    fn init_corpus_source_nonexistent_returns_nonzero() {
3061        let _g = crate::test_util::global_state_lock();
3062        assert_ne!(source("/nonexistent/path/zshrs_test_xyz_zzz"), 0);
3063    }
3064
3065    /// `noop_function()` doesn't panic.
3066    #[test]
3067    fn init_corpus_noop_function_does_not_panic() {
3068        let _g = crate::test_util::global_state_lock();
3069        noop_function();
3070    }
3071
3072    /// `noop_function_int(0)` doesn't panic.
3073    #[test]
3074    fn init_corpus_noop_function_int_zero_no_panic() {
3075        let _g = crate::test_util::global_state_lock();
3076        noop_function_int(0);
3077    }
3078
3079    /// `zleentry` with negative cmd code returns None.
3080    #[test]
3081    fn init_corpus_zleentry_negative_returns_none() {
3082        let _g = crate::test_util::global_state_lock();
3083        assert!(zleentry(-1).is_none());
3084    }
3085
3086    // ═══════════════════════════════════════════════════════════════════
3087    // Additional C-parity tests for Src/init.c tccap_get_name / init_term
3088    // / noop helpers.
3089    // ═══════════════════════════════════════════════════════════════════
3090
3091    /// c:756 — `tccap_get_name(cap)` for cap >= 39 (TC_COUNT) returns
3092    /// empty string (Rust port out-of-range guard, c:771).
3093    #[test]
3094    fn tccap_get_name_out_of_range_returns_empty_pin() {
3095        let _g = crate::test_util::global_state_lock();
3096        assert_eq!(tccap_get_name(39), "", "TC_COUNT boundary returns ''");
3097        assert_eq!(tccap_get_name(100), "");
3098        assert_eq!(tccap_get_name(usize::MAX), "");
3099    }
3100
3101    /// c:756 — `tccap_get_name(0)` returns a non-empty cap name
3102    /// (first entry of tccapnams table).
3103    #[test]
3104    fn tccap_get_name_first_entry_is_nonempty() {
3105        let _g = crate::test_util::global_state_lock();
3106        let n = tccap_get_name(0);
3107        assert!(!n.is_empty(), "tccapnams[0] must be a valid cap name");
3108    }
3109
3110    /// c:756 — `tccap_get_name` is deterministic across calls.
3111    #[test]
3112    fn tccap_get_name_is_deterministic() {
3113        let _g = crate::test_util::global_state_lock();
3114        for i in 0..39 {
3115            let first = tccap_get_name(i);
3116            for _ in 0..5 {
3117                assert_eq!(tccap_get_name(i), first, "tccap_get_name({}) impure", i);
3118            }
3119        }
3120    }
3121
3122    /// c:756 — every in-range cap name fits in ASCII and has
3123    /// non-control content (termcap caps are 2-char ASCII codes).
3124    #[test]
3125    fn tccap_get_name_in_range_returns_printable_ascii() {
3126        let _g = crate::test_util::global_state_lock();
3127        for i in 0..39 {
3128            let n = tccap_get_name(i);
3129            if n.is_empty() {
3130                continue;
3131            }
3132            for c in n.chars() {
3133                assert!(
3134                    c.is_ascii() && !c.is_control(),
3135                    "tccap_get_name({}) = {:?} has non-printable char {:?}",
3136                    i,
3137                    n,
3138                    c
3139                );
3140            }
3141        }
3142    }
3143
3144    /// c:1713 — `noop_function` returns nothing, has no observable effect.
3145    /// Repeated calls don't accumulate state.
3146    #[test]
3147    fn noop_function_is_idempotent() {
3148        let _g = crate::test_util::global_state_lock();
3149        for _ in 0..100 {
3150            noop_function();
3151        }
3152    }
3153
3154    /// c:1720 — `noop_function_int` accepts any i32 value without panic.
3155    #[test]
3156    fn noop_function_int_accepts_any_value() {
3157        let _g = crate::test_util::global_state_lock();
3158        noop_function_int(0);
3159        noop_function_int(-1);
3160        noop_function_int(i32::MAX);
3161        noop_function_int(i32::MIN);
3162    }
3163
3164    /// `zleentry` with very large positive cmd code returns None
3165    /// (out-of-range cmd dispatch).
3166    #[test]
3167    fn zleentry_unknown_cmd_returns_none() {
3168        let _g = crate::test_util::global_state_lock();
3169        assert!(zleentry(99999).is_none(), "unknown cmd → None");
3170    }
3171
3172    /// c:1209 — `init_bltinmods` is idempotent (safe to call multiple times).
3173    #[test]
3174    fn init_bltinmods_is_idempotent() {
3175        let _g = crate::test_util::global_state_lock();
3176        init_bltinmods();
3177        init_bltinmods();
3178    }
3179
3180    // ═══════════════════════════════════════════════════════════════════
3181    // Additional C-parity tests for Src/init.c
3182    // c:747 tccapnams / c:756 tccap_get_name / c:1713 noop_function /
3183    // c:1199 fallback_compctlread.
3184    // ═══════════════════════════════════════════════════════════════════
3185
3186    /// c:747 — `tccapnams` table has exactly 39 entries (TC_COUNT).
3187    #[test]
3188    fn tccapnams_table_size_is_tc_count() {
3189        assert_eq!(tccapnams.len(), 39, "TC_COUNT must be 39");
3190    }
3191
3192    /// c:747 — every tccapnams entry is exactly 2 ASCII chars
3193    /// (termcap 2-letter capability code convention).
3194    #[test]
3195    fn tccapnams_entries_are_two_ascii_chars() {
3196        for (i, &n) in tccapnams.iter().enumerate() {
3197            assert_eq!(n.len(), 2, "tccapnams[{}] = {:?} must be 2 chars", i, n);
3198            assert!(
3199                n.chars().all(|c| c.is_ascii_alphabetic()),
3200                "tccapnams[{}] = {:?} must be ASCII alpha",
3201                i,
3202                n
3203            );
3204        }
3205    }
3206
3207    /// c:747 — no duplicates in tccapnams.
3208    #[test]
3209    fn tccapnams_has_no_duplicates() {
3210        let mut seen = std::collections::HashSet::new();
3211        for &n in tccapnams.iter() {
3212            assert!(seen.insert(n), "duplicate tccap name: {:?}", n);
3213        }
3214    }
3215
3216    /// c:756 — `tccap_get_name(39)` is at TC_COUNT boundary → empty.
3217    #[test]
3218    fn tccap_get_name_at_tc_count_boundary_returns_empty() {
3219        assert_eq!(tccap_get_name(39), "", "TC_COUNT bound returns empty");
3220    }
3221
3222    /// c:756 — `tccap_get_name(38)` is last valid index → non-empty.
3223    #[test]
3224    fn tccap_get_name_last_valid_index_returns_nonempty() {
3225        let n = tccap_get_name(38);
3226        assert!(!n.is_empty(), "tccap_get_name(38) must be non-empty");
3227        assert_eq!(n, tccapnams[38], "matches table");
3228    }
3229
3230    /// c:756 — `tccap_get_name(usize::MAX)` is far out of range → empty.
3231    #[test]
3232    fn tccap_get_name_usize_max_returns_empty() {
3233        assert_eq!(tccap_get_name(usize::MAX), "");
3234    }
3235
3236    /// c:756 — `tccap_get_name` is a pure function.
3237    #[test]
3238    fn tccap_get_name_is_pure() {
3239        for i in [0usize, 1, 5, 10, 38, 39, 100] {
3240            let a = tccap_get_name(i);
3241            let b = tccap_get_name(i);
3242            assert_eq!(a, b, "tccap_get_name({}) must be deterministic", i);
3243        }
3244    }
3245
3246    /// c:1199 — `fallback_compctlread` returns 1 (failure) without args.
3247    #[test]
3248    fn fallback_compctlread_returns_one_pin() {
3249        let _g = crate::test_util::global_state_lock();
3250        assert_eq!(fallback_compctlread(""), 1, "fallback always fails");
3251        assert_eq!(fallback_compctlread("anything"), 1);
3252    }
3253
3254    /// c:1713 — `noop_function` is a true no-op (no panic, no state change).
3255    #[test]
3256    fn noop_function_does_nothing_observable() {
3257        for _ in 0..100 {
3258            noop_function();
3259        }
3260    }
3261
3262    /// c:1720 — `noop_function_int` accepts any i32 without panic.
3263    #[test]
3264    fn noop_function_int_accepts_full_i32_range() {
3265        for v in [i32::MIN, -1, 0, 1, 42, i32::MAX] {
3266            noop_function_int(v);
3267        }
3268    }
3269
3270    /// c:1159 — `zleentry(0)` returns None (no live ZLE for cmd=0).
3271    #[test]
3272    fn zleentry_zero_cmd_returns_none() {
3273        let _g = crate::test_util::global_state_lock();
3274        assert_eq!(zleentry(0), None, "cmd=0 unwired without live ZLE");
3275    }
3276
3277    // ═══════════════════════════════════════════════════════════════════
3278    // Additional C-parity tests for Src/init.c
3279    // c:464 tccap_get_name / c:1081 source / c:1116 sourcehome /
3280    // c:1159 zleentry / c:1199 fallback_compctlread / c:1149 noop_function /
3281    // c:1154 noop_function_int / c:1143 init_bltinmods
3282    // ═══════════════════════════════════════════════════════════════════
3283
3284    /// c:464 — `tccap_get_name` returns &'static str (compile-time type pin).
3285    #[test]
3286    fn tccap_get_name_returns_static_str_type() {
3287        let _: &'static str = tccap_get_name(0);
3288    }
3289
3290    /// c:1081 — `source("")` empty path returns i32 (compile-time pin).
3291    #[test]
3292    fn source_returns_i32_type() {
3293        let _g = crate::test_util::global_state_lock();
3294        let _: i32 = source("");
3295    }
3296
3297    /// c:1081 — `source` is deterministic for the same nonexistent path.
3298    #[test]
3299    fn source_nonexistent_is_deterministic() {
3300        let _g = crate::test_util::global_state_lock();
3301        let first = source("/__never_exists_zshrs_source__");
3302        for _ in 0..3 {
3303            assert_eq!(
3304                source("/__never_exists_zshrs_source__"),
3305                first,
3306                "source must be deterministic"
3307            );
3308        }
3309    }
3310
3311    /// c:1116 — `sourcehome("")` empty arg is safe.
3312    #[test]
3313    fn sourcehome_empty_no_panic() {
3314        let _g = crate::test_util::global_state_lock();
3315        sourcehome("");
3316    }
3317
3318    /// c:1159 — `zleentry` returns Option<String> (compile-time type pin).
3319    #[test]
3320    fn zleentry_returns_option_string_type() {
3321        let _g = crate::test_util::global_state_lock();
3322        let _: Option<String> = zleentry(0);
3323    }
3324
3325    /// c:1199 — `fallback_compctlread` returns i32.
3326    #[test]
3327    fn fallback_compctlread_returns_i32_type() {
3328        let _g = crate::test_util::global_state_lock();
3329        let _: i32 = fallback_compctlread("");
3330    }
3331
3332    /// c:1199 — `fallback_compctlread` is deterministic for any name.
3333    #[test]
3334    fn fallback_compctlread_is_deterministic() {
3335        let _g = crate::test_util::global_state_lock();
3336        for n in ["", "name", "anything"] {
3337            let first = fallback_compctlread(n);
3338            for _ in 0..3 {
3339                assert_eq!(
3340                    fallback_compctlread(n),
3341                    first,
3342                    "fallback_compctlread({:?}) must be deterministic",
3343                    n
3344                );
3345            }
3346        }
3347    }
3348
3349    /// c:1143 — `init_bltinmods` returns void (no panic check).
3350    #[test]
3351    fn init_bltinmods_no_panic_full_sweep() {
3352        let _g = crate::test_util::global_state_lock();
3353        for _ in 0..5 {
3354            init_bltinmods();
3355        }
3356    }
3357
3358    /// c:1149 — `noop_function` returns void (compile-time void type).
3359    #[test]
3360    fn noop_function_signature_void() {
3361        let _: () = noop_function();
3362    }
3363
3364    /// c:1154 — `noop_function_int(N)` returns void.
3365    #[test]
3366    fn noop_function_int_signature_void() {
3367        let _: () = noop_function_int(0);
3368    }
3369
3370    /// c:1116 — `sourcehome("/")` root dir safe.
3371    #[test]
3372    fn sourcehome_root_dir_no_panic() {
3373        let _g = crate::test_util::global_state_lock();
3374        sourcehome("/");
3375    }
3376
3377    // ═══════════════════════════════════════════════════════════════════
3378    // Additional C-parity tests for Src/init.c
3379    // c:464 tccap_get_name / c:491 init_term / c:1081 source /
3380    // c:1159 zleentry / c:1199 fallback_compctlread + lifecycle pins
3381    // ═══════════════════════════════════════════════════════════════════
3382
3383    /// c:464 — `tccap_get_name` returns `&'static str` (compile-time pin, alt).
3384    #[test]
3385    fn tccap_get_name_returns_static_str_pin_alt() {
3386        let _: &'static str = tccap_get_name(0);
3387    }
3388
3389    /// c:464 — `tccap_get_name(TC_COUNT)` and beyond return empty.
3390    /// C source: `if (cap >= 39 /* TC_COUNT */) return "";`.
3391    #[test]
3392    fn tccap_get_name_oob_returns_empty() {
3393        assert_eq!(tccap_get_name(39), "", "TC_COUNT itself returns empty");
3394        assert_eq!(tccap_get_name(100), "", "way past TC_COUNT returns empty");
3395        assert_eq!(
3396            tccap_get_name(usize::MAX),
3397            "",
3398            "MAX returns empty (no panic)"
3399        );
3400    }
3401
3402    /// c:464 — `tccap_get_name` is deterministic for in-range indices.
3403    #[test]
3404    fn tccap_get_name_deterministic_for_valid_indices() {
3405        for cap in 0..39usize {
3406            let first = tccap_get_name(cap);
3407            for _ in 0..3 {
3408                assert_eq!(
3409                    tccap_get_name(cap),
3410                    first,
3411                    "tccap_get_name({}) must be pure",
3412                    cap
3413                );
3414            }
3415        }
3416    }
3417
3418    /// c:464 — every in-range cap returns non-empty (zsh ships a name
3419    /// for every TC_* slot; an empty entry would be a corpus drift).
3420    #[test]
3421    fn tccap_get_name_all_in_range_caps_non_empty() {
3422        for cap in 0..39usize {
3423            assert!(
3424                !tccap_get_name(cap).is_empty(),
3425                "TC_{} (cap idx {}) must have a non-empty cap name",
3426                cap,
3427                cap
3428            );
3429        }
3430    }
3431
3432    /// c:491 — `init_term` returns i32 (compile-time pin).
3433    #[test]
3434    fn init_term_returns_i32_type() {
3435        let _g = crate::test_util::global_state_lock();
3436        let _: i32 = init_term();
3437    }
3438
3439    /// c:1081 — `source(empty)` returns i32 (compile-time pin, alt).
3440    #[test]
3441    fn source_returns_i32_pin_alt() {
3442        let _g = crate::test_util::global_state_lock();
3443        let _: i32 = source("");
3444    }
3445
3446    /// c:1081 — `source("/__nonexistent_xyz__")` is non-fatal (returns).
3447    #[test]
3448    fn source_nonexistent_file_no_panic() {
3449        let _g = crate::test_util::global_state_lock();
3450        let _ = source("/__definitely_not_a_file_zshrs_xyz__");
3451    }
3452
3453    /// c:1159 — `zleentry` is deterministic (pure dispatch).
3454    #[test]
3455    fn zleentry_is_deterministic() {
3456        let _g = crate::test_util::global_state_lock();
3457        for cmd in 0..5i32 {
3458            let first = zleentry(cmd);
3459            for _ in 0..3 {
3460                assert_eq!(
3461                    zleentry(cmd),
3462                    first,
3463                    "zleentry({}) must be deterministic",
3464                    cmd
3465                );
3466            }
3467        }
3468    }
3469
3470    /// c:1199 — `fallback_compctlread("")` is safe (empty name no-op).
3471    #[test]
3472    fn fallback_compctlread_empty_name_safe() {
3473        let _g = crate::test_util::global_state_lock();
3474        let _ = fallback_compctlread("");
3475    }
3476
3477    /// c:1149 — `noop_function` called repeatedly is safe.
3478    #[test]
3479    fn noop_function_repeated_calls_safe() {
3480        for _ in 0..100 {
3481            noop_function();
3482        }
3483    }
3484
3485    /// c:1154 — `noop_function_int` for various ints is safe + void.
3486    #[test]
3487    fn noop_function_int_various_inputs_safe() {
3488        for n in [-1, 0, 1, i32::MIN, i32::MAX] {
3489            let _: () = noop_function_int(n);
3490        }
3491    }
3492
3493    /// c:1116 — `sourcehome("nonexistent_file_xyz")` safe (no-op when
3494    /// the home-relative path doesn't exist).
3495    #[test]
3496    fn sourcehome_nonexistent_file_safe() {
3497        let _g = crate::test_util::global_state_lock();
3498        sourcehome("__definitely_no_such_zshrc_xyz__");
3499    }
3500
3501    // ═══════════════════════════════════════════════════════════════════
3502    // Additional C-parity pins for Src/init.c
3503    // c:464 tccap_get_name / c:491 init_term / c:1081 source /
3504    // c:1116 sourcehome / c:1143 init_bltinmods / c:1149 noop_function /
3505    // c:1159 zleentry / c:1199 fallback_compctlread
3506    // ═══════════════════════════════════════════════════════════════════
3507
3508    /// c:1143 — `init_bltinmods` is idempotent (safe to call repeatedly).
3509    #[test]
3510    fn init_bltinmods_idempotent_repeated_calls() {
3511        let _g = crate::test_util::global_state_lock();
3512        for _ in 0..10 {
3513            init_bltinmods();
3514        }
3515    }
3516
3517    /// c:464 — `tccap_get_name` for index 0 returns non-empty (first cap).
3518    #[test]
3519    fn tccap_get_name_index_zero_non_empty() {
3520        let _g = crate::test_util::global_state_lock();
3521        let _ = tccap_get_name(0);
3522    }
3523
3524    /// c:464 — `tccap_get_name(usize::MAX)` doesn't panic.
3525    #[test]
3526    fn tccap_get_name_usize_max_no_panic() {
3527        let _g = crate::test_util::global_state_lock();
3528        let r = tccap_get_name(usize::MAX);
3529        // OOB returns empty per the OOB test that already exists.
3530        assert!(r.is_empty(), "OOB must return empty, got {:?}", r);
3531    }
3532
3533    /// c:1116 — `sourcehome("")` empty name safe (no-op when path empty).
3534    #[test]
3535    fn sourcehome_empty_name_no_panic() {
3536        let _g = crate::test_util::global_state_lock();
3537        sourcehome("");
3538    }
3539
3540    /// c:1081 — `source` return type i32 (compile-time pin).
3541    #[test]
3542    fn source_returns_i32_type_compile_pin() {
3543        let _g = crate::test_util::global_state_lock();
3544        let _: i32 = source("/dev/null");
3545    }
3546
3547    /// c:1081 — `source("")` empty path doesn't panic.
3548    #[test]
3549    fn source_empty_path_no_panic() {
3550        let _g = crate::test_util::global_state_lock();
3551        let _ = source("");
3552    }
3553
3554    /// c:1159 — `zleentry` return type Option<String> (compile-time pin, alt).
3555    #[test]
3556    fn zleentry_returns_option_string_type_alt() {
3557        let _g = crate::test_util::global_state_lock();
3558        let _: Option<String> = zleentry(0);
3559    }
3560
3561    /// c:1159 — `zleentry(-1)` (invalid cmd) doesn't panic.
3562    #[test]
3563    fn zleentry_negative_cmd_no_panic() {
3564        let _g = crate::test_util::global_state_lock();
3565        let _ = zleentry(-1);
3566    }
3567
3568    /// c:1199 — `fallback_compctlread` returns i32 (compile-time pin, alt).
3569    #[test]
3570    fn fallback_compctlread_returns_i32_type_alt() {
3571        let _g = crate::test_util::global_state_lock();
3572        let _: i32 = fallback_compctlread("test");
3573    }
3574
3575    /// c:1149 — `noop_function` returns void (compile-time pin).
3576    #[test]
3577    fn noop_function_returns_void_type() {
3578        let _g = crate::test_util::global_state_lock();
3579        let _: () = noop_function();
3580    }
3581
3582    /// c:1154 — `noop_function_int` returns void (compile-time pin).
3583    #[test]
3584    fn noop_function_int_returns_void_type() {
3585        let _g = crate::test_util::global_state_lock();
3586        let _: () = noop_function_int(42);
3587    }
3588
3589    /// c:464 — `tccap_get_name` is deterministic for OOB (same empty value
3590    /// across calls).
3591    #[test]
3592    fn tccap_get_name_oob_deterministic() {
3593        let _g = crate::test_util::global_state_lock();
3594        let a = tccap_get_name(99_999);
3595        let b = tccap_get_name(99_999);
3596        assert_eq!(a, b, "OOB must be deterministic");
3597    }
3598}