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