Skip to main content

zsh/ported/
exec.rs

1//! Faithful Rust ports of free functions and file-static globals from
2//! `Src/exec.c`. The wordcode-VM dispatch tree (`execlist` / `execpline`
3//! / `execcmd` / `execsimple` etc.) that drives execution in C zsh is
4//! NOT replicated here — zshrs runs the fusevm bytecode VM instead
5//! (see `src/vm_helper.rs` + `src/fusevm_bridge.rs`).
6//!
7//! What lives here are the parts of `Src/exec.c` that ARE faithful
8//! ports and don't depend on the C-side wordcode walker:
9//!
10//! - **`trap_state` / `trap_return` / `forklevel`** — file-static
11//!   integer globals from `Src/exec.c:134 / :155 / :1052`, exposed as
12//!   atomics shared between this module, `Src/signals.c`'s port at
13//!   `src/ported/signals.rs`, and `Src/params.c`'s port at
14//!   `src/ported/params.rs`.
15//! - **`gethere`** (`Src/exec.c:4573`) — turn a here-document into a
16//!   here-string. Called from the lexer port (`src/ported/lex.rs`).
17//! - **`getoutput`** (`Src/exec.c:4712`) — command-substitution body
18//!   runner. Called from the parameter-expansion port
19//!   (`src/ported/subst.rs`).
20//! - **`loadautofn`** + **`getfpfunc`** (`Src/exec.c:5682` / `:6219`)
21//!   — `$fpath` walker + autoload file installer. Called from
22//!   `bin_autoload` / `bin_functions -c` in `src/ported/builtin.rs`.
23//! - **`resolvebuiltin`** (`Src/exec.c:2703`) — module-autoload guard
24//!   used by the dispatch walk in `execcmd_exec`.
25//! - **`execcmd_compile_head`** — fusevm-bytecode-time head resolver
26//!   mirroring the head section (`c:2904-3275`) of C's `execcmd_exec`.
27//!   NOT a faithful port; the canonical 7-arg `execcmd_exec` port lives
28//!   alongside it.
29//! - **`execcmd_exec`** (`Src/exec.c:2900`) — canonical 7-arg port of
30//!   the C function (locals + dispatch walk through builtin/shfunc/external
31//!   invocation). Used by future tree-walker callers; the fusevm
32//!   bytecode flow goes through `execcmd_compile_head` instead.
33
34use std::os::unix::fs::PermissionsExt;
35use std::sync::atomic::Ordering;
36
37// `with_executor` import removed — all ShellExecutor reach-in calls
38// routed through `crate::ported::exec::*` fn-ptrs installed by
39// fusevm_bridge at startup. See memory feedback_no_exec_script_from_ported.
40use crate::ported::builtin::{cd_able_vars, fixdir, BUILTINS, DOPRINTDIR, EXIT_VAL, LASTVAL};
41use crate::ported::builtins::rlimits::setlimits;
42use crate::ported::builtins::sched::zleactive;
43use crate::ported::compat::zgettime_monotonic_if_available;
44use crate::ported::config_h::DEFAULT_PATH;
45use crate::ported::context::{zcontext_restore, zcontext_save};
46use crate::ported::hashtable::{
47    cmdnam_unhashed, cmdnamtab_lock, dircache_set, hashdir, pathchecked, shfunctab_lock,
48};
49use crate::ported::hist::{strinbeg, strinend};
50use crate::ported::init::{shout, underscorelen, underscoreused, zunderscore, SHTTY};
51use crate::ported::input::{inpop, inpush};
52use crate::ported::jobs::{expandjobtab, get_usage, release_pgrp, waitforpid, JOBTAB, THISJOB};
53use crate::ported::lex::{
54    hgetc, parsestr, tok, untokenize, ztokens, LEXERR, LEX_LEXSTOP, LEX_LINENO,
55};
56use crate::ported::mem::{dupstring, dyncat, popheap, pushheap};
57use crate::ported::modules::clone::mypgrp;
58use crate::ported::options::{dosetopt, opt_state_set, sticky};
59use crate::ported::params::{
60    endparamscope, getsparam, locallevel, paramtab, setiparam, zgetenv, zputenv,
61};
62use crate::ported::parse::{closedumps, ecrawstr, parse_list};
63use crate::ported::prompt::{cmdpop, cmdpush};
64use crate::ported::signals::{
65    intrap, queue_signals, settrap, signal_mask, signal_unblock, sigtrapped, trapisfunc,
66    traplocallevel, unqueue_signals, unsettrap,
67};
68use crate::ported::signals_h::{
69    child_block, child_unblock, dont_queue_signals, signal_default, signal_ignore, winch_unblock,
70    SIGCOUNT,
71};
72use crate::ported::subst::{quotesubst, singsub};
73use crate::ported::utils::{
74    errflag, fdtable_get, fdtable_set, gettempfile, gettempname, inc_locallevel, movefd, pathprog,
75    printprompt4, quotedzputs, redup, unmeta, unmetafy, write_loop, zclose, zerr, zwarn,
76    ERRFLAG_ERROR, MAX_ZSH_FD,
77};
78use crate::ported::r#loop::{execcase, execfor, execif, execrepeat, execselect, exectry, execwhile};
79use crate::ported::zsh_h::{
80    builtin, cmdnam, emulation_options, eprog, execstack, funcwrap, hashnode, isset, jobfile,
81    multio, redir, shfunc, unset, wc_code, Emulation_options, Inang, Inpar, Meta, Nularg, Outpar,
82    Pound, BINF_BUILTIN, BINF_CLEARENV, BINF_COMMAND, BINF_DASH, BINF_EXEC, BINF_PREFIX, CHASEDOTS,
83    CHASELINKS, CLOBBER, CLOBBEREMPTY, CS_CMDSUBST, ERRFLAG_INT, FDT_EXTERNAL, FDT_INTERNAL,
84    FDT_PROC_SUBST, FDT_SAVED_MASK, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHDIRS, INP_LINENO,
85    INTERACTIVE, IS_CLOBBER_REDIR, IS_DASH, JOBTEXTSIZE, MAX_PIPESTATS, MONITOR, MULTIOS,
86    MULTIOUNIT, PATHDIRS, PM_LOADDIR, PM_READONLY, PM_UNDEFINED, POSIXBUILTINS, POSIXJOBS,
87    POSIXTRAPS, REDIRF_FROM_HEREDOC, REDIR_CLOSE, REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE,
88    REDIR_OUTPIPE, USEZLE, VERBOSE, WC_LIST, WC_LIST_TYPE, WC_PIPE, WC_PIPE_END, WC_PIPE_TYPE,
89    WC_REDIR, WC_REDIR_TYPE, WC_REDIR_VARID, WC_SIMPLE, WC_SIMPLE_ARGC, WC_SUBLIST, WC_SUBLIST_END,
90    WC_SUBLIST_FLAGS, WC_SUBLIST_TYPE, WC_TYPESET, ZSIG_FUNC, ZSIG_IGNORED, Z_END,
91};
92use crate::ported::zsh_system_h::timespec as ZshTimespec;
93use crate::ported::ztype_h::{inull, itok};
94use crate::zsh_h::XTRACE;
95
96/// Port of the anonymous `enum { ... }` from `Src/exec.c:35-40`.
97/// Flag bits passed as the `addflags` argument to `addvars` /
98/// `addvarsfromargs`:
99///   - `ADDVAR_EXPORT`  (1<<0) — export each assignment for the
100///                                command `VAR=val cmd ...` form.
101///   - `ADDVAR_RESTORE` (1<<2) — the variable list is being restored
102///                                later (implicit local scope), so
103///                                suppress `ASSPM_WARN`.
104pub const ADDVAR_EXPORT: i32 = 1 << 0; // c:37 (Src/exec.c)
105/// `ADDVAR_RESTORE` constant.
106pub const ADDVAR_RESTORE: i32 = 1 << 2; // c:39 (Src/exec.c)
107
108/// Port of `int trap_state;` from `Src/exec.c:134`. Tracks whether
109/// a trap handler is currently being processed and, paired with
110/// `TRAP_RETURN` below, whether a `return` inside the trap should
111/// promote to `TRAP_STATE_FORCE_RETURN` to unwind the trap caller.
112///
113/// Values: `TRAP_STATE_INACTIVE = 0`, `TRAP_STATE_PRIMED = 1`,
114/// `TRAP_STATE_FORCE_RETURN = 2` (see `Src/zsh.h`).
115pub static TRAP_STATE: std::sync::atomic::AtomicI32 = // c:134 (Src/exec.c)
116    std::sync::atomic::AtomicI32::new(0);
117
118/// Port of `int trap_return;` from `Src/exec.c:155`. Carries the
119/// pending exit status from inside a trap; sentinel `-2` means
120/// "running an EXIT/DEBUG-style trap at the current level"
121/// (signals.c:1166). Promoted to the user's `return N` value by
122/// `bin_return` when POSIX-trap semantics apply (builtin.c:5852).
123pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = // c:155 (Src/exec.c)
124    std::sync::atomic::AtomicI32::new(0);
125
126/// Port of `int forklevel;` from `Src/exec.c:1053`. Records the
127/// `locallevel` at the most recent fork point (set at c:1221:
128/// `forklevel = locallevel;` inside `entersubsh()`). Used by:
129///   - `signals.c:808` SIGPIPE handler — `!forklevel` distinguishes
130///     the top-level shell from a forked subshell.
131///   - `exec.c:6146` — `if (locallevel > forklevel)` decides whether
132///     a function-defined trap should fire on this subshell exit.
133///   - `params.c:3724` — WARNCREATEGLOBAL nest-depth check.
134///
135/// Initialised to 0 (no fork has occurred yet). Set to `locallevel`
136/// at every `entersubsh()` entry per c:1221.
137pub static FORKLEVEL: std::sync::atomic::AtomicI32 = // c:1052 (Src/exec.c)
138    std::sync::atomic::AtomicI32::new(0);
139
140// =============================================================================
141// File-static globals from Src/exec.c. Bucket choices per PORT_PLAN.md:
142//   - Per-evaluator transient state → thread_local Cell (bucket 1)
143//   - Shell-wide shared state       → AtomicI32 / Mutex (bucket 2)
144// All names match C exactly. Surrounding doc-comments cite the C
145// declaration line.
146// =============================================================================
147
148/// Port of `int noerrexit;` from `Src/exec.c:72`. Bit-flags that
149/// suppress ERREXIT triggering on the next command(s). Bits:
150/// `NOERREXIT_EXIT` (in `if`/`while`/`until` test contexts),
151/// `NOERREXIT_RETURN` (after `return`), `NOERREXIT_UNTIL_EXEC`
152/// (until next exec'd command). Bucket-1 — per-evaluator (each
153/// recursive eval has its own suppression frame).
154pub static noerrexit: std::sync::atomic::AtomicI32 = // c:72 (Src/exec.c)
155    std::sync::atomic::AtomicI32::new(0);
156
157/// Port of `int this_noerrexit;` from `Src/exec.c:109`. When set,
158/// suppress ERREXIT for THIS one command only (consumed + cleared
159/// before the next command starts). Set by `execcursh` and the
160/// `((expr))` arith path so a 0-result doesn't trigger errexit.
161pub static this_noerrexit: std::sync::atomic::AtomicI32 = // c:109 (Src/exec.c)
162    std::sync::atomic::AtomicI32::new(0);
163
164/// Port of `mod_export int noerrs;` from `Src/exec.c:117`. When
165/// non-zero, suppress `zerr()` output (lex error reporting during
166/// `parse_string`, `parseopts` etc.). Saved/restored by
167/// `execsave`/`execrestore`.
168/// Port of `static char list_pipe_text[JOBTEXTSIZE]` from
169/// `Src/exec.c:463`. Holds the textual rendering of the in-flight
170/// pipe list; saved across nested execlist invocations at
171/// exec.c:1372-1380 (zeroed on entry, restored from
172/// `old_list_pipe_text` at c:1634-1638) and round-tripped through
173/// execsave/execrestore (c:6448 / c:6484). zshrs models it as a
174/// length-bounded String guarded by a Mutex — the C `char[80]` cap
175/// is a buffer-overflow guard, but matching length matters for the
176/// `jobs` builtin's pipe-list rendering.
177pub static LIST_PIPE_TEXT: std::sync::Mutex<String> = std::sync::Mutex::new(String::new()); // c:463 (Src/exec.c)
178
179// c:117 (Src/exec.c) — `mod_export int noerrs;`
180//
181// C has exactly ONE `noerrs`. This port had TWO disjoint storages: an
182// `AtomicI32` here, and `utils::noerrs_lock()`'s `Mutex<i32>` (flagged in
183// utils.rs as "NOT IN UTILS.C"). The four diagnostic emitters — `zerr`,
184// `zwarn`, `zerrnam`, `zwarnnam` (utils.rs:221/244/262/279) — read the utils
185// one, while `docomplete`'s suppression window (`noerrs = 1` around
186// `doexpansion`, zle_tricky.c:825-828) wrote this one. So the suppression
187// never reached the printer and completion-time diagnostics leaked onto the
188// screen: `ls *(` printed `zsh: bad pattern: *(` where zsh prints nothing,
189// consuming a row and shifting the whole match grid.
190//
191// Unified onto the storage every emitter already consults; the accessor lives
192// at `crate::ported::utils::noerrs_lock()`. Do not reintroduce a second one.
193// The storage is `crate::ported::utils::noerrs_lock()`; read/write it directly.
194
195/// Port of `int nohistsave;` from `Src/exec.c:122`. When non-zero,
196/// `addhistnode` no-ops so trap firings / `eval` invocations don't
197/// pollute `$HISTCMD`. Tracked alongside `noerrs` in the trap path.
198pub static nohistsave: std::sync::atomic::AtomicI32 = // c:122 (Src/exec.c)
199    std::sync::atomic::AtomicI32::new(0);
200
201/// Port of `int subsh;` from `Src/exec.c:160`. Subshell depth — bumped
202/// every time `entersubsh` forks a sub-shell, used by signal handling
203/// (different SIGINT semantics in subshells) and by `${$$}` (`$$`
204/// stays at the top-level pid).
205pub static subsh: std::sync::atomic::AtomicI32 = // c:160 (Src/exec.c)
206    std::sync::atomic::AtomicI32::new(0);
207
208/// Port of `int zsh_subshell;` from `Src/params.c:108`. Visible
209/// `$ZSH_SUBSHELL` parameter — incremented by `entersubsh()` each time
210/// the shell forks into a subshell (real or fake-exec). Distinct from
211/// `subsh` which records whether we ARE a subshell; `zsh_subshell` is
212/// the visible depth count.
213pub static zsh_subshell: std::sync::atomic::AtomicI32 = // c:108 (Src/params.c)
214    std::sync::atomic::AtomicI32::new(0);
215
216/// Port of `mod_export volatile int retflag;` from `Src/exec.c:165`.
217/// Set by `bin_return` to unwind the function-call stack. Cleared
218/// by `runshfunc` on entry, checked by `execlist`'s main loop.
219///
220/// Re-export alias of the canonical [`crate::ported::builtin::RETFLAG`] — C
221/// has ONE `retflag` (exec.c:165). All return-flow logic (execlist, the
222/// `return` builtin, signals) uses `RETFLAG`; this lowercase twin was never
223/// stored, so `zpty`/`system` module read-loops that check `exec::retflag`
224/// saw a permanent 0 and never aborted on a pending `return`.
225pub use crate::ported::builtin::RETFLAG as retflag; // c:165 (Src/exec.c)
226
227/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
228/// recent `$(cmd)` command-substitution child. Used by exit-status
229/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
230/// the pid `waitpid`-d for it.
231pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
232    std::sync::atomic::AtomicI32::new(0);
233
234/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
235/// Pid of the most recent process-substitution child (`<(cmd)` /
236/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
237/// jobs aren't wait-collected by the parent until the fd is closed.
238pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
239    std::sync::atomic::AtomicI32::new(0);
240
241/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
242/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
243/// runs alongside a substitution.
244pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
245    std::sync::atomic::AtomicI32::new(0);
246
247/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
248/// `lastval` is updated from `cmdoutval` after the command
249/// (i.e. the command had substitutions whose exit status matters).
250pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
251    std::sync::atomic::AtomicI32::new(0);
252
253/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
254/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
255/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
256/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
257/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
258/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
259pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
260    std::sync::atomic::AtomicI32::new(0);
261
262/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
263/// currently-executing pipeline is the long-running pipe-into-loop
264/// shape (`cat foo | while read a; do ... done`) — drives the
265/// super/sub-job tracking documented in the famous `Allen Edeln…`
266/// comment block above this declaration in C.
267pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
268    std::sync::atomic::AtomicI32::new(0);
269
270/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
271/// dispatch of a "simple" pipeline (single-stage / no shell-construct
272/// tail) so the `list_pipe` machinery short-circuits.
273pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
274    std::sync::atomic::AtomicI32::new(0);
275
276/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
277/// PID of the sub-shell created to host the loop-after-pipe pattern;
278/// passed up the recursive `execlist` stack so the cat-job's super-
279/// job entry can record it.
280pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
281    std::sync::atomic::AtomicI32::new(0);
282
283/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
284/// `execpline` doesn't wait for the pipeline; used during the
285/// list_pipe sub-shell fork bookkeeping.
286pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
287    std::sync::atomic::AtomicI32::new(0);
288
289/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
290/// pipeline depth (counts nested pipelines within the current
291/// `execlist` call chain).
292pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
293    std::sync::atomic::AtomicI32::new(0);
294
295/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
296/// Set in the child after the list_pipe fork so the child knows to
297/// continue executing the loop body (vs the parent which records
298/// the pid + returns).
299pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
300    std::sync::atomic::AtomicI32::new(0);
301
302/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
303/// table index of the pipeline's first-stage job (the `cat` in
304/// `cat foo | while ...`).
305pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
306    std::sync::atomic::AtomicI32::new(0);
307
308/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
309/// `printprompt4` has emitted the `$PS4` prefix for the current
310/// xtrace command — prevents double-printing when an inner sub-eval
311/// also wants to xtrace.
312pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
313    std::sync::atomic::AtomicI32::new(0);
314
315/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
316///
317/// File-static "execsubst parameters" — callers (execcmd_exec at
318/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
319/// uses them as the `flags` arg to prefork() and the gate on
320/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
321/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
322/// when the dispatched builtin has `BINF_NOGLOB`.
323pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
324    std::sync::atomic::AtomicI32::new(0);
325pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
326    std::sync::atomic::AtomicI32::new(1);
327
328/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
329/// of the linked exec-context save stack — `execsave` pushes a frame
330/// before signal-handler / trap dispatch; `execrestore` pops it
331/// afterwards so the interrupted command resumes with its state intact.
332pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
333    std::sync::Mutex::new(None);
334
335/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
336/// `stty` argument string captured by `addvars` when the command's
337/// inline env contains `STTY=...`. Applied by `execute` before fork
338/// + exec so the spawned program sees its tty configured. Reset to
339/// `None` after consumption to avoid infinite recursion.
340pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
341    std::sync::Mutex::new(None);
342
343/// Convert a here-document into a here-string. Line-by-line port of
344/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
345/// input stream via `hgetc()` until the terminator line is matched,
346/// returning the collected body as a string. `strp` is in/out: on
347/// entry the raw terminator (possibly with token markers + leading
348/// tabs); on return the munged terminator (after `quotesubst` +
349/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
350///
351/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
352/// Rust's `String` auto-grows so the OOM branch is effectively
353/// unreachable, but the return type stays `Option<String>` to mirror
354/// the C signature which can return NULL.
355///
356/// Port of `gethere()` from `Src/exec.c:4570` — C decl `gethere(char **strp, int typ)`.
357pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
358    // c:4573 (Src/exec.c)
359    let mut buf: String; // c:4575 char *buf
360    let mut bsiz: usize; // c:4576 int bsiz
361    let mut qt: i32 = 0; // c:4576 int qt = 0
362    let mut strip: i32 = 0; // c:4576 int strip = 0
363                            // c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
364                            // `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
365                            // C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
366                            // `s` (the loop iterator for the inull-scan) stays local to its
367                            // for-loop. `c` mirrors the C `char c`.
368    let mut t: usize; // c:4577 char *t
369    let mut c: Option<char>; // c:4577 char c
370    let mut str: String = strp.clone(); // c:4578 char *str = *strp
371
372    // c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
373    for s in str.chars() {
374        if crate::token_char::token_byte(s).is_some_and(inull) {
375            // c:4581
376            qt = 1; // c:4582
377            break; // c:4583
378        }
379    }
380    str = quotesubst(&str); // c:4585
381    str = untokenize(&str); // c:4586
382    if typ == REDIR_HEREDOCDASH {
383        // c:4587
384        strip = 1; // c:4588
385                   // c:4589-4590 — while (*str == '\t') str++;
386        while str.starts_with('\t') {
387            str.remove(0);
388        }
389    }
390    *strp = str.clone(); // c:4592 *strp = str
391
392    // c:4593 — bptr = buf = zalloc(bsiz = 256);
393    bsiz = 256;
394    buf = String::with_capacity(bsiz);
395    let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
396
397    // c:4594 — for (;;)
398    loop {
399        t = buf.len(); // c:4595 t = bptr
400
401        // c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
402        loop {
403            c = hgetc();
404            if !(c == Some('\t') && strip != 0) {
405                break;
406            }
407        }
408
409        // c:4599 — for (;;) — inner body-read loop
410        loop {
411            // c:4600-4613 — buffer-growth realloc dance. Rust's
412            // String auto-grows; nothing to do.
413            // c:4614 — if (lexstop || c == '\n') break;
414            if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
415                break;
416            }
417            // c:4616 — if (!qt && c == '\\')
418            if qt == 0 && c == Some('\\') {
419                buf.push('\\'); // c:4617 *bptr++ = c
420                c = hgetc(); // c:4618
421                if c == Some('\n') {
422                    // c:4619
423                    buf.pop(); // c:4620 bptr--
424                    c = hgetc(); // c:4621
425                    continue; // c:4622
426                }
427            }
428            if let Some(ch) = c {
429                // c:4625 *bptr++ = c
430                buf.push(ch);
431            }
432            c = hgetc(); // c:4626
433        }
434        // c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
435
436        // c:4629-4630 — if (!strcmp(t, str)) break;
437        if &buf[t..] == str.as_str() {
438            break;
439        }
440        // c:4631-4634 — if (lexstop) { t = bptr; break; }
441        // C's ingetc sets `lexstop = 1` when input is exhausted
442        // (Src/input.c:289-292), so the flag check alone suffices
443        // there. zshrs's hgetc signals the same exhaustion by
444        // returning None WITHOUT setting LEX_LEXSTOP — treat both as
445        // the c:4631 condition, else an unterminated heredoc loops
446        // here forever appending '\n'. Gap #3 2026-06-12 (A04
447        // "Here-documents don't perform shell expansion" wedge).
448        if LEX_LEXSTOP.with(|f| f.get()) || c.is_none() {
449            t = buf.len();
450            break;
451        }
452        // c:4635 — *bptr++ = '\n';
453        buf.push('\n');
454    }
455    // c:4637 — *t = '\0';
456    buf.truncate(t);
457
458    // c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
459    // The C dance frees the realloc'd block and re-allocates via the
460    // string-heap allocator. Rust drops the old String when reassigned.
461    buf = dupstring(&buf);
462
463    if qt == 0 {
464        // c:4641
465        // c:4642 — int ef = errflag;
466        let ef = errflag.load(Ordering::Relaxed);
467        // c:4644 — parsestr(&buf);
468        if let Ok(parsed) = parsestr(&buf) {
469            buf = parsed;
470        }
471        // c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
472        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
473            let cur = errflag.load(Ordering::Relaxed);
474            errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
475        }
476    }
477    Some(buf) // c:4651 return buf
478}
479
480/// Port of `LinkList getoutput(char *cmd, int qt)` from
481/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
482/// active executor, then routes the captured stdout through
483/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
484///
485/// C return shape: `LinkList` of `char*`. Rust port returns
486/// `Vec<String>` (same shape, owned).
487///
488/// `qt` matches C exactly:
489///   - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
490///     entire output as a single-element vec. C c:4858-4862: if
491///     output empty, returns a single Nularg sentinel so callers
492///     see "empty value" rather than "no value".
493///   - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
494///     `spacesplit(buf, allownull=false)` per c:4865-4871, and
495///     `shtokenize` each word when `GLOBSUBST` is set (c:4868-4869)
496///     so the substituted words glob downstream.
497///
498/// Both arms — `$(< file)` (c:4746) and `$( cmd )` (c:4772) — go through
499/// the same c:4858-4872 tail (inlined twice — see the cross-reference
500/// notes at :619 and :3829), mirroring C where both go through `readoutput`.
501///
502/// Uses `with_executor` (panics on missing VM context), not
503/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
504/// `execpline` directly — there's no "no shell" code path. The
505/// silent-no-op pattern (return empty string when no executor) would
506/// mask catastrophic state corruption as "command produced no output",
507/// which is the failure mode the `subst.rs:496` warning block flags.
508/* $(...) */
509/// Port of `getoutput()` from `Src/exec.c:4713` — C decl `getoutput(char *cmd, int qt)`.
510// c:4709
511/// `getoutput` — see implementation.
512pub fn getoutput(cmd: &str, qt: i32) -> Option<Vec<String>> {
513    // c:4713
514    // c:4715 — `Eprog prog;`
515    let prog: Option<eprog>;
516    // c:4716 — `int pipes[2];`  (collapsed: in-process executor; no fork)
517    // c:4717 — `pid_t pid;`     (collapsed)
518    let mut s: String; // c:4718
519                       // c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
520                       //                !sourcelevel && unset(INTERACTIVECOMMENTS));
521                       //                prog = parse_string(cmd, 0); nocomments = onc;`
522    let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
523    let new_nc = crate::ported::zsh_h::interact()
524        && crate::ported::init::sourcelevel.load(Ordering::Relaxed) == 0
525        && !isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
526    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
527    prog = parse_string(cmd, 0);
528    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
529
530    if prog.is_none() {
531        // c:4725
532        return None; // c:4726 return NULL
533    }
534    let prog = prog.unwrap();
535
536    if !isset(crate::ported::zsh_h::EXECOPT) {
537        // c:4728
538        return Some(Vec::new()); // c:4729 newlinklist()
539    }
540
541    // c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
542    if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
543        /* $(< word) */
544        // c:4732
545        s = red_name;
546        s = singsub(&s); // c:4737
547        if errflag.load(Ordering::Relaxed) != 0 {
548            return None; // c:4739
549        }
550        let s = untokenize(&s); // c:4740
551        let path_meta = unmeta(&s); // c:4741 unmeta(s)
552        let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
553            Ok(c) => c,
554            Err(_) => return Some(Vec::new()),
555        };
556        let stream = unsafe {
557            libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
558        };
559        if stream == -1 {
560            // c:4798 — `zwarn("%e: %s", errno, s);`
561            //
562            // `zwarn`, NOT `zerr`: `zerr` raises ERRFLAG_ERROR, which
563            // abandons the rest of the line, so `print -r -- "[$(<f)]"`
564            // against a missing `f` printed nothing and exited 1. zsh
565            // warns, substitutes empty, and runs the command — the
566            // observed output is `[]` with status 0.
567            //
568            // `%e` is zsh's errno formatter (`zsh_errno_msg`, utils.c:355),
569            // which lowercases strerror's first letter: "no such file or
570            // directory". Rust's `io::Error` Display renders "No such file
571            // or directory (os error 2)" instead.
572            let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
573            zwarn(&format!(
574                "{}: {}",
575                crate::ported::utils::zsh_errno_msg(errno),
576                s
577            ));
578            LASTVAL.store(1, Ordering::Relaxed);
579            cmdoutval.store(1, Ordering::Relaxed);
580            return Some(Vec::new()); // c:4800
581        }
582        // c:4746 — `retval = readoutput(stream, qt, &readerror);`
583        let mut readerror: i32 = 0;
584        let retval = readoutput(stream, qt, &mut readerror); // c:4746
585        if readerror != 0 {
586            // c:4803-4805 — `zwarn("error when reading %s: %e", s, readerror);`
587            // Non-fatal for the same reason as the open failure above.
588            zwarn(&format!(
589                "error when reading {}: {}", // c:4804
590                s,
591                crate::ported::utils::zsh_errno_msg(readerror)
592            ));
593            LASTVAL.store(1, Ordering::Relaxed);
594            cmdoutval.store(1, Ordering::Relaxed);
595        }
596        return Some(retval); // c:4751
597    }
598
599    // c:4753-4790 — Full fork path: mpipe + zfork + parent
600    // readoutput / waitforpid / child execode + _realexit. fusevm runs
601    // command substitution in-process, so the fork shape collapses to a
602    // synchronous executor call. C control points preserved as cites:
603    //   c:4753 mpipe       — handled by ShellExecutor pipe wiring
604    //   c:4758 child_block — no-op (no fork)
605    //   c:4760 zfork       — replaced by in-process exec
606    //   c:4768-4776 parent — equivalent to executor return
607    //   c:4778-4789 child  — entersubsh+execode+_realexit collapse.
608    //     The `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` at c:4781 is NOT
609    //     dropped by that collapse: `run_command_substitution` below enters
610    //     `SubshStateGuard`, which applies the deltas that are correct
611    //     without a fork (opts[MONITOR], shout, opts[USEZLE], zleactive) and
612    //     restores them on return. See SubshStateGuard for what is skipped
613    //     and why. `_realexit()` has no in-process counterpart — the
614    //     executor simply returns the captured buffer.
615    cmdoutval.store(0, Ordering::Relaxed); // c:4759
616    let buf = crate::ported::exec::run_command_substitution(cmd);
617    LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed); // c:4775
618
619    // ── c:4772 `retval = readoutput(pipes[0], qt, NULL);` ─────────────
620    // The fd died with the fork, but readoutput's POST-READ half (c:4858-
621    // 4872) still applies verbatim, so it is inlined here.
622    //
623    // !!! THIS BLOCK IS DUPLICATED at `readoutput` (:3829). See the note
624    // there for why build.rs's no-new-functions gate rules out a shared
625    // helper. ANY EDIT HERE MUST BE MIRRORED AT :3829 AND VICE VERSA.
626    //
627    // What was missing before: the c:4868-4869 `if (isset(GLOBSUBST))
628    // shtokenize(*words);` step. `readoutput` has it, this copy did not, so
629    // `setopt globsubst; echo $(echo '*')` printed `*` where zsh expands the
630    // glob — while `setopt globsubst; echo $(< star.txt)`, which reaches
631    // `readoutput` through the c:4746 arm, globbed correctly.
632    //
633    // `buf` arrives as a plain (un-metafied) String from
634    // `run_command_substitution`; `readoutput` now hands its own tail the
635    // same representation (see the deviation note at :3792).
636    //
637    // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
638    let s = buf.trim_end_matches('\n');
639    // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
640    if qt != 0 {
641        // c:4861
642        if s.is_empty() {
643            return Some(vec![String::from(Nularg)]); // c:4862
644        }
645        return Some(vec![s.to_string()]); // c:4864
646    }
647    // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
648    let mut words = crate::ported::utils::spacesplit(s, false); // c:4867
649    if isset(crate::ported::zsh_h::GLOBSUBST) {
650        // c:4870
651        for w in words.iter_mut() {
652            crate::ported::glob::shtokenize(w); // c:4870
653        }
654    }
655    Some(words)
656}
657
658/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
659/// int ignore_loaddir)` from `Src/exec.c:5682`. Walks `$fpath` for a
660/// file named `shf->node.nam`, reads it, installs the text body on
661/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
662///
663/// C body (abridged):
664///   1. `name = shf->node.nam`
665///   2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
666///   3. If !test_only && file found: parse → store eprog on
667///      `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
668///   4. Returns shf on success, NULL on failure.
669///
670/// Rust port: returns 0 = success, 1 = failure (matches the
671/// existing call-site convention in `bin_functions -c`). Stores
672/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
673/// `hashtable.rs:362`); the parser pass that converts text →
674/// Eprog runs lazily at first call site.
675/// Port of `loadautofn()` from `Src/exec.c:5682` — C decl `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)`.
676pub fn loadautofn(
677    shf: *mut shfunc, // c:5682 (Src/exec.c)
678    _ks: i32,
679    autol: i32,
680    current_fpath: i32, // c:5735 — 4th C param is `current_fpath`, NOT an
681                        // "ignore loaddir" flag; `eval_autoload` passes
682                        // `OPT_ISSET(ops,'d')` here (c:3182).
683) -> i32 {
684    if shf.is_null() {
685        return 1;
686    }
687    // c:5054 — `name = shf->node.nam`.
688    let name = unsafe { (*shf).node.nam.clone() };
689    // c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
690    let mut dir_path: Option<String> = None;
691    let mut dump_hit: Option<(eprog, i32)> = None;
692    // A function autoloaded by ABSOLUTE PATH (`autoload -Uz /dir/name`) — or
693    // one already resolved via `autoload -r` — caches its directory in
694    // `shf.filename` with PM_LOADDIR set. Load-on-call must search THAT dir
695    // (`shf.filename/name`), not walk `$fpath` (which won't contain it).
696    // Without this, `autoload -Uz $fdir/.hist.*` (zsh-hist) named the
697    // functions correctly but every call died with "definition file not
698    // found". Mirrors C loadautofn's PM_LOADDIR spec-path branch.
699    //
700    // c:5747-5757 —
701    //     if (shf->filename && shf->filename[0] == '/' &&
702    //         (shf->node.flags & PM_LOADDIR))
703    //     {
704    //         char *spec_path[2];
705    //         spec_path[0] = dupstring(shf->filename);
706    //         spec_path[1] = NULL;
707    //         prog = getfpfunc(shf->node.nam, &ksh, &fdir, spec_path, 0);
708    //         if (prog == &dummy_eprog &&
709    //             (current_fpath || (shf->node.flags & PM_CUR_FPATH)))
710    //             prog = getfpfunc(shf->node.nam, &ksh, &fdir, NULL, 0);
711    //     }
712    //     else
713    //         prog = getfpfunc(shf->node.nam, &ksh, &fdir, NULL, 0);
714    let fn_flags = unsafe { (*shf).node.flags } as u32;
715    let loaddir_spec: Option<Vec<String>> = {
716        let s = unsafe { &*shf };
717        // c:5747 — the spec-path arm needs an ABSOLUTE filename, not just
718        // PM_LOADDIR.
719        if s.filename.as_deref().is_some_and(|f| f.starts_with('/')) && (fn_flags & PM_LOADDIR) != 0
720        {
721            s.filename.clone().map(|d| vec![d])
722        } else {
723            None
724        }
725    };
726    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
727    // C runs getfpfunc (c:5753/c:5759) — a walk of `$fpath`, then a read and
728    // parse of the file — on the first call of every autoload in every
729    // process. autoloads.rkyv records what that search chose and read
730    // (`autoload_cache::ResolvedLoad`), so a plain `$fpath` autoload whose
731    // entry is still current installs the stored body without touching
732    // `$fpath` at all. With an explicit load directory (the c:5747 arm) the
733    // entry must have come from that directory.
734    let shard_hit = {
735        let dirs = match loaddir_spec.as_deref() {
736            Some(spec) => spec.to_vec(),
737            None => crate::ported::params::getaparam("fpath").unwrap_or_default(),
738        };
739        crate::autoload_cache::try_resolve(&name, &dirs)
740    };
741    let (path, dump_ksh, from_wordcode, body) = if let Some((dir, resolved)) = shard_hit {
742        dir_path = Some(dir.clone());
743        let hit = (
744            format!("{dir}/{name}"),
745            (resolved.dump_ksh >= 0).then_some(resolved.dump_ksh),
746            resolved.from_wordcode,
747            resolved.body.clone(),
748        );
749        crate::autoload_cache::stage_hit(&name, resolved);
750        hit
751    } else {
752        let mut looked_up = getfpfunc(
753            &name,
754            &mut dir_path,
755            loaddir_spec.as_deref(),
756            0,
757            &mut dump_hit,
758        ); // c:5753 / c:5759
759           // c:5754-5756 — the explicit load directory missed; `-d` (PM_CUR_FPATH,
760           // set by `autoload -d`, c:3383) or an explicit `current_fpath` argument
761           // means "also try $fpath". The Rust port never retried, so
762           // `autoload -dUz $PWD/extra/def; def` and
763           // `def() { autoload -dXUz $PWD/extra; }; def` both reported
764           // "function definition file not found" where zsh loads ./def
765           // (C04funcdef:33,40).
766        if looked_up.is_none()
767            && loaddir_spec.is_some()
768            && (current_fpath != 0 || (fn_flags & crate::ported::zsh_h::PM_CUR_FPATH) != 0)
769        {
770            dir_path = None;
771            dump_hit = None;
772            looked_up = getfpfunc(&name, &mut dir_path, None, 0, &mut dump_hit); // c:5756
773        }
774        let path = match looked_up {
775            Some(p) => p,
776            None => {
777                // !!! WARNING: RUST-ONLY BRANCH — NO DIRECT C COUNTERPART !!!
778                // compsys ships as native Rust functions (src/compsys/router.rs),
779                // so names like `_main_complete` have no definition file in
780                // $fpath. C zsh always loads them from files; zshrs must let
781                // `autoload +X -Uz _main_complete` (e.g. fzf-tab via zinit's
782                // :zinit-tmp-subst-autoload, zinit.zsh:356) succeed without one.
783                // Mark the stub loaded and return success — call-time dispatch
784                // short-circuits to the native fn (vm_helper.rs:2276), so no
785                // funcdef/body is needed.
786                if crate::compsys::router::is_intercepted(&name) {
787                    unsafe {
788                        (*shf).node.flags &= !(PM_UNDEFINED as i32);
789                    }
790                    if let Ok(mut tab) = shfunctab_lock().write() {
791                        if let Some(existing) = tab.get_mut(&name) {
792                            existing.node.flags &= !(PM_UNDEFINED as i32);
793                        }
794                    }
795                    return 0;
796                }
797                // c:Src/exec.c:5713-5719 — file not found path. C:
798                //   `if (prog == &dummy_eprog) {
799                //        locallevel--;
800                //        zwarn("%s: function definition file not found",
801                //              shf->node.nam);
802                //        locallevel++;
803                //        popheap();
804                //        return NULL;
805                //    }`
806                // C's getfpfunc returns &dummy_eprog as the "not found"
807                // sentinel when test_only==0; loadautofn detects it and
808                // emits the diagnostic before returning NULL. Rust's
809                // getfpfunc returns Option::None for the same condition,
810                // so we emit the same diagnostic here. The locallevel
811                // dance is preserved as a comment because the Rust
812                // port's zwarn doesn't reference locallevel in the
813                // format string itself (the dance in C is only to keep
814                // the prefix line counter consistent with the function-
815                // body context). Bug #107 in docs/BUGS.md.
816                crate::ported::utils::zwarn(&format!("{}: function definition file not found", name));
817                return 1; // c:5719 NULL
818            }
819        };
820        let _ = autol;
821        // Previously the Rust port treated this parameter as
822        // "test_only" and early-returned when set, so the `+X`
823        // call from `eval_autoload` (`loadautofn(shf, mode, 1, d)`)
824        // never actually loaded the file. C's parameter is `autol`
825        // (autoload mode), NOT a test-only flag — the C body
826        // unconditionally loads/parses regardless of autol. autol=1
827        // controls the EF_RUN / map-flag dance for the wordcode prog
828        // (c:5725-5749), but the loaded-body / PM_UNDEFINED-clear
829        // path runs in all cases. Removing the early-return so
830        // `autoload -U +X funcname` actually loads the body and
831        // `type funcname` reports `function from /path/file` instead
832        // of `autoload shell function`. Bug #160 in docs/BUGS.md.
833        // c:5100-5140 — read the file. C uses zopen + read + parse_string +
834        // execsave; Rust port stores raw text on the ShFunc and defers
835        // parse-to-Eprog until the first call.
836        //
837        // c:Src/exec.c:6238 / parse.c:3833 — when getfpfunc resolved the
838        // function out of a compiled `.zwc` dump, there is no source file
839        // to read; the wordcode Eprog came back through `dump_hit`. C
840        // executes that wordcode directly (`shf->funcdef = stripkshdef(
841        // prog, ...)`, exec.c:5753-5755). zshrs executes function bodies
842        // through the fusevm bytecode pipeline which consumes source
843        // text, so bridge wordcode → text with the canonical text.c
844        // renderer (`getpermtext`, ported at text.rs:189) — the same
845        // walker `functions NAME` printing uses for wordcode-backed
846        // funcdefs (C hashtable.c:954). The downstream
847        // `autoload_register_source` step (vm_helper.rs:3162) performs
848        // the `stripkshdef` shape decision, matching c:5725-5760.
849        // c:5706-5710 — the ksh-mode precedence chain:
850        //   `if (ksh == 1) { ksh = fksh; if (ksh == 1)
851        //        ksh = PM_KSHSTORED ? 2 : PM_ZSHSTORED ? 0 : 1; }`
852        // The dump header flag (FDHF_KSHLOAD/FDHF_ZSHLOAD via `*ksh`
853        // from try_dump_file) outranks the stub's PM_*STORED bits, which
854        // are only consulted when the dump says 1 (no explicit style).
855        // zshrs's load/register split (vm_helper's
856        // `autoload_register_source` makes the c:5725 ksh-vs-zsh
857        // decision later, from the tab entry's flags + KSHAUTOLOAD) —
858        // fold a decisive dump flag into the PM bits so the downstream
859        // decision sees the same precedence.
860        let dump_ksh = dump_hit.as_ref().map(|(_, k)| *k);
861        // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
862        // C installs the dump's wordcode itself (c:5753-5755) and never lexes it
863        // again; zshrs installs a `getpermtext` deparse whose real compile happens
864        // at the call that defines the function, so the provenance has to be
865        // recorded for that later compile. See vm_helper::autoload_note_wordcode_body.
866        let from_wordcode = dump_hit.is_some();
867        let body = match dump_hit {
868            // The wordcode C executes carries a line number on every pipe
869            // (`WCB_PIPE(type, toklineno + 1)`, c:Src/parse.c:911/935/944, read
870            // back by `lineno = WC_PIPE_LINENO(pcode) - 1` at c:Src/exec.c:2057
871            // and `lineno = code - 1` at c:Src/exec.c:1356), so a dump-loaded
872            // function reports exactly the `$LINENO` of its ORIGINAL source.
873            //
874            // `getpermtext` cannot reproduce that: `gettext2` is a pretty-printer
875            // (`Src/text.c`), so it drops comments and blank lines and re-breaks
876            // compounds onto its own lines — `if X; then` becomes `if X` NEWLINE
877            // `then`. Re-parsing that text yields a DIFFERENT line for every
878            // statement, and `$LINENO` / error prefixes / `funcfiletrace` inside
879            // any `.zwc`-loaded function drift from zsh (measured: 29 vs 32 for
880            // `_parameters` loaded out of a `comp_utils.zwc` digest).
881            //
882            // !!! WARNING: RUST-ONLY BRANCH — NO DIRECT C COUNTERPART !!!
883            // C has no choice to make here: `shf->funcdef = stripkshdef(prog, …)`
884            // (c:Src/exec.c:5753-5755) runs the DUMP's wordcode and never looks at
885            // the source file again. zshrs executes function bodies as TEXT, so it
886            // has to render the wordcode back — and that render is what loses the
887            // line numbers. The source file is preferred ONLY when it still renders
888            // to the same program as the dump; then it is provably the text the
889            // wordcode was compiled from and keeps the original line numbering.
890            //
891            // The previous version skipped that test and took the source file
892            // whenever it existed, on the theory that try_dump_file's mtime gate
893            // (parse.rs, c:Src/parse.c:3762-3784) already proved they agree. It
894            // does not: the gate is `stc.st_mtime >= stn.st_mtime` at SECOND
895            // granularity, so a source rewritten within the same second as the
896            // dump — or back-dated — passes it while holding completely different
897            // code. `zcompile f; print 'print FRESH' > f; touch f; autoload f; f`
898            // ran FRESH where zsh runs the compiled body.
899            Some((prog, _ksh)) => {
900                let dump_text = crate::ported::text::getpermtext(Box::new(prog), None, 0); // c:5753
901                // The equality test below LEXES the source file, and it is asking
902                // whether that file is the text the dump's wordcode was compiled
903                // from. `zcompile` resolved the dump with the lexer state C uses
904                // for a compile, so the comparison has to be made in that same
905                // state or it answers a different question: under RCQUOTES an
906                // adjacent quote pair re-lexes as one literal quote
907                // (c:Src/lex.c:1328) and a live alias rewrites words from inside
908                // the lexer (c:Src/lex.c:1909), so a source that IS the dump's
909                // original compared unequal and the deparse was taken instead —
910                // losing the original line numbering for no reason. Same pin the
911                // `source` leg uses (vm_helper::execute_zwc_program).
912                let _relex = crate::vm_helper::ZwcRelexGuard::enter();
913                // Raw-byte read (c:Src/exec.c:5745 `getfpfunc` → `metafy`):
914                // an autoload file is not required to be UTF-8.
915                match crate::script_bytes::read_script_file(&path) {
916                    // Both sides go through the SAME renderer, so a `.zwc` written
917                    // by C zsh (metafied string pool) still compares equal to a
918                    // locally parsed source.
919                    Ok(t)
920                        if crate::ported::exec::parse_string(&t, 1).is_some_and(|p| {
921                            crate::ported::text::getpermtext(Box::new(p), None, 0) == dump_text
922                        }) =>
923                    {
924                        t
925                    }
926                    _ => dump_text,
927                }
928            }
929            // c:Src/exec.c:5745 `getfpfunc` reads the function file with
930            // `read()` and hands the bytes to `metafy` — there is no encoding
931            // requirement. `read_to_string` rejected the file on the first
932            // non-UTF-8 byte and this arm returned 1, so an $fpath completer
933            // carrying one legacy byte autoloaded to NOTHING, silently.
934            None => match crate::script_bytes::read_script_file(&path) {
935                Ok(t) => t,
936                Err(_) => return 1,
937            },
938        };
939        if let Some(dir) = dir_path.as_deref() {
940            crate::autoload_cache::stage_search(&name, dir, &body, dump_ksh, from_wordcode);
941        }
942        (path, dump_ksh, from_wordcode, body)
943    };
944    // c:Src/exec.c:5735/5757 — `loadautofnsetfile(shf, fdir)`. The
945    // helper stamps PM_LOADDIR alongside the filename when fdir is
946    // present, so `whence -v NAME` later concatenates the directory
947    // with `/NAME` (PM_LOADDIR branch at hashtable.rs:1350). zshrs's
948    // prior `shf->filename = dir_path` assignment skipped the flag
949    // → `type colors` printed `from /path/to/functions` instead of
950    // `from /path/to/functions/colors`. Mirror C exactly.
951    unsafe {
952        loadautofnsetfile(&mut *shf, dir_path.as_deref().or(Some(&path)));
953    }
954    // c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
955    unsafe {
956        (*shf).node.flags &= !(PM_UNDEFINED as i32);
957    }
958    // c:5706-5710 fold (see comment above): decisive dump style wins
959    // over the stub's stored-style bits.
960    let (ksh_on, ksh_off): (i32, i32) = match dump_ksh {
961        Some(2) => (
962            crate::ported::zsh_h::PM_KSHSTORED as i32,
963            crate::ported::zsh_h::PM_ZSHSTORED as i32,
964        ),
965        Some(0) => (
966            crate::ported::zsh_h::PM_ZSHSTORED as i32,
967            crate::ported::zsh_h::PM_KSHSTORED as i32,
968        ),
969        _ => (0, 0),
970    };
971    unsafe {
972        (*shf).node.flags = ((*shf).node.flags | ksh_on) & !ksh_off;
973    }
974    // c:5753-5755 — C's `shf->funcdef = stripkshdef(prog, …)` installs the
975    // dump's own wordcode; zshrs installs TEXT and compiles it later, so mark
976    // (or, on a plain-file load, un-mark) the name so the compile that finally
977    // lexes this text knows it is a deparse and pins the lexer accordingly.
978    crate::vm_helper::autoload_note_wordcode_body(
979        &name,
980        if from_wordcode { Some(&body[..]) } else { None },
981    );
982    // Sync the body string into the Rust-side ShFunc table so the
983    // lazy-parse path can find it later.
984    if let Ok(mut tab) = shfunctab_lock().write() {
985        if let Some(existing) = tab.get_mut(&name) {
986            existing.body = Some(body);
987            existing.node.flags = (existing.node.flags | ksh_on) & !ksh_off;
988            // c:5657 loadautofnsetfile — store the fpath DIRECTORY absolutized
989            // with PM_LOADDIR (not the raw relative `./fns`), so `whence -v`
990            // prints the absolute source. The `+X` (eval-autoload) path relies
991            // on this since it does not re-register through vm_helper.
992            loadautofnsetfile(existing, dir_path.as_deref());
993        } else {
994            let mut shf = shfunc {
995                node: hashnode {
996                    next: None,
997                    nam: name.clone(),
998                    flags: ksh_on, // c:5706-5710 dump-style fold
999                },
1000                filename: None,
1001                lineno: 0,
1002                funcdef: None,
1003                redir: None,
1004                sticky: None,
1005                body: Some(body),
1006                redir_text: None,
1007            };
1008            loadautofnsetfile(&mut shf, dir_path.as_deref()); // c:5657
1009            tab.add(shf);
1010        }
1011    }
1012    0
1013}
1014
1015/// Port of `getfpfunc()` from `Src/exec.c:6219` — C decl `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)`.
1016/// supplied `spec_path` slice) for a file named `name` and writes the
1017/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
1018/// Returns `Some(file_path)` on success, `None` when not found.
1019///
1020/// Per dir, the compiled-dump lookup runs FIRST (c:6238
1021/// `try_dump_file(*pp, s, buf, ksh, test_only)`) — a directory
1022/// digest `<dir>.zwc` or per-function `<dir>/<name>.zwc` wins over
1023/// the plain file when newer (mtime logic inside try_dump_file,
1024/// c:parse.c:3762-3784). On a dump hit the loaded program + ksh
1025/// mode (C's `*ksh` out-param) are written through `dump_out` and
1026/// the nominal `<dir>/<name>` path is returned; the caller must
1027/// check `dump_out` before reading the returned path as a plain
1028/// file.
1029pub fn getfpfunc(
1030    name: &str,
1031    dir_path_out: &mut Option<String>, // c:6219 (Src/exec.c)
1032    spec_path: Option<&[String]>,
1033    test_only: i32,                      // c:6219 `int test_only`
1034    dump_out: &mut Option<(eprog, i32)>, // c:6219 `int *ksh` + dump Eprog return
1035) -> Option<String> {
1036    // C reads $fpath via `getaparam("fpath")` (the param-table array form
1037    // tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
1038    // misses any in-script modification like `fpath=(/some/dir $fpath)`
1039    // because that mutates the internal param table, not the inherited
1040    // process env. Fall back to env only when the param table is empty
1041    // (cold start before any param-table init).
1042    let dirs: Vec<String> = match spec_path {
1043        Some(s) => s.to_vec(),
1044        None => crate::ported::params::getaparam("fpath")
1045            .filter(|v| !v.is_empty())
1046            .or_else(|| getsparam("FPATH").map(|v| v.split(':').map(String::from).collect()))
1047            .or_else(|| {
1048                std::env::var("FPATH")
1049                    .ok()
1050                    .map(|v| v.split(':').map(String::from).collect())
1051            })
1052            .unwrap_or_default(),
1053    };
1054    for dir in &dirs {
1055        if dir.is_empty() {
1056            continue;
1057        }
1058        let path = format!("{}/{}", dir, name); // c:6230 snprintf(buf, ..., "%s/%s", *pp, s)
1059                                                // c:6238 — `if ((r = try_dump_file(*pp, s, buf, ksh, test_only)))`
1060                                                // — the .zwc digest / per-function dump is tried BEFORE the
1061                                                // plain file in each directory.
1062        if let Some(hit) = crate::ported::parse::try_dump_file(dir, name, &path, test_only != 0) {
1063            *dump_out = Some(hit);
1064            *dir_path_out = Some(dir.clone()); // c:6240 `*fdir = *pp;`
1065            return Some(path); // c:6241
1066        }
1067        if std::path::Path::new(&path).exists() {
1068            // c:6245 access(buf, R_OK)
1069            *dir_path_out = Some(dir.clone());
1070            return Some(path);
1071        }
1072    }
1073    None
1074}
1075
1076/// Port of `resolvebuiltin()` from `Src/exec.c:2703`. — C decl `resolvebuiltin(const char *cmdarg, HashNode hn)`.
1077/// the autoloaded-builtin stub firing.
1078///
1079/// C body:
1080/// ```c
1081/// if (!((Builtin) hn)->handlerfunc) {
1082///     char *modname = dupstring(((Builtin) hn)->optstr);
1083///     (void)ensurefeature(modname, "b:",
1084///                         (hn->flags & BINF_AUTOALL) ? NULL : hn->nam);
1085///     hn = builtintab->getnode(builtintab, cmdarg);
1086///     if (!hn) {
1087///         lastval = 1;
1088///         zerr("autoloading module %s failed to define builtin: %s",
1089///              modname, cmdarg);
1090///         return NULL;
1091///     }
1092/// }
1093/// return hn;
1094/// ```
1095///
1096/// zshrs split: the C autoload stub (builtintab node with NULL
1097/// handlerfunc, installed by `add_autobin` c:426) lives in the
1098/// `autoload_builtins` ledger (name → module). This fn is the
1099/// dispatch-time consult:
1100///   - `None` — name has no autoload stub; caller continues its
1101///     normal lookup chain.
1102///   - `Some(0)` — module loaded; caller re-dispatches the (now
1103///     registered) builtin.
1104///   - `Some(1)` — load failed or the loaded module didn't define
1105///     the feature; diagnostics already printed (load_module's
1106///     `failed to load module` zwarn, or the c:2718 zerr here).
1107///     Caller returns status 1.
1108///
1109/// Ledger upkeep: the entry is removed in every fired path —
1110///   - success: C's addbuiltin (module.c:411-415) replaces the stub
1111///     with the real node, so the stub is gone;
1112///   - load failure: C's execbuiltin head (Src/builtin.c:264-267)
1113///     hits the still-NULL handlerfunc and `deletebuiltin`s the
1114///     stub — a second call reports `command not found` / 127
1115///     (probed: zsh 5.9 `zmodload -ab zsh/bogus mybltn; mybltn;
1116///     mybltn` → rc1=1, rc2=127).
1117///
1118/// AUTOALL note: the ledger doesn't carry BINF_AUTOALL, so the
1119/// ensurefeature arg is always `Some(name)` (the non-AUTOALL form).
1120/// The AUTOALL path is unreachable for builtins via `zmodload -a MOD`
1121/// (both shells error "`/' is invalid in a builtin"); revisit if the
1122/// ledger grows flags.
1123pub fn resolvebuiltin(name: &str) -> Option<i32> {
1124    // c:2700
1125    let mut tab = crate::ported::module::MODULESTAB.lock().ok()?;
1126    // c:2705 — `if (!((Builtin) hn)->handlerfunc)`: ledger hit IS the
1127    // "no handlerfunc" stub in zshrs.
1128    let module = tab.autoload_builtins.get(name)?.clone();
1129    // c:2706 — `modname = dupstring(hn->optstr)` (done: `module`).
1130    // Stub fires exactly once per registration (see ledger upkeep
1131    // in the doc above).
1132    tab.autoload_builtins.remove(name);
1133    // c:2711-2713 — ensurefeature(modname, "b:", hn->nam).
1134    let _ = crate::ported::module::ensurefeature(&mut tab, &module, "b:", Some(name));
1135    // c:2714 — `hn = builtintab->getnode(builtintab, cmdarg);`
1136    // zshrs analog: the module booted (is_loaded) AND the name is in
1137    // the static builtintab (createbuiltintable pre-registers every
1138    // module bintab entry).
1139    let defined =
1140        tab.is_loaded(&module) && crate::ported::builtin::createbuiltintable().contains_key(name);
1141    if defined {
1142        return Some(0); // c:2723 `return hn;`
1143    }
1144    if tab.is_loaded(&module) {
1145        // c:2716-2720 — module loaded but feature missing.
1146        crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed); // c:2717 lastval = 1
1147        crate::ported::utils::zerr(&format!(
1148            "autoloading module {} failed to define builtin: {}",
1149            module, name
1150        )); // c:2718
1151    }
1152    // Load failure: load_module already printed `failed to load
1153    // module \`...'`; C's execbuiltin head returns 1 silently
1154    // (Src/builtin.c:264-267).
1155    Some(1)
1156}
1157
1158/// Port of `static struct builtin commandbn` from `Src/exec.c:276`.
1159///
1160/// c:275 — /* structure for command builtin for when it is used with -v or -V */
1161///
1162/// `BUILTIN("command", 0, bin_whence, 0, -1, BIN_COMMAND, "pvV", NULL)`.
1163/// This is a SEPARATE descriptor from the `BIN_PREFIX("command", …)` row in
1164/// `builtintab` (which carries no handler at all): c:3209 swaps `hn` to this
1165/// one when `command -v` / `command -V` is seen, so the dispatch lands in
1166/// `bin_whence` with funcid `BIN_COMMAND`.
1167pub static commandbn: std::sync::LazyLock<crate::ported::zsh_h::builtin> =
1168    std::sync::LazyLock::new(|| {
1169        crate::ported::builtin::BUILTIN(
1170            "command",
1171            0,
1172            Some(crate::ported::builtin::bin_whence),
1173            0,
1174            -1,
1175            crate::ported::hashtable_h::BIN_COMMAND,
1176            Some("pvV"),
1177            None,
1178        ) // c:282
1179    });
1180
1181/// Dispatch decision returned by `execcmd_compile_head` — the
1182/// fusevm-bytecode-time head resolver that mirrors the local-variable
1183/// state the C `execcmd_exec` function carries through `c:2913-2916`
1184/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
1185/// precmd-modifier strip count. The fusevm bytecode compiler reads
1186/// this to emit the correct dispatch opcode in
1187/// `src/extensions/compile_zsh.rs::compile_simple`.
1188///
1189/// Not a C struct — invented to bridge the divergence between the
1190/// C wordcode-walker (which mutates locals + falls through to
1191/// invocation) and zshrs's split parse → compile → VM pipeline.
1192#[allow(non_camel_case_types)]
1193#[derive(Debug, Default, Clone)]
1194pub struct execcmd_dispatch {
1195    /// Number of `BINF_PREFIX` words to strip from the head of args.
1196    /// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
1197    pub precmd_skip: usize,
1198    /// Set when the head (after strip) is a real builtin
1199    /// (`Src/exec.c:3065 is_builtin = 1`).
1200    pub is_builtin: bool,
1201    /// Set when the head (after strip) is a shell function
1202    /// (`Src/exec.c:3053 is_shfunc = 1`).
1203    pub is_shfunc: bool,
1204    /// `cflags` accumulator from `Src/exec.c:2915` — gathers
1205    /// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
1206    /// BINF_NOGLOB` bits encountered during the precommand-modifier
1207    /// walk (c:3062 `cflags |= hn->flags`).
1208    pub cflags: u32,
1209    /// `command -p` requested: use the default `$PATH` for lookup
1210    /// (`Src/exec.c:3160 use_defpath = 1`). NOT YET HONORED by the
1211    /// fusevm compiler — flagged for follow-up.
1212    pub use_defpath: bool,
1213    /// `command -v` / `command -V` requested: the dispatch target
1214    /// flips to `bin_whence` per `Src/exec.c:3149-3157`
1215    /// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
1216    /// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
1217    /// instead of resolving the post-strip head.
1218    pub has_command_vv: bool,
1219    /// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
1220    /// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
1221    pub exec_argv0: Option<String>,
1222    /// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
1223    /// — the `else` arm of `if (redir && nonempty(redir))`). Covers
1224    /// bare `exec` / `noglob` / `command`. Caller emits
1225    /// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
1226    /// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
1227    /// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
1228    /// returns 0 without complaining about missing redirs).
1229    pub is_empty_command: bool,
1230    /// `preargs` as the walk left it (the c:3086 / c:3165 / c:3177 /
1231    /// c:3209 / c:3239 `uremnode`s): the command words are
1232    /// `preargs[precmd_skip..]`, with `command -p` / `--` and the `exec`
1233    /// option words already removed.
1234    pub preargs: Vec<String>,
1235}
1236
1237/// !!! WARNING: RUST-ONLY HELPER !!!
1238///
1239/// No function of this name exists in `Src/exec.c`. This helper stands in for
1240/// the head section of `execcmd_exec` (c:2901; locals + the BINF_PREFIX
1241/// precommand-modifier walk at c:2966-3096), called at fusevm
1242/// bytecode-compile time. It does NOT perform dispatch the way the C function
1243/// does.
1244/// !!! NOT A PORT OF C `execcmd_exec` !!!
1245///
1246/// This is a fusevm-bytecode-time head resolver invoked by
1247/// `src/extensions/compile_zsh.rs::compile_simple` and the
1248/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
1249/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
1250/// file under the C-faithful name `execcmd_exec`.
1251///
1252/// This helper mirrors the head section (`c:2904-3275`) of the C
1253/// function — local initialisation, the precommand-modifier walk
1254/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
1255/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
1256/// sub-option parsers — and returns the resulting dispatch
1257/// decision via `execcmd_dispatch`. The fusevm compiler reads
1258/// that struct to decide which `Op::CallBuiltin` /
1259/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
1260/// correct post-strip `argc`.
1261///
1262/// =================== WARNING — DIVERGENCE ====================
1263///
1264/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
1265/// `multio` redirections, evaluates `varspc` assignments, then calls
1266/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
1267/// stops after the precmd-modifier walk and only returns the head
1268/// decision; runtime dispatch is driven by the bytecode the fusevm
1269/// compiler emits.
1270///
1271/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
1272/// wordcode iterator state — zshrs doesn't traverse wordcode here,
1273/// so the args list arrives already-expanded as a `&[String]`
1274/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
1275/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
1276///
1277/// =============================================================
1278pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
1279    // c:2900 (Src/exec.c)
1280
1281    // c:2904-2916 — locals.
1282    let mut hn: Option<&'static builtin> = None; // c:2904
1283    let mut is_shfunc = false; // c:2913
1284    let mut is_builtin = false; // c:2913
1285    let mut use_defpath = false; // c:2913
1286    let mut cflags: u32 = 0; // c:2915
1287    let mut orig_cflags: u32 = 0; // c:2915
1288    let _ = orig_cflags;
1289    // c:3263 — `char *exec_argv0 = NULL;` (declared inside the
1290    // BINF_EXEC arm; hoisted here so the dispatch struct can carry it
1291    // out after the loop terminates).
1292    let mut exec_argv0: Option<String> = None;
1293    // c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
1294    // (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
1295    // compiler can emit `bin_whence` instead of resolving the head.
1296    let mut has_command_vv = false;
1297
1298    // c:2962-2973 — `%job` head: rewrite `%name` → `fg|bg|disown %name`.
1299    // Not in scope for the compile-time dispatch walk: jobspec
1300    // expansion happens at runtime in fusevm; the bytecode emits a
1301    // direct `fg`/`bg` call when it sees a leading `%`. Flagged for
1302    // follow-up when the canonical port lands.
1303
1304    // c:2975-2986 — AUTORESUME prefix-match against jobtab. Same
1305    // status as the %job head: runtime concern, deferred.
1306
1307    // c:3013-3091 — precommand-modifier walk.
1308    let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
1309    let mut precmd_skip: usize = 0;
1310
1311    // c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
1312    if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
1313        // c:3018
1314        // c:3029 — `while (nonempty(preargs))`.
1315        while precmd_skip < preargs.len() {
1316            // c:3029
1317            // c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
1318            let cmdarg = untokenize(&preargs[precmd_skip]);
1319            // c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
1320            // already performed prefork expansion on `preargs`, so
1321            // `has_token` is effectively false here; the C `break` on
1322            // unexpanded tokens is unreachable in this entry point.
1323
1324            // c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
1325            // even disabled builtins so the reserved-word form
1326            // (`integer x`, `local foo`) still dispatches to the
1327            // typeset family. The static `BUILTINS` array doesn't
1328            // expose a separate disabled-bit lookup; one path covers
1329            // both. Effect is identical for the precmd-modifier walk.
1330
1331            // c:3098-3103 — `} else if (isset(POSIXBUILTINS) && (cflags &
1332            // BINF_EXEC)) { /* POSIX doesn't allow "exec" to operate on
1333            // builtins or shell functions. */ break; }`
1334            let typeset_hit = type_ == WC_TYPESET && BUILTINS.iter().any(|b| b.node.nam == cmdarg); // c:3088-3089
1335            if !typeset_hit && isset(POSIXBUILTINS) && (cflags & BINF_EXEC) != 0 {
1336                break; // c:3103
1337            }
1338
1339            // c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
1340            // BINF_COMMAND)) && shfunctab->getnode(...))` — shell
1341            // function takes precedence unless a `builtin`/`command`
1342            // modifier preceded it.
1343            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
1344                // c:3106 — `(hn = shfunctab->getnode(shfunctab, cmdarg))`.
1345                // `shfunctab->getnode` is `gethashnode`
1346                // (Src/hashtable.c:821 in `createshfunctable`), i.e. an
1347                // O(1) hash probe that also skips DISABLED nodes. The
1348                // previous Rust code walked the whole table with
1349                // `iter().any(...)`, which is O(#functions) per command
1350                // head AND ignored the DISABLED bit `gethashnode`
1351                // honours. With a real `.zcompdump` loaded the table
1352                // holds ~50k autoload stubs, so every compiled command
1353                // head paid a 50k-entry String scan under the read lock.
1354                if shfunctab_lock()
1355                    .read()
1356                    .map(|t| !t.getnode(&cmdarg).is_null())
1357                    .unwrap_or(false)
1358                {
1359                    is_shfunc = true; // c:3107
1360                    break; // c:3108
1361                }
1362            }
1363            // c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
1364            let entry = BUILTINS.iter().find(|b| b.node.nam == cmdarg);
1365            let Some(entry) = entry else {
1366                // c:3056-3058
1367                break;
1368            };
1369            hn = Some(entry);
1370            // c:3061-3063 — accumulate cflags.
1371            orig_cflags |= cflags;
1372            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
1373            cflags |= entry.node.flags as u32;
1374            // c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
1375            // builtin, stop.
1376            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
1377                // c:3064
1378                // WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
1379                // to autoload the builtin's module if its
1380                // `handlerfunc` is NULL. In zshrs, builtins live in
1381                // two places: the static `BUILTINS` table (which
1382                // mirrors C `handlerfunc`, often `None` for ports
1383                // dispatched through fusevm) AND fusevm's
1384                // `register_builtins` map (the actual runtime
1385                // dispatcher). A null `handlerfunc` in the static
1386                // table is NOT an autoload failure for us — it
1387                // means dispatch routes through fusevm. So we
1388                // skip the resolvebuiltin call here; the faithful
1389                // port remains available for future callers that
1390                // genuinely need module-autoload semantics.
1391                is_builtin = true; // c:3065
1392                break; // c:3077
1393            }
1394            // c:3086 — `uremnode(preargs, firstnode(preargs))`.
1395            precmd_skip += 1;
1396            // c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
1397            //   (...); if (!firstnode(preargs)) break; }`. zshrs has
1398            // no `execcmd_getargs` (args arrive pre-expanded); the
1399            // bounds-check at the top of `while precmd_skip <
1400            // preargs.len()` handles the empty case identically.
1401
1402            // c:3092-3177 — BINF_COMMAND sub-option parsing
1403            // (`command -p / -v / -V`).
1404            if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
1405                // c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
1406                //                int has_p = 0, has_vV = 0, has_other = 0;`
1407                let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
1408                let mut pnode: Option<usize> = None; // c:3102
1409                let mut has_p = false; // c:3104
1410                let mut has_vv = false; // c:3104
1411                let mut has_other = false; // c:3104
1412                                           // c:3107 — `while (IS_DASH(*argdata))`
1413                while argnode < preargs.len()
1414                    && IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
1415                {
1416                    let argdata = preargs[argnode].clone(); // c:3106
1417                    let bytes = argdata.as_bytes();
1418                    // c:3108-3111 — stop on bare `-` or `--`.
1419                    if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
1420                        // c:3109
1421                        break; // c:3111
1422                    }
1423                    // c:3112-3133 — scan flag chars.
1424                    for &c in &bytes[1..] {
1425                        // c:3112
1426                        match c as char {
1427                            'p' => {
1428                                // c:3114
1429                                has_p = true; // c:3122
1430                                pnode = Some(argnode); // c:3123
1431                            }
1432                            'v' | 'V' => {
1433                                // c:3125-3126
1434                                has_vv = true; // c:3127
1435                            }
1436                            _ => {
1437                                // c:3129
1438                                has_other = true; // c:3130
1439                            }
1440                        }
1441                    }
1442                    // c:3134-3138 — unknown flag → don't try, leave alone.
1443                    if has_other {
1444                        // c:3134
1445                        has_p = false; // c:3136
1446                        has_vv = false; // c:3136
1447                        break; // c:3137
1448                    }
1449                    // c:3140-3147 — advance to next arg.
1450                    argnode += 1; // c:3141 nextnode(argnode)
1451                    if argnode >= preargs.len() {
1452                        // c:3142 — execcmd_getargs (skipped: pre-expanded)
1453                        break; // c:3145
1454                    }
1455                }
1456                // c:3149-3157 — `-v`/`-V` → dispatch to whence.
1457                if has_vv {
1458                    // c:3149
1459                    // c:3154 `pushnode(preargs, "command")` — C re-inserts
1460                    // "command" so bin_whence sees it as argv[0]. zshrs
1461                    // surfaces this via `has_command_vv`; the fusevm
1462                    // compiler emits the equivalent whence call.
1463                    has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
1464                    is_builtin = true;
1465                    break; // c:3157
1466                } else if has_p {
1467                    // c:3158
1468                    use_defpath = true; // c:3160
1469                    if let Some(pn) = pnode {
1470                        // c:3165 — `uremnode(preargs, pnode)`. zshrs:
1471                        // remove the `-p`-bearing arg from preargs.
1472                        if pn < preargs.len() {
1473                            preargs.remove(pn);
1474                            // precmd_skip already accounts for the
1475                            // stripped `command` prefix; we just removed
1476                            // the `-p` flag which sat at preargs[pn].
1477                            // No precmd_skip change needed — the head
1478                            // remains where it was.
1479                        }
1480                    }
1481                }
1482                // c:3176-3177 — `--` trailing end-of-options strip.
1483                if argnode < preargs.len() {
1484                    let argdata = &preargs[argnode];
1485                    let b = argdata.as_bytes();
1486                    if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
1487                        // c:3176
1488                        preargs.remove(argnode); // c:3177
1489                    }
1490                }
1491            } else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
1492                // c:3178-3275 — BINF_EXEC sub-option parsing
1493                // (`exec -a NAME -l -c`).
1494                let mut argnode: usize = precmd_skip; // c:3185
1495                let mut error_done = false;
1496                // c:3196 — `while (argdata && IS_DASH(*argdata) &&
1497                //                  strlen(argdata) >= 2)`
1498                while argnode < preargs.len() {
1499                    let argdata = preargs[argnode].clone();
1500                    let bytes = argdata.as_bytes();
1501                    if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
1502                        break; // c:3196 loop guard
1503                    }
1504                    let oldnode = argnode; // c:3197
1505                    argnode += 1; // c:3198 nextnode(oldnode)
1506                                  // c:3203-3208 — empty next → error.
1507                    if argnode >= preargs.len() {
1508                        // c:3203
1509                        zerr(
1510                            // c:3204
1511                            "exec requires a command to execute",
1512                        );
1513                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
1514                        error_done = true;
1515                        break; // c:3207 goto done
1516                    }
1517                    // c:3209 — `uremnode(preargs, oldnode)`.
1518                    preargs.remove(oldnode);
1519                    argnode -= 1; // re-anchor — `argnode` was the post-removed slot
1520                                  // c:3210-3211 — `--` stops option scan.
1521                    if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
1522                        // c:3210
1523                        break; // c:3211
1524                    }
1525                    // c:3212-3258 — scan flag chars after the leading `-`.
1526                    let mut k = 1usize;
1527                    while k < bytes.len() && !error_done {
1528                        let cmdopt = bytes[k] as char; // c:3212
1529                        match cmdopt {
1530                            'a' => {
1531                                // c:3214 — `-a` ARGV0 override.
1532                                if k + 1 < bytes.len() {
1533                                    // c:3216 — `-aNAME` inline form.
1534                                    exec_argv0 =
1535                                        Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
1536                                    k = bytes.len(); // c:3219 position past end
1537                                } else {
1538                                    // c:3220 — `-a NAME` separate form.
1539                                    if argnode >= preargs.len() {
1540                                        // c:3274-3279 — `if (!argnode)`.
1541                                        zerr("exec requires a command to execute");
1542                                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
1543                                        error_done = true;
1544                                        break;
1545                                    }
1546                                    // c:3280-3283 — `if (!nextnode(argnode))`:
1547                                    // the NAME must still leave a command.
1548                                    if argnode + 1 >= preargs.len() {
1549                                        // c:3230
1550                                        zerr(
1551                                            // c:3231
1552                                            "exec flag -a requires a parameter",
1553                                        );
1554                                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
1555                                        error_done = true;
1556                                        break; // c:3234 goto done
1557                                    }
1558                                    exec_argv0 = Some(preargs[argnode].clone()); // c:3236
1559                                    preargs.remove(argnode); // c:3239
1560                                }
1561                            }
1562                            'c' => {
1563                                // c:3242
1564                                cflags |= BINF_CLEARENV; // c:3243
1565                            }
1566                            'l' => {
1567                                // c:3245
1568                                cflags |= BINF_DASH; // c:3246
1569                            }
1570                            _ => {
1571                                // c:3248
1572                                zerr(
1573                                    // c:3249
1574                                    &format!("unknown exec flag -{}", cmdopt),
1575                                );
1576                                errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
1577                                error_done = true;
1578                                break; // c:3256
1579                            }
1580                        }
1581                        k += 1;
1582                    }
1583                    if error_done {
1584                        break;
1585                    }
1586                }
1587                // c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
1588                // the actual `setenv` to the fusevm compiler / external
1589                // exec path; we surface `exec_argv0` via the dispatch
1590                // struct so the caller can apply it before fork+exec.
1591                if let Some(ref a0) = exec_argv0 {
1592                    // c:3263 — `remnulargs + untokenize` then setenv.
1593                    let cleaned = untokenize(a0); // c:3266-3267
1594                    exec_argv0 = Some(cleaned);
1595                }
1596                if error_done {
1597                    return execcmd_dispatch {
1598                        precmd_skip,
1599                        is_builtin,
1600                        is_shfunc,
1601                        cflags,
1602                        use_defpath,
1603                        has_command_vv,
1604                        exec_argv0,
1605                        is_empty_command: false,
1606                        preargs,
1607                    };
1608                }
1609            }
1610            // c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
1611            // unset(POSIXBUILTINS)) break;`. After processing a
1612            // `command` precmd modifier (and its -p/-v/-V flags), the
1613            // C loop exits with hn cleared so the dispatch falls
1614            // through to external lookup. Without this, the next
1615            // iteration would find `command print` → print's builtin
1616            // and dispatch to it; zsh's intentional behaviour is to
1617            // skip builtins under `command` (unless POSIXBUILTINS is
1618            // set, where the loop continues normally).
1619            if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
1620                hn = None; // c:3275 hn = NULL
1621                break; // c:3277
1622            }
1623        }
1624    }
1625
1626    // c:3309-3406 — "Empty command" branch. When the precmd-modifier
1627    // walk above strips every word with nothing left to dispatch
1628    // (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
1629    // C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
1630    //
1631    // - redir-present + do_exec       → nullexec=1 (continue to run)
1632    // - redir-present + varspc        → nullexec=2 (continue)
1633    // - redir-present + no nullcmd    → `zerr("redirection with no command")`
1634    //                                   lastval=1, return
1635    // - redir-present + SHNULLCMD     → args=[":"]
1636    // - redir-present + readnullcmd   → args=[readnullcmd]
1637    // - redir-present + default       → args=[nullcmd]
1638    // - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
1639    // - NO redir + default            → lastval=cmdoutval, return (c:3372-3406)
1640    //
1641    // zshrs's `execcmd_compile_head` doesn't receive `redir` (it
1642    // takes `args` only). The cases that DEPEND on redirs are handled by
1643    // `compile_zsh.rs::compile_redir` before this dispatch fires; the
1644    // remaining cases collapse into the single `is_empty_command`
1645    // flag below. Both NO-redir sub-cases produce the same observable
1646    // outcome (lastval=0, return without invoking anything), so a
1647    // single flag suffices.
1648    let is_empty_command = precmd_skip >= preargs.len();
1649
1650    // =================== WARNING — DIVERGENCE ====================
1651    // c:3285+: prefork-substitution, magic_assign decision, multio
1652    // setup, varspc evaluation, and the actual execbuiltin /
1653    // runshfunc / execute call. ~1300 lines of interpreter-only
1654    // code, entirely replaced by fusevm bytecode dispatch in
1655    // `src/extensions/compile_zsh.rs::compile_simple` and the
1656    // opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
1657    // The return value below feeds those compile-time decisions.
1658    // =============================================================
1659
1660    let _ = hn;
1661    execcmd_dispatch {
1662        precmd_skip,
1663        is_builtin,
1664        is_shfunc,
1665        cflags,
1666        use_defpath,
1667        has_command_vv,
1668        exec_argv0,
1669        is_empty_command,
1670        preargs,
1671    }
1672}
1673
1674// =============================================================================
1675// Leaf-function ports — c:283 (parse_string) and below. Added incrementally to
1676// chip at the ~5500 lines of exec.c still un-ported beyond the wordcode
1677// walker (execlist / execpline / execcmd which the fusevm bytecode VM
1678// replaces — see the WARNING block in execcmd_exec).
1679// =============================================================================
1680
1681/// Port of `parse_string()` from `Src/exec.c:283` — C decl `parse_string(char *s, int reset_lineno)`.
1682///
1683/// C body:
1684/// ```c
1685/// Eprog p; zlong oldlineno;
1686/// zcontext_save();
1687/// inpush(s, INP_LINENO, NULL);
1688/// strinbeg(0);
1689/// oldlineno = lineno;
1690/// if (reset_lineno) lineno = 1;
1691/// p = parse_list();
1692/// lineno = oldlineno;
1693/// if (tok == LEXERR && !lastval) lastval = 1;
1694/// strinend();
1695/// inpop();
1696/// zcontext_restore();
1697/// return p;
1698/// ```
1699///
1700/// Parses an arbitrary string as a zsh command list, returning the
1701/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
1702/// `bin_eval` for `eval`, and the autoload path.
1703pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
1704    use crate::ported::lex::{LEX_FILE_WINDOW_STRIN, LEX_INPUT, LEX_POS, LEX_UNGET_BUF};
1705
1706    // c:285-286
1707    let p: Option<eprog>;
1708    let oldlineno: i64;
1709
1710    zcontext_save(); // c:288
1711
1712    // zshrs-only: park the `LEX_INPUT` window. C has ONE reader — the
1713    // input stack — so `inpush` at c:289 is all the isolation `parse_list`
1714    // needs. zshrs lexes from a second source as well, the `LEX_INPUT` /
1715    // `LEX_POS` char window (lex.rs:5181), which `inpush` does not touch,
1716    // so a nested parse ran on whatever text the OUTER lexer still had
1717    // parked there. That was invisible while every caller's window was
1718    // already drained at execution time, and became live when `bin_dot`
1719    // started running a plain sourced file per command
1720    // (`execute_script_per_command`, vm_helper.rs:3321): the file is now
1721    // the lexer's own window WHILE its commands execute, so
1722    // `zstyle -e ':x' lc 'reply=(1)'` (zutil.rs:856 → here) parsed the
1723    // REST OF THE FILE as its eval body and everything after that line
1724    // silently vanished. Draining the window makes `hgetc`'s two-input
1725    // bridge read only the `inpush`ed frame — the same parking
1726    // `parsestrnoerr` (lex.rs:3402-3444) and `parse_isolated`
1727    // (vm_helper.rs:843-877) already do around their own nested parses.
1728    let saved_input = LEX_INPUT.with_borrow(|w| w.clone());
1729    let saved_pos = LEX_POS.get();
1730    let saved_unget = LEX_UNGET_BUF.with_borrow(|b| b.clone());
1731    let saved_file_window = LEX_FILE_WINDOW_STRIN.get();
1732    let saved_in_lexstop = crate::ported::input::lexstop.with(|c| c.get());
1733    LEX_INPUT.with_borrow_mut(|w| w.clear());
1734    LEX_POS.set(0);
1735    LEX_UNGET_BUF.with_borrow_mut(|b| b.clear());
1736    // The pushed frame is a STRING unit, whatever the outer window was.
1737    LEX_FILE_WINDOW_STRIN.set(0);
1738
1739    inpush(s, INP_LINENO, None); // c:289
1740    strinbeg(0); // c:290
1741    oldlineno = LEX_LINENO.get() as i64; // c:291
1742    if reset_lineno != 0 {
1743        // c:292
1744        LEX_LINENO.set(1); // c:293
1745    }
1746    p = parse_list(); // c:294
1747    LEX_LINENO.set(oldlineno as u64); // c:295
1748                                      // c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
1749    if tok() == LEXERR && LASTVAL.load(Ordering::Relaxed) == 0 {
1750        LASTVAL.store(1, Ordering::Relaxed);
1751    }
1752    strinend(); // c:298
1753    inpop(); // c:299
1754
1755    // Put the outer window back where the nested parse found it. Draining
1756    // it set the input-side `lexstop`, whose lex.rs half is the only one
1757    // `zcontext` covers (parse_isolated, vm_helper.rs:857), so the outer
1758    // reader would otherwise resume at EOF.
1759    LEX_INPUT.with_borrow_mut(|w| *w = saved_input);
1760    LEX_POS.set(saved_pos);
1761    LEX_UNGET_BUF.with_borrow_mut(|b| *b = saved_unget);
1762    LEX_FILE_WINDOW_STRIN.set(saved_file_window);
1763    crate::ported::input::lexstop.with(|c| c.set(saved_in_lexstop));
1764
1765    zcontext_restore(); // c:300
1766    p // c:301
1767}
1768
1769/// Port of `isgooderr()` from `Src/exec.c:652` — C decl `isgooderr(int e, char *dir)`.
1770///
1771/// C body:
1772/// ```c
1773/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
1774/// return ((e != EACCES || !access(dir, X_OK)) &&
1775///         e != ENOENT && e != ENOTDIR);
1776/// ```
1777///
1778/// errno classifier for `execve` failures during PATH search: if the
1779/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
1780/// it's "expected" (try next PATH entry); otherwise it's a real
1781/// failure worth surfacing.
1782pub fn isgooderr(e: i32, dir: &str) -> bool {
1783    // c:652
1784    // c:Src/exec.c:658-659 — `(e != EACCES || !access(dir, X_OK)) &&
1785    //   e != ENOENT && e != ENOTDIR`. C's `access(dir, X_OK)` returns
1786    //   0 on success / -1 on failure. The previous Rust port used
1787    //   `metadata().permissions().mode() & 0o111` which reports the
1788    //   X bit even when the path doesn't exist as the EFFECTIVE caller
1789    //   (root, ACLs, capabilities all flip access() vs raw mode).
1790    //   `/no/such/dir` metadata() fails → returned false for
1791    //   dir_x_ok, then `!false` = true, giving "good error" for a
1792    //   nonexistent path. Use libc::access directly to match C exactly.
1793    let unmeta_dir = unmeta(dir);
1794    let cstr = std::ffi::CString::new(unmeta_dir.as_bytes()).unwrap_or_default();
1795    let access_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1796    (e != libc::EACCES || access_ok) && e != libc::ENOENT && e != libc::ENOTDIR
1797}
1798
1799/// Port of `iscom()` from `Src/exec.c:962` — C decl `iscom(char *s)`.
1800///
1801/// C body:
1802/// ```c
1803/// struct stat statbuf;
1804/// char *us = unmeta(s);
1805/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
1806///         S_ISREG(statbuf.st_mode));
1807/// ```
1808///
1809/// True iff `s` names an executable regular file (X-perm + S_IFREG).
1810/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
1811/// validate candidate paths before exec.
1812pub fn iscom(s: &str) -> bool {
1813    // c:962
1814    let us = unmeta(s); // c:965
1815                        // c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
1816    let cstr = match std::ffi::CString::new(us.as_str()) {
1817        Ok(c) => c,
1818        Err(_) => return false,
1819    };
1820    let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1821    if !x_ok {
1822        return false;
1823    }
1824    let meta = match std::fs::metadata(&us) {
1825        Ok(m) => m,
1826        Err(_) => return false,
1827    };
1828    meta.file_type().is_file()
1829}
1830
1831/// Port of `isreallycom()` from `Src/exec.c:973` — C decl `isreallycom(Cmdnam cn)`.
1832///
1833/// Verify that a hashed/cached cmdnamtab entry still names a real
1834/// external command (X-perm + regular file). For HASHED entries
1835/// (`cn->u.cmd` carries the absolute path), test the path directly;
1836/// otherwise concatenate `name[0] + "/" + nam` and test that.
1837/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
1838/// turn into a failed `execve` syscall.
1839pub fn isreallycom(cn: &cmdnam) -> bool {
1840    // c:972
1841    let fullnam: String;
1842    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
1843        // c:977-978 — `strcpy(fullnam, cn->u.cmd);`
1844        fullnam = cn.cmd.clone().unwrap_or_default();
1845    } else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
1846        // c:979-980 — `if (!cn->u.name) return 0;`
1847        return false;
1848    } else {
1849        // c:982-984 — `strcpy + strcat("/") + strcat(nam)`
1850        let path0 = &cn.name.as_ref().unwrap()[0];
1851        fullnam = format!("{}/{}", path0, cn.node.nam);
1852    }
1853    iscom(&fullnam) // c:986
1854}
1855
1856/// Port of `isrelative()` from `Src/exec.c:996` — C decl `isrelative(char *s)`.
1857///
1858/// C body:
1859/// ```c
1860/// if (*s != '/') return 1;
1861/// for (; *s; s++)
1862///     if (*s == '.' && s[-1] == '/' &&
1863///         (s[1] == '/' || s[1] == '\0' ||
1864///          (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
1865///         return 1;
1866/// return 0;
1867/// ```
1868///
1869/// True iff `s` either doesn't start with `/` OR contains a `./` or
1870/// `../` component anywhere. Used by `cd` resolution and PATH-cache
1871/// invalidation to detect non-canonical paths.
1872pub fn isrelative(s: &str) -> i32 {
1873    // c:996
1874    let bytes = s.as_bytes();
1875    if bytes.is_empty() || bytes[0] != b'/' {
1876        // c:998
1877        return 1; // c:999
1878    }
1879    // c:1000-1004 — walk for `./` or `../` components.
1880    for i in 1..bytes.len() {
1881        let c = bytes[i];
1882        let prev = bytes[i - 1];
1883        if c == b'.' && prev == b'/' {
1884            let next = bytes.get(i + 1).copied().unwrap_or(0);
1885            if next == b'/' || next == 0 {
1886                // c:1002
1887                return 1;
1888            }
1889            if next == b'.' {
1890                let next2 = bytes.get(i + 2).copied().unwrap_or(0);
1891                if next2 == b'/' || next2 == 0 {
1892                    // c:1003
1893                    return 1;
1894                }
1895            }
1896        }
1897    }
1898    0 // c:1005
1899}
1900
1901/// Port of `setunderscore()` from `Src/exec.c:2652` — C decl `setunderscore(char *str)`.
1902///
1903/// C body:
1904/// ```c
1905/// queue_signals();
1906/// if (str && *str) {
1907///     size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
1908///     if (nl > underscorelen || (underscorelen - nl) > 64) {
1909///         zfree(zunderscore, underscorelen);
1910///         zunderscore = (char *) zalloc(underscorelen = nl);
1911///     }
1912///     strcpy(zunderscore, str);
1913///     underscoreused = l;
1914/// } else {
1915///     ... reset zunderscore = "" ...
1916/// }
1917/// unqueue_signals();
1918/// ```
1919///
1920/// Sets the `$_` global to the last argument of the most recent
1921/// command. Called from `execcmd_exec` (c:3936) per `last_status`
1922/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
1923pub fn setunderscore(str: &str) {
1924    // c:2652
1925    queue_signals(); // c:2654
1926    if !str.is_empty() {
1927        // c:2655 `if (str && *str)`
1928        // c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
1929        let mut zu = zunderscore.lock().unwrap();
1930        *zu = str.to_string();
1931        let nl = (str.len() + 1 + 31) & !31; // c:2656
1932        underscorelen.store(nl, Ordering::Relaxed); // c:2660
1933        underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
1934    // c:2663
1935    } else {
1936        // c:2664
1937        let mut zu = zunderscore.lock().unwrap();
1938        zu.clear(); // c:2669 `*zunderscore = '\0';`
1939        underscoreused.store(1, Ordering::Relaxed); // c:2670
1940    }
1941    unqueue_signals(); // c:2672
1942}
1943
1944/// Port of `mpipe()` from `Src/exec.c:5160` — C decl `mpipe(int *pp)`.
1945///
1946/// C body:
1947/// ```c
1948/// if (pipe(pp) < 0) {
1949///     zerr("pipe failed: %e", errno);
1950///     return -1;
1951/// }
1952/// pp[0] = movefd(pp[0]);
1953/// pp[1] = movefd(pp[1]);
1954/// return 0;
1955/// ```
1956///
1957/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
1958/// fd range via `movefd`. Used by `getpipe` / `getproc` /
1959/// `spawnpipes` for process substitution and pipeline wiring.
1960pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
1961    // c:5160
1962    let mut fds: [libc::c_int; 2] = [-1; 2];
1963    if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
1964        // c:5162
1965        zerr(&format!(
1966            // c:5163
1967            "pipe failed: {}",
1968            std::io::Error::last_os_error()
1969        ));
1970        return -1; // c:5164
1971    }
1972    pp[0] = movefd(fds[0]); // c:5166
1973    pp[1] = movefd(fds[1]); // c:5167
1974    0 // c:5168
1975}
1976
1977/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
1978/// from `Src/exec.c:5289`. Anonymous-function name marker used by
1979/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
1980/// `() { ... }` anonymous function dispatch.
1981pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
1982
1983/// Port of `is_anonymous_function_name()` from `Src/exec.c:5300` — C decl `is_anonymous_function_name(const char *name)`.
1984///
1985/// C body:
1986/// ```c
1987/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
1988/// ```
1989///
1990/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
1991/// reporting and `whence -v` to skip / annotate anonymous functions.
1992pub fn is_anonymous_function_name(name: &str) -> i32 {
1993    // c:5300
1994    if name == ANONYMOUS_FUNCTION_NAME {
1995        // c:5302
1996        1
1997    } else {
1998        0
1999    }
2000}
2001
2002/// Port of `execsave()` from `Src/exec.c:6438` — C decl `execsave(void)`.
2003///
2004/// C body:
2005/// ```c
2006/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
2007/// es->list_pipe_pid = list_pipe_pid;
2008/// es->nowait = nowait;
2009/// es->pline_level = pline_level;
2010/// es->list_pipe_child = list_pipe_child;
2011/// es->list_pipe_job = list_pipe_job;
2012/// strcpy(es->list_pipe_text, list_pipe_text);
2013/// es->lastval = lastval;
2014/// es->noeval = noeval;
2015/// es->badcshglob = badcshglob;
2016/// es->cmdoutpid = cmdoutpid;
2017/// es->cmdoutval = cmdoutval;
2018/// es->use_cmdoutval = use_cmdoutval;
2019/// es->procsubstpid = procsubstpid;
2020/// es->trap_return = trap_return;
2021/// es->trap_state = trap_state;
2022/// es->trapisfunc = trapisfunc;
2023/// es->traplocallevel = traplocallevel;
2024/// es->noerrs = noerrs;
2025/// es->this_noerrexit = this_noerrexit;
2026/// es->underscore = ztrdup(zunderscore);
2027/// es->next = exstack;
2028/// exstack = es;
2029/// noerrs = cmdoutpid = 0;
2030/// ```
2031///
2032/// Snapshot every transient exec-context global onto the `exstack`
2033/// linked list so a signal-handler / trap-firing nested eval can
2034/// scribble freely; `execrestore` pops the frame back. Called by
2035/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
2036pub fn execsave() {
2037    // c:6438
2038    // c:6442 — `es = zalloc(sizeof(execstack));`
2039    let mut es = Box::new(execstack {
2040        // c:6442
2041        next: None,
2042        list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
2043        nowait: nowait.load(Ordering::Relaxed),               // c:6444
2044        pline_level: pline_level.load(Ordering::Relaxed),     // c:6445
2045        list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
2046        list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
2047        list_pipe_text: {
2048            // c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
2049            let mut buf = [0u8; JOBTEXTSIZE];
2050            if let Ok(s) = LIST_PIPE_TEXT.lock() {
2051                let bytes = s.as_bytes();
2052                let n = bytes.len().min(JOBTEXTSIZE - 1);
2053                buf[..n].copy_from_slice(&bytes[..n]);
2054            }
2055            buf
2056        },
2057        lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
2058        // c:6450 — `es->noeval = noeval;`. Snapshot math.c's
2059        // `int noeval` (the parse-only side-effect-skip counter)
2060        // via math.rs's pub accessor.
2061        noeval: crate::ported::math::m_noeval(),
2062        // c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
2063        // csh-glob diagnostic counter (glob.c:103 / glob.rs
2064        // BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
2065        // the outer command's per-line accounting.
2066        badcshglob: crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed), // c:6451
2067        cmdoutpid: cmdoutpid.load(Ordering::Relaxed),                        // c:6452
2068        cmdoutval: cmdoutval.load(Ordering::Relaxed),                        // c:6453
2069        use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed),                // c:6454
2070        procsubstpid: procsubstpid.load(Ordering::Relaxed),                  // c:6455
2071        trap_return: TRAP_RETURN.load(Ordering::Relaxed),                    // c:6456
2072        trap_state: TRAP_STATE.load(Ordering::Relaxed),                      // c:6457
2073        trapisfunc: trapisfunc.load(Ordering::Relaxed),                      // c:6458
2074        traplocallevel: traplocallevel.load(Ordering::Relaxed),              // c:6459
2075        noerrs: *crate::ported::utils::noerrs_lock().lock().unwrap(),        // c:6460
2076        this_noerrexit: this_noerrexit.load(Ordering::Relaxed),              // c:6461
2077        // c:6462 — `es->underscore = ztrdup(zunderscore);`
2078        underscore: Some(zunderscore.lock().unwrap().clone()),
2079    });
2080    // c:6463-6464 — `es->next = exstack; exstack = es;`
2081    let mut head = exstack.lock().unwrap();
2082    es.next = head.take();
2083    *head = Some(es);
2084    // c:6465 — `noerrs = cmdoutpid = 0;`
2085    *crate::ported::utils::noerrs_lock().lock().unwrap() = 0;
2086    cmdoutpid.store(0, Ordering::Relaxed);
2087}
2088
2089/// Port of `execrestore()` from `Src/exec.c:6470` — C decl `execrestore(void)`.
2090///
2091/// C body:
2092/// ```c
2093/// struct execstack *en = exstack;
2094/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
2095/// queue_signals();
2096/// exstack = exstack->next;
2097/// list_pipe_pid = en->list_pipe_pid;
2098/// nowait = en->nowait;
2099/// pline_level = en->pline_level;
2100/// list_pipe_child = en->list_pipe_child;
2101/// list_pipe_job = en->list_pipe_job;
2102/// strcpy(list_pipe_text, en->list_pipe_text);
2103/// lastval = en->lastval;
2104/// noeval = en->noeval;
2105/// badcshglob = en->badcshglob;
2106/// cmdoutpid = en->cmdoutpid;
2107/// cmdoutval = en->cmdoutval;
2108/// use_cmdoutval = en->use_cmdoutval;
2109/// procsubstpid = en->procsubstpid;
2110/// trap_return = en->trap_return;
2111/// trap_state = en->trap_state;
2112/// trapisfunc = en->trapisfunc;
2113/// traplocallevel = en->traplocallevel;
2114/// noerrs = en->noerrs;
2115/// this_noerrexit = en->this_noerrexit;
2116/// setunderscore(en->underscore);
2117/// zsfree(en->underscore);
2118/// free(en);
2119/// unqueue_signals();
2120/// ```
2121///
2122/// Pop the top `execstack` frame and restore every transient
2123/// exec-context global. Inverse of `execsave`.
2124pub fn execrestore() {
2125    // c:6470
2126    let mut head = exstack.lock().unwrap();
2127    let en = match head.take() {
2128        // c:6472 + c:6477
2129        Some(en) => en,
2130        None => {
2131            // c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
2132            crate::DPUTS!(true, "BUG: execrestore() without execsave()");
2133            return;
2134        }
2135    };
2136    queue_signals(); // c:6476
2137    *head = en.next; // c:6477
2138    drop(head); // release lock before scalar restores
2139
2140    list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
2141    nowait.store(en.nowait, Ordering::Relaxed); // c:6480
2142    pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
2143    list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
2144    list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
2145                                                              // c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
2146    if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
2147        let nul = en
2148            .list_pipe_text
2149            .iter()
2150            .position(|&b| b == 0)
2151            .unwrap_or(JOBTEXTSIZE);
2152        *s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
2153    }
2154    LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
2155                                                  // c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
2156                                                  // counter from the saved frame.
2157    crate::ported::math::m_noeval_set(en.noeval);
2158    // c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
2159    // diagnostic counter saved on entry.
2160    crate::ported::glob::BADCSHGLOB.store(en.badcshglob, Ordering::Relaxed);
2161    cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
2162    cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
2163    use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
2164    procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
2165    TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
2166    TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
2167    trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
2168    traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
2169    *crate::ported::utils::noerrs_lock().lock().unwrap() = en.noerrs; // c:6496
2170    this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
2171                                                                // c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
2172    if let Some(ref u) = en.underscore {
2173        setunderscore(u); // c:6498
2174    }
2175    // c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
2176    unqueue_signals(); // c:6502
2177}
2178
2179/// Port of `execstring()` from `Src/exec.c:1228` — C decl `execstring(char *s, int dont_change_job, int exiting, char *context)`.
2180///
2181/// C body:
2182/// ```c
2183/// Eprog prog;
2184/// pushheap();
2185/// if (isset(VERBOSE)) {
2186///     zputs(s, stderr);
2187///     fputc('\n', stderr);
2188///     fflush(stderr);
2189/// }
2190/// if ((prog = parse_string(s, 0)))
2191///     execode(prog, dont_change_job, exiting, context);
2192/// popheap();
2193/// ```
2194///
2195/// Public entry — execute an arbitrary string as a zsh command list.
2196/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
2197/// body executors, command substitution body runners.
2198///
2199/// =================== WARNING — DIVERGENCE ====================
2200/// The C path is `parse_string` → `execode` → `execlist` (wordcode
2201/// walker). zshrs replaces `execode/execlist` with the fusevm
2202/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
2203/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
2204/// parse+execute chain delegates to the fusevm entry. When `execlist`
2205/// lands as a strict 1:1 port, swap the delegate for the canonical
2206/// chain.
2207/// =============================================================
2208pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
2209    // c:1228
2210    pushheap(); // c:1232
2211                // c:1233-1237 — VERBOSE banner.
2212    if isset(VERBOSE) {
2213        // c:1233
2214        let mut stderr = std::io::stderr().lock();
2215        use std::io::Write;
2216        let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
2217        let _ = stderr.write_all(b"\n"); // c:1235
2218        let _ = stderr.flush(); // c:1236
2219    }
2220    // c:1238-1239 — parse + execode. zshrs delegates the parse+VM
2221    // chain to the fusevm pipeline via the exec_hooks fn-ptr
2222    // installed by fusevm_bridge at startup. Direct
2223    // `with_executor` / ShellExecutor reach-in from src/ported/ is
2224    // forbidden — see memory feedback_no_exec_script_from_ported.
2225    // c:1277 — `parse_string(s, 0)`: reset_lineno is 0, so the string is
2226    // parsed from the line being executed, not from 1. A `${| … }` body
2227    // re-run on the glob path left `lineno` at 1 and the NOMATCH error
2228    // for its own word said `zsh:1:`.
2229    let running = crate::ported::lex::lineno();
2230    if crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_at_lineno(s, running))
2231        .is_none()
2232    {
2233        let _ = crate::ported::exec::execute_script_zsh_pipeline(s);
2234    }
2235    popheap(); // c:1240
2236}
2237
2238// =====================================================================
2239// Function-wrapper chain — `Src/module.c:570` `FuncWrap wrappers`, as
2240// walked by `runshfunc` (`Src/exec.c:6166`).
2241// =====================================================================
2242
2243/// Which module-registered wrapper a [`BodyWrap`] node stands for.
2244///
2245/// C reaches the handler through a fn pointer stored in the node
2246/// (`WrapFunc handler` — `Src/zsh.h:1365`); this port dispatches on the
2247/// discriminant instead, because the two handlers take a body DELEGATE
2248/// that a `WrapFunc` pointer cannot carry (see [`BodyWrap`]).
2249#[derive(Clone, Copy)]
2250enum WrapKind {
2251    /// `WRAPDEF(comp_wrapper)` — `Src/Zle/complete.c:1694-1695`.
2252    Comp,
2253    /// `WRAPDEF(wrap_private)` — `Src/Modules/param_private.c:541-542`.
2254    Private,
2255    /// `WRAPDEF(zprof_wrapper)` — `Src/Modules/zprof.c:318-320`.
2256    Zprof,
2257    /// `WRAPDEF(ksh93_wrapper)` — `Src/Modules/ksh93.c:230-232`.
2258    Ksh93,
2259}
2260
2261/// One node of the `wrappers` linked list C keeps at `Src/module.c:570`,
2262/// tail-appended by `addwrapper` (`Src/module.c:592-597`) from a module's
2263/// `boot_`.
2264///
2265/// Why the live chain is this table and not `module::WRAPPERS`: C's
2266/// `struct funcwrap` (`Src/zsh.h:1362-1367`) stores an
2267/// `int (*handler)(Eprog prog, FuncWrap w, char *name)`, and each handler
2268/// re-enters the chain on its own with `runshfunc(prog, w->next, name)`
2269/// (`Src/Zle/complete.c:1591`, `Src/Modules/param_private.c:556`) because
2270/// in C the function body is an `Eprog` anyone can walk. A zshrs function
2271/// body is not — it is `doshfunc`'s caller-supplied `body_runner` closure
2272/// (fusevm chunk runner, Rust compsys port, or plugin override), so the
2273/// port's handlers take the continuation as an explicit delegate that a
2274/// plain fn pointer has nowhere to hold. `zsh_h::funcwrap` and
2275/// `module::WRAPPERS` keep C's pointer shape so the `addwrapper` /
2276/// `deletewrapper` ports stay 1:1; this table is the same list expressed
2277/// in the shape `runshfunc` can actually call.
2278struct BodyWrap {
2279    /// The module whose `boot_` calls `addwrapper(m, wrapper)`. C reads it
2280    /// back through `wrap->module` for the refcount at c:6178/6180 and the
2281    /// deferred `unload_module` at c:6184.
2282    module: &'static str,
2283    /// Which handler this node holds.
2284    kind: WrapKind,
2285}
2286
2287/// The chain in `addwrapper` tail-append order — module boot order, so
2288/// the wrapper of the module booted first ends up OUTERMOST.
2289static BODY_WRAPPERS: &[BodyWrap] = &[
2290    // `Src/Modules/ksh93.c:230-232` — `static struct funcwrap wrapper[] =
2291    // { WRAPDEF(ksh93_wrapper) };` — installed by c:260
2292    // `return addwrapper(m, wrapper);` in `zsh/ksh93`'s `boot_`, removed
2293    // by c:269 `deletewrapper(m, wrapper);` in `cleanup_`. zsh/ksh93 is a
2294    // `load=yes` module (config.modules) that boots ahead of
2295    // zsh/param/private and zsh/zprof, so its node comes first. The
2296    // handler never calls the body — it creates the `.sh.*` locals and
2297    // returns 1 (c:227), letting the walk continue.
2298    BodyWrap {
2299        module: "zsh/ksh93",
2300        kind: WrapKind::Ksh93,
2301    },
2302    // `Src/Zle/complete.c:1694-1695` — `static struct funcwrap wrapper[] =
2303    // { WRAPDEF(comp_wrapper) };` — installed by c:1767
2304    // `return addwrapper(m, wrapper);` in `zsh/complete`'s `boot_`.
2305    BodyWrap {
2306        module: "zsh/complete",
2307        kind: WrapKind::Comp,
2308    },
2309    // `Src/Modules/param_private.c:541-542` — `static struct funcwrap
2310    // wrapper[] = { WRAPDEF(wrap_private) };` — installed by c:712
2311    // `return addwrapper(m, wrapper);` in `zsh/param/private`'s `boot_`.
2312    BodyWrap {
2313        module: "zsh/param/private",
2314        kind: WrapKind::Private,
2315    },
2316    // `Src/Modules/zprof.c:318-320` — `static struct funcwrap wrapper[] =
2317    // { WRAPDEF(zprof_wrapper) };` — installed by c:362
2318    // `return addwrapper(m, wrapper);` in `zsh/zprof`'s `boot_`, removed
2319    // by c:371 `deletewrapper(m, wrapper);` in `cleanup_`. `zsh/zprof` is
2320    // never linked in at startup, so it always boots after the two nodes
2321    // above and `addwrapper`'s tail-append (`Src/module.c:591-595`) puts
2322    // it LAST — innermost, closest to the body, which is what makes its
2323    // measured window the function itself.
2324    BodyWrap {
2325        module: "zsh/zprof",
2326        kind: WrapKind::Zprof,
2327    },
2328];
2329
2330/// Port of `runshfunc()` from `Src/exec.c:6166` — C decl `runshfunc(Eprog prog, FuncWrap wrap, char *name)`.
2331/// the wrapper-chain walk that stands between
2332/// `doshfunc` and a function body. C:
2333///
2334/// ```c
2335/// queue_signals();
2336/// ou = zalloc(ouu = underscoreused);
2337/// if (ou) memcpy(ou, zunderscore, underscoreused);
2338/// while (wrap) {                       // wrapper chain
2339///     wrap->module->wrapper++;
2340///     cont = wrap->handler(prog, wrap->next, name);
2341///     wrap->module->wrapper--;
2342///     if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
2343///         unload_module(wrap->module);
2344///     if (!cont) {                     // wrapper handled it
2345///         if (ou) zfree(ou, ouu);
2346///         unqueue_signals();
2347///         return;
2348///     }
2349///     wrap = wrap->next;
2350/// }
2351/// startparamscope();
2352/// execode(prog, 1, 0, "shfunc");
2353/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
2354/// endparamscope();
2355/// unqueue_signals();
2356/// ```
2357///
2358/// **RUST-ONLY ADAPTATION — what this port does and does NOT own.** When
2359/// zshrs replaced C's wordcode walk with a body delegate, everything in
2360/// the listing above except the chain walk moved into `doshfunc`, which is
2361/// the only caller:
2362///
2363/// | C line | Where it lives in zshrs |
2364/// |---|---|
2365/// | c:6171 / c:6202 `queue_signals` / `unqueue_signals` | `doshfunc`'s own outer pair (c:5835 / c:6128) already brackets this window; C's inner pair is a nested refcount bump inside it |
2366/// | c:6173-6175 / c:6196-6198 `$_` save / `setunderscore` | `doshfunc`, around this call (`saved_zunderscore`) |
2367/// | c:6194 `startparamscope()` | `doshfunc` — `inc_locallevel()` before the funcstack push |
2368/// | c:6200 `endparamscope()` | `doshfunc`'s epilogue |
2369/// | c:6195 `execode(prog, 1, 0, "shfunc")` | the `body` delegate |
2370///
2371/// Duplicating any of them here would double-apply it, so the body below
2372/// is c:6177-6193 plus the c:6195 body call, and nothing else.
2373///
2374/// WARNING: param names don't match C — Rust=(_prog, wrap, name, body) vs
2375/// C=(prog, wrap, name). `wrap` is an INDEX into [`BODY_WRAPPERS`] rather
2376/// than a `FuncWrap` pointer (a `Vec`-shaped chain has no `->next` to
2377/// hand out); `wrap = 0` is C's `wrappers` head and `wrap = i + 1` is the
2378/// `wrap->next` a handler re-enters with. `_prog` is unused because the
2379/// body arrives as `body`, and the return value is the body's exit status
2380/// (C returns void and communicates through `lastval`).
2381pub fn runshfunc(
2382    _prog: Option<&eprog>, // c:6166
2383    wrap: usize,           // c:6166 (FuncWrap wrap — see WARNING)
2384    name: &str,            // c:6166
2385    body: &mut dyn FnMut() -> i32,
2386) -> i32 {
2387    // c:6166
2388    let mut w_idx = wrap;
2389    while w_idx < BODY_WRAPPERS.len() {
2390        // c:6177 — `while (wrap)`
2391        let w = &BODY_WRAPPERS[w_idx];
2392
2393        // Chain membership. C asks nothing here because a wrapper is in
2394        // the list only while its module has it registered; this port
2395        // evaluates the equivalent condition per node.
2396        let armed = match w.kind {
2397            // zshrs statically links the completion system — `compadd` /
2398            // `compset` are always present and `zsh/complete` is never
2399            // dlopen'd — so there is no boot event to key on. The
2400            // wrapper's own first statement is the gate that matters and
2401            // it is exact: `if (incompfunc != 1) return 1;`
2402            // (`Src/Zle/complete.c:1558-1559`). `incompfunc` is 1 only
2403            // between `Src/Zle/compcore.c:815` and c:841, i.e. strictly
2404            // inside `callcompfunc`, which is the only thing that can run
2405            // a completion function at all. Hoisting the test here keeps
2406            // an ordinary function call at one relaxed load with no chain
2407            // node entered.
2408            WrapKind::Comp => crate::ported::zle::complete::INCOMPFUNC.load(Ordering::Relaxed) == 1,
2409            // Module BOOT STATE (`MOD_INIT_B` — the bit `zmodload -e`
2410            // reads and `load_module` sets after `do_boot_module`,
2411            // `Src/module.c:2317`), which is exactly C's condition for
2412            // `wrap_private` being in the list: the `private` dispatch
2413            // runs `require_module` via the `Src/exec.c:2710` autofeature
2414            // path, which boots the module. Deliberately NOT
2415            // `is_loaded()` — default-registered static modules carry
2416            // `MOD_LINKED` from startup, which would arm the wrapper
2417            // before any `private` was ever used.
2418            WrapKind::Private => crate::ported::module::MODULESTAB
2419                .lock()
2420                .map(|t| {
2421                    t.modules
2422                        .get(w.module)
2423                        .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
2424                        .unwrap_or(false)
2425                })
2426                .unwrap_or(false),
2427            // Genuine `addwrapper` membership: `zsh/zprof`'s `boot_`
2428            // calls `addwrapper(m, wrapper)` (`Src/Modules/zprof.c:362`)
2429            // and its `cleanup_` calls `deletewrapper` (c:371), and both
2430            // mirror the list into `module::WRAPPERS_ADDED`. So this is
2431            // C's `while (wrap)` test verbatim — "is this node in the
2432            // `wrappers` list" — costing one relaxed load, with no lock
2433            // and no allocation when `zsh/zprof` was never loaded.
2434            WrapKind::Zprof => {
2435                (crate::ported::module::WRAPPERS_ADDED.load(Ordering::Relaxed)
2436                    & crate::ported::module::WRAPPER_BIT_ZPROF)
2437                    != 0
2438            }
2439            // Same `addwrapper` membership test as zprof: zsh/ksh93's boot_
2440            // (c:260) and cleanup_ (c:269) mirror it into WRAPPERS_ADDED.
2441            WrapKind::Ksh93 => {
2442                (crate::ported::module::WRAPPERS_ADDED.load(Ordering::Relaxed)
2443                    & crate::ported::module::WRAPPER_BIT_KSH93)
2444                    != 0
2445            }
2446        };
2447        if !armed {
2448            // Not in C's `wrappers` list right now — no node to step over.
2449            w_idx += 1;
2450            continue;
2451        }
2452
2453        // c:6178 — `wrap->module->wrapper++;` — hold the module open so a
2454        // recursive unload during the handler defers until we return.
2455        if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2456            if let Some(m) = tab.modules.get_mut(w.module) {
2457                m.wrapper += 1;
2458            }
2459        }
2460
2461        // c:6179 — `cont = wrap->handler(prog, wrap->next, name);`. The
2462        // closure IS `wrap->next` made callable: the rest of the chain,
2463        // then the body — which is what each handler's own
2464        // `runshfunc(prog, w, name)` reaches in C.
2465        let mut status = 0;
2466        let cont = {
2467            let mut run_next = || runshfunc(_prog, w_idx + 1, name, &mut *body);
2468            match w.kind {
2469                WrapKind::Comp => crate::ported::zle::complete::comp_wrapper(
2470                    std::ptr::null(),
2471                    std::ptr::null(),
2472                    name,
2473                    || status = run_next(), // complete.c:1591
2474                ),
2475                WrapKind::Private => crate::ported::modules::param_private::wrap_private(
2476                    std::ptr::null(),
2477                    std::ptr::null(),
2478                    std::ptr::null_mut(),
2479                    || status = run_next(), // param_private.c:556
2480                ),
2481                WrapKind::Zprof => crate::ported::modules::zprof::zprof_wrapper(
2482                    std::ptr::null(),
2483                    std::ptr::null(),
2484                    name,
2485                    || status = run_next(), // zprof.c:285
2486                ),
2487                // ksh93.c:143-227 never re-enters the chain: it sets up the
2488                // `.sh.*` locals and returns 1, so `run_next` stays unused
2489                // and the walk continues at c:6192.
2490                WrapKind::Ksh93 => {
2491                    let cname = std::ffi::CString::new(name).unwrap_or_default();
2492                    crate::ported::modules::ksh93::ksh93_wrapper(
2493                        std::ptr::null(),
2494                        std::ptr::null(),
2495                        cname.as_ptr() as *mut libc::c_char,
2496                    )
2497                }
2498            }
2499        };
2500
2501        // c:6180 — `wrap->module->wrapper--;`
2502        // c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD))
2503        //                    unload_module(wrap->module);`
2504        let should_unload = {
2505            if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2506                if let Some(m) = tab.modules.get_mut(w.module) {
2507                    m.wrapper -= 1;
2508                    m.wrapper == 0 && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
2509                } else {
2510                    false
2511                }
2512            } else {
2513                false
2514            }
2515        };
2516        if should_unload {
2517            if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2518                let _ = tab.unload_module(w.module); // c:6184
2519            }
2520        }
2521
2522        if cont == 0 {
2523            // c:6186 — the wrapper claimed the call and has already run
2524            // the body through the delegate.
2525            return status; // c:6190
2526        }
2527        w_idx += 1; // c:6192
2528    }
2529    // c:6195 — `execode(prog, 1, 0, "shfunc");`. execode runs execlist,
2530    // whose loop is `while (wc_code(code) == WC_LIST && !breaks && !retflag
2531    // && !errflag)` (c:1390), and doshfunc saved `breaks` without clearing
2532    // it (c:5859): a function entered while a `break` is still unwinding —
2533    // `repeat 3 do f ${ for …; do break 2; done }; done` — runs no list and
2534    // leaves lastval alone (D10nofork.ztst "break N propagates").
2535    if crate::ported::builtin::BREAKS.load(Ordering::Relaxed) != 0 {
2536        return crate::ported::builtin::LASTVAL.load(Ordering::Relaxed);
2537    }
2538    body()
2539}
2540
2541/// Port of `sticky_emulation_dup()` from `Src/exec.c:5501` — C decl `sticky_emulation_dup(Emulation_options src, int useheap)`.
2542///
2543/// C body (`useheap` selects between heap-arena and permanent zalloc;
2544/// Rust collapses both into owned `Box` clones):
2545/// ```c
2546/// Emulation_options newsticky = useheap ?
2547///     hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
2548/// newsticky->emulation = src->emulation;
2549/// if (src->n_on_opts) {
2550///     size_t sz = src->n_on_opts * sizeof(*src->on_opts);
2551///     newsticky->n_on_opts = src->n_on_opts;
2552///     newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
2553///     memcpy(newsticky->on_opts, src->on_opts, sz);
2554/// }
2555/// if (src->n_off_opts) {
2556///     size_t sz = src->n_off_opts * sizeof(*src->off_opts);
2557///     newsticky->n_off_opts = src->n_off_opts;
2558///     newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
2559///     memcpy(newsticky->off_opts, src->off_opts, sz);
2560/// }
2561/// return newsticky;
2562/// ```
2563///
2564/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
2565/// at function-def time to snapshot the pending `sticky` global so
2566/// the function carries its own immutable copy.
2567pub fn sticky_emulation_dup(src: &emulation_options, _useheap: i32) -> Emulation_options {
2568    // c:5501
2569    // c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
2570    let mut newsticky = Box::new(emulation_options {
2571        emulation: src.emulation, // c:5505
2572        n_on_opts: 0,
2573        n_off_opts: 0,
2574        on_opts: Vec::new(),
2575        off_opts: Vec::new(),
2576    });
2577    // c:5506-5511 — copy on_opts.
2578    if src.n_on_opts != 0 {
2579        // c:5506
2580        newsticky.n_on_opts = src.n_on_opts; // c:5508
2581        newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
2582    }
2583    // c:5512-5517 — copy off_opts.
2584    if src.n_off_opts != 0 {
2585        // c:5512
2586        newsticky.n_off_opts = src.n_off_opts; // c:5514
2587        newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
2588    }
2589    newsticky // c:5519
2590}
2591
2592/// Port of `sticky_emulation_differs()` from `Src/exec.c:5770` — C decl `int sticky_emulation_differs(Emulation_options sticky2)`.
2593///
2594/// C body:
2595/// ```c
2596/// /* If no new sticky emulation, not a different emulation */
2597/// if (!sticky2)
2598///     return 0;
2599/// /* If no current sticky emulation, different */
2600/// if (!sticky)
2601///     return 1;
2602/// /* If basic emulation different, different */
2603/// if (sticky->emulation != sticky2->emulation)
2604///     return 1;
2605/// /* If differing numbers of options, different */
2606/// if (sticky->n_on_opts != sticky2->n_on_opts ||
2607///     sticky->n_off_opts != sticky2->n_off_opts)
2608///     return 1;
2609/// /* If different options turned on, different */
2610/// if (sticky->n_on_opts &&
2611///     memcmp(sticky->on_opts, sticky2->on_opts,
2612///            sticky->n_on_opts * sizeof(*sticky->on_opts)) != 0)
2613///     return 1;
2614/// /* If different options turned on, different */
2615/// if (sticky->n_off_opts &&
2616///     memcmp(sticky->off_opts, sticky2->off_opts,
2617///            sticky->n_off_opts * sizeof(*sticky->off_opts)) != 0)
2618///     return 1;
2619/// return 0;
2620/// ```
2621pub fn sticky_emulation_differs(sticky2: Option<&emulation_options>) -> i32 {
2622    // c:5829
2623    let Some(sticky2) = sticky2 else {
2624        return 0; // c:5832-5833
2625    };
2626    let guard = sticky.lock().unwrap_or_else(|e| e.into_inner());
2627    let Some(ref cur) = *guard else {
2628        return 1; // c:5835-5836
2629    };
2630    if cur.emulation != sticky2.emulation {
2631        return 1; // c:5838-5839
2632    }
2633    if cur.n_on_opts != sticky2.n_on_opts || cur.n_off_opts != sticky2.n_off_opts {
2634        return 1; // c:5841-5843
2635    }
2636    if cur.n_on_opts != 0 && cur.on_opts != sticky2.on_opts {
2637        return 1; // c:5850-5853
2638    }
2639    if cur.n_off_opts != 0 && cur.off_opts != sticky2.off_opts {
2640        return 1; // c:5855-5858
2641    }
2642    0 // c:5859
2643}
2644
2645/// Port of `shfunc_set_sticky()` from `Src/exec.c:5527` — C decl `shfunc_set_sticky(Shfunc shf)`.
2646///
2647/// C body:
2648/// ```c
2649/// if (sticky)
2650///     shf->sticky = sticky_emulation_dup(sticky, 0);
2651/// else
2652///     shf->sticky = NULL;
2653/// ```
2654///
2655/// Stamp the function with the current pending sticky-emulation
2656/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
2657pub fn shfunc_set_sticky(shf: &mut shfunc) {
2658    // c:5527
2659    let sticky_guard = sticky.lock().unwrap();
2660    if let Some(ref s) = *sticky_guard {
2661        // c:5529
2662        shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
2663    } else {
2664        // c:5531
2665        shf.sticky = None; // c:5532
2666    }
2667}
2668
2669/// Port of `search_defpath()` from `Src/exec.c:691` — C decl `search_defpath(char *cmd, char *pbuf, int plen)`.
2670///
2671/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
2672/// Used by `command -p` to bypass the user's `$PATH` and search the
2673/// system default (`/bin:/usr/bin:...`).
2674pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
2675    // c:691
2676    // c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
2677    for ps in DEFAULT_PATH.split(':') {
2678        // c:695
2679        // c:697 — `if (*ps == '/')`.
2680        if !ps.starts_with('/') {
2681            continue;
2682        }
2683        // c:700-707 — PATH_MAX bounds check on `<dir>` segment.
2684        if ps.len() >= plen {
2685            // c:700 / c:704
2686            continue; // c:701 / c:705
2687        }
2688        // c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
2689        let full_len = ps.len() + 1 + cmd.len();
2690        if full_len >= plen {
2691            // c:709
2692            continue; // c:710
2693        }
2694        let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
2695                                             // c:712 — `if (iscom(pbuf)) return pbuf;`
2696        if iscom(&buf) {
2697            // c:712
2698            return Some(buf); // c:713
2699        }
2700    }
2701    None // c:716
2702}
2703
2704/// Port of `checkclobberparam()` from `Src/exec.c:2178` — C decl `checkclobberparam(struct redir *f)`.
2705///
2706/// C body:
2707/// ```c
2708/// struct value vbuf; Value v;
2709/// char *s = f->varid; int fd;
2710/// if (!s) return 1;
2711/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
2712/// if (v->pm->node.flags & PM_READONLY) {
2713///     zwarn("can't allocate file descriptor to readonly parameter %s",
2714///           f->varid);
2715///     errno = 0;
2716///     return 0;
2717/// }
2718/// /* We can't clobber the value in the parameter if it's
2719///  * already an opened file descriptor */
2720/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
2721///     (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
2722///     fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
2723///     zwarn("can't clobber parameter %s containing file descriptor %d",
2724///          f->varid, fd);
2725///     errno = 0;
2726///     return 0;
2727/// }
2728/// return 1;
2729/// ```
2730///
2731/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
2732/// name) is writable and (under NOCLOBBER) doesn't currently hold an
2733/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
2734/// already emitted).
2735///
2736/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
2737/// NOCLOBBER is set and the param's value is the fd-number of an
2738/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
2739/// the existing fd doesn't get clobbered by the upcoming open(2).
2740pub fn checkclobberparam(f: &redir) -> i32 {
2741    // c:2178
2742    // c:2182 — `char *s = f->varid;`
2743    let s = match &f.varid {
2744        Some(v) => v.clone(),
2745        None => return 1, // c:2185-2186 — `if (!s) return 1;`
2746    };
2747    // c:2186 — `if (!(v = getvalue(&vbuf, &s, 0))) return 1;`
2748    let mut vbuf = crate::ported::zsh_h::value {
2749        pm: None,
2750        arr: Vec::new(),
2751        scanflags: 0,
2752        valflags: 0,
2753        start: 0,
2754        end: 0,
2755    };
2756    let mut cursor: &str = s.as_str();
2757    let v_opt = crate::ported::params::getvalue(Some(&mut vbuf), &mut cursor, 0);
2758    if v_opt.is_none() {
2759        return 1; // c:2187
2760    }
2761    // c:2188-2197 — readonly refusal via v->pm->node.flags.
2762    let readonly = vbuf
2763        .pm
2764        .as_ref()
2765        .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
2766        .unwrap_or(false);
2767    if readonly {
2768        // c:2191
2769        zwarn(&format!(
2770            // c:2192
2771            "can't allocate file descriptor to readonly parameter {}",
2772            s
2773        ));
2774        // c:2195 — `errno = 0;` not flagged as a system error.
2775        return 0; // c:2196
2776    }
2777    // c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
2778    // AND the param holds a valid fd that's already in our fdtable as
2779    // FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
2780    // open so we don't clobber it.
2781    if !isset(CLOBBER) {
2782        // c:2201 — `getstrvalue(v)` — read the param's string form.
2783        let val_str = crate::ported::params::getstrvalue(Some(&mut vbuf));
2784        if let Ok(fd) = val_str.trim().parse::<i32>() {
2785            // c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
2786            let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2787            if fd >= 0 && fd <= max_fd {
2788                let kind = fdtable_get(fd);
2789                if kind == FDT_EXTERNAL {
2790                    zwarn(&format!("{}: file descriptor {} already open", s, fd)); // c:2206-2210
2791                    return 0; // c:2211
2792                }
2793            }
2794        }
2795    }
2796    1 // c:2214
2797}
2798
2799/// Port of `clobber_open()` from `Src/exec.c:2221` — C decl `clobber_open(struct redir *f)`.
2800///
2801/// C body:
2802/// ```c
2803/// struct stat buf;
2804/// int fd, oerrno;
2805/// char *ufname = unmeta(f->name);
2806/// /* If clobbering, just open. */
2807/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
2808///     return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
2809/// /* If not clobbering, attempt to create file exclusively. */
2810/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
2811///     return fd;
2812/// /* If that fails, we are still allowed to open non-regular files. */
2813/// oerrno = errno;
2814/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
2815///     if (!fstat(fd, &buf)) {
2816///         if (!S_ISREG(buf.st_mode)) return fd;
2817///         /* CLOBBER_EMPTY allows re-use of empty regular files. */
2818///         if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
2819///     }
2820///     close(fd);
2821/// }
2822/// errno = oerrno;
2823/// return -1;
2824/// ```
2825///
2826/// Open the redir target for write with the NOCLOBBER rules:
2827/// - CLOBBER set or `>|` form → just open with O_TRUNC
2828/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
2829///   files (FIFOs, devices, sockets) OR empty regular files under
2830///   CLOBBEREMPTY.
2831pub fn clobber_open(f: &redir) -> i32 {
2832    // c:2221
2833    let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
2834    let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
2835        Ok(c) => c,
2836        Err(_) => return -1,
2837    };
2838    // c:2228-2230 — clobber path: just open + truncate.
2839    if isset(CLOBBER) || IS_CLOBBER_REDIR(f.typ) {
2840        // c:2228
2841        // c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
2842        let fd = unsafe {
2843            libc::open(
2844                ufname.as_ptr(),
2845                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
2846                0o666 as libc::c_uint,
2847            )
2848        };
2849        return fd; // c:2230
2850    }
2851    // c:2233-2235 — try O_EXCL create first.
2852    let fd = unsafe {
2853        // c:2233
2854        libc::open(
2855            ufname.as_ptr(),
2856            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
2857            0o666 as libc::c_uint,
2858        )
2859    };
2860    if fd >= 0 {
2861        return fd; // c:2235
2862    }
2863    // c:2240 — `oerrno = errno;` — save for restoration on the recover path.
2864    let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2865    // c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
2866    // OR (CLOBBEREMPTY && size == 0).
2867    let fd = unsafe {
2868        // c:2241
2869        libc::open(
2870            ufname.as_ptr(),
2871            libc::O_WRONLY | libc::O_NOCTTY,
2872            0o666 as libc::c_uint,
2873        )
2874    };
2875    if fd != -1 {
2876        let mut buf: libc::stat = unsafe { std::mem::zeroed() };
2877        if unsafe { libc::fstat(fd, &mut buf) } == 0 {
2878            // c:2242
2879            // c:2243-2244 — non-regular file: accept.
2880            if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
2881                // c:2243
2882                return fd; // c:2244
2883            }
2884            // c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
2885            if isset(CLOBBEREMPTY) && buf.st_size == 0 {
2886                // c:2256
2887                return fd; // c:2257
2888            }
2889        }
2890        unsafe {
2891            libc::close(fd);
2892        } // c:2259
2893    }
2894    // c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
2895    // "file exists" not the noisier "couldn't reopen" trailing errno.
2896    // Per-platform errno setter: __error() on macOS, __errno_location()
2897    // on Linux. Without cfg gating the build breaks on Linux (CI).
2898    #[cfg(target_os = "macos")]
2899    unsafe {
2900        *libc::__error() = oerrno;
2901    }
2902    #[cfg(target_os = "linux")]
2903    unsafe {
2904        *libc::__errno_location() = oerrno;
2905    }
2906    -1 // c:2263
2907}
2908
2909/// Port of `findcmd()` from `Src/exec.c:897` — C decl `findcmd(char *arg0, int docopy, int default_path)`.
2910/// Walk `$PATH` (or DEFAULT_PATH under
2911/// `default_path=1`) for `arg0`, returning the matching path on
2912/// success. `_docopy` is the C source's "duplicate the result"
2913/// flag — Rust ownership covers it without an explicit copy step.
2914/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
2915/// `command -p`).
2916pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
2917    // c:897
2918    // c:903-908 — if (default_path) → search_defpath; return.
2919    if default_path != 0 {
2920        return search_defpath(arg0, libc::PATH_MAX as usize);
2921    }
2922    // c:936-938 — the table comes FIRST, and a miss populates it:
2923    //     cn = (Cmdnam) cmdnamtab->getnode(cmdnamtab, arg0);
2924    //     if (!cn && isset(HASHCMDS))
2925    //         cn = hashcmd(arg0, path);
2926    // This port went straight to the `$PATH` walk below, so `whence -p foo`
2927    // after `hash foo=/usr/bin/awk` reported nothing (a HASHED entry names a
2928    // path that no `$PATH` directory contains), and no lookup through
2929    // `findcmd` — `whence`, `type`, `command -v`, compctl — ever hashed
2930    // anything, where C hashes on every one of them.
2931    let cn: Option<cmdnam> = {
2932        // c:936
2933        let hit = cmdnamtab_lock()
2934            .read()
2935            .ok()
2936            .and_then(|t| t.get_including_disabled(arg0).cloned());
2937        match hit {
2938            Some(c) => Some(c),
2939            // c:937-938
2940            None if isset(crate::ported::zsh_h::HASHCMDS) => {
2941                let dirs: Vec<String> = getsparam("PATH")
2942                    .unwrap_or_default()
2943                    .split(':')
2944                    .map(String::from)
2945                    .collect();
2946                hashcmd(arg0, &dirs)
2947            }
2948            None => None,
2949        }
2950    };
2951    // c:939-940 — `if ((int) ztrlen(arg0) >= PATH_MAX) return NULL;`.
2952    if arg0.len() > libc::PATH_MAX as usize {
2953        return None;
2954    }
2955    // c:Src/exec.c:914-920 — `/`-bearing arg path resolution.
2956    //   if ((s = strchr(arg0, '/'))) {
2957    //       RET_IF_COM(arg0);   // ← unconditional accept on iscom hit
2958    //       if (arg0 == s || unset(PATHDIRS) ||
2959    //           !strncmp(arg0, "./", 2) ||
2960    //           !strncmp(arg0, "../", 3))
2961    //           return NULL;
2962    //   }
2963    // The Rust port had the iscom check gated on `starts_with('/')`,
2964    // so `type ./target/debug/zshrs` returned None even when the
2965    // file was executable. Bug #496 family.
2966    if arg0.contains('/') {
2967        if iscom(arg0) {
2968            return Some(arg0.to_string()); // c:915 RET_IF_COM
2969        }
2970        // c:916-919 — absolute OR PATHDIRS-off OR `./` / `../` →
2971        // give up here (no $PATH walk for these). Relative without
2972        // those prefixes falls through to the $PATH scan below for
2973        // the PATHDIRS=set case.
2974        if arg0.starts_with('/')
2975            || !isset(PATHDIRS)
2976            || arg0.starts_with("./")
2977            || arg0.starts_with("../")
2978        {
2979            return None;
2980        }
2981        // else fall through to PATH walk.
2982    }
2983    // c:948-977 — resolve through the node the table gave us.
2984    //     if (cn) {
2985    //         if (cn->node.flags & HASHED) nn = cn->u.cmd;
2986    //         else {
2987    //             for (pp = path; pp < cn->u.name; pp++)
2988    //                 if (**pp != '/') { buf = "<*pp>/<arg0>"; RET_IF_COM(buf); }
2989    //             nn = "<*cn->u.name>/<arg0>";
2990    //         }
2991    //         RET_IF_COM(nn);
2992    //     }
2993    let path = getsparam("PATH").unwrap_or_default();
2994    let dirs: Vec<&str> = path.split(':').collect();
2995    if let Some(cn) = cn.as_ref() {
2996        // c:948
2997        let nn = if (cn.node.flags & (crate::ported::zsh_h::HASHED as i32)) != 0 {
2998            // c:951-954
2999            cn.cmd.clone()
3000        } else {
3001            // c:956-968 — the `$path` entries BEFORE the matched one still
3002            // get a look-in, because `hashcmd` only ever matches an ABSOLUTE
3003            // entry (c:1058 `if (**pp == '/')`) and a relative one earlier in
3004            // `$path` takes precedence. `cn.name` is C's `cn->u.name` tail
3005            // slice, so its length recovers the index C compares against.
3006            let matched_at = dirs.len().saturating_sub(cn.name.as_ref().map_or(0, |v| v.len()));
3007            for d in dirs.iter().take(matched_at) {
3008                // c:956
3009                if !d.starts_with('/') {
3010                    // c:957
3011                    let buf = if d.is_empty() {
3012                        arg0.to_string() // c:959 — an empty entry means `.`
3013                    } else {
3014                        format!("{}/{}", d, arg0) // c:960-965
3015                    };
3016                    if iscom(&buf) {
3017                        return Some(buf); // c:966 RET_IF_COM
3018                    }
3019                }
3020            }
3021            // c:969-974 — `"%s/%s", *(cn->u.name), cn->node.nam`.
3022            cn.name
3023                .as_ref()
3024                .and_then(|v| v.first())
3025                .map(|d| format!("{}/{}", d, arg0))
3026        };
3027        if let Some(nn) = nn {
3028            if iscom(&nn) {
3029                return Some(nn); // c:976 RET_IF_COM
3030            }
3031        }
3032    }
3033    // c:978-988 — walk `path[]` (the shell `$path` array). Read $PATH
3034    // from paramtab so shell-private edits via `path=(...)` take
3035    // effect (not OS env only).
3036    for dir in dirs {
3037        if dir.is_empty() {
3038            continue;
3039        }
3040        let candidate = format!("{}/{}", dir, arg0);
3041        if iscom(&candidate) {
3042            return Some(candidate);
3043        }
3044    }
3045    None // c:989
3046}
3047
3048/// Port of `addfd()` from `Src/exec.c:2397` — C decl `addfd(int forked, int *save, struct multio **mfds, int fd1, int fd2, int rflag, char *varid)`.
3049///                             int fd1, int fd2, int rflag, char *varid)`
3050/// from `Src/exec.c:2397`.
3051///
3052/// C body (~100 lines, three branches):
3053/// ```c
3054/// if (varid) {
3055///     /* {varid}>file form — move fd above 10 and bind $varid to it */
3056/// } else if (!mfds[fd1] || unset(MULTIOS)) {
3057///     /* new multio OR MULTIOS off — first redir on this fd */
3058/// } else {
3059///     /* additional redir on a fd that's already a multio (split or extend) */
3060/// }
3061/// ```
3062///
3063/// Register `fd2` (already-open) as a redirection target for `fd1`.
3064/// Three branches: `varid` writes the moved fd to `$varid` and bumps
3065/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
3066/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
3067/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
3068/// via `mpipe`, or appends another fd to an already-split stream
3069/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
3070///
3071/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
3072/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
3073/// longer a hard cap (still 8 for the initial allocation, grown
3074/// on demand thereafter).
3075///
3076/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
3077/// stores the int value but doesn't expose a bitwise-OR setter; we
3078/// re-read + OR + re-store as two atomic-feeling steps.
3079pub fn addfd(
3080    forked: i32,
3081    save: &mut [i32; 10],
3082    mfds: &mut [Option<Box<multio>>; 10],
3083    fd1: i32,
3084    fd2: i32,
3085    rflag: i32,
3086    varid: Option<&str>,
3087) {
3088    // c:2397
3089    let mut pipes: [i32; 2] = [-1; 2]; // c:2400
3090
3091    // c:2402-2417 — `if (varid)` branch — {varid}>file shape.
3092    if let Some(vid) = varid {
3093        // c:2402
3094        let fd_moved = movefd(fd2); // c:2404
3095        if fd_moved == -1 {
3096            // c:2405
3097            zerr(&format!(
3098                // c:2406
3099                "cannot move fd {}: {}",
3100                fd2,
3101                std::io::Error::last_os_error()
3102            ));
3103            return; // c:2407
3104        }
3105        // c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
3106        fdtable_set(fd_moved, FDT_EXTERNAL);
3107        // c:2410 — `setiparam(varid, (zlong)fd1);`
3108        setiparam(vid, fd_moved as i64);
3109        // c:2415-2416 — `if (errflag) zclose(fd1);`
3110        // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
3111        // the C line cited above tests the WHOLE errflag, so masking here let an
3112        // interrupted shell keep going where zsh stops.
3113        if errflag.load(Ordering::Relaxed) != 0 {
3114            // c:2415
3115            let _ = zclose(fd_moved); // c:2416
3116        }
3117        return;
3118    }
3119    // c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
3120    let fd1u = fd1 as usize;
3121    if fd1u >= mfds.len() {
3122        return;
3123    }
3124    if mfds[fd1u].is_none() || unset(MULTIOS) {
3125        // c:2418
3126        if mfds[fd1u].is_none() {
3127            // c:2419 — `starting a new multio`
3128            // c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
3129            mfds[fd1u] = Some(Box::new(multio {
3130                ct: 0,
3131                rflag: 0,
3132                pipe: -1,
3133                // c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
3134                // grow on demand via push() below. Pre-fill MULTIOUNIT
3135                // slots with -1 so existing indexed writes (fds[0], fds[1])
3136                // still work without explicit resize().
3137                fds: vec![-1; MULTIOUNIT],
3138            }));
3139            // c:2421 — `if (!forked && save[fd1] == -2)`
3140            if forked == 0 && save[fd1u] == -2 {
3141                if fd1 == fd2 {
3142                    // c:2422
3143                    save[fd1u] = -1; // c:2423
3144                } else {
3145                    // c:2424
3146                    let fd_n = movefd(fd1); // c:2425
3147                    if fd_n < 0 {
3148                        // c:2430
3149                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3150                        if e != libc::EBADF {
3151                            // c:2431
3152                            zerr(&format!(
3153                                // c:2432
3154                                "cannot duplicate fd {}: {}",
3155                                fd1,
3156                                std::io::Error::from_raw_os_error(e)
3157                            ));
3158                            mfds[fd1u] = None; // c:2433
3159                            closemnodes(mfds); // c:2434
3160                            return; // c:2435
3161                        }
3162                    } else {
3163                        // c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
3164                        crate::DPUTS!(
3165                            fdtable_get(fd_n) != FDT_INTERNAL,
3166                            "Saved file descriptor not marked as internal"
3167                        );
3168                        // c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
3169                        let cur = fdtable_get(fd_n);
3170                        fdtable_set(fd_n, cur | FDT_SAVED_MASK);
3171                    }
3172                    save[fd1u] = fd_n; // c:2442
3173                }
3174            }
3175        }
3176        // c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
3177        // handled above; this is the non-varid branch.)
3178        let _ = redup(fd2, fd1);
3179        // c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
3180        if let Some(mn) = mfds[fd1u].as_mut() {
3181            mn.ct = 1; // c:2448
3182            mn.fds[0] = fd1; // c:2449
3183            mn.rflag = rflag; // c:2450
3184        }
3185    } else {
3186        // c:2451 — extend existing multio.
3187        // c:2452-2456 — rflag mismatch check.
3188        let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
3189        if cur_rflag != rflag {
3190            // c:2452
3191            zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
3192            closemnodes(mfds); // c:2454
3193            return; // c:2455
3194        }
3195        let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
3196        if cur_ct == 1 {
3197            // c:2457 — split the stream.
3198            // c:2458 — `int fdN = movefd(fd1);`
3199            let fd_n = movefd(fd1);
3200            if fd_n < 0 {
3201                // c:2459
3202                zerr(&format!(
3203                    // c:2460
3204                    "multio failed for fd {}: {}",
3205                    fd1,
3206                    std::io::Error::last_os_error()
3207                ));
3208                closemnodes(mfds); // c:2461
3209                return; // c:2462
3210            }
3211            if let Some(mn) = mfds[fd1u].as_mut() {
3212                mn.fds[0] = fd_n; // c:2464
3213            }
3214            // c:2465 — `fdN = movefd(fd2);`
3215            let fd_n2 = movefd(fd2);
3216            if fd_n2 < 0 {
3217                // c:2466
3218                zerr(&format!(
3219                    // c:2467
3220                    "multio failed for fd {}: {}",
3221                    fd2,
3222                    std::io::Error::last_os_error()
3223                ));
3224                closemnodes(mfds); // c:2468
3225                return; // c:2469
3226            }
3227            if let Some(mn) = mfds[fd1u].as_mut() {
3228                mn.fds[1] = fd_n2; // c:2471
3229            }
3230            // c:2472 — `mpipe(pipes)`
3231            if mpipe(&mut pipes) < 0 {
3232                // c:2472
3233                zerr(&format!(
3234                    // c:2473
3235                    "multio failed for fd {}: {}",
3236                    fd2,
3237                    std::io::Error::last_os_error()
3238                ));
3239                closemnodes(mfds); // c:2474
3240                return; // c:2475
3241            }
3242            // c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
3243            if let Some(mn) = mfds[fd1u].as_mut() {
3244                mn.pipe = pipes[(1 - rflag) as usize];
3245            }
3246            // c:2478 — `redup(pipes[rflag], fd1);`
3247            let _ = redup(pipes[rflag as usize], fd1);
3248            // c:2479 — `mfds[fd1]->ct = 2;`
3249            if let Some(mn) = mfds[fd1u].as_mut() {
3250                mn.ct = 2;
3251            }
3252        } else {
3253            // c:2480 — extend already-split stream.
3254            // c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
3255            //                              sizeof + ct*sizeof(int));`
3256            // Rust's `Vec<i32>` grows on demand; ensure capacity for the
3257            // new slot before the indexed write below.
3258            if let Some(mn) = mfds[fd1u].as_mut() {
3259                while mn.fds.len() <= cur_ct as usize {
3260                    mn.fds.push(-1);
3261                }
3262            }
3263            // c:2487 — `if ((fdN = movefd(fd2)) < 0)`
3264            let fd_n = movefd(fd2);
3265            if fd_n < 0 {
3266                zerr(&format!(
3267                    // c:2488
3268                    "multio failed for fd {}: {}",
3269                    fd2,
3270                    std::io::Error::last_os_error()
3271                ));
3272                closemnodes(mfds); // c:2489
3273                return; // c:2490
3274            }
3275            // c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
3276            if let Some(mn) = mfds[fd1u].as_mut() {
3277                let slot = mn.ct as usize;
3278                if slot < mn.fds.len() {
3279                    mn.fds[slot] = fd_n;
3280                    mn.ct += 1;
3281                }
3282            }
3283        }
3284    }
3285}
3286
3287/// Port of `closemn()` from `Src/exec.c:2273` — C decl `closemn(struct multio **mfds, int fd, int type)`.
3288///
3289/// C body (abridged — the meat is the fork-into-tee-or-cat child):
3290/// ```c
3291/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
3292///     struct multio *mn = mfds[fd];
3293///     char buf[TCBUFSIZE]; int len, i;
3294///     pid_t pid; struct timespec bgtime;
3295///     child_block();
3296///     if ((pid = zfork(&bgtime))) {
3297///         for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
3298///         zclose(mn->pipe);
3299///         if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
3300///         mn->ct = 1; mn->fds[0] = fd;
3301///         addproc(pid, NULL, 1, &bgtime, -1, -1);
3302///         child_unblock(); return;
3303///     }
3304///     /* pid == 0 (child) */
3305///     opts[INTERACTIVE] = 0;
3306///     dont_queue_signals();
3307///     child_unblock();
3308///     closeallelse(mn);
3309///     if (mn->rflag) {
3310///         /* tee process: read mn->pipe, write each mn->fds[i] */
3311///     } else {
3312///         /* cat process: read each mn->fds[i], write mn->pipe */
3313///     }
3314///     _exit(0);
3315/// } else if (fd >= 0 && type == REDIR_CLOSE)
3316///     mfds[fd] = NULL;
3317/// ```
3318///
3319/// Success-path close of a multio. For ct>=2 (multiple-output
3320/// redirection), forks a tee/cat child that proxies bytes between
3321/// the original fd and the per-output fds. Single-output multios
3322/// (ct=1) skip the fork entirely and just clear the slot.
3323///
3324/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
3325/// tee/cat child in the current job's auxprocs.
3326pub fn closemn(mfds: &mut [Option<Box<multio>>; 10], fd: i32, type_: i32) {
3327    // c:2273
3328    // c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
3329    let needs_tee = fd >= 0
3330        && (fd as usize) < mfds.len()
3331        && mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
3332    if needs_tee {
3333        // c:2275
3334        // Take the multio out of the slot so we can move pieces into
3335        // the child without aliasing the slot.
3336        let mn = mfds[fd as usize].take().unwrap();
3337        let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
3338                                   // c:2287 — `child_block();` block SIGCHLD before fork race.
3339        child_block();
3340        // c:2288 — `pid = zfork(&bgtime);`
3341        let mut bgtime = ZshTimespec {
3342            tv_sec: 0,
3343            tv_nsec: 0,
3344        };
3345        let pid = zfork(Some(&mut bgtime));
3346        if pid != 0 {
3347            // c:2288 parent branch
3348            // c:2289-2290 — close all per-output fds.
3349            for i in 0..mn.ct as usize {
3350                if i < mn.fds.len() {
3351                    let _ = zclose(mn.fds[i]); // c:2290
3352                }
3353            }
3354            let _ = zclose(mn.pipe); // c:2291
3355            if pid == -1 {
3356                // c:2292
3357                // c:2293 — `mfds[fd] = NULL;` already done via .take()
3358                child_unblock(); // c:2294
3359                return; // c:2295
3360            }
3361            // c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
3362            let mut mn_back = mn;
3363            mn_back.ct = 1; // c:2297
3364            mn_back.fds[0] = fd; // c:2298
3365            mfds[fd as usize] = Some(mn_back);
3366            // c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
3367            // the tee/cat child in the current job's auxprocs (aux=true).
3368            if let Some(jt) = JOBTAB.get() {
3369                let mut guard = jt.lock().unwrap();
3370                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
3371                if tj >= 0 {
3372                    if let Some(j) = guard.get_mut(tj as usize) {
3373                        crate::ported::jobs::addproc(
3374                            j,
3375                            pid,
3376                            "",
3377                            true,
3378                            Some(std::time::Instant::now()),
3379                            -1,
3380                            -1,
3381                        );
3382                    }
3383                }
3384            }
3385            let _ = bgtime;
3386            child_unblock(); // c:2300
3387            return; // c:2301
3388        }
3389        // c:2303 — child branch (pid == 0).
3390        opt_state_set("interactive", false); // c:2304
3391        dont_queue_signals(); // c:2305
3392        child_unblock(); // c:2306
3393        closeallelse(&mn); // c:2307
3394                           // c:2308-2333 — tee or cat loop.
3395        if mn.rflag != 0 {
3396            // c:2308 — `mn->rflag` set → tee process
3397            // c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
3398            loop {
3399                let len = unsafe {
3400                    libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
3401                };
3402                if len == 0 {
3403                    break;
3404                }
3405                if len < 0 {
3406                    // c:2311
3407                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3408                    if e == libc::EINTR {
3409                        // c:2312
3410                        continue;
3411                    } else {
3412                        break; // c:2315
3413                    }
3414                }
3415                // c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
3416                for i in 0..mn.ct as usize {
3417                    if i >= mn.fds.len() {
3418                        break;
3419                    }
3420                    if write_loop(mn.fds[i], &buf[..len as usize]).is_err() {
3421                        break; // c:2319
3422                    }
3423                }
3424            }
3425        } else {
3426            // c:2321 — cat process
3427            for i in 0..mn.ct as usize {
3428                if i >= mn.fds.len() {
3429                    break;
3430                }
3431                // c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
3432                loop {
3433                    let len = unsafe {
3434                        libc::read(mn.fds[i], buf.as_mut_ptr() as *mut libc::c_void, buf.len())
3435                    };
3436                    if len == 0 {
3437                        break;
3438                    }
3439                    if len < 0 {
3440                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3441                        // c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
3442                        if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
3443                            continue;
3444                        } else {
3445                            break; // c:2329
3446                        }
3447                    }
3448                    // c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
3449                    if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
3450                        break; // c:2332
3451                    }
3452                }
3453            }
3454        }
3455        // c:2335 — `_exit(0);`
3456        unsafe {
3457            libc::_exit(0);
3458        }
3459    } else if fd >= 0 && type_ == REDIR_CLOSE {
3460        // c:2336
3461        // c:2337 — `mfds[fd] = NULL;`
3462        if (fd as usize) < mfds.len() {
3463            mfds[fd as usize] = None;
3464        }
3465    }
3466}
3467
3468/// Port of `closemnodes()` from `Src/exec.c:2344` — C decl `closemnodes(struct multio **mfds)`.
3469///
3470/// C body:
3471/// ```c
3472/// int i, j;
3473/// for (i = 0; i < 10; i++)
3474///     if (mfds[i]) {
3475///         for (j = 0; j < mfds[i]->ct; j++)
3476///             zclose(mfds[i]->fds[j]);
3477///         mfds[i] = NULL;
3478///     }
3479/// ```
3480///
3481/// Failure-path cleanup: close every fd stashed in any of the 10
3482/// multio slots and null the slot. Called from `execcmd_exec` when
3483/// a redirect setup fails partway through and we need to roll back.
3484pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
3485    // c:2344
3486    for i in 0..10 {
3487        // c:2348
3488        if let Some(mn) = mfds[i].take() {
3489            // c:2349
3490            for j in 0..mn.ct as usize {
3491                // c:2350
3492                if j < mn.fds.len() {
3493                    let _ = zclose(mn.fds[j]); // c:2351
3494                }
3495            }
3496            // c:2352 — `mfds[i] = NULL;` — handled by .take() above.
3497        }
3498    }
3499}
3500
3501/// Port of `closeallelse()` from `Src/exec.c:2358` — C decl `closeallelse(struct multio *mn)`.
3502///
3503/// C body:
3504/// ```c
3505/// int i, j;
3506/// long openmax;
3507/// openmax = fdtable_size;
3508/// for (i = 0; i < openmax; i++)
3509///     if (mn->pipe != i) {
3510///         for (j = 0; j < mn->ct; j++)
3511///             if (mn->fds[j] == i) break;
3512///         if (j == mn->ct)
3513///             zclose(i);
3514///     }
3515/// ```
3516///
3517/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
3518/// stashed in `mn->fds`. Called inside the multio tee/cat child
3519/// process to release every fd the parent had open — only the pipe
3520/// + per-output fds stay alive for the read/write loop.
3521pub fn closeallelse(mn: &multio) {
3522    // c:2358
3523    // c:2363 — `openmax = fdtable_size;`. fdtable_size starts at
3524    // `zopenmax()` (c:Src/init.c:1931) and only grows (check_fd_table,
3525    // c:Src/utils.c:1974-1981) to cover max_zsh_fd, so it spans every
3526    // descriptor the process may hold — not just the ones the table
3527    // records. Stopping at max_zsh_fd left the tee holding a descriptor
3528    // the table never saw (a user `exec 3>&1 3>&2` target, a Rust-side
3529    // pipe), and a tee that holds its own pipe's write end never exits.
3530    let openmax = std::cmp::max(
3531        MAX_ZSH_FD.load(Ordering::Relaxed) + 1,
3532        crate::ported::compat::zopenmax() as i32,
3533    ); // c:2363
3534    for i in 0..openmax {
3535        // c:2365
3536        if mn.pipe == i {
3537            // c:2366
3538            continue;
3539        }
3540        // c:2367-2369 — scan mn->fds[] for i; skip-close if found.
3541        let mut found = false;
3542        for j in 0..mn.ct as usize {
3543            // c:2367
3544            if j < mn.fds.len() && mn.fds[j] == i {
3545                // c:2368
3546                found = true;
3547                break; // c:2369
3548            }
3549        }
3550        // c:2370-2371 — `if (j == mn->ct) zclose(i);`
3551        if !found {
3552            // c:2371 zclose → c:Src/utils.c:2145 `close(fd)`, blind. zclose's
3553            // skip-unowned guard exists for sibling threads recycling fds;
3554            // this runs in the forked child, which has only one thread.
3555            unsafe { libc::close(i) }; // c:2371
3556        }
3557    }
3558}
3559
3560/// Port of `fixfds()` from `Src/exec.c:4523` — C decl `fixfds(int *save)`.
3561///
3562/// C body:
3563/// ```c
3564/// int old_errno = errno;
3565/// int i;
3566/// for (i = 0; i != 10; i++)
3567///     if (save[i] != -2)
3568///         redup(save[i], i);
3569/// errno = old_errno;
3570/// ```
3571///
3572/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
3573/// means "no save was made for this fd"; any other value is the
3574/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
3575/// caller's errno across the loop so a downstream caller diagnoses
3576/// the original failure, not a noisy dup2 errno.
3577pub fn fixfds(save: &[i32; 10]) {
3578    // c:4523
3579    let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
3580    for i in 0..10i32 {
3581        // c:4528 — `for (i = 0; i != 10; i++)`
3582        if save[i as usize] != -2 {
3583            // c:4529
3584            redup(save[i as usize], i); // c:4530
3585        }
3586    }
3587    // c:4531 — `errno = old_errno;`
3588    #[cfg(target_os = "macos")]
3589    unsafe {
3590        *libc::__error() = old_errno;
3591    }
3592    #[cfg(target_os = "linux")]
3593    unsafe {
3594        *libc::__errno_location() = old_errno;
3595    }
3596}
3597
3598/// Port of `closem()` from `Src/exec.c:4546` — C decl `closem(int how, int all)`.
3599///
3600/// C body:
3601/// ```c
3602/// int i;
3603/// for (i = 10; i <= max_zsh_fd; i++)
3604///     if (fdtable[i] != FDT_UNUSED &&
3605///         (all || (fdtable[i] != FDT_PROC_SUBST &&
3606///                  fdtable[i] != FDT_EXTERNAL)) &&
3607///         (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
3608///         if (i == SHTTY) SHTTY = -1;
3609///         zclose(i);
3610///     }
3611/// ```
3612///
3613/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
3614/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
3615/// type filter); otherwise only fds whose low-nibble type equals
3616/// `how` are closed. `all == 0` preserves user-visible fds
3617/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
3618/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
3619/// reference if we just closed the controlling tty.
3620pub fn closem(how: i32, all: i32) {
3621    // c:4546
3622    let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
3623    for i in 10i32..=max {
3624        // c:4550
3625        let kind = fdtable_get(i); // c:4551 fdtable[i]
3626        if kind == FDT_UNUSED {
3627            // c:4551
3628            continue;
3629        }
3630        // c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
3631        if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
3632            continue;
3633        }
3634        // c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
3635        if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
3636            continue;
3637        }
3638        // c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
3639        if i == SHTTY.load(Ordering::Relaxed) {
3640            // c:4560
3641            SHTTY.store(-1, Ordering::Relaxed); // c:4561
3642        }
3643        // c:4562 — `zclose(i);`
3644        let _ = zclose(i);
3645    }
3646}
3647
3648/// Port of `hashcmd()` from `Src/exec.c:1010` — C decl `hashcmd(char *arg0, char **pp)`.
3649///
3650/// C body:
3651/// ```c
3652/// Cmdnam cn;
3653/// char *s, buf[PATH_MAX+1];
3654/// char **pq;
3655/// if (*arg0 == '/') return NULL;
3656/// for (; *pp; pp++)
3657///     if (**pp == '/') {
3658///         s = buf;
3659///         struncpy(&s, *pp, PATH_MAX);
3660///         *s++ = '/';
3661///         if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
3662///         strcpy(s, arg0);
3663///         if (iscom(buf)) break;
3664///     }
3665/// if (!*pp) return NULL;
3666/// cn = (Cmdnam) zshcalloc(sizeof *cn);
3667/// cn->node.flags = 0;
3668/// cn->u.name = pp;
3669/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
3670/// if (isset(HASHDIRS)) {
3671///     for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
3672///     pathchecked = pp + 1;
3673/// }
3674/// return cn;
3675/// ```
3676///
3677/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
3678/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
3679/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
3680/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
3681/// so subsequent commands hit the cache.
3682///
3683/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
3684/// `None` if `arg0` is absolute or no PATH entry contains it.
3685pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
3686    // c:1010
3687    // c:1055 — `if ((*arg0 == '/') || !strncmp(arg0, "./", 2) ||
3688    //            !strncmp(arg0, "../", 3)) return NULL;`
3689    // The doc-comment above quotes an older zsh where only the leading `/`
3690    // was rejected; current C rejects the two explicitly-relative spellings
3691    // too, and this port had kept the old single test.
3692    if arg0.starts_with('/') || arg0.starts_with("./") || arg0.starts_with("../") {
3693        return None; // c:1056
3694    }
3695    // c:1018-1028 — walk pp[] for first matching absolute entry.
3696    let mut found_idx: Option<usize> = None;
3697    for (i, dir) in pp.iter().enumerate() {
3698        // c:1018
3699        if !dir.starts_with('/') {
3700            // c:1019
3701            continue;
3702        }
3703        // c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
3704        if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
3705            // c:1023
3706            continue; // c:1024
3707        }
3708        let buf = format!("{}/{}", dir, arg0); // c:1025
3709        if iscom(&buf) {
3710            // c:1026
3711            found_idx = Some(i);
3712            break; // c:1027
3713        }
3714    }
3715    // c:1030-1031 — `if (!*pp) return NULL;`
3716    let pp_idx = match found_idx {
3717        Some(i) => i,
3718        None => return None, // c:1031
3719    };
3720    // c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
3721    let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
3722    let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
3723                                                // c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
3724    if let Ok(mut tab) = cmdnamtab_lock().write() {
3725        tab.add(cn.clone());
3726    }
3727    // c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
3728    // including the matching one, then bump pathchecked past it.
3729    if isset(HASHDIRS) {
3730        // c:1038
3731        let start = pathchecked.load(Ordering::Relaxed); // c:1039
3732        for pq in start..=pp_idx {
3733            // c:1039
3734            if pq < pp.len() {
3735                hashdir(&pp[pq], pq); // c:1040
3736            }
3737        }
3738        pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
3739    }
3740    Some(cn) // c:1044
3741}
3742
3743/// Port of `zfork()` from `Src/exec.c:349` — C decl `zfork(struct timespec *ts)`.
3744///
3745/// C body:
3746/// ```c
3747/// pid_t pid;
3748/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
3749///     zerr("job table full");
3750///     return -1;
3751/// }
3752/// if (ts) zgettime_monotonic_if_available(ts);
3753/// queue_signals();
3754/// pid = fork();
3755/// unqueue_signals();
3756/// if (pid == -1) {
3757///     zerr("fork failed: %e", errno);
3758///     return -1;
3759/// }
3760/// #ifdef HAVE_GETRLIMIT
3761/// if (!pid) setlimits(NULL);
3762/// #endif
3763/// return pid;
3764/// ```
3765///
3766/// fork(2) wrapper with jobtab capacity check + child rlimit
3767/// re-application. Used by every subshell-spawning path: pipelines,
3768/// process substitution, async commands, command substitution.
3769pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
3770    // c:349
3771    let pid: libc::pid_t;
3772
3773    // c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
3774    let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
3775    let thisjob = *thisjob_lock.lock().unwrap();
3776    if thisjob != -1 {
3777        // c:356
3778        let needed = (thisjob + 1) as usize;
3779        let needs_expand = JOBTAB
3780            .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3781            .lock()
3782            .map(|t| needed >= t.len().saturating_sub(1))
3783            .unwrap_or(false);
3784        if needs_expand {
3785            let mut tab = JOBTAB.get().unwrap().lock().unwrap();
3786            if !expandjobtab(&mut tab, needed) {
3787                // c:357
3788                zerr("job table full"); // c:357
3789                return -1; // c:358
3790            }
3791        }
3792    }
3793    // c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
3794    if let Some(ts) = ts {
3795        zgettime_monotonic_if_available(ts);
3796    }
3797    // c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
3798    queue_signals(); // c:368
3799    pid = unsafe { libc::fork() }; // c:369
3800    unqueue_signals(); // c:370
3801                       // c:371-374 — fork failure.
3802    if pid == -1 {
3803        // c:371
3804        zerr(&format!(
3805            // c:372
3806            "fork failed: {}",
3807            std::io::Error::last_os_error()
3808        ));
3809        return -1; // c:373
3810    }
3811    // c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
3812    #[cfg(unix)]
3813    if pid == 0 {
3814        // c:376
3815        let _ = setlimits(""); // c:378
3816    }
3817    pid // c:380
3818}
3819
3820/// Port of `loadautofnsetfile()` from `Src/exec.c:5657` — C decl `loadautofnsetfile(Shfunc shf, char *fdir)`.
3821///
3822/// C body:
3823/// ```c
3824/// if (!(shf->node.flags & PM_LOADDIR) ||
3825///     strcmp(shf->filename, fdir) != 0) {
3826///     dircache_set(&shf->filename, NULL);
3827///     if (fdir) {
3828///         shf->node.flags |= PM_LOADDIR;
3829///         dircache_set(&shf->filename, fdir);
3830///     } else {
3831///         shf->node.flags &= ~PM_LOADDIR;
3832///         shf->filename = ztrdup(shf->node.nam);
3833///     }
3834/// }
3835/// ```
3836///
3837/// Update `shf->filename` to the autoload directory `fdir`. Routes
3838/// through the refcounted `dircache_set` so identical directory
3839/// strings are shared across shfunc table entries.
3840pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
3841    // c:5657
3842    // c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
3843    let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
3844    let same = match (&shf.filename, fdir) {
3845        (Some(a), Some(b)) => a == b,
3846        _ => false,
3847    };
3848    if !loaddir || !same {
3849        // c:5664
3850        // c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
3851        dircache_set(&mut shf.filename, None);
3852        if let Some(fdir) = fdir {
3853            // c:5668
3854            shf.node.flags |= PM_LOADDIR as i32; // c:5670
3855            dircache_set(&mut shf.filename, Some(fdir)); // c:5671
3856        } else {
3857            // c:5672
3858            shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
3859            shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
3860        }
3861    }
3862}
3863
3864/// Port of `commandnotfound()` from `Src/exec.c:669` — C decl `commandnotfound(char *arg0, LinkList args)`.
3865///
3866/// C body:
3867/// ```c
3868/// Shfunc shf = (Shfunc)
3869///     shfunctab->getnode(shfunctab, "command_not_found_handler");
3870/// if (!shf) {
3871///     lastval = 127;
3872///     return 1;
3873/// }
3874/// pushnode(args, arg0);
3875/// lastval = doshfunc(shf, args, 1);
3876/// return 0;
3877/// ```
3878///
3879/// Look up the user-defined `command_not_found_handler` shfunc and
3880/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
3881/// 1 if no handler (so caller emits the standard "command not found"
3882/// error). Sets `$?` to 127 in the no-handler path.
3883pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
3884    // c:669
3885    // c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
3886    let has_handler = shfunctab_lock()
3887        .read()
3888        .map(|t| t.get("command_not_found_handler").is_some())
3889        .unwrap_or(false);
3890    if !has_handler {
3891        // c:674
3892        LASTVAL.store(127, Ordering::Relaxed); // c:675
3893        return 1; // c:676
3894    }
3895    // c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
3896    // is the first positional arg per C convention).
3897    args.insert(0, arg0.to_string());
3898    args.insert(0, "command_not_found_handler".to_string());
3899    // c:680 — `lastval = doshfunc(shf, args, 1);`. Direct doshfunc
3900    // call mirrors C — body_runner routes through the host body-only
3901    // entry so the function body runs once inside doshfunc's scope.
3902    let shf_clone: Option<shfunc> = shfunctab_lock()
3903        .read()
3904        .ok()
3905        .and_then(|t| t.get("command_not_found_handler").cloned());
3906    if let Some(mut shf) = shf_clone {
3907        let body_args = args.clone();
3908        let body_runner = move || -> i32 {
3909            crate::ported::exec::run_function_body("command_not_found_handler", &body_args[1..])
3910                .unwrap_or(0)
3911        };
3912        let lv = doshfunc(&mut shf, args.clone(), true, body_runner);
3913        LASTVAL.store(lv, Ordering::Relaxed);
3914    }
3915    0 // c:681
3916}
3917
3918/// Port of `namedpipe()` from `Src/exec.c:5001` — C decl `namedpipe(void)`.
3919///
3920/// C body (#ifdef HAVE_FIFOS branch):
3921/// ```c
3922/// char *tnam = gettempname(NULL, 1);
3923/// if (!tnam) {
3924///     zerr("failed to create named pipe: %e", errno);
3925///     return NULL;
3926/// }
3927/// if (mkfifo(tnam, 0600) < 0) {
3928///     zerr("failed to create named pipe: %s, %e", tnam, errno);
3929///     return NULL;
3930/// }
3931/// return tnam;
3932/// ```
3933///
3934/// Create a FIFO with a unique name for process substitution. Used by
3935/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
3936pub fn namedpipe() -> Option<String> {
3937    // c:5001
3938    let tnam = gettempname(None, true); // c:5003
3939    let tnam = match tnam {
3940        Some(t) => t,
3941        None => {
3942            // c:5005
3943            zerr(&format!(
3944                // c:5006
3945                "failed to create named pipe: {}",
3946                std::io::Error::last_os_error()
3947            ));
3948            return None; // c:5007
3949        }
3950    };
3951    // c:5010 — `mkfifo(tnam, 0600)`.
3952    let cstr = match std::ffi::CString::new(tnam.as_str()) {
3953        Ok(c) => c,
3954        Err(_) => return None,
3955    };
3956    if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
3957        // c:5010
3958        zerr(&format!(
3959            // c:5014
3960            "failed to create named pipe: {}, {}",
3961            tnam,
3962            std::io::Error::last_os_error()
3963        ));
3964        return None; // c:5015
3965    }
3966    Some(tnam) // c:5017
3967}
3968
3969/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
3970///
3971/// C body:
3972/// ```c
3973/// char *str;
3974/// Eprog prog;
3975/// for (str = cmd + 2; *str && *str != Outpar; str++);
3976/// if (!*str || cmd[1] != Inpar) {
3977///     char *errstr = dupstrpfx(cmd, 2);
3978///     untokenize(errstr);
3979///     zerr("unterminated `%s...)'", errstr);
3980///     return NULL;
3981/// }
3982/// *str = '\0';
3983/// if (eptr) *eptr = str+1;
3984/// if (!(prog = parse_string(cmd + 2, 0))) {
3985///     zerr("parse error in process substitution");
3986///     return NULL;
3987/// }
3988/// return prog;
3989/// ```
3990///
3991/// Port of `readoutput()` from `Src/exec.c:4805` — C decl `readoutput(int in, int qt, int *readerror)`.
3992/// Drain a command-substitution pipe fd and
3993/// return the captured output split per `qt`.
3994///
3995/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
3996/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
3997/// per c:4861).
3998/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
3999/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
4000///
4001/// `readerror` is set to the errno on read failure, 0 on clean EOF.
4002pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
4003    // c:4805
4004    let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
4005    let mut readret: isize = 0; // c:4818 readret tracks last read return
4006                                // c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
4007                                // dance keeps SIGCHLD live so the foreground process can be reaped
4008                                // while we drain. zshrs's in-process command-sub runs without the
4009                                // queue (no fork), but the C call surface is preserved for parity.
4010    dont_queue_signals(); // c:4824
4011    child_unblock(); // c:4825
4012    let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
4013    loop {
4014        // c:4826
4015        // c:4828 — `readret = read(in, inbuf, 64);`
4016        let r = unsafe { libc::read(in_fd, inbuf.as_mut_ptr() as *mut libc::c_void, inbuf.len()) };
4017        readret = r as isize;
4018        if readret <= 0 {
4019            // c:4829
4020            if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
4021                // c:4830 — `if (readret < 0 && errno == EINTR) continue;`
4022                continue;
4023            }
4024            break; // c:4832
4025        }
4026        // c:4835-4849 — `for (bufptr = inbuf; ...) { c = *bufptr; if
4027        // (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; } ... *ptr++ = c; }`
4028        //
4029        // !!! DELIBERATE DEVIATION — the byte-level metafication is DROPPED.
4030        //
4031        // C's buffer is a `char *` that may hold arbitrary bytes, so it
4032        // escapes every `imeta` byte (NUL, and Meta 0x83 .. Marker 0xa2 —
4033        // `Src/utils.c:4195-4201`) to keep raw input bytes from colliding
4034        // with the token bytes the lexer/glob layers use.
4035        //
4036        // zshrs's buffer is a Rust `String`. A lone 0x83 is not valid UTF-8,
4037        // so `String::from_utf8_lossy` below replaced every emitted `Meta`
4038        // with U+FFFD — the metafied form could never survive the conversion,
4039        // and the "escaped" payload byte was left stranded next to a
4040        // replacement char. `utils::spacesplit`'s Meta decoder
4041        // (`utils.rs:5307`, `bytes[i] == Meta`) can never see a real Meta in a
4042        // `String` for the same reason, so nothing downstream un-did it
4043        // either. The loop was pure corruption for any input containing a
4044        // byte in {0x00} ∪ [0x83,0xa2] — which includes the UTF-8 tails of
4045        // common characters (U+2003 EM SPACE is `e2 80 83`, U+2022 BULLET is
4046        // `e2 80 a2`).
4047        //
4048        // The in-process `$( cmd )` arm in `getoutput` (:619) never had a
4049        // metafication step at all — it receives a plain `String` from
4050        // `run_command_substitution`. Dropping it here is what makes the two
4051        // arms hand this tail the SAME representation, which is the
4052        // property C gets for free by routing both arms through `readoutput`.
4053        buf.extend_from_slice(&inbuf[..readret as usize]); // c:4848
4054    }
4055    child_block(); // c:4854
4056                   // c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
4057    *readerror = if readret < 0 {
4058        std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
4059    } else {
4060        0
4061    };
4062    // c:4857 — `close(in);`
4063    unsafe {
4064        libc::close(in_fd);
4065    }
4066    // ── c:4858-4872 tail ──────────────────────────────────────────────
4067    // !!! THIS BLOCK IS DUPLICATED at `getoutput` (:619), which is the
4068    // `$( cmd )` arm. C needs no duplicate: both of getoutput's arms
4069    // (c:4746 `$(< file)` and c:4772 the pipe) call `readoutput`, so they
4070    // share this tail by construction. zshrs collapses the fork, so its
4071    // `$( cmd )` arm holds an in-memory String and has no fd to hand
4072    // `readoutput`. Factoring the tail into a helper is what build.rs's
4073    // no-new-functions gate on src/ported/ forbids (there is no C function
4074    // to name it after), and its own remedy #1 is "inline the body at every
4075    // call site" — so the two copies must be kept in lockstep BY HAND.
4076    // ANY EDIT HERE MUST BE MIRRORED AT :619 AND VICE VERSA. The copies
4077    // already drifted once: the getoutput copy (:619) had silently lost the
4078    // c:4868-4869 `isset(GLOBSUBST)` → `shtokenize` step, so `setopt
4079    // globsubst; echo $(echo '*')` did not glob while `$(< f)` did.
4080    // c:4835-4849 — this is where C's `metafy` lands the read bytes. The
4081    // byte-level loop stays dropped (see the deviation note at :3799), but
4082    // the CONVERSION must still be lossless: `String::from_utf8_lossy` here
4083    // turned every byte that is not valid UTF-8 into U+FFFD, so `$(< f)` on
4084    // a latin-1 file printed `caf\xef\xbf\xbd` where zsh prints `caf\xe9`.
4085    // `decode_script_bytes` keeps valid UTF-8 as `char`s and Meta-encodes
4086    // only the bytes that cannot be, which `utils::unmetafy_str` turns back
4087    // into the original byte at the output boundary.
4088    let s = crate::script_bytes::decode_script_bytes(&buf);
4089    // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
4090    let s = s.trim_end_matches('\n');
4091    // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
4092    if qt != 0 {
4093        // c:4861
4094        if s.is_empty() {
4095            return vec![String::from(Nularg)]; // c:4862
4096        }
4097        return vec![s.to_string()]; // c:4864
4098    }
4099    // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
4100    let mut words = crate::ported::utils::spacesplit(s, false); // c:4867
4101    if isset(crate::ported::zsh_h::GLOBSUBST) {
4102        // c:4870
4103        for w in words.iter_mut() {
4104            crate::ported::glob::shtokenize(w); // c:4870
4105        }
4106    }
4107    words
4108}
4109
4110/// Port of `parsecmd()` from `Src/exec.c:4934` — C decl `parsecmd(char *cmd, char **eptr)`.
4111/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
4112/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
4113/// remainder is the command up to the matching `Outpar`. Returns the
4114/// parsed Eprog (and writes the post-`)` cursor through `eptr`, as a
4115/// BYTE offset into `cmd`). The tokens are multi-byte in a Rust `String`,
4116/// so the scan walks chars: comparing single bytes against `Inpar` /
4117/// `Outpar` (as the port did) could never match and every call failed.
4118pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
4119    // c:4934
4120    let mut it = cmd.char_indices();
4121    let _marker = it.next(); // cmd[0]
4122    let second = it.next().map(|(_, c)| c); // cmd[1]
4123    let body_start = it.next().map_or(cmd.len(), |(i, _)| i); // cmd + 2
4124    // c:4939 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
4125    let close = cmd[body_start..].find(Outpar).map(|i| body_start + i);
4126    // c:4940 — `if (!*str || cmd[1] != Inpar)`.
4127    let close = match close {
4128        Some(c) if second == Some(Inpar) => c,
4129        _ => {
4130            // c:4941-4946 — `This can happen if the expression is being parsed
4131            // inside another construct, e.g. as a value within ${..:..} etc.
4132            // So print a proper error message instead of the not very
4133            // useful but traditional "oops".`
4134            let pfx_end = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4135            let errstr = untokenize(&cmd[..pfx_end]); // c:4947-4948
4136            zerr(&format!("unterminated `{}...)'", errstr)); // c:4949
4137            return None; // c:4950
4138        }
4139    };
4140    // c:4952 — `*str = '\0';` — cmd[close] becomes the terminator.
4141    // c:4953-4954 — `if (eptr) *eptr = str + 1;`
4142    if let Some(p) = eptr {
4143        *p = close + Outpar.len_utf8();
4144    }
4145    // c:4955 — `parse_string(cmd + 2, 0)`.
4146    let prog = parse_string(&cmd[body_start..close], 0);
4147    if prog.is_none() {
4148        // c:4955
4149        zerr("parse error in process substitution"); // c:4956
4150        return None; // c:4957
4151    }
4152    prog // c:4959
4153}
4154
4155
4156/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
4157/// front of a script when probing for a `#!` shebang line.
4158pub const POUNDBANGLIMIT: usize = 128;
4159
4160/// Port of `makecline()` from `Src/exec.c:2046` — C decl `makecline(LinkList list)`.
4161///
4162/// Builds the argv array from a command's args list. The C version
4163/// allocates with a 4-slot prepad (2 reserved at the front for the
4164/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
4165/// doesn't need this since we rebuild the Vec on shebang re-exec
4166/// (see zexecve WARNING e).
4167///
4168/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
4169/// (stderr), preceded by the PS4 prefix when first command of the line.
4170pub fn makecline(list: &[String]) -> Vec<String> {
4171    // c:2046
4172    if isset(XTRACE) {
4173        // c:2055
4174        if doneps4.load(Ordering::Relaxed) == 0 {
4175            // c:2056
4176            printprompt4(); // c:2057
4177        }
4178        let mut first = true;
4179        let mut err = std::io::stderr().lock();
4180        use std::io::Write;
4181        for s in list.iter() {
4182            // c:2059
4183            if !first {
4184                let _ = err.write_all(b" "); // c:2063
4185            }
4186            first = false;
4187            let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
4188        }
4189        let _ = err.write_all(b"\n"); // c:2065
4190        let _ = err.flush(); // c:2066
4191    }
4192    list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
4193}
4194
4195/// Port of `execute()` from `Src/exec.c:723` — C decl `execute(LinkList args, int flags, int defpath)`.
4196/// The canonical "child runs the simple
4197/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
4198/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
4199/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
4200/// commandnotfound-handler fallback and the final exit-code escape
4201/// (127 not-found / 126 noperm).
4202///
4203/// =================== WARNING — DIVERGENCE ====================
4204/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
4205///     fast-path wired via cmdnamtab_lock(); jumps direct to
4206///     `cn.cmd` absolute path before the $PATH scan. Unhashed
4207///     cursor-walk (c:830-846) still falls to the full $PATH scan;
4208///     observable behavior matches C when the hash hit is HASHED.
4209/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
4210///     not-yet-ported `doshfunc` for the `command_not_found_handler`
4211///     shell function. Already routes through executor dispatch
4212///     (see exec.rs:2783).
4213/// (c) `_realexit()` (c:810, 874) — bare `std::process::exit`.
4214/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
4215///     FD_CLOEXEC platform default (macOS, Linux).
4216/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
4217///     `defpath` (`command -p`) walks DEFAULT_PATH via
4218///     search_defpath (already ported).
4219/// =============================================================
4220pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
4221    // c:723
4222    let mut eno: i32 = 0;
4223    let mut ee: i32; // c:729
4224                     // c:737 — `arg0 = (char *) peekfirst(args);` — the COMMAND WORD.
4225    let arg0 = if args.is_empty() {
4226        return;
4227    } else {
4228        args[0].clone()
4229    }; // c:737
4230       // c:733-748 — STTY pre-exec handling.
4231    {
4232        let mut stty = STTYval.lock().unwrap();
4233        if let Some(s) = stty.take() {
4234            // c:738 — STTYval = 0 to break recursion.
4235            if !s.is_empty()
4236                && unsafe { libc::isatty(0) } != 0
4237                && unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
4238            {
4239                drop(stty);
4240                let cmd = format!("stty {}", s); // c:739
4241                execstring(&cmd, 1, 0, "stty"); // c:743
4242            }
4243        }
4244    }
4245    // c:758-777 — ARGV0 override.
4246    //   c:758-759 — /* If ARGV0 is in the commands environment, we use *
4247    //                * that as argv[0] for this external command       */
4248    // NOTE: C rewrites only `argv[0]` (the exec argv), NEVER `arg0`
4249    // (`peekfirst(args)`, c:737) — `arg0` stays the COMMAND WORD and is what
4250    // the slash-exec / cmdnamtab / $PATH scan below resolves. The previous
4251    // Rust port assigned `arg0 = args[0].clone()` in both arms, so
4252    // `ARGV0=sh $cmdvar args` looked up `sh` on $PATH instead of `$cmdvar`
4253    // (E03posix:16 — `command not found: sh`), and a `-` precommand made the
4254    // shell search for `-name`.
4255    if let Some(z) = zgetenv("ARGV0") {
4256        args[0] = z.clone(); // c:761 `argv[0] = ztrdup(z);`
4257        unsafe {
4258            let key = std::ffi::CString::new("ARGV0").unwrap();
4259            libc::unsetenv(key.as_ptr()); // c:768
4260        }
4261    } else if (flags & BINF_DASH) != 0 {
4262        // c:772-776 — /* Else if the pre-command `-' was given, we add `-' *
4263        //              * to the front of argv[0] for this command.         */
4264        args[0] = format!("-{}", arg0); // c:775-776
4265    }
4266    let argv = makecline(args); // c:771
4267    let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
4268        Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
4269    } else {
4270        None
4271    };
4272    let newenvp = newenvp_owned.as_deref();
4273    closem(FDT_XTRACE, 0); // c:779
4274                           // c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
4275    child_unblock(); // c:786
4276    if arg0.len() >= libc::PATH_MAX as usize {
4277        // c:787
4278        zerr(&format!("command too long: {}", arg0)); // c:788
4279        unsafe {
4280            libc::_exit(1);
4281        } // c:789
4282    }
4283    // c:791-801 — slash in arg0 → direct exec.
4284    if let Some(slash_pos) = arg0.find('/') {
4285        let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
4286        let is_dot = arg0.starts_with('.')
4287            && (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
4288        if slash_pos == 0 || unset(PATHDIRS) || is_dot {
4289            // c:794
4290            // c:797 — `zerr("%e: %s", lerrno, arg0)`. `%e` is zerrmsg's
4291            // errno arm (Src/utils.c:348-365): strerror with the first
4292            // letter lowercased, NOT Rust's `io::Error` Display (which
4293            // appends " (os error N)" and keeps the capital, so `~`
4294            // printed `Permission denied (os error 13): /Users/wizard`
4295            // where zsh prints `permission denied: /Users/wizard`).
4296            zerr(&format!(
4297                "{}: {}",
4298                crate::ported::utils::zsh_errno_msg(lerrno),
4299                arg0
4300            )); // c:797
4301            let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
4302                126
4303            } else {
4304                127
4305            };
4306            unsafe {
4307                libc::_exit(code);
4308            } // c:798
4309        }
4310    }
4311    if defpath != 0 {
4312        // c:804 — `command -p` default-path search.
4313        let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
4314            Some(p) => p, // c:808
4315            None => {
4316                if commandnotfound(&arg0, args) == 0 {
4317                    // c:809
4318                    unsafe {
4319                        libc::_exit(LASTVAL.load(Ordering::Relaxed));
4320                    }
4321                }
4322                zerr(&format!("command not found: {}", crate::ported::utils::nicedupstring(&arg0))); // c:818 — `%s` is nicezputs (c:Src/utils.c:316)
4323                unsafe {
4324                    libc::_exit(127);
4325                } // c:812
4326            }
4327        };
4328        ee = zexecve(&pbuf, &argv, newenvp); // c:815
4329        let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
4330        if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
4331            // c:819
4332            eno = ee;
4333        }
4334    } else {
4335        // c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
4336        // jump straight to the absolute path stored in `cn.cmd`,
4337        // skipping the full $PATH scan (one exec attempt vs N).
4338        // c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
4339        let hashed_path: Option<String> = {
4340            let tab = cmdnamtab_lock().read().ok();
4341            tab.and_then(|t| {
4342                t.get(&arg0).and_then(|cn| {
4343                    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
4344                        // c:827-828 — `strcpy(nn, cn->u.cmd);`
4345                        cn.cmd.clone()
4346                    } else {
4347                        None
4348                    }
4349                })
4350            })
4351        };
4352        if let Some(nn) = hashed_path {
4353            // c:848 — `ee = zexecve(nn, argv, newenvp);`
4354            ee = zexecve(&nn, &argv, newenvp);
4355            let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
4356            if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
4357                eno = ee;
4358            }
4359            // If the hashed entry's exec failed without a "good" error,
4360            // we still need the $PATH fallback — fall through.
4361            if eno == 0 && ee != 0 {
4362                // Reset for the $PATH scan below.
4363                ee = 0;
4364            }
4365        }
4366        // c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
4367        // optimization but C also walks the rest of `path` if the hashed
4368        // exec failed with a non-"good" error).
4369        let path_str = getsparam("PATH").unwrap_or_default();
4370        for pp in path_str.split(':') {
4371            if pp.is_empty() || pp == "." {
4372                // c:856
4373                ee = zexecve(&arg0, &argv, newenvp); // c:857
4374                if isgooderr(ee, pp) {
4375                    eno = ee;
4376                }
4377            } else {
4378                // c:860
4379                let candidate = format!("{}/{}", pp, arg0); // c:861-864
4380                ee = zexecve(&candidate, &argv, newenvp); // c:865
4381                if isgooderr(ee, pp) {
4382                    eno = ee;
4383                }
4384            }
4385        }
4386    }
4387    // c:871-881 — final error reporting.
4388    if eno != 0 {
4389        // c:871
4390        // c:872 — `zerr("%e: %s", eno, arg0)`; same `%e` (Src/utils.c:
4391        // 348-365) rendering as the slash-in-arg0 branch above.
4392        zerr(&format!(
4393            "{}: {}",
4394            crate::ported::utils::zsh_errno_msg(eno),
4395            arg0
4396        )); // c:872
4397    } else if commandnotfound(&arg0, args) == 0 {
4398        // c:873
4399        unsafe {
4400            libc::_exit(LASTVAL.load(Ordering::Relaxed));
4401        } // c:874
4402    } else {
4403        zerr(&format!("command not found: {}", crate::ported::utils::nicedupstring(&arg0))); // c:903 — `%s` is nicezputs (c:Src/utils.c:316)
4404    }
4405    let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
4406        126
4407    } else {
4408        127
4409    }; // c:881
4410    unsafe {
4411        libc::_exit(code);
4412    }
4413}
4414
4415/// Port of `zexecve()` from `Src/exec.c:504` — C decl `zexecve(char *pth, char **argv, char **newenvp)`.
4416/// Wraps `execve(2)` with:
4417///   - `$_` env var stamped to absolute `pth` (c:514-520)
4418///   - winch signal unblock right before the syscall (c:527)
4419///   - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
4420///     bytes, parses a `#!interp arg` shebang and re-execs the
4421///     interpreter (c:534-628). For `ENOEXEC` with no shebang,
4422///     binary-safety check then falls back to `/bin/sh script` per
4423///     POSIX (c:588-628).
4424///
4425/// Returns `errno` from the failing exec — execve only returns on
4426/// failure, so success means the calling process is already replaced.
4427///
4428/// =================== WARNING — DIVERGENCE ====================
4429/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
4430///     string; Rust uses a stack `String` (consumed by `zputenv`).
4431/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
4432///     unconditionally as a no-op when FD_CLOEXEC is platform default.
4433/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
4434///     c:639-642 — handled implicitly via &str ↔ CString.
4435/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
4436///     drop the metafy and pass byte ranges to zerr directly.
4437/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
4438///     (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
4439///     `Vec<String>` with interp + optional arg + original argv tail
4440///     since Vec doesn't expose negative indexing.
4441/// (f) `environ` is FFI-loaded only when `newenvp` is None.
4442/// =============================================================
4443pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
4444    // c:504
4445    use std::ffi::CString;
4446    // c:514-520 — `_=pth` env stamping.
4447    let pth_abs = if pth.starts_with('/') {
4448        // c:516
4449        pth.to_string() // c:517
4450    } else {
4451        // c:518
4452        format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
4453    };
4454    zputenv(&format!("_={}", pth_abs)); // c:520
4455    closedumps(); // c:522
4456    winch_unblock(); // c:527
4457    let cpth = match CString::new(pth) {
4458        Ok(c) => c,
4459        Err(_) => return libc::ENOENT,
4460    };
4461    let cargs: Vec<CString> = argv
4462        .iter()
4463        .filter_map(|a| CString::new(a.as_str()).ok())
4464        .collect();
4465    let mut argv_ptrs: Vec<*const libc::c_char> = cargs.iter().map(|c| c.as_ptr()).collect();
4466    argv_ptrs.push(std::ptr::null());
4467    let env_holder: Vec<CString>;
4468    let env_ptrs: Vec<*const libc::c_char>;
4469    let envp: *const *const libc::c_char = match newenvp {
4470        Some(env) => {
4471            env_holder = env
4472                .iter()
4473                .filter_map(|e| CString::new(e.as_str()).ok())
4474                .collect();
4475            env_ptrs = {
4476                let mut v: Vec<*const libc::c_char> =
4477                    env_holder.iter().map(|c| c.as_ptr()).collect();
4478                v.push(std::ptr::null());
4479                v
4480            };
4481            env_ptrs.as_ptr()
4482        }
4483        None => unsafe {
4484            extern "C" {
4485                static environ: *const *const libc::c_char;
4486            }
4487            environ
4488        },
4489    };
4490    unsafe {
4491        libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
4492    }
4493    let eno = std::io::Error::last_os_error()
4494        .raw_os_error()
4495        .unwrap_or(libc::ENOEXEC); // c:534
4496    if eno == libc::ENOEXEC || eno == libc::ENOENT {
4497        // c:534 — the `#!` / shebang-less recovery; see zexecve_recover.
4498        match crate::vm_helper::zexecve_recover(pth, argv, eno) {
4499            // c:566/571/581/585/627 — `execve(<prog>, <argv>, newenvp);`
4500            Ok((prog, argv_new)) => return zexecve(&prog, &argv_new, newenvp),
4501            Err(e) => return e, // c:643
4502        }
4503    }
4504    eno // c:643
4505}
4506
4507/// Port of `getoutputfile()` from `Src/exec.c:4910` — C decl `getoutputfile(char *cmd, char **eptr)`.
4508/// `=(cmd)` process substitution.
4509///
4510/// Substitutes the cmd's stdout into a temp file, returns the
4511/// filename. Optimised path: `=(<<<heredoc-str)` writes the
4512/// heredoc body directly without a fork.
4513///
4514/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
4515///     so the temp file gets cleaned at job exit.
4516/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
4517///     Behavior matches the `full=0` case anyway.
4518/// (c) This is a REAL fork (`zfork` below), not a collapsed one, so the
4519///     child gets the full `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)`
4520///     (c:4986) via the canonical port at exec.rs:4329 — no in-process
4521///     `SubshStateGuard` substitute is needed or wanted here.
4522/// (d) `execode` is now ported (exec.rs:6047) — the body still
4523///     re-feeds through fusevm for cache coherence with execstring.
4524/// (e) `_realexit` flushes stdio + jobs + history. We use bare
4525///     `std::process::exit(0)` for now.
4526/// (f) TMPSUFFIX link()-rename block (c:4951-4958) deferred; rare
4527///     `setopt suffix_alias` interaction with =(…).
4528pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4529    // c:4910
4530    let bytes = cmd.as_bytes();
4531    let _ = bytes;
4532    // c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
4533    let mut ends_at: usize = 0;
4534    let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
4535    if let Some(p) = eptr {
4536        *p = ends_at;
4537    }
4538    let mut nam = gettempname(None, true)?; // c:4924
4539                                            // c:4927 — `simple_redir_name` opt for `=(<<<str)`.
4540    let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
4541        // c:4933
4542        let mut sub = singsub(&raw); // c:4933
4543        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4544            // c:4934
4545            String::new() // c:4935 — sentinel; checked below
4546        } else {
4547            sub = untokenize(&sub); // c:4937
4548            dyncat(&sub, "\n") // c:4938
4549        }
4550    });
4551    if let Some(ref sv) = s {
4552        if sv.is_empty() {
4553            s = None;
4554        }
4555    }
4556    if s.is_none() {
4557        // c:4942
4558        child_block(); // c:4943
4559    }
4560    // c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
4561    let c_nam = match std::ffi::CString::new(nam.clone()) {
4562        Ok(c) => c,
4563        Err(_) => {
4564            if s.is_none() {
4565                child_unblock();
4566            }
4567            return None;
4568        }
4569    };
4570    let fd = unsafe {
4571        libc::open(
4572            c_nam.as_ptr(),
4573            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
4574            0o600 as libc::c_uint,
4575        )
4576    };
4577    if fd < 0 {
4578        // c:4945
4579        zerr(&format!(
4580            "process substitution failed: {}",
4581            std::io::Error::last_os_error()
4582        )); // c:4946
4583        if s.is_none() {
4584            child_unblock(); // c:4948
4585        }
4586        return None; // c:4949
4587    }
4588    // c:4951-4958 — TMPSUFFIX link block (see WARNING f).
4589    // c:4960 — `addfilelist(nam, 0);` — register temp file in current
4590    // job's filelist so it's unlinked at job exit (not relying on the
4591    // OS temp-reaper).
4592    // zshrs runs commands in-process and the VM owns that job-end unlink
4593    // (the `=(cmd)` temp files the compiled path creates are parked there
4594    // too), so the parent also parks the name there on every return below.
4595    // Not before the fork: the child's own commands would drain the
4596    // inherited list and unlink the file it is writing.
4597    if let Some(jt) = JOBTAB.get() {
4598        let mut guard = jt.lock().unwrap();
4599        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4600        if tj >= 0 {
4601            if let Some(j) = guard.get_mut(tj as usize) {
4602                crate::ported::jobs::addfilelist(j, Some(&nam), 0);
4603            }
4604        }
4605    }
4606    if let Some(sv) = s {
4607        // c:4962 — optimised here-string write path.
4608        let mut buf: Vec<u8> = sv.into_bytes();
4609        let _len = unmetafy(&mut buf); // c:4965
4610        let _ = write_loop(fd, &buf); // c:4966
4611        unsafe {
4612            libc::close(fd);
4613        } // c:4967
4614        crate::fusevm_bridge::psub_pending_file_add(&nam);
4615        return Some(nam); // c:4968
4616    }
4617    // c:4971 — `cmdoutpid = pid = zfork(NULL)`.
4618    let pid = zfork(None);
4619    cmdoutpid.store(pid, Ordering::Relaxed);
4620    if pid == -1 {
4621        // c:4972
4622        unsafe {
4623            libc::close(fd);
4624        } // c:4973
4625        child_unblock(); // c:4974
4626        crate::fusevm_bridge::psub_pending_file_add(&nam);
4627        return Some(nam); // c:4975
4628    } else if pid != 0 {
4629        // c:4976 — parent.
4630        unsafe {
4631            libc::close(fd);
4632        } // c:4977
4633        let _ = waitforpid(pid); // c:4978
4634        cmdoutval.store(0, Ordering::Relaxed); // c:4979
4635        crate::fusevm_bridge::psub_pending_file_add(&nam);
4636        return Some(nam); // c:4980
4637    }
4638    // c:4983 — child.
4639    closem(FDT_UNUSED, 0); // c:4984
4640    let _ = redup(fd, 1); // c:4985
4641    entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
4642    cmdpush(CS_CMDSUBST as u8); // c:4987
4643                                // c:4988 — execode — WARNING (d).
4644    // The re-fed text is what parsecmd parsed: after the two marker chars,
4645    // up to the Outpar ending at `ends_at` (a byte offset). The markers are
4646    // multi-byte chars, so a `cmd[2..]` byte slice split them.
4647    let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4648    let body_end = ends_at.saturating_sub(Outpar.len_utf8());
4649    let body = if body_end > body_start && body_end <= cmd.len() {
4650        untokenize(&cmd[body_start..body_end])
4651    } else {
4652        String::new()
4653    };
4654    let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
4655    cmdpop(); // c:4989
4656    unsafe {
4657        libc::close(1);
4658    } // c:4990
4659      // _realexit — WARNING (e)
4660    std::process::exit(0); // c:4991
4661    #[allow(unreachable_code)]
4662    {
4663        // c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
4664        let _ = &mut nam;
4665        unsafe {
4666            libc::kill(libc::getpid(), libc::SIGKILL);
4667        }
4668        None
4669    }
4670}
4671
4672/// Port of `getproc()` from `Src/exec.c:5025` — C decl `getproc(char *cmd, char **eptr)`.
4673/// `<(cmd)` / `>(cmd)` process substitution
4674/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
4675///
4676/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
4677///     path c:5037-5064) is omitted; modern Linux/macOS both
4678///     provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
4679///     unused here.
4680/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
4681///     the current job (c:5141-5142).
4682/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
4683///     c:5087.
4684/// (d) `entersubsh` is ported at exec.rs:4329 — wired below at
4685///     c:5095 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`) in the real
4686///     forked child.
4687/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
4688///     through fusevm for cache coherence.
4689/// (f) `_realexit` flushes stdio + jobs + history. We use bare
4690///     `std::process::exit(LASTVAL)` for now.
4691/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
4692pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4693    // c:5025
4694    let bytes = cmd.as_bytes();
4695    let out: i32 = if cmd.starts_with(Inang) {
4696        1 // c:5032 — `<(...)` writer-side child
4697    } else {
4698        0
4699    };
4700    // c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
4701    // proc subst needs a host job to attach the child to.
4702    let tj_check = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4703    if tj_check == -1 {
4704        zerr(&format!("process substitution {} cannot be used here", cmd)); // c:5069
4705        return None; // c:5070
4706    }
4707    // c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
4708    let mut ends_at: usize = 0;
4709    let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
4710    if let Some(p) = eptr {
4711        *p = ends_at;
4712    }
4713    let mut pipes: [i32; 2] = [-1; 2];
4714    if mpipe(&mut pipes) < 0 {
4715        // c:5075
4716        return None;
4717    }
4718    let mut bgtime: ZshTimespec = libc::timespec {
4719        tv_sec: 0,
4720        tv_nsec: 0,
4721    };
4722    let pid = zfork(Some(&mut bgtime)); // c:5077
4723    if pid != 0 {
4724        // c:5077 — parent path.
4725        let pnam = format!("{}/{}", crate::ported::config_h::PATH_DEV_FD, pipes[(1 - out) as usize]); // c:5144
4726        let _ = zclose(pipes[out as usize]); // c:5079
4727        if pid == -1 {
4728            // c:5080
4729            let _ = zclose(pipes[(1 - out) as usize]); // c:5082
4730            return None; // c:5083
4731        }
4732        let fd = pipes[(1 - out) as usize]; // c:5085
4733        fdtable_set(fd, FDT_PROC_SUBST); // c:5086
4734                                         // c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
4735                                         // pipe fd in the current job's filelist so it's closed at job exit.
4736        if let Some(jt) = JOBTAB.get() {
4737            let mut guard = jt.lock().unwrap();
4738            let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4739            if tj >= 0 {
4740                if let Some(j) = guard.get_mut(tj as usize) {
4741                    crate::ported::jobs::addfilelist(j, None, fd);
4742                }
4743            }
4744        }
4745        // c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
4746        // record the proc-subst writer-side child in the job's
4747        // auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
4748        // child), C omits the addproc — symmetric here.
4749        if out == 0 {
4750            if let Some(jt) = JOBTAB.get() {
4751                let mut guard = jt.lock().unwrap();
4752                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4753                if tj >= 0 {
4754                    if let Some(j) = guard.get_mut(tj as usize) {
4755                        crate::ported::jobs::addproc(
4756                            j,
4757                            pid,
4758                            "",
4759                            true,
4760                            Some(std::time::Instant::now()),
4761                            -1,
4762                            -1,
4763                        );
4764                    }
4765                }
4766            }
4767        }
4768        procsubstpid.store(pid, Ordering::Relaxed); // c:5092
4769        return Some(pnam); // c:5093
4770    }
4771    // c:5095 — child.
4772    entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
4773    let _ = redup(pipes[out as usize], out); // c:5096
4774    closem(FDT_UNUSED, 0); // c:5097
4775    cmdpush(CS_CMDSUBST as u8); // c:5100
4776    // The re-fed text is what parsecmd parsed: after the two marker chars,
4777    // up to the Outpar ending at `ends_at` (a byte offset). The markers are
4778    // multi-byte chars, so a `cmd[2..]` byte slice split them.
4779    let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4780    let body_end = ends_at.saturating_sub(Outpar.len_utf8());
4781    let body = if body_end > body_start && body_end <= cmd.len() {
4782        untokenize(&cmd[body_start..body_end])
4783    } else {
4784        String::new()
4785    };
4786    let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
4787    cmdpop(); // c:5102
4788    let _ = zclose(out); // c:5103
4789    std::process::exit(LASTVAL.load(Ordering::Relaxed)); // c:5104
4790}
4791
4792/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
4793/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
4794pub mod esub {
4795    // c:1056
4796    /// `ASYNC` constant.
4797    pub const ASYNC: i32 = 0x01; // c:1058
4798    /// `PGRP` constant.
4799    pub const PGRP: i32 = 0x02; // c:1063
4800    /// `KEEPTRAP` constant.
4801    pub const KEEPTRAP: i32 = 0x04; // c:1065
4802    /// `FAKE` constant.
4803    pub const FAKE: i32 = 0x08; // c:1067
4804    /// `REVERTPGRP` constant.
4805    pub const REVERTPGRP: i32 = 0x10; // c:1069
4806    /// `NOMONITOR` constant.
4807    pub const NOMONITOR: i32 = 0x20; // c:1071
4808    /// `JOB_CONTROL` constant.
4809    pub const JOB_CONTROL: i32 = 0x40; // c:1073
4810}
4811
4812/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
4813/// Out-arg used by `entersubsh()` to hand back the group-leader pid
4814/// and the list-pipe job index the parent should track. Only filled
4815/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
4816/// groups).
4817#[allow(non_camel_case_types)]
4818#[derive(Default)]
4819pub struct entersubsh_ret {
4820    pub gleader: i32,       // c:1122
4821    pub list_pipe_job: i32, // c:1123
4822}
4823
4824/// Port of `entersubsh()` from `Src/exec.c:1084` — C decl `entersubsh(int flags, struct entersubsh_ret *retp)`.
4825/// Called by every child fork to switch the
4826/// process into subshell mode: traps reset, monitor disabled, signals
4827/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
4828/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
4829///
4830/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
4831///     1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
4832///     ESUB_PGRP+sync path establishes pipeline group-leadership
4833///     (list_pipe_job inherit or thisjob-as-leader), filling
4834///     entersubsh_ret with the chosen gleader + list_pipe_job index.
4835/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
4836///     `clearjobtab(&mut JobTable, monitor)`; we get the global table
4837///     via a TABLE handle similar to other jobs.rs entries.
4838/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
4839/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
4840///     mypgrp` — direct C parity (jobs.rs:3406 provides the call).
4841/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
4842///     uses `opts_set_off(USEZLE)`; zleactive is the atomic in
4843///     builtins/sched.rs.
4844/// =============================================================
4845pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
4846    // c:1083
4847    let monitor: i32;
4848    let job_control_ok: i32;
4849    // c:1088-1092 — reset traps unless KEEPTRAP.
4850    if (flags & esub::KEEPTRAP) == 0 {
4851        // c:1088
4852        for sig in 0..=SIGCOUNT {
4853            // c:1089
4854            let st = {
4855                let guard = sigtrapped.lock().unwrap();
4856                guard.get(sig as usize).copied().unwrap_or(0)
4857            };
4858            let func_set = (st & ZSIG_FUNC) != 0; // c:1090
4859            let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
4860            if !func_set && !posix_ignored {
4861                unsettrap(sig); // c:1092
4862            }
4863        }
4864    }
4865    monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
4866    job_control_ok = if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
4867        // c:1094
4868        1
4869    } else {
4870        0
4871    };
4872    EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
4873    if (flags & esub::NOMONITOR) != 0 {
4874        // c:1096
4875        dosetopt(MONITOR, 0, 0); // c:1097
4876    }
4877    if !isset(MONITOR) {
4878        // c:1098
4879        if (flags & esub::ASYNC) != 0 {
4880            // c:1099
4881            let _ = settrap(libc::SIGINT, None, 0); // c:1100
4882            let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
4883            if unsafe { libc::isatty(0) } != 0 {
4884                // c:1102
4885                unsafe {
4886                    libc::close(0);
4887                } // c:1103
4888                let devnull = std::ffi::CString::new("/dev/null").unwrap();
4889                if unsafe { libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) } != 0 {
4890                    // c:1104
4891                    zerr(&format!(
4892                        // c:1105
4893                        "can't open /dev/null: {}",
4894                        std::io::Error::last_os_error()
4895                    ));
4896                    unsafe {
4897                        libc::_exit(1);
4898                    } // c:1106
4899                }
4900            }
4901        }
4902    } else if (flags & esub::PGRP) != 0 {
4903        // c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
4904        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4905        if thisjob != -1 {
4906            let lpj = list_pipe_job.load(Ordering::Relaxed);
4907            let lp = list_pipe.load(Ordering::Relaxed);
4908            let lpc = list_pipe_child.load(Ordering::Relaxed);
4909            if let Some(jt) = JOBTAB.get() {
4910                let mut guard = jt.lock().unwrap();
4911                let lpj_gleader = guard.get(lpj as usize).map(|j| j.gleader).unwrap_or(0);
4912                if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
4913                    // c:1111-1124 — inherit list_pipe_job's group leader.
4914                    let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
4915                        || (unsafe { libc::killpg(lpj_gleader, 0) } == -1
4916                            && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH))
4917                    {
4918                        // c:1115-1117 — primary group leader gone; this child becomes leader.
4919                        let new_gl = if lpc != 0 {
4920                            mypgrp.load(Ordering::Relaxed)
4921                        } else {
4922                            unsafe { libc::getpid() }
4923                        };
4924                        if let Some(j) = guard.get_mut(lpj as usize) {
4925                            j.gleader = new_gl;
4926                        }
4927                        if let Some(j) = guard.get_mut(thisjob as usize) {
4928                            j.gleader = new_gl;
4929                        }
4930                        unsafe { libc::setpgid(0, new_gl) };
4931                        if (flags & esub::ASYNC) == 0 {
4932                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
4933                        }
4934                        new_gl
4935                    } else {
4936                        lpj_gleader
4937                    };
4938                    if let Some(r) = retp {
4939                        if (flags & esub::ASYNC) == 0 {
4940                            r.gleader = pgid; // c:1122
4941                            r.list_pipe_job = lpj; // c:1123
4942                        }
4943                    }
4944                } else {
4945                    // c:1126-1151 — standard group-leader-takeover path.
4946                    let thisjob_gleader =
4947                        guard.get(thisjob as usize).map(|j| j.gleader).unwrap_or(0);
4948                    if thisjob_gleader == 0 || unsafe { libc::setpgid(0, thisjob_gleader) } == -1 {
4949                        let new_gl = unsafe { libc::getpid() };
4950                        if let Some(j) = guard.get_mut(thisjob as usize) {
4951                            j.gleader = new_gl; // c:1138
4952                        }
4953                        if lpj != thisjob {
4954                            let lpj_was_unset = guard
4955                                .get(lpj as usize)
4956                                .map(|j| j.gleader == 0)
4957                                .unwrap_or(true);
4958                            if lpj_was_unset {
4959                                if let Some(j) = guard.get_mut(lpj as usize) {
4960                                    j.gleader = new_gl; // c:1140-1141
4961                                }
4962                            }
4963                        }
4964                        unsafe { libc::setpgid(0, new_gl) }; // c:1142
4965                        if (flags & esub::ASYNC) == 0 {
4966                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
4967                            if let Some(r) = retp {
4968                                r.gleader = new_gl; // c:1146
4969                                if lpj != thisjob {
4970                                    r.list_pipe_job = lpj; // c:1148
4971                                }
4972                            }
4973                        }
4974                    }
4975                }
4976            }
4977        } else {
4978            // No real job slot; basic setpgid fallback.
4979            unsafe { libc::setpgid(0, 0) };
4980        }
4981    }
4982    if (flags & esub::FAKE) == 0 {
4983        // c:1153
4984        subsh.store(1, Ordering::Relaxed); // c:1154
4985    }
4986    // c:1161 — `zsh_subshell++;` regardless of FAKE.
4987    zsh_subshell.fetch_add(1, Ordering::Relaxed);
4988    // c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
4989    if (flags & esub::REVERTPGRP) != 0
4990        && unsafe { libc::getpid() } == mypgrp.load(Ordering::Relaxed)
4991    {
4992        release_pgrp(); // c:1163
4993    }
4994    *shout.lock().unwrap() = 0; // c:1164 — shout = NULL
4995    if (flags & esub::NOMONITOR) != 0 {
4996        // c:1165
4997        signal_ignore(libc::SIGTTOU); // c:1171
4998        signal_ignore(libc::SIGTTIN); // c:1172
4999        signal_ignore(libc::SIGTSTP); // c:1173
5000    } else if job_control_ok == 0 {
5001        // c:1174
5002        signal_default(libc::SIGTTOU); // c:1181
5003        signal_default(libc::SIGTTIN); // c:1182
5004        signal_default(libc::SIGTSTP); // c:1183
5005    }
5006    let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
5007    if interact {
5008        signal_default(libc::SIGTERM); // c:1186
5009        let int_st = sigtrapped
5010            .lock()
5011            .unwrap()
5012            .get(libc::SIGINT as usize)
5013            .copied()
5014            .unwrap_or(0);
5015        if (int_st & ZSIG_IGNORED) == 0 {
5016            // c:1187
5017            signal_default(libc::SIGINT); // c:1188
5018        }
5019        let pipe_st = sigtrapped
5020            .lock()
5021            .unwrap()
5022            .get(libc::SIGPIPE as usize)
5023            .copied()
5024            .unwrap_or(0);
5025        if pipe_st == 0 {
5026            // c:1189
5027            signal_default(libc::SIGPIPE); // c:1190
5028        }
5029    }
5030    let quit_st = sigtrapped
5031        .lock()
5032        .unwrap()
5033        .get(libc::SIGQUIT as usize)
5034        .copied()
5035        .unwrap_or(0);
5036    if (quit_st & ZSIG_IGNORED) == 0 {
5037        // c:1192
5038        signal_default(libc::SIGQUIT); // c:1193
5039    }
5040    // c:1202-1205 — unblock any trapped signals while in `intrap`.
5041    if intrap.load(Ordering::Relaxed) != 0 {
5042        // c:1202
5043        for sig in 1..=SIGCOUNT {
5044            let st = sigtrapped
5045                .lock()
5046                .unwrap()
5047                .get(sig as usize)
5048                .copied()
5049                .unwrap_or(0);
5050            if st != 0 && st != ZSIG_IGNORED {
5051                // c:1204
5052                let m = signal_mask(sig);
5053                let _ = signal_unblock(&m); // c:1205
5054            }
5055        }
5056    }
5057    if job_control_ok == 0 {
5058        // c:1206
5059        dosetopt(MONITOR, 0, 0); // c:1207
5060    }
5061    dosetopt(USEZLE, 0, 0); // c:1208
5062    zleactive.store(0, Ordering::Relaxed); // c:1209
5063                                           // c:1214-1217 — close saved fds.
5064    let max = MAX_ZSH_FD.load(Ordering::Relaxed);
5065    for i in 10..=max {
5066        if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
5067            // c:1215
5068            let _ = zclose(i); // c:1216
5069        }
5070    }
5071    // c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
5072    // at jobs.rs:1695 which handles ALL the C body including the
5073    // oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
5074    let mut dummy_table = crate::exec_jobs::JobTable::new();
5075    crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
5076    let _ = get_usage(); // c:1220
5077    FORKLEVEL.store(
5078        // c:1221 — `forklevel = locallevel;`
5079        locallevel.load(Ordering::Relaxed),
5080        Ordering::Relaxed,
5081    );
5082    // c:1263-1264 (55061) — `ancestor_loops += loops; loops = 0;`
5083    crate::ported::builtin::ANCESTOR_LOOPS.fetch_add(
5084        crate::ported::builtin::LOOPS.swap(0, Ordering::Relaxed),
5085        Ordering::Relaxed,
5086    );
5087}
5088
5089/// RAII guard applying the fork-safe subset of
5090/// `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` (`Src/exec.c:1083`) around a
5091/// command substitution that zshrs runs **in-process**, restoring every
5092/// touched global on drop.
5093///
5094/// C's `getoutput()` forks, and only the child runs `entersubsh`:
5095///
5096/// ```c
5097///     /* pid == 0 */
5098///     child_unblock();
5099///     zclose(pipes[0]);
5100///     redup(pipes[1], 1);
5101///     entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL);   /* c:4781 */
5102///     cmdpush(CS_CMDSUBST);
5103///     execode(prog, 0, 1, "cmdsubst");
5104/// ```
5105///
5106/// so every state delta dies with the child and the parent is untouched.
5107/// zshrs collapses that fork (`getoutput` below → `run_command_substitution`),
5108/// which means the deltas were previously never applied **at all**: inside
5109/// `$( )` the shell kept the caller's `zleactive` / `opts[USEZLE]`, so every
5110/// reader of them took the "we are inside the line editor" branch. Observed
5111/// failure: `lt=( ${(f)"$(fc -l -200)"} )` bound to a ZLE widget died with
5112/// `no interactive history within ZLE` — `bin_fc`'s guard
5113/// (`builtin.rs:2757`, a correct port of `Src/builtin.c:1523-1527`) firing on
5114/// a stale `zleactive`. Same class of bug for every other reader:
5115/// `jobs.rs:1114` (time-report suppression), `utils.rs:1785` (screen
5116/// ownership), `signals.rs:2050` (refresh-on-signal).
5117///
5118/// APPLIED (pure restorable scalar state; correct without a fork):
5119///   * c:1097 / c:1207 — `opts[MONITOR] = 0`. ESUB_NOMONITOR is set and
5120///     `job_control_ok` is 0 (no ESUB_JOB_CONTROL), so both C branches
5121///     agree. A substitution body must not do job control.
5122///   * c:1164 — `shout = NULL`. Its readers already handle the NULL case
5123///     the way the C child does (`putshout` falls back to stderr,
5124///     `checkrmall` returns 1 at `utils.rs:3565` per c:2871, `mailcheck`
5125///     stays silent per c:1670).
5126///   * c:1208 — `opts[USEZLE] = 0`.
5127///   * c:1209 — `zleactive = 0`.
5128///
5129/// DELIBERATELY SKIPPED — each would be wrong, or is already handled
5130/// elsewhere on the in-process path:
5131///   * c:1127-1131 `unsettrap(sig)` — APPLIED, but not here: the trap table is
5132///     not scalar state (trap bodies and `sigaction` dispositions), so
5133///     `run_command_substitution` resets it with the same helper `( … )`
5134///     uses (`fusevm_bridge::entersubsh_reset_traps`) and puts the parent's
5135///     flags and dispositions back on exit.
5136///   * c:1095 `exit_val = 0` — ALREADY DONE on this path:
5137///     `vm_helper.rs:3893` swaps `EXIT_VAL` to 0 before the nested VM runs
5138///     and restores it at `vm_helper.rs:4004`.
5139///   * c:1110-1151 `setpgrp`/`attachtty`/gleader bookkeeping — process-group
5140///     and tty surgery. With no fork this moves the interactive shell's own
5141///     process group and hands the terminal away.
5142///   * c:1154 `subsh = 1` — `init.rs:2289` does `if subsh != 0 { realexit() }`.
5143///     With no fork, an `exit` inside `$( )` would then hard-kill the
5144///     interactive shell instead of ending the substitution.
5145///   * c:1161 `zsh_subshell++` — ALREADY DONE on this path by
5146///     `CmdSubstSubshellBump` (`fusevm_bridge.rs:232`, entered at
5147///     `vm_helper.rs:3734`). Bumping again would make `$ZSH_SUBSHELL` read 2
5148///     inside `$( )` instead of 1.
5149///   * c:1165-1193 `signal_ignore`/`signal_default` — per-process signal
5150///     dispositions. Defaulting SIGINT/SIGQUIT/SIGTSTP in the live
5151///     interactive shell would let ^C kill and ^Z stop the shell itself.
5152///   * c:1202-1205 `signal_unblock` — the process signal mask is owned by
5153///     the trap dispatcher's block/unblock pairing; unblocking here breaks
5154///     that pairing.
5155///   * c:1214-1217 close FDT_SAVED_MASK fds — fd surgery. These are the
5156///     parent's saved descriptors for redirection restore; closing them is
5157///     unrecoverable.
5158///   * c:1219 `clearjobtab(monitor)` — would destroy the parent's job table.
5159///   * c:1220 `get_usage()` — overwrites the parent's `times` baseline with
5160///     no fork to discard it, and nothing inside the substitution reads it.
5161///   * c:1221 `forklevel = locallevel` — FORKLEVEL gates local-scope
5162///     unwinding; the in-process body pushes and pops its own locals
5163///     normally, so moving the fork boundary would strand them.
5164///
5165/// Nesting is safe: each guard saves what it observed on entry and restores
5166/// exactly that on drop, so LIFO nesting (`$( … $( … ) … )`) unwinds
5167/// correctly.
5168pub struct SubshStateGuard {
5169    saved_monitor: bool,
5170    saved_shout: usize,
5171    saved_usezle: bool,
5172    saved_zleactive: i32,
5173    saved_subsh: i32,
5174    saved_loops: (i32, i32),
5175}
5176
5177/// The `zleactive` the shell had before the innermost in-process subshell
5178/// state was applied, or `-1` when no such state is in effect.
5179///
5180/// !!! WARNING: RUST-ONLY STATE !!! C has no counterpart and needs none.
5181/// `Src/exec.c:1248`'s `zleactive = 0;` is inside `entersubsh`, and every
5182/// caller reaches it in the FORKED CHILD — `getoutput`, the `$(...)` one,
5183/// calls it at `Src/exec.c:4838` under the `/* pid == 0 */` label
5184/// (c:4834), with the parent left at `waitforpid(pid, 0)` on c:4830.
5185/// The parent's `zleactive` is never touched,
5186/// so a signal handler that lands in the parent while a `$(...)` is running
5187/// still sees ZLE as active — which is the whole reason `adjustwinsize`'s
5188/// `if (zleactive && resetzle)` (`Src/utils.c:1954`) can repaint on a resize
5189/// that arrives during a completion.
5190///
5191/// `SubshStateGuard` runs that child-side write IN THE PARENT because zshrs
5192/// executes command substitutions in-process, so the parent's own handler
5193/// read 0 and declined to repaint. This publishes what C's parent would have
5194/// had, for the handler to consult. Only the outermost guard records, so a
5195/// nested substitution cannot overwrite it with its own zero.
5196pub static SUBSH_PARENT_ZLEACTIVE: std::sync::atomic::AtomicI32 =
5197    std::sync::atomic::AtomicI32::new(-1);
5198
5199/// Nesting depth of the in-process subshell state above.
5200pub static SUBSH_STATE_DEPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
5201
5202impl SubshStateGuard {
5203    /// Apply the deltas listed above and capture the values to restore.
5204    pub fn enter() -> Self {
5205        let g = SubshStateGuard {
5206            saved_monitor: isset(MONITOR),
5207            saved_shout: *shout.lock().unwrap(),
5208            saved_usezle: isset(USEZLE),
5209            saved_zleactive: zleactive.load(Ordering::Relaxed),
5210            saved_subsh: subsh.load(Ordering::Relaxed),
5211            saved_loops: (
5212                crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
5213                crate::ported::builtin::ANCESTOR_LOOPS.load(Ordering::Relaxed),
5214            ),
5215        };
5216        // c:1263-1264 (55061) — `ancestor_loops += loops; loops = 0;`
5217        crate::ported::builtin::ANCESTOR_LOOPS.fetch_add(
5218            crate::ported::builtin::LOOPS.swap(0, Ordering::Relaxed),
5219            Ordering::Relaxed,
5220        );
5221        // c:1153-1154 — `if (!(flags & ESUB_FAKE)) subsh = 1;`. The
5222        // substitution's body is "in a subshell" for every consumer that
5223        // asks, which is what keeps PRINT_EXIT_VALUE quiet inside
5224        // `x=$(false)` (c:4309 `&& !subsh`).
5225        subsh.store(1, Ordering::Relaxed); // c:1154
5226                                           // `force = 1`: C assigns the `opts[]` slots directly, so the
5227                                           // dosetopt gatekeeping (c:743-861, which only guards turning
5228                                           // options ON) must not apply in either direction.
5229        dosetopt(MONITOR, 0, 1); // c:1097 / c:1207
5230        *shout.lock().unwrap() = 0; // c:1164
5231        dosetopt(USEZLE, 0, 1); // c:1208
5232        // Publish the pre-substitution value BEFORE zeroing it, so a signal
5233        // handler that lands in this process during the substitution can read
5234        // the `zleactive` C's parent would still have. Outermost guard only.
5235        if SUBSH_STATE_DEPTH.fetch_add(1, Ordering::SeqCst) == 0 {
5236            SUBSH_PARENT_ZLEACTIVE.store(g.saved_zleactive, Ordering::SeqCst);
5237        }
5238        zleactive.store(0, Ordering::Relaxed); // c:1209
5239        g
5240    }
5241}
5242
5243impl Drop for SubshStateGuard {
5244    fn drop(&mut self) {
5245        // Reverse order of `enter`; the C child never restores because it
5246        // `_realexit()`s, so this half has no C counterpart to cite.
5247        subsh.store(self.saved_subsh, Ordering::Relaxed);
5248        crate::ported::builtin::LOOPS.store(self.saved_loops.0, Ordering::Relaxed);
5249        crate::ported::builtin::ANCESTOR_LOOPS.store(self.saved_loops.1, Ordering::Relaxed);
5250        if SUBSH_STATE_DEPTH.fetch_sub(1, Ordering::SeqCst) <= 1 {
5251            SUBSH_PARENT_ZLEACTIVE.store(-1, Ordering::SeqCst);
5252        }
5253        zleactive.store(self.saved_zleactive, Ordering::Relaxed);
5254        dosetopt(USEZLE, self.saved_usezle as i32, 1);
5255        *shout.lock().unwrap() = self.saved_shout;
5256        dosetopt(MONITOR, self.saved_monitor as i32, 1);
5257    }
5258}
5259
5260/// The state a forked subshell owns a private copy of — the shell's hash
5261/// tables, its environment, its working directory and its process
5262/// attributes — taken on entry to an in-process subshell and put back on
5263/// exit.
5264///
5265/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5266/// `getoutput` (`Src/exec.c:4816`) and `( … )` (`c:2880`) run the body in
5267/// a `zfork()` child. `entersubsh` (`c:1123-1261`) resets traps, job
5268/// control, `subsh` and `zsh_subshell`, but it does not touch any of the
5269/// state below: the child simply writes its fork-copied tables, its own
5270/// `environ`, its own cwd, umask and rlimits, and all of it dies at
5271/// `_realexit()` (`c:4843`). The parent never sees
5272///     x=$(alias a=b; export v=1; cd /tmp; umask 077)
5273/// because nothing it owns was written. zshrs runs both forms IN
5274/// PROCESS, so each piece the body can write must be handed back here.
5275///
5276/// Cost of `save`, per substitution: every table is copy-on-write, so a
5277/// refcount bump each, and only a body that writes a table pays for
5278/// copying it; the environment is one byte copy (`environ_image`); the
5279/// directory stack and the two rlimit arrays are short vectors; the cwd
5280/// is one `stat` and the umask two `umask(2)` calls.
5281/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5282/// The entry-directory descriptor `SubshForkCopy` holds for the in-process
5283/// subshell. A forked child can never move its parent; zshrs's body shares
5284/// the process cwd, so the parent must be put back into the same directory
5285/// object, not the same path. The fd is recorded `FDT_INTERNAL`, like every
5286/// other descriptor the shell keeps for itself (`movefd`,
5287/// `Src/utils.c:1990-2011`), but sits past the script's `{var}` range
5288/// (`lowfd::movefd_past_script`), and is close-on-exec so an external command
5289/// run by the body never inherits it. Dropping closes it.
5290struct SubshCwdFd(i32);
5291
5292impl SubshCwdFd {
5293    fn open() -> Self {
5294        let fd = unsafe { libc::open(b".\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_CLOEXEC) };
5295        if fd < 0 {
5296            return SubshCwdFd(-1);
5297        }
5298        // Moved past the script's `{var}` range as well: a forked child has
5299        // no such descriptor, so `(exec {v}>f; print $v)` must still get 11.
5300        let moved = crate::lowfd::movefd_past_script(fd);
5301        if moved == fd {
5302            unsafe { libc::close(fd) };
5303            return SubshCwdFd(-1);
5304        }
5305        SubshCwdFd(moved)
5306    }
5307}
5308
5309impl Drop for SubshCwdFd {
5310    fn drop(&mut self) {
5311        if self.0 >= 0 {
5312            crate::ported::utils::zclose(self.0);
5313        }
5314    }
5315}
5316
5317pub struct SubshForkCopy {
5318    /// `aliastab` (`Src/hashtable.c:1177`) — `alias`, `unalias`,
5319    /// `disable -a`, `aliases[x]=…`.
5320    aliastab: crate::ported::hashtable::alias_table,
5321    /// `sufaliastab` (`Src/hashtable.c:1182`) — `alias -s`, `unalias -s`.
5322    sufaliastab: crate::ported::hashtable::alias_table,
5323    /// `nameddirtab` (`Src/hashnameddir.c:48`) — `hash -d`, `unhash -d`,
5324    /// `nameddirs[x]=…`, and the nodes a `~name` lookup adds.
5325    nameddirtab: crate::cow_map::CowArc<crate::ported::hashtable::hashtable_nodes<crate::ported::zsh_h::nameddir>>,
5326    /// `allusersadded` (`Src/hashnameddir.c:53`) — set by a
5327    /// `fillnameddirtable` that ran in the body; the parent's table
5328    /// never got those user entries, so its flag must not claim it did.
5329    allusersadded: i32,
5330    /// `cmdnamtab` (`Src/hashtable.c:590`) — `hash`, `unhash`, `hash -r`,
5331    /// `commands[x]=…`, and a fill run by reading `$commands`.
5332    cmdnamtab: crate::ported::hashtable::cmdnam_table,
5333    /// `pathchecked` (`Src/hashtable.c:595`) — how far a fill has walked
5334    /// `$path`; it describes `cmdnamtab`, so it travels with it.
5335    pathchecked: usize,
5336    /// `environ` — every `export`, `unset` of an exported name, `cd`
5337    /// (`PWD`/`OLDPWD`) and `allexport` assignment writes it with
5338    /// `setenv`/`unsetenv` (`Src/params.c:5320`, `c:5537`).
5339    environ: crate::ported::params::environ_image,
5340    /// The process working directory, as the device and inode of `.`.
5341    /// `cd`, `pushd` and `popd` in the body move it for the whole
5342    /// process; `restore` compares and moves back only if they did.
5343    cwd: Option<(u64, u64)>,
5344    /// An fd open on the entry directory itself. `restore` goes back
5345    /// through it, so a body that renames the directory out from under
5346    /// the parent (`( cd .. && mv foo bar )` run from `foo`) still returns
5347    /// the parent to the very directory it was in, as a fork does.
5348    cwd_fd: SubshCwdFd,
5349    /// `$PWD` on entry — the fallback when `cwd_fd` could not be opened.
5350    pwd: Option<String>,
5351    /// `dirstack` (`Src/builtin.c:744`) — `pushd` / `popd`.
5352    dirstack: Vec<String>,
5353    /// The file-creation mask — `umask` (`Src/builtin.c:7483`).
5354    umask: libc::mode_t,
5355    /// `limits[]` and `current_limits[]` (`Src/exec.c:315`) — `ulimit`,
5356    /// `limit`, `unlimit`. `zfork` applies the child's copy to the child
5357    /// (`c:381-383` `setlimits(NULL)`), so the parent never sees it. Both
5358    /// arrays are needed and are rolled back differently: `limits[]` is
5359    /// what the shell WANTS, `current_limits[]` what `setrlimit` was
5360    /// actually given (`zsetlimit`, `c:319-332`, only calls `setrlimit`
5361    /// where they disagree), so `limit descriptors 256` without `-s`
5362    /// moves only the first.
5363    limits: Vec<libc::rlimit>,
5364    current_limits: Vec<libc::rlimit>,
5365    /// `zstyletab` (`Src/Modules/zutil.c:106`) — `zstyle`, `zstyle -d`.
5366    zstyletab: crate::ported::modules::zutil::style_table,
5367    /// `modulestab` (`Src/module.c:49`) as `(name → flags)`: `zmodload`
5368    /// flips `MOD_INIT_B` / `MOD_UNLOAD`, and loading a module the table
5369    /// has never seen adds a node (see `restore`).
5370    modules: std::collections::HashMap<String, i32>,
5371    /// Builtins carrying `DISABLED` in `builtintab` (`Src/builtin.c:146`)
5372    /// — `disable`/`enable` (`c:535-546`).
5373    builtins_disabled: std::collections::HashSet<String>,
5374    /// Reserved words carrying `DISABLED` in `reswdtab`
5375    /// (`Src/hashtable.c:1114`) — `disable -r`. Changes how the parent
5376    /// PARSES, e.g. `repeat 2 print r` stops being a loop.
5377    reswds_disabled: std::collections::HashSet<String>,
5378    /// `schedcmds` (`Src/Builtins/sched.c:52`) — `sched`.
5379    schedcmds: Option<Box<crate::ported::builtins::sched::schedcmd>>,
5380    /// `watch_fds` (`Src/Zle/zle_main.c:204`) — `zle -F`. A forked
5381    /// child's handlers die with it; kept here, a `$(cd x)` whose `chpwd`
5382    /// hook armed one (zinit's `@zinit-scheduler`: `exec {AFD}< <(…);
5383    /// zle -F $AFD @zinit-scheduler`) left the parent watching a descriptor
5384    /// the frame had closed: `failed to close file descriptor 17`.
5385    watch_fds: Vec<crate::ported::zle::zle_h::watch_fd>,
5386    /// `shtimer` (`Src/params.c:147`) — assigning `SECONDS` moves it.
5387    shtimer: std::time::Duration,
5388    /// `donetrap` (`Src/exec.c:1413`) — a ZERR trap that ran inside the body
5389    /// sets it (`c:1658`) in the forked child only. The parent's own check
5390    /// for the subshell's failing status (`c:1651-1659`) must still see the
5391    /// value it had on entry, or `(false)` runs ZERR once instead of twice.
5392    donetrap: i32,
5393    /// `zpc_disables[]` (`Src/pattern.c:4179`) — `disable -p` / `enable -p`.
5394    /// A fork leaves the parent's array alone, so `( disable -p '*' )`
5395    /// must not keep `*` disabled afterwards. Held as the
5396    /// `savepatterndisables()` bitmap (`c:4220`).
5397    pattern_disables: u32,
5398}
5399
5400impl SubshForkCopy {
5401    /// Take the parent's state. O(1) per table; see the type docs for
5402    /// the rest.
5403    pub fn save() -> Self {
5404        use std::os::unix::fs::MetadataExt;
5405        crate::ported::builtins::rlimits::ensure_limits_initialized();
5406        let rlimits = |a: &std::sync::OnceLock<std::sync::Mutex<Vec<libc::rlimit>>>| {
5407            a.get()
5408                .and_then(|l| l.lock().ok().map(|g| g.clone()))
5409                .unwrap_or_default()
5410        };
5411        SubshForkCopy {
5412            aliastab: crate::ported::hashtable::aliastab_lock()
5413                .read()
5414                .map(|t| t.snapshot())
5415                .unwrap_or_default(),
5416            sufaliastab: crate::ported::hashtable::sufaliastab_lock()
5417                .read()
5418                .map(|t| t.snapshot())
5419                .unwrap_or_default(),
5420            nameddirtab: crate::ported::hashnameddir::nameddirtab()
5421                .lock()
5422                .map(|t| t.clone())
5423                .unwrap_or_default(),
5424            allusersadded: crate::ported::hashnameddir::allusersadded.load(Ordering::Relaxed),
5425            cmdnamtab: crate::ported::hashtable::cmdnamtab_lock()
5426                .read()
5427                .map(|t| t.snapshot())
5428                .unwrap_or_default(),
5429            pathchecked: pathchecked.load(Ordering::SeqCst),
5430            environ: crate::ported::params::environ_image::save(),
5431            cwd: std::fs::metadata(".").ok().map(|m| (m.dev(), m.ino())),
5432            cwd_fd: SubshCwdFd::open(),
5433            pwd: getsparam("PWD"),
5434            dirstack: crate::ported::modules::parameter::DIRSTACK
5435                .lock()
5436                .map(|d| d.clone())
5437                .unwrap_or_default(),
5438            // umask(2) can only be read by setting it; put it straight back.
5439            umask: unsafe {
5440                let m = libc::umask(0o022);
5441                libc::umask(m);
5442                m
5443            },
5444            limits: rlimits(&crate::ported::builtins::rlimits::LIMITS),
5445            current_limits: rlimits(&crate::ported::builtins::rlimits::CURRENT_LIMITS),
5446            zstyletab: crate::ported::modules::zutil::zstyletab
5447                .lock()
5448                .map(|t| t.clone())
5449                .unwrap_or_default(),
5450            modules: crate::ported::module::MODULESTAB
5451                .lock()
5452                .map(|t| t.modules.iter().map(|(k, v)| (k.clone(), v.node.flags)).collect())
5453                .unwrap_or_default(),
5454            builtins_disabled: crate::ported::builtin::BUILTINS_DISABLED
5455                .lock()
5456                .map(|s| s.clone())
5457                .unwrap_or_default(),
5458            reswds_disabled: crate::ported::hashtable::reswdtab_lock()
5459                .read()
5460                .map(|t| {
5461                    t.iter()
5462                        .filter(|(_, r)| (r.node.flags & crate::ported::zsh_h::DISABLED as i32) != 0)
5463                        .map(|(n, _)| n.clone())
5464                        .collect()
5465                })
5466                .unwrap_or_default(),
5467            schedcmds: crate::ported::builtins::sched::schedcmd::subsh_save(),
5468            watch_fds: crate::ported::zle::zle_main::WATCH_FDS
5469                .lock()
5470                .map(|t| t.clone())
5471                .unwrap_or_default(),
5472            shtimer: crate::ported::params::shtimer_lock()
5473                .lock()
5474                .map(|t| *t)
5475                .unwrap_or_default(),
5476            donetrap: DONETRAP.load(Ordering::Relaxed),
5477            pattern_disables: crate::ported::pattern::savepatterndisables(),
5478        }
5479    }
5480
5481    /// Put the parent's state back, discarding whatever the subshell
5482    /// body did to it — the in-process stand-in for the child exiting.
5483    pub fn restore(self) {
5484        use std::os::unix::fs::MetadataExt;
5485        // modulestab first: rolling a module back runs `cleanup_module`
5486        // → `setfeatureenables` (`Src/module.c:3354`) → `deleteparamdef`
5487        // (c:1128), which looks its parameters up in the LIVE paramtab —
5488        // so callers restore this struct before they put paramtab back.
5489        //
5490        // A `zmodload zsh/X` for a module with no node yet takes
5491        // load_module's allocate-on-miss branch (c:2229-2237) and CREATES
5492        // the node. In C that node is allocated in the forked child and
5493        // dies with it; here it survives, and restoring flags alone would
5494        // never touch it — `(zmodload zsh/datetime)` left the parent with a
5495        // loaded node and `zmodload -e zsh/datetime` answering 0 where zsh
5496        // answers 1. So nodes the parent did not have are rolled back the
5497        // way a real unload does it — `cleanup_module` (c:1922) →
5498        // `finish_module` (c:1930), undoing the feature enables the load
5499        // made in the module's own statics — and dropped; then the flags
5500        // of the parent's nodes are put back.
5501        if let Ok(mut t) = crate::ported::module::MODULESTAB.lock() {
5502            let strays: Vec<String> = t
5503                .modules
5504                .keys()
5505                .filter(|n| !self.modules.contains_key(*n))
5506                .cloned()
5507                .collect();
5508            for name in &strays {
5509                let loaded = t
5510                    .modules
5511                    .get(name)
5512                    .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
5513                    .unwrap_or(false);
5514                if loaded {
5515                    let _ = crate::ported::module::cleanup_module(&mut t, name);
5516                    let _ = crate::ported::module::finish_module(&mut t, name);
5517                }
5518            }
5519            t.modules.retain(|name, _| self.modules.contains_key(name));
5520            for (name, saved_flags) in &self.modules {
5521                if let Some(m) = t.modules.get_mut(name) {
5522                    m.node.flags = *saved_flags;
5523                }
5524            }
5525        }
5526        if let Ok(mut t) = crate::ported::modules::zutil::zstyletab.lock() {
5527            *t = self.zstyletab;
5528        }
5529        if let Ok(mut s) = crate::ported::builtin::BUILTINS_DISABLED.lock() {
5530            *s = self.builtins_disabled;
5531        }
5532        if let Ok(mut t) = crate::ported::hashtable::reswdtab_lock().write() {
5533            let names: Vec<String> = t.iter().map(|(n, _)| n.clone()).collect();
5534            for n in names {
5535                if self.reswds_disabled.contains(&n) {
5536                    t.disable(&n);
5537                } else {
5538                    t.enable(&n);
5539                }
5540            }
5541        }
5542        crate::ported::builtins::sched::schedcmd::subsh_restore(self.schedcmds);
5543        if let Ok(mut t) = crate::ported::zle::zle_main::WATCH_FDS.lock() {
5544            *t = self.watch_fds;
5545        }
5546        if let Ok(mut t) = crate::ported::params::shtimer_lock().lock() {
5547            *t = self.shtimer;
5548        }
5549        DONETRAP.store(self.donetrap, Ordering::Relaxed);
5550        crate::ported::pattern::restorepatterndisables(self.pattern_disables); // c:4258
5551        if let Ok(mut t) = crate::ported::hashtable::aliastab_lock().write() {
5552            t.restore(self.aliastab);
5553        }
5554        if let Ok(mut t) = crate::ported::hashtable::sufaliastab_lock().write() {
5555            t.restore(self.sufaliastab);
5556        }
5557        if let Ok(mut t) = crate::ported::hashnameddir::nameddirtab().lock() {
5558            *t = self.nameddirtab;
5559        }
5560        crate::ported::hashnameddir::allusersadded.store(self.allusersadded, Ordering::Relaxed);
5561        if let Ok(mut t) = crate::ported::hashtable::cmdnamtab_lock().write() {
5562            t.restore(self.cmdnamtab);
5563        }
5564        pathchecked.store(self.pathchecked, Ordering::SeqCst);
5565        self.environ.restore();
5566        let here = std::fs::metadata(".").ok().map(|m| (m.dev(), m.ino()));
5567        if here != self.cwd {
5568            // By descriptor first: the entry path may no longer name the
5569            // entry directory (the body renamed it), and chdir-by-path
5570            // would then leave the parent somewhere else entirely.
5571            let back = self.cwd_fd.0 >= 0 && unsafe { libc::fchdir(self.cwd_fd.0) } == 0;
5572            if !back {
5573                if let Some(pwd) = &self.pwd {
5574                    let _ = std::env::set_current_dir(pwd);
5575                }
5576            }
5577        }
5578        if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
5579            *d = self.dirstack;
5580        }
5581        unsafe {
5582            libc::umask(self.umask);
5583        }
5584        // `zsetlimit` (c:319-332) applied to before/after: replay
5585        // `setrlimit` only for the resources whose `current_limits[]` the
5586        // BODY moved, so a `limit` the parent set without `-s` is not
5587        // suddenly installed here. A body that LOWERED a hard limit is the
5588        // one case this cannot undo — the process cannot raise it again;
5589        // C escapes that only because the child dies with it.
5590        if let Some(lock) = crate::ported::builtins::rlimits::CURRENT_LIMITS.get() {
5591            if let Ok(mut cur) = lock.lock() {
5592                for (i, want) in self.current_limits.iter().enumerate() {
5593                    let Some(have) = cur.get(i).copied() else {
5594                        continue;
5595                    };
5596                    if have.rlim_max == want.rlim_max && have.rlim_cur == want.rlim_cur {
5597                        continue; // c:321-322
5598                    }
5599                    if unsafe { libc::setrlimit(i as _, want) } == 0 {
5600                        cur[i] = *want; // c:329
5601                    }
5602                }
5603            }
5604        }
5605        if let Some(lock) = crate::ported::builtins::rlimits::LIMITS.get() {
5606            if let Ok(mut g) = lock.lock() {
5607                *g = self.limits;
5608            }
5609        }
5610    }
5611}
5612
5613/// The descriptors a bare `exec` redirection moved inside an in-process
5614/// subshell, each with a copy of what it was before, and the ones the body
5615/// opened for itself (`{var}`, `sysopen -u`, `zsystem flock -f`).
5616///
5617/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5618/// `exec 3>file` / `exec 3>&-` with no command is permanent — c:4035
5619/// "specifically *don't* restore the original fd's" — but inside
5620/// `$( … )` or `( … )` it is permanent only for the forked child
5621/// (`Src/exec.c:4816`, `c:2880`), whose fd table dies with it. zshrs runs
5622/// the body in process, so the parent's descriptors are saved here the
5623/// first time the body's permanent redirection touches each one, and put
5624/// back when the frame is dropped; a descriptor from 10 up that the body
5625/// allocated is closed, as the child's exit would (`(exec {v}>f); exec
5626/// {w}>g` gives `w` 11 in zsh). A body that redirects nothing permanently
5627/// costs nothing.
5628pub struct SubshFdFrame {
5629    /// Only `enter` builds one, so every frame on `SUBSH_FD_FRAMES` has
5630    /// exactly one owner to pop it.
5631    _entered: (),
5632}
5633
5634thread_local! {
5635    /// `(fd, copy, fdtable kind)` per frame, innermost last; a copy of -1
5636    /// means the fd was closed on entry. Thread-local: a frame is entered and left on
5637    /// the thread that runs the body.
5638    static SUBSH_FD_FRAMES: std::cell::RefCell<Vec<Vec<(i32, i32, i32)>>> =
5639        const { std::cell::RefCell::new(Vec::new()) };
5640}
5641
5642impl SubshFdFrame {
5643    /// Open a frame for a subshell body.
5644    pub fn enter() -> Self {
5645        SUBSH_FD_FRAMES.with(|f| f.borrow_mut().push(Vec::new()));
5646        SubshFdFrame { _entered: () }
5647    }
5648
5649    /// Called before a permanent redirection changes `fd`: keep a copy
5650    /// (past the script's `{var}` range — see `lowfd::movefd_past_script`)
5651    /// unless the innermost frame already has one. Outside any frame
5652    /// the redirection really is permanent and nothing is kept.
5653    pub fn touch(fd: i32) {
5654        if fd < 0 {
5655            return;
5656        }
5657        SUBSH_FD_FRAMES.with(|f| {
5658            if let Some(frame) = f.borrow_mut().last_mut() {
5659                if !frame.iter().any(|(saved_fd, _, _)| *saved_fd == fd) {
5660                    let copy = unsafe {
5661                        libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, crate::lowfd::EXTENSION_FD_FLOOR)
5662                    };
5663                    frame.push((fd, copy, crate::ported::utils::fdtable_get(fd)));
5664                }
5665            }
5666        });
5667    }
5668
5669    /// Called when the body takes a descriptor from 10 up for the script
5670    /// (fdtable_set to a user kind): it did not exist for the parent, so
5671    /// the frame closes it on the way out.
5672    pub fn opened(fd: i32) {
5673        if fd < 10 {
5674            return;
5675        }
5676        SUBSH_FD_FRAMES.with(|f| {
5677            if let Ok(mut frames) = f.try_borrow_mut() {
5678                if let Some(frame) = frames.last_mut() {
5679                    if !frame.iter().any(|(saved_fd, _, _)| *saved_fd == fd) {
5680                        frame.push((fd, -1, crate::ported::zsh_h::FDT_UNUSED));
5681                    }
5682                }
5683            }
5684        });
5685    }
5686}
5687
5688impl Drop for SubshFdFrame {
5689    /// Put the parent's descriptors back, most recent first.
5690    fn drop(&mut self) {
5691        let frame = SUBSH_FD_FRAMES.with(|f| f.borrow_mut().pop().unwrap_or_default());
5692        if frame.is_empty() {
5693            return;
5694        }
5695        // Bytes the body printed belong to the body's fd 1.
5696        let _ = std::io::Write::flush(&mut std::io::stdout());
5697        for (fd, copy, kind) in frame.into_iter().rev() {
5698            if copy >= 0 {
5699                unsafe {
5700                    libc::dup2(copy, fd);
5701                    libc::close(copy);
5702                }
5703                if fd >= 10 {
5704                    crate::ported::utils::fdtable_set(fd, kind);
5705                }
5706            } else if fd >= 10 {
5707                crate::ported::utils::zclose(fd); // also drops the fdtable mark
5708            } else {
5709                unsafe { libc::close(fd) };
5710            }
5711        }
5712    }
5713}
5714
5715/// Port of `getpipe()` from `Src/exec.c:5119` — C decl `getpipe(char *cmd, int nullexec)`.
5716///
5717/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
5718/// pipe pair: parent gets back the readable (`<(...)`) or writable
5719/// (`>(...)`) end as an fd; child runs the substituted command with
5720/// its stdio redirected into the other end.
5721///
5722/// ```c
5723/// Eprog prog;
5724/// int pipes[2], out = *cmd == Inang;
5725/// pid_t pid;
5726/// struct timespec bgtime;
5727/// char *ends;
5728/// if (!(prog = parsecmd(cmd, &ends))) return -1;
5729/// if (*ends) { zerr("invalid syntax..."); return -1; }
5730/// if (mpipe(pipes) < 0) return -1;
5731/// if ((pid = zfork(&bgtime))) {
5732///     zclose(pipes[out]);
5733///     if (pid == -1) { zclose(pipes[!out]); return -1; }
5734///     if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
5735///     procsubstpid = pid;
5736///     return pipes[!out];
5737/// }
5738/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
5739/// redup(pipes[out], out);
5740/// closem(FDT_UNUSED, 0);
5741/// cmdpush(CS_CMDSUBST);
5742/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
5743/// cmdpop();
5744/// _realexit();
5745/// ```
5746///
5747/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
5748///     procsubst pid recording site (c:5141-5142) earlier this
5749///     session; the child IS now recorded in `JOBTAB[thisjob]`.
5750/// (b) `entersubsh` IS ported (exec.rs:4329) including the
5751///     ESUB_PGRP pipeline group-leadership path, and is called from
5752///     the real forked child below for getpipe's
5753///     `entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL)`.
5754/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
5755///     can route through execode for the parsed eprog. Currently
5756///     this caller still uses the fusevm pipeline for cache
5757///     coherence with execstring; switch over when the wordcode
5758///     walker becomes the primary path.
5759/// (d) `_realexit()` flushes stdio + jobs + history. We use bare
5760///     `std::process::exit(lastval)` for now.
5761pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
5762    // c:5119
5763    let bytes = cmd.as_bytes();
5764    let out: i32 = if cmd.starts_with(Inang) {
5765        1 // c:5122 — `<(...)` reads from child, child writes to fd 1
5766    } else {
5767        0 // `>(...)` — child reads from fd 0
5768    };
5769    let mut ends_at: usize = 0;
5770    let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
5771    if prog.is_none() {
5772        // c:5127
5773        return -1; // c:5128
5774    }
5775    // c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
5776    if ends_at < bytes.len() && bytes[ends_at] != 0 {
5777        zerr("invalid syntax for process substitution in redirection"); // c:5130
5778        return -1; // c:5131
5779    }
5780    let mut pipes: [i32; 2] = [-1; 2];
5781    if mpipe(&mut pipes) < 0 {
5782        // c:5133
5783        return -1;
5784    }
5785    // c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
5786    let mut bgtime: ZshTimespec = libc::timespec {
5787        tv_sec: 0,
5788        tv_nsec: 0,
5789    };
5790    let pid = zfork(Some(&mut bgtime)); // c:5135
5791    if pid != 0 {
5792        // c:5135 — parent.
5793        let _ = zclose(pipes[out as usize]); // c:5136
5794        if pid == -1 {
5795            // c:5137
5796            let _ = zclose(pipes[(1 - out) as usize]); // c:5138
5797            return -1; // c:5139
5798        }
5799        // c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
5800        if nullexec == 0 {
5801            if let Some(jt) = JOBTAB.get() {
5802                let mut guard = jt.lock().unwrap();
5803                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5804                if tj >= 0 {
5805                    if let Some(j) = guard.get_mut(tj as usize) {
5806                        crate::ported::jobs::addproc(
5807                            j,
5808                            pid,
5809                            "",
5810                            true, // aux=1 for proc subst
5811                            Some(std::time::Instant::now()),
5812                            -1,
5813                            -1,
5814                        );
5815                    }
5816                }
5817            }
5818        }
5819        procsubstpid.store(pid, Ordering::Relaxed); // c:5143
5820        return pipes[(1 - out) as usize]; // c:5144
5821    }
5822    // c:5146 — child path.
5823    entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
5824    let _ = redup(pipes[out as usize], out); // c:5147
5825    closem(FDT_UNUSED, 0); // c:5148
5826    cmdpush(CS_CMDSUBST as u8); // c:5149
5827                                // c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
5828    // The re-fed text is what parsecmd parsed: after the two marker chars,
5829    // up to the Outpar ending at `ends_at` (a byte offset). The markers are
5830    // multi-byte chars, so a `cmd[2..]` byte slice split them.
5831    let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
5832    let body_end = ends_at.saturating_sub(Outpar.len_utf8());
5833    let body = if body_end > body_start && body_end <= cmd.len() {
5834        untokenize(&cmd[body_start..body_end])
5835    } else {
5836        String::new()
5837    };
5838    let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
5839    cmdpop(); // c:5151
5840              // c:5152 — _realexit() — WARNING (d).
5841    std::process::exit(LASTVAL.load(Ordering::Relaxed));
5842}
5843
5844/// Port of `spawnpipes()` from `Src/exec.c:5184` — C decl `spawnpipes(LinkList l, int nullexec)`.
5845///
5846/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
5847/// entry fires `getpipe(name, nullexec || varid)` and stashes the
5848/// resulting fd into `f->fd2`.
5849///
5850/// ```c
5851/// LinkNode n;
5852/// Redir f;
5853/// char *str;
5854/// n = firstnode(l);
5855/// for (; n; incnode(n)) {
5856///     f = (Redir) getdata(n);
5857///     if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
5858///         str = f->name;
5859///         f->fd2 = getpipe(str, nullexec || f->varid);
5860///     }
5861/// }
5862/// ```
5863///
5864/// =================== WARNING — DIVERGENCE ====================
5865/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
5866/// in place of `LinkList`. The walk is identical; the only behavior
5867/// difference is that LinkList iteration in C lets callers splice
5868/// nodes mid-walk — we never do that here so it's a no-op divergence.
5869/// =============================================================
5870pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
5871    // c:5184
5872    for f in l.iter_mut() {
5873        // c:5191
5874        if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
5875            // c:5193
5876            let str_ = f.name.clone().unwrap_or_default(); // c:5194
5877            let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
5878                1
5879            } else {
5880                nullexec
5881            };
5882            f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
5883        }
5884    }
5885}
5886
5887/// Port of `cancd2()` from `Src/exec.c:6411` — C decl `cancd2(char *s)`.
5888///
5889/// C body:
5890/// ```c
5891/// struct stat buf;
5892/// char *us, *us2 = NULL;
5893/// int ret;
5894/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
5895///     if (*s != '/')
5896///         us = tricat(pwd[1] ? pwd : "", "/", s);
5897///     else
5898///         us = ztrdup(s);
5899///     fixdir(us2 = us);
5900/// } else
5901///     us = unmeta(s);
5902/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
5903/// if (us2) free(us2);
5904/// return ret;
5905/// ```
5906///
5907/// True iff `s` is a directory we can `cd` into (X-perm). With
5908/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
5909/// (joining with PWD if relative) so `cd /foo/bar/..` works without
5910/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
5911pub fn cancd2(s: &str) -> i32 {
5912    // c:6411
5913    let us: String;
5914    // c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
5915    let chasedots = isset(CHASEDOTS); // c:6422
5916    let chaselinks = isset(CHASELINKS);
5917    if !chasedots && !chaselinks {
5918        // c:6422
5919        // c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
5920        let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
5921        let mut raw = if !s.starts_with('/') {
5922            // c:6423
5923            format!(
5924                "{}/{}",
5925                if pwd_str.len() > 1 { &pwd_str[..] } else { "" },
5926                s
5927            )
5928        } else {
5929            s.to_string()
5930        };
5931        // c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
5932        fixdir(&mut raw);
5933        us = raw;
5934    } else {
5935        // c:6428
5936        us = unmeta(s); // c:6429
5937    }
5938    // c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
5939    let cstr = match std::ffi::CString::new(us.as_str()) {
5940        Ok(c) => c,
5941        Err(_) => return 0,
5942    };
5943    if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
5944        return 0;
5945    }
5946    let meta = match std::fs::metadata(&us) {
5947        Ok(m) => m,
5948        Err(_) => return 0,
5949    };
5950    if !meta.file_type().is_dir() {
5951        return 0;
5952    }
5953    1
5954}
5955
5956/// Port of `cancd()` from `Src/exec.c:6370` — C decl `cancd(char *s)`.
5957///
5958/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
5959/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
5960/// it, else `None`.
5961///
5962/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
5963/// `doprintdir = -1` when a non-trivial path is found (so `cd`
5964/// echoes the resolved path).
5965pub fn cancd(s: &str) -> Option<String> {
5966    // c:6370
5967    // c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
5968    //                (s[1]=='.' && (s[2]=='/' || !s[2])))`.
5969    let bytes = s.as_bytes();
5970    let nocdpath = bytes.first().copied() == Some(b'.')
5971        && (bytes.get(1).copied() == Some(b'/')
5972            || bytes.get(1).is_none()
5973            || (bytes.get(1).copied() == Some(b'.')
5974                && (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
5975    // c:6376 — `if (*s != '/')` branch.
5976    if !s.starts_with('/') {
5977        // c:6376
5978        // c:6379-6380 — `if (cancd2(s)) return s;`
5979        if cancd2(s) != 0 {
5980            return Some(s.to_string());
5981        }
5982        // c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
5983        let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
5984        if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
5985            return None; // c:6382
5986        }
5987        // c:6383-6397 — CDPATH walk.
5988        if !nocdpath {
5989            let cdpath_str = getsparam("CDPATH").unwrap_or_default();
5990            for cp in cdpath_str.split(':') {
5991                // c:6384
5992                let sbuf = if !cp.is_empty() {
5993                    format!("{}/{}", cp, s) // c:6386
5994                } else {
5995                    s.to_string() // c:6391
5996                };
5997                if cancd2(&sbuf) != 0 {
5998                    // c:6393
5999                    DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
6000                    return Some(sbuf); // c:6395
6001                }
6002            }
6003        }
6004        // c:6398-6403 — `cd_able_vars()` fallback.
6005        if let Some(t) = cd_able_vars(s) {
6006            // c:6398
6007            if cancd2(&t) != 0 {
6008                // c:6399
6009                DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
6010                return Some(t); // c:6401
6011            }
6012        }
6013        return None; // c:6404
6014    }
6015    // c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
6016    if cancd2(s) != 0 {
6017        Some(s.to_string())
6018    } else {
6019        None
6020    }
6021}
6022
6023/// Port of `simple_redir_name()` from `Src/exec.c:4689` — C decl `simple_redir_name(Eprog prog, int redir_type)`.
6024///
6025/// Test if an Eprog encodes a single simple-command consisting of a
6026/// SINGLE redirection of the requested type with NO command body
6027/// (the `cat < foo` shape). When true, returns the redir target name
6028/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
6029/// `open(2)` instead of fork+pipe+exec.
6030///
6031/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
6032/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
6033/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
6034/// same shape — this port replicates the same offset reads.
6035pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
6036    // c:4689
6037    let pc = &prog.prog;
6038    // c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
6039    // offsets matching C's `pc[0]..pc[6]` checks.
6040    if pc.len() < 7 {
6041        return None;
6042    }
6043
6044    if wc_code(pc[0]) != WC_LIST
6045        || (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0  // c:4695
6046        || wc_code(pc[1]) != WC_SUBLIST
6047        || WC_SUBLIST_FLAGS(pc[1]) != 0  // c:4696
6048        || WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END  // c:4697
6049        || wc_code(pc[2]) != WC_PIPE
6050        || WC_PIPE_TYPE(pc[2]) != WC_PIPE_END  // c:4698
6051        || wc_code(pc[3]) != WC_REDIR
6052        || WC_REDIR_TYPE(pc[3]) != redir_type  // c:4699
6053        || WC_REDIR_VARID(pc[3]) != 0  // c:4700
6054        || pc[4] != 0  // c:4701
6055        || wc_code(pc[6]) != WC_SIMPLE
6056        || WC_SIMPLE_ARGC(pc[6]) != 0
6057    // c:4702
6058    {
6059        return None; // c:4706
6060    }
6061    // c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
6062    Some(dupstring(&ecrawstr(prog, 5, None)))
6063}
6064
6065/// Port of `getherestr()` from `Src/exec.c:4655` — C decl `getherestr(struct redir *fn)`.
6066///
6067/// C body:
6068/// ```c
6069/// char *s, *t;
6070/// int fd, len;
6071/// t = fn->name;
6072/// singsub(&t);
6073/// untokenize(t);
6074/// unmetafy(t, &len);
6075/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
6076///     t[len++] = '\n';
6077/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
6078///     return -1;
6079/// write_loop(fd, t, len);
6080/// close(fd);
6081/// fd = open(s, O_RDONLY | O_NOCTTY);
6082/// unlink(s);
6083/// return fd;
6084/// ```
6085///
6086/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
6087/// tempfile, then re-open read-only and unlink — gives the consumer a
6088/// read fd whose backing file is already cleaned up.
6089pub fn getherestr(fn_: &redir) -> i32 {
6090    // c:4655
6091    let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
6092    t = singsub(&t); // c:4661
6093    t = untokenize(&t); // c:4662
6094                        // c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
6095                        // Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
6096    let mut bytes: Vec<u8> = t.into_bytes();
6097    let _len = unmetafy(&mut bytes);
6098    // c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
6099    if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
6100        // c:4671
6101        bytes.push(b'\n'); // c:4672
6102    }
6103    // c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
6104    let (fd, s) = match gettempfile(None) {
6105        Some(p) => p,
6106        None => return -1, // c:4674
6107    };
6108    // c:4675 — `write_loop(fd, t, len);`
6109    let _ = write_loop(fd, &bytes); // c:4675
6110                                    // c:4676 — `close(fd);`
6111    let _ = zclose(fd); // c:4676
6112                        // c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
6113    let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
6114    let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
6115                                                                                        // c:4678 — `unlink(s);`
6116    unsafe {
6117        libc::unlink(cstr.as_ptr());
6118    } // c:4678
6119    new_fd // c:4679
6120}
6121
6122/// Port of `quote_tokenized_output()` from `Src/exec.c:2114` — C decl `quote_tokenized_output(char *str, FILE *file)`.
6123///
6124/// C body (abridged):
6125/// ```c
6126/// for (; *s; s++) {
6127///     switch (*s) {
6128///         case Meta: putc(*++s ^ 32, file); continue;
6129///         case Nularg: continue;
6130///         case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
6131///             putc('\\', file); break;
6132///         case '\t': fputs("$'\\t'", file); continue;
6133///         case '\n': fputs("$'\\n'", file); continue;
6134///         case '\r': fputs("$'\\r'", file); continue;
6135///         case '=': if (s == str) putc('\\', file); break;
6136///         default:
6137///             if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
6138///     }
6139///     putc(*s, file);
6140/// }
6141/// ```
6142///
6143/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
6144/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
6145/// surface as unescaped chars (`*`, `(`) while literal special chars
6146/// get backslash-escaped — round-tripping through the shell.
6147pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
6148    // c:2114
6149    let bytes = str_in.as_bytes();
6150    let mut i = 0usize;
6151    while i < bytes.len() {
6152        // c:2118 `for (; *s; s++)`
6153        let c = bytes[i];
6154        match c {
6155            x if x == Meta => {
6156                // c:2120 — `case Meta: putc(*++s ^ 32, file);`
6157                if i + 1 < bytes.len() {
6158                    file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
6159                    i += 2;
6160                } else {
6161                    i += 1;
6162                }
6163                continue; // c:2122
6164            }
6165            x if x as char == Nularg => {
6166                // c:2124
6167                i += 1;
6168                continue; // c:2126
6169            }
6170            b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']' | b'*'
6171            | b'?' | b'$' | b' ' => {
6172                // c:2128-2142
6173                file.write_all(b"\\")?; // c:2143
6174            }
6175            b'\t' => {
6176                // c:2146
6177                file.write_all(b"$'\\t'")?; // c:2147
6178                i += 1;
6179                continue;
6180            }
6181            b'\n' => {
6182                // c:2150
6183                file.write_all(b"$'\\n'")?; // c:2151
6184                i += 1;
6185                continue;
6186            }
6187            b'\r' => {
6188                // c:2154
6189                file.write_all(b"$'\\r'")?; // c:2155
6190                i += 1;
6191                continue;
6192            }
6193            b'=' => {
6194                // c:2158 — `if (s == str) putc('\\', file);`
6195                if i == 0 {
6196                    file.write_all(b"\\")?; // c:2160
6197                }
6198            }
6199            _ => {
6200                // c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
6201                if itok(c) {
6202                    // c:2164
6203                    let pound = Pound as u8;
6204                    if c >= pound {
6205                        let idx = (c - pound) as usize;
6206                        let zt = ztokens.as_bytes();
6207                        if idx < zt.len() {
6208                            file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
6209                        }
6210                    }
6211                    i += 1;
6212                    continue;
6213                }
6214            }
6215        }
6216        file.write_all(&[c])?; // c:2171
6217        i += 1;
6218    }
6219    Ok(())
6220}
6221
6222// =====================================================================
6223// Wordcode-VM control-flow dispatch — faithful ports of the C
6224// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
6225//
6226// Each function below takes `&mut estate` and returns `i32` to mirror
6227// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
6228// line `// c:NNN` citations track the C source line.
6229//
6230// zshrs's primary execution path is the fusevm bytecode VM. These
6231// wordcode-VM entries exist for C-name parity with the upstream
6232// interpreter so that future bridging code can drive zshrs through
6233// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
6234// zshrs primitives don't yet model their C counterpart (e.g.
6235// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
6236// helper is declared with a comment citing the C source file:line
6237// where the canonical body lives — same pattern as the canonical
6238// `ksh93::ksh93_wrapper` port at c:152-227.
6239// =====================================================================
6240
6241use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
6242use crate::ported::pattern::{patcompile, pattry};
6243use crate::ported::r#loop::try_tryflag;
6244
6245// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
6246use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
6247use crate::ported::linklist::LinkList;
6248use crate::ported::mem::freeheap;
6249use crate::ported::params::setloopvar;
6250use crate::ported::params::{assignaparam, assignsparam, unsetparam};
6251use crate::ported::parse::{ecgetlist, ecgetstr};
6252use crate::ported::pattern::haswilds;
6253use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
6254use crate::ported::subst::{globlist, prefork};
6255use crate::ported::zsh_h::{
6256    estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
6257    WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP,
6258    WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
6259    WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
6260    WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
6261};
6262use crate::ported::zsh_h::{
6263    ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
6264    PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
6265    WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
6266};
6267use crate::ported::zsh_h::{
6268    CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
6269    CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
6270};
6271
6272// --- Local stubs for C primitives not yet ported elsewhere ------------
6273//
6274// These mirror the C functions of the same names. Each cites the C
6275// source file:line where the canonical body lives. They are inlined
6276// here (rather than a separate `pub fn` in the owning C-file module)
6277// because the owning ports are pending the wider exec-substrate
6278// work (sub-PR). Once those land, these locals collapse to direct
6279// `crate::ported::<owner>::<fn>` calls.
6280
6281/// Port of `execsubst()` from `Src/exec.c:2684` — C decl `execsubst(LinkList strs)`.
6282///
6283/// C body (c:2684-2693):
6284/// ```c
6285/// void execsubst(LinkList strs) {
6286///     if (strs) {
6287///         prefork(strs, esprefork, NULL);
6288///         if (esglob && !errflag) {
6289///             LinkList ostrs = strs;
6290///             globlist(strs, 0);
6291///             strs = ostrs;
6292///         }
6293///     }
6294/// }
6295/// ```
6296///
6297/// `execsubst` runs `prefork` (parameter / arithmetic / command
6298/// substitution expansion + IFS-split) over the whole list, then
6299/// (when `esglob` is set) `globlist` to do filename globbing on the
6300/// result.
6301pub(crate) fn execsubst(list: &mut Vec<String>) {
6302    // c:2684
6303    if list.is_empty() {
6304        return; // c:2686 `if (strs)`
6305    }
6306    let mut ll: crate::ported::subst::LinkList = std::mem::take(list).into_iter().collect();
6307    let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
6308    let mut rf: i32 = 0;
6309    prefork(&mut ll, prefork_flags, &mut rf); // c:2687
6310    if esglob.load(Ordering::Relaxed) != 0 && errflag.load(Ordering::Relaxed) == 0 {
6311        // c:2688 `if (esglob && !errflag)`
6312        globlist(&mut ll, 0); // c:2690
6313    }
6314    *list = ll.into_iter().collect();
6315}
6316
6317/// Port of `addvars()` from `Src/exec.c:2499` — C decl `addvars(Estate state, Wordcode pc, int addflags)`.
6318/// Direct port of `static void addvars(Estate state, Wordcode pc,
6319/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
6320/// nodes stacked inline of a simple command — the `var=value` and
6321/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
6322/// at `pc`, extracts each assignment's name + value (scalar or array),
6323/// optionally preforks + globs the tokenised RHS, and routes through
6324/// `assignsparam` (scalar) or `assignaparam` (array).
6325///
6326/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
6327/// stderr (C uses xtrerr; zshrs uses eprint!).
6328///
6329/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
6330/// into the file-static `STTYval` for `execute()` to apply pre-exec.
6331fn addvars(state: &mut estate, pc: usize, addflags: i32) {
6332    // c:2501 — locals.
6333    let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
6334    let xtr: bool; // c:2502 `int xtr,`
6335    let mut isstr: bool; // c:2502 `int isstr,`
6336    let mut htok: i32 = 0; // c:2502 `int htok = 0;`
6337    let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
6338    let mut name: String;
6339    let mut flags: i32; // c:2504 `int flags;`
6340    let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
6341    let mut ac: wordcode; // c:2507 `wordcode ac;`
6342                          // c:2508 `local_list1(svl);` — stack-local one-element LinkList
6343                          // for the scalar-assignment path. Rust uses a fresh LinkList per
6344                          // iteration; equivalent semantics.
6345
6346    // c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
6347    // when the assignment list is implicitly local (ADDVAR_RESTORE).
6348    flags = if (addflags & ADDVAR_RESTORE) == 0 {
6349        ASSPM_WARN // c:2516
6350    } else {
6351        0 // c:2516
6352    };
6353    xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
6354    if xtr {
6355        // c:2518
6356        printprompt4(); // c:2519
6357        doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
6358    }
6359    state.pc = pc; // c:2522 `state->pc = pc;`
6360
6361    // c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
6362    loop {
6363        if state.pc >= state.prog.prog.len() {
6364            break;
6365        }
6366        ac = state.prog.prog[state.pc];
6367        state.pc += 1;
6368        if wc_code(ac) != WC_ASSIGN {
6369            // Step back so the WC_SIMPLE / outer dispatcher sees the
6370            // non-assignment opcode. C's `state->pc++` post-increment
6371            // already pointed past WC_ASSIGN; we need to unconsume.
6372            state.pc -= 1;
6373            break;
6374        }
6375        let mut myflags = flags; // c:2524 `int myflags = flags;`
6376        name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
6377        if htok != 0 {
6378            // c:2526 `if (htok) untokenize(name);`
6379            name = untokenize(&name).to_string(); // c:2527
6380        }
6381        if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
6382            // c:2528
6383            myflags |= ASSPM_AUGMENT; // c:2529
6384        }
6385        if xtr {
6386            // c:2530
6387            // c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
6388            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
6389                eprint!("{}+=", name); // c:2532
6390            } else {
6391                eprint!("{}=", name); // c:2532
6392            }
6393        }
6394
6395        // c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
6396        isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
6397        if isstr {
6398            // c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
6399            let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
6400            vl = LinkList::new();
6401            vl.push_back(svl_val);
6402            // c:2535 `vl = &svl;` — vl already points at the new list.
6403        } else {
6404            // c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
6405            let items = ecgetlist(
6406                state,
6407                WC_ASSIGN_NUM(ac) as usize,
6408                EC_DUPTOK,
6409                Some(&mut htok),
6410            );
6411            vl = LinkList::new();
6412            for it in items {
6413                vl.push_back(it);
6414            }
6415            if errflag.load(Ordering::Relaxed) != 0 {
6416                // c:2538-2541
6417                state.pc = opc; // c:2539
6418                return; // c:2540
6419            }
6420        }
6421
6422        // c:2544 `if (vl && htok) {`
6423        if htok != 0 {
6424            // c:2545 `int prefork_ret = 0;`
6425            let mut prefork_ret: i32 = 0;
6426            // c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
6427            //                          : PREFORK_ASSIGN), &prefork_ret);
6428            let pf_flags = if isstr {
6429                PREFORK_SINGLE | PREFORK_ASSIGN
6430            } else {
6431                PREFORK_ASSIGN
6432            };
6433            prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
6434            if errflag.load(Ordering::Relaxed) != 0 {
6435                // c:2548
6436                state.pc = opc; // c:2549
6437                return; // c:2550
6438            }
6439            if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
6440                // c:2552
6441                myflags |= ASSPM_KEY_VALUE; // c:2553
6442            }
6443            // c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
6444            //                  haswilds((char *)getdata(firstnode(vl)))))`
6445            let needs_glob = if !isstr {
6446                true
6447            } else {
6448                isset(GLOBASSIGN)
6449                    && isstr
6450                    && !vl.is_empty()
6451                    && haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
6452            };
6453            if needs_glob {
6454                globlist(&mut vl, prefork_ret); // c:2556
6455                                                // c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
6456                                                //                  unsetparam(name);`
6457                if isset(GLOBASSIGN) && isstr {
6458                    unsetparam(&name); // c:2562
6459                }
6460                if errflag.load(Ordering::Relaxed) != 0 {
6461                    // c:2563
6462                    state.pc = opc; // c:2564
6463                    return; // c:2565
6464                }
6465            }
6466        }
6467        // c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
6468        // — scalar-assignment path: zero or one element after prefork.
6469        if isstr && (vl.is_empty() || vl.len() == 1) {
6470            let val: String; // c:2571 `char *val;`
6471            if vl.is_empty() {
6472                // c:2574
6473                val = String::new(); // c:2575 `val = ztrdup("");`
6474            } else {
6475                // c:2577 `untokenize(peekfirst(vl));`
6476                let peek = vl.nodes.front().cloned().unwrap_or_default();
6477                val = untokenize(&peek).to_string(); // c:2577-2578
6478                                                     // c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
6479                                                     // we just cloned the front above. Equivalent.
6480            }
6481            if xtr {
6482                // c:2580
6483                eprint!("{}", quotedzputs(&val)); // c:2581
6484                eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
6485            }
6486            // c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
6487            let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
6488                // c:2585 `if (strcmp(name, "STTY") == 0)`
6489                if name == "STTY" {
6490                    // c:2586-2587 — `STTYval = ztrdup(val);`
6491                    let mut stty = STTYval.lock().unwrap();
6492                    *stty = Some(val.clone()); // c:2587
6493                }
6494                // c:2589 `allexp = opts[ALLEXPORT];`
6495                let allexp = isset(ALLEXPORT);
6496                // c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
6497                opt_state_set("allexport", true);
6498                if isset(KSHARRAYS) {
6499                    // c:2591
6500                    unsetparam(&name); // c:2592
6501                }
6502                let pm = assignsparam(&name, &val, myflags); // c:2593
6503                                                             // c:2594 `opts[ALLEXPORT] = allexp;` — restore.
6504                opt_state_set("allexport", allexp);
6505                pm
6506            } else {
6507                // c:2595
6508                assignsparam(&name, &val, myflags) // c:2596
6509            };
6510            if pm.is_none() {
6511                // c:2597 `if (!pm)`
6512                LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
6513                                                     // c:2599-2604 — "cheating" comment: don't zerr.
6514                if cmdoutval.load(Ordering::Relaxed) == 0 {
6515                    // c:2605 `if (!cmdoutval)`
6516                    cmdoutval.store(1, Ordering::Relaxed); // c:2606
6517                }
6518            }
6519            if errflag.load(Ordering::Relaxed) != 0 {
6520                // c:2608
6521                state.pc = opc; // c:2609
6522                return; // c:2610
6523            }
6524            continue; // c:2612
6525        }
6526        // c:2614 `if (vl) { ... }` — array-assignment path: drain vl
6527        // into a fresh `char **arr`.
6528        // c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
6529        arr = Vec::with_capacity(vl.len() + 1);
6530        while let Some(s) = vl.pop_front() {
6531            arr.push(s);
6532        }
6533        // c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
6534        if xtr {
6535            // c:2624
6536            eprint!("( "); // c:2625
6537            for s in &arr {
6538                // c:2626 `for (ptr = arr; *ptr; ptr++)`
6539                eprint!("{}", quotedzputs(s)); // c:2627
6540                eprint!(" "); // c:2628
6541            }
6542            eprint!(") "); // c:2630
6543        }
6544        // c:2632 `if (!assignaparam(name, arr, myflags))`
6545        if assignaparam(&name, arr, myflags).is_none() {
6546            LASTVAL.store(1, Ordering::Relaxed); // c:2633
6547                                                 // c:2634-2638 — "cheating" comment.
6548            if cmdoutval.load(Ordering::Relaxed) == 0 {
6549                // c:2639
6550                cmdoutval.store(1, Ordering::Relaxed); // c:2640
6551            }
6552        }
6553        if errflag.load(Ordering::Relaxed) != 0 {
6554            // c:2642
6555            state.pc = opc; // c:2643
6556            return; // c:2644
6557        }
6558    }
6559    state.pc = opc; // c:2647 `state->pc = opc;`
6560}
6561
6562// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
6563// match expression at the call sites in execsimple. Not a separate
6564// Rust fn — every C-side reference to
6565// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
6566
6567// --- exec.c entries ---------------------------------------------------
6568
6569/// Port of `execcursh()` from `Src/exec.c:469` — C decl `execcursh(Estate state, int do_exec)`.
6570/// Execute a `{ ... }` current-shell command
6571/// group: skip the trailing try-only word, optionally drop a stale
6572/// job slot, then run the inner list.
6573pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
6574    // c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
6575    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
6576    let end = state.pc + WC_CURSH_SKIP(prior) as usize;
6577    // c:475 — `state->pc++;` skip the try/always-only word.
6578    state.pc += 1;
6579    // c:482-486 — drop empty job slot before nested cmd: if outer-pipe
6580    // bookkeeping is clean AND thisjob is a real job that's not the
6581    // pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
6582    // leaking job-table slots when execcursh recurses.
6583    {
6584        let lp = list_pipe.load(Ordering::Relaxed);
6585        let lpj = list_pipe_job.load(Ordering::Relaxed);
6586        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
6587        if lp == 0 && tj != -1 && tj != lpj {
6588            if let Some(jt) = JOBTAB.get() {
6589                let mut guard = jt.lock().unwrap();
6590                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
6591                if !has {
6592                    if let Some(j) = guard.get_mut(tj as usize) {
6593                        crate::ported::jobs::deletejob(j, false);
6594                    }
6595                }
6596            }
6597        }
6598    }
6599    cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
6600    let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
6601    cmdpop(); // c:489 — `cmdpop();`
6602    state.pc = end; // c:491 — `state->pc = end;`
6603    this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
6604    LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
6605}
6606
6607// `(...)` subshell — no dedicated C function (handled inline by
6608// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
6609// In zshrs the subshell branch is folded into `execpline` and
6610// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
6611// inner-list walk since fusevm bytecode handles the forking via
6612// Op::Subshell at a higher layer.
6613
6614/// Port of `execcond()` from `Src/exec.c:5204` — C decl `execcond(Estate state, UNUSED(int do_exec))`.
6615/// Run a `[[ ... ]]` cond expression.
6616pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
6617    state.pc -= 1; // c:5208 — `state->pc--;`
6618                   // c:5209-5213 — XTRACE prelude.
6619    if isset(XTRACE) {
6620        printprompt4();
6621        eprint!("[[");
6622        // c:5212 — `tracingcond++;` not modeled in zshrs.
6623    }
6624    cmdpush(CS_COND as u8); // c:5214
6625                            // c:5215 — `stat = evalcond(state, NULL);` — TODO faithful: needs
6626                            // the wordcode-level evalcond from Src/cond.c which is distinct
6627                            // from the test-builtin evalcond ported in cond.rs. Pending.
6628    let stat: i32 = 0;
6629    // c:5219-5221 — `if (stat == 2) errflag |= ERRFLAG_ERROR;`
6630    if stat == 2 {
6631        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
6632    }
6633    cmdpop(); // c:5222
6634    if isset(XTRACE) {
6635        eprintln!(" ]]");
6636    }
6637    stat // c:5230 — `return stat;`
6638}
6639
6640/// Port of `execarith()` from `Src/exec.c:5235` — C decl `execarith(Estate state, UNUSED(int do_exec))`.
6641/// Run a `(( ... ))` arithmetic command;
6642/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
6643/// parse error.
6644pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
6645    if isset(XTRACE) {
6646        printprompt4();
6647        eprint!("((");
6648    }
6649    cmdpush(CS_MATH as u8); // c:5247
6650    let mut htok: i32 = 0;
6651    let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
6652    if htok != 0 {
6653        e = singsub(&e); // c:5250 — `singsub(&e);`
6654    }
6655    if isset(XTRACE) {
6656        eprint!(" {}", e);
6657    }
6658    let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
6659    cmdpop(); // c:5256
6660    if isset(XTRACE) {
6661        eprintln!(" ))");
6662    }
6663    // c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
6664    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
6665        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
6666        return 2;
6667    }
6668    // c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
6669    let val = val_result.unwrap();
6670    if val.type_ == MN_INTEGER {
6671        if val.l == 0 {
6672            1
6673        } else {
6674            0
6675        }
6676    } else if val.d == 0.0 {
6677        1
6678    } else {
6679        0
6680    }
6681}
6682
6683/// Port of `exectime()` from `Src/exec.c:5272` — C decl `exectime(Estate state, UNUSED(int do_exec))`.
6684/// Run `time pipeline`: drives execpline with
6685/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
6686pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
6687    let jb = *THISJOB
6688        .get_or_init(|| std::sync::Mutex::new(-1))
6689        .lock()
6690        .unwrap(); // c:5283
6691    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
6692    // c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
6693    if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
6694        // c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
6695        // shell+kids time deltas since last call.
6696        crate::ported::jobs::shelltime(None, None, None, 0);
6697        return 0; // c:5286
6698    }
6699    // c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
6700    let slcode = state.prog.prog[state.pc];
6701    state.pc += 1;
6702    use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
6703    let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
6704    *THISJOB
6705        .get_or_init(|| std::sync::Mutex::new(-1))
6706        .lock()
6707        .unwrap() = jb; // c:5289
6708    LASTVAL.load(Ordering::Relaxed) // c:5290
6709}
6710
6711/// Port of `execshfunc()` from `Src/exec.c:5540` — C decl `execshfunc(Shfunc shf, LinkList args)`.
6712/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
6713/// Promoted to top-level pub fn so execcmd_exec at the shfunc
6714/// dispatch site (c:4102-4105) can route through it. The real port
6715/// owns queue_signals + cmdstack + sfcontext setup before calling
6716/// doshfunc; doshfunc itself is unported, so we route the body
6717/// through `runshfunc` (exec.rs:1700), which carries the
6718/// wrapper-chain + zunderscore restore. Degraded vs C (no cmdstack
6719/// push, no sfcontext flip, no XTRACE arg-trace) but the function
6720/// body executes and `lastval` is updated.
6721pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
6722    // c:5546-5547 — `if (errflag) return;`
6723    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
6724    // the C line cited above tests the WHOLE errflag, so masking here let an
6725    // interrupted shell keep going where zsh stops.
6726    if errflag.load(Ordering::Relaxed) != 0 {
6727        return;
6728    }
6729    // c:5550-5557 — drop empty job slot before nested shfunc invoke:
6730    // if outer-pipe bookkeeping is clean AND thisjob is a real job
6731    // that's not the pipe-leader AND has no procs yet, deletejob()
6732    // recycles it. Avoids leaking job-table slots across recursive
6733    // function calls. Same pattern as execcursh's c:482-486.
6734    {
6735        let lp = list_pipe.load(Ordering::Relaxed);
6736        let lpj = list_pipe_job.load(Ordering::Relaxed);
6737        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
6738        if lp == 0 && tj != -1 && tj != lpj {
6739            if let Some(jt) = JOBTAB.get() {
6740                let mut guard = jt.lock().unwrap();
6741                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
6742                if !has {
6743                    // c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
6744                    //                jobtab[thisjob].filelist = NULL;` — preserve
6745                    //                the filelist so deletejob doesn't unlink temp
6746                    //                files. Rust take()s the Vec into a local.
6747                    let _last_file_list: Vec<jobfile> = if let Some(j) = guard.get_mut(tj as usize)
6748                    {
6749                        std::mem::take(&mut j.filelist)
6750                    } else {
6751                        Vec::new()
6752                    };
6753                    if let Some(j) = guard.get_mut(tj as usize) {
6754                        crate::ported::jobs::deletejob(j, false); // c:5556
6755                    }
6756                }
6757            }
6758        }
6759    }
6760    // c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
6761    // emit PS4 prefix + space-separated quoted args on the trace
6762    // stream so `set -x` shows the function invocation line.
6763    if isset(XTRACE) {
6764        printprompt4();
6765        for (i, a) in args.iter().enumerate() {
6766            if i > 0 {
6767                eprint!(" ");
6768            }
6769            eprint!("{}", quotedzputs(a));
6770        }
6771        eprintln!();
6772    }
6773    // c:5572-5578 cmdstack/sfcontext setup: omit (no cmdstack in
6774    // zshrs yet — replaced by tracing).
6775    // c:5580 — `doshfunc(shf, args, 0);` — doshfunc swaps PPARAMS
6776    // ($1, $2, …) to the function's args, runs the body via
6777    // runshfunc, then restores. doshfunc itself isn't ported yet
6778    // so we do the swap-and-restore inline here.
6779    // c:5580 — `doshfunc(shf, args, 0);`. The C path always has
6780    // `funcdef` populated since C parses at definition time. zshrs
6781    // compiles to fusevm chunks instead, so `funcdef` is None for
6782    // user-defined functions; only `body` (source string) carries
6783    // the definition. When that's the case, build a one-shot eprog
6784    // whose `strs` carries the source so runshfunc's script-pipeline
6785    // arm (execute_script_zsh_pipeline) executes the body.
6786    let prog_owned: Option<eprog> = if shf.funcdef.is_some() {
6787        None
6788    } else if let Some(ref body) = shf.body {
6789        Some(eprog {
6790            strs: Some(body.clone()),
6791            ..Default::default()
6792        })
6793    } else if (shf.node.flags as u32 & PM_UNDEFINED) != 0 {
6794        // c:Src/builtin.c:3763 `shf->funcdef = mkautofn(shf);` — an
6795        // `autoload`ed-but-not-yet-loaded function is NOT body-less in C.
6796        // `mkautofn` (c:3786-3809) hands it a five-word program whose only
6797        // instruction is `WCB_AUTOFN()`, which `execautofn`
6798        // (Src/exec.c:5786) turns into `loadautofn`; `functions NAME`
6799        // prints that program back as `builtin autoload -X`.
6800        //
6801        // zshrs's autoload stub carries neither `funcdef` nor `body`, so
6802        // this arm produced `None` and `execshfunc` returned having done
6803        // NOTHING, status 0. That silently killed every autoloaded function
6804        // reached through `execcmd_exec` — i.e. every call whose command
6805        // word is not a compile-time literal (`$c`, `my$v`, `"$f"`), since
6806        // a literal head goes through `CallFunction` →
6807        // `dispatch_function_call`, which has its own autoload prelude.
6808        // `_sh` dispatches with `_$variant "$@"` (Completion/Unix/Command/
6809        // _sh sh:51), so `sh <TAB>` ran `_bash` as a no-op and completed
6810        // nothing.
6811        //
6812        // Emit mkautofn's program as its printed source; zshrs's
6813        // `autoload -X` arm (builtin.rs `bin_functions`, c:3613-3654) is
6814        // the port of `execautofn`'s effect and re-invokes the function
6815        // with the original arguments via `eval_autoload` (c:3167-3177).
6816        Some(eprog {
6817            strs: Some("builtin autoload -X".to_string()),
6818            ..Default::default()
6819        })
6820    } else {
6821        None
6822    };
6823    let prog_ref: Option<&eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
6824        (Some(p), _) => Some(p),
6825        (_, Some(p)) => Some(p),
6826        _ => None,
6827    };
6828    if let Some(_prog) = prog_ref {
6829        // c:5580 — `doshfunc(shf, args, 0);`. Direct doshfunc call —
6830        // noreturnval=0 means the body's return value updates LASTVAL
6831        // (caller of execfuncdef reads it back). PPARAMS swap +
6832        // restore happens INSIDE doshfunc's scope; body_runner just
6833        // runs the body.
6834        let name_for_body = shf.node.nam.clone();
6835        let body_args_owned: Vec<String> = if args.len() > 1 {
6836            args[1..].to_vec()
6837        } else {
6838            Vec::new()
6839        };
6840        let body_runner = move || -> i32 {
6841            crate::ported::exec::run_function_body(&name_for_body, &body_args_owned).unwrap_or(0)
6842        };
6843        crate::ported::utils::xtrerr.store(2, Ordering::Relaxed); // c:5634 xtrerr = stderr;
6844        let _ = doshfunc(shf, args.clone(), false, body_runner);
6845    }
6846    // c:5582-5589 cmdstack restore/free: omit (no cmdstack).
6847}
6848
6849/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
6850/// from `Src/exec.c:5823-6158`.
6851///
6852/// C body's scope-management sequence ported here. The C source's
6853/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
6854/// is replaced by `body_runner` — zshrs runs function bodies through
6855/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
6856/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
6857/// callback hands the live executor back to the caller (typically
6858/// the fusevm bridge) for the actual body run. Every line of scope
6859/// save/restore around the body call mirrors C exactly.
6860///
6861/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
6862/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
6863/// c:6042; zshrs delegates to a closure because the body-execution
6864/// pipeline (fusevm) differs from C's (wordcode). The closure
6865/// fully replaces the runshfunc call and returns the body's exit
6866/// status (which doshfunc reads as `lastval` for the `noreturnval`
6867/// path).
6868#[allow(non_snake_case)]
6869/// Port of the module-global `oflags` in `Src/exec.c` (set at c:5970 via
6870/// `funcsave->oflags = oflags;`). Holds the attribute flags of the function
6871/// CURRENTLY executing, so a nested call can tell whether its caller carried
6872/// PM_TAGGED_LOCAL — that is what makes `functions -T` non-recursive.
6873/// zshrs-original storage shape (atomic rather than a plain global); the
6874/// save/restore discipline matches C's funcsave. Bug #1058.
6875pub static FUNC_OFLAGS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
6876
6877/// Port of `doshfunc()` from `Src/exec.c:5823` — C decl `doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`.
6878pub fn doshfunc(
6879    shfunc: &mut shfunc,                  // c:5823
6880    doshargs: Vec<String>,                // c:5823
6881    noreturnval: bool,                    // c:5823
6882    mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
6883) -> i32 {
6884    use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
6885    // TEMP-INSTRUMENTATION (remove before commit): shell-function invocation
6886    // volume, to tell "few slow calls" from "very many cheap calls".
6887    {
6888        use std::sync::atomic::{AtomicU64, Ordering as O};
6889        static CALLS: AtomicU64 = AtomicU64::new(0);
6890        let n = CALLS.fetch_add(1, O::Relaxed) + 1;
6891        if n % 5000 == 0 {
6892            tracing::info!(target: "fncount", calls = n, "doshfunc cumulative");
6893        }
6894    }
6895    use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
6896    use crate::ported::modules::parameter::FUNCSTACK;
6897    use crate::ported::params::endparamscope;
6898    use crate::ported::params::locallevel as locallevel_atomic;
6899    use crate::ported::zsh_h::{
6900        FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_TAGGED, PM_TAGGED_LOCAL, PM_UNDEFINED,
6901        PM_WARNNESTED,
6902    };
6903    use std::sync::atomic::Ordering;
6904
6905    let name = shfunc.node.nam.clone(); // c:5827
6906    let flags = shfunc.node.flags; // c:5828
6907                                   // Lineage tap, before the funcstack frame goes on: the op must
6908                                   // record where the *caller* stands, not the body about to run.
6909    if crate::provenance::active() {
6910        // c:5978-5998 — doshargs[0] is the function name, doshargs[1..] the
6911        // positionals the body will see as $1, $2, …; the chain records those,
6912        // not the name again.
6913        let prov_args: &[String] = if doshargs.len() > 1 {
6914            &doshargs[1..]
6915        } else {
6916            &[]
6917        };
6918        crate::provenance::on_func_call(
6919            &name,
6920            prov_args,
6921            shfunc.body.as_deref(),
6922            shfunc.filename.as_deref(),
6923            shfunc.lineno,
6924        );
6925    }
6926    let fname = dupstring(&name); // c:5829
6927    let _ = fname; // c:5829 (kept for parity)
6928
6929    // c:6000-6004 — FUNCNEST guard. C:
6930    //   if (zsh_funcnest >= 0 && funcstack && funcstacksz >= zsh_funcnest) {
6931    //       zerr("maximum nested function level reached; increase FUNCNEST?");
6932    //       goto undoshfunc;
6933    //   }
6934    // This was previously stubbed out on the assumption that the zshrs
6935    // fusevm "doesn't recurse via real stack frames" — but it DOES:
6936    // dispatch_function_call -> doshfunc -> dispatch_function_call -> …
6937    // nests real native frames, so an accidental infinite recursion
6938    // (e.g. a self-referential hook, a wrapped function that re-invokes
6939    // itself, a zle widget cycle) overflowed the OS stack and
6940    // SEGFAULTED instead of producing zsh's graceful error. Check at
6941    // entry — before queue_signals / inc_locallevel / the funcsave
6942    // snapshot — so the early return needs no unwinding. FUNCNEST < 0
6943    // means "unlimited" (zsh's `zsh_funcnest >= 0` gate). Depth is the
6944    // count of already-active frames on FUNCSTACK; the 501st nested
6945    // call (depth == 500) trips it, matching C's `>=` on the default
6946    // FUNCNEST=500.
6947    let funcnest = crate::ported::params::getiparam("FUNCNEST");
6948    if funcnest >= 0 {
6949        // Count only real function frames (FS_FUNC) — the raw FUNCSTACK
6950        // Vec also carries FS_EVAL/FS_SOURCE/anon frames, which inflate
6951        // the depth several× and would falsely trip on legitimately deep
6952        // (but finite) recursion. FS_FUNC-only matches zsh's nesting
6953        // depth (`${#funcstack}`) and its funcnest accounting.
6954        let depth = crate::ported::modules::parameter::FUNCSTACK
6955            .lock()
6956            .map(|s| s.iter().filter(|f| f.tp == FS_FUNC).count())
6957            .unwrap_or(0) as i64;
6958        if depth >= funcnest {
6959            zerr("maximum nested function level reached; increase FUNCNEST?");
6960            crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
6961            return 1;
6962        }
6963    }
6964
6965    // c:5835 — `queue_signals();` Lots of memory + global-state changes.
6966    queue_signals();
6967
6968    // c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
6969    // Pinned so a recursive unload doesn't free the eprog under us.
6970    // (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
6971    // already pin until call ends. C does explicit refcount on
6972    // `funcdef->nref` via useeprog.)
6973
6974    // c:5856-5916 — Funcsave allocation + per-field snapshot.
6975    let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
6976    let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
6977    let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
6978    let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
6979    let funcsave_numpipestats = {
6980        // c:5864
6981        NUMPIPESTATS
6982            .get_or_init(|| std::sync::Mutex::new(0))
6983            .lock()
6984            .map(|n| *n)
6985            .unwrap_or(0)
6986    };
6987    let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
6988                                                                // c:5866-5867 — trap_state PRIMED branch decrements trap_return.
6989    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
6990        // c:5866
6991        TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
6992    }
6993    // c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
6994    // return-suppress so a `return` inside the body fires errexit
6995    // checks normally.
6996    noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
6997
6998    // c:5872-5880 — noreturnval branch: deep-copy pipestats so the
6999    // function body's pipestats writes are restored on exit.
7000    let funcsave_pipestats: Option<Vec<i32>> = if noreturnval {
7001        // c:5872
7002        let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
7003        p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
7004    } else {
7005        None
7006    };
7007
7008    // c:5882-5896 — TRAPEXIT special case (deep-copy shfunc so
7009    // starttrapscope doesn't rug-pull). zshrs doesn't yet support
7010    // running TRAPEXIT directly via doshfunc; flagged for follow-up.
7011    // (Skip: name = "TRAPEXIT" path.)
7012    let _ = name.as_str(); // sentinel for the eventual port.
7013
7014    // c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
7015    // tags SIGEXIT for deferred restoration at scope end.
7016    crate::ported::signals::starttrapscope();
7017    // c:5899 — `startpatternscope();`
7018    crate::ported::pattern::startpatternscope();
7019
7020    // c:5901 — `pptab = pparams;` — save outer positional params.
7021    let pptab: Vec<String> = crate::ported::builtin::PPARAMS
7022        .lock()
7023        .map(|p| p.clone())
7024        .unwrap_or_default();
7025
7026    // c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
7027    let funcsave_scriptname = crate::ported::utils::scriptname_get();
7028    if (flags as u32 & PM_UNDEFINED) == 0 {
7029        // c:5902
7030        crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
7031    }
7032
7033    // c:5964-5965 — `funcsave->zoptind = zoptind; funcsave->optcind
7034    // = optcind;`. C makes OPTIND implicitly function-local: a
7035    // `getopts` loop inside the function gets its own counter that
7036    // snaps back to the caller's on return. zshrs keeps the counter
7037    // in the `$OPTIND` param plus the ZOPTIND/OPTCIND trackers
7038    // `getopts` syncs against, so all three are snapshotted here.
7039    //
7040    // OPTARG is deliberately NOT snapshotted. The funcsave struct
7041    // (c:44-52) is `char opts[OPT_SIZE]; char *argv0; int zoptind,
7042    // lastval, optcind, numpipestats; …` — there is no `zoptarg`
7043    // member, and `grep -n zoptarg Src/exec.c` returns nothing, so C
7044    // lets a callee's OPTARG leak to its caller. Bug #513 added an
7045    // OPTARG save/restore citing "C's `funcsave->zoptind / zoptarg`
7046    // save/restore pair"; that field does not exist. The extra
7047    // restore was observable:
7048    //     g() { OPTARG=inner; }; h() { local OPTARG=Z; g; print $OPTARG }
7049    //     zsh   -> inner        zshrs (before) -> Z
7050    //     g() { getopts "a:" o -a VAL; }
7051    //     k() { local OPTARG=Z OPTIND=1; g; print $OPTARG }
7052    //     zsh   -> VAL          zshrs (before) -> Z
7053    let funcsave_optind: Option<String> = crate::ported::params::getsparam("OPTIND");
7054    let funcsave_optcind = crate::ported::builtin::OPTCIND.load(Ordering::Relaxed); // c:5965
7055                                                                                    // c:5966-5969 — `if (!isset(POSIXBUILTINS)) { zoptind = 1; optcind = 0; }`.
7056                                                                                    // The snapshot above is only half the contract: C also RESETS the
7057                                                                                    // counter on entry so an inner `getopts` loop starts fresh at the
7058                                                                                    // first positional, independent of the caller's OPTIND. Without
7059                                                                                    // this, a function whose body runs `getopts` after the caller
7060                                                                                    // advanced OPTIND mis-parses its own args — e.g. `add-zsh-hook`
7061                                                                                    // (which `getopts`-parses `precmd func`) printed its usage and
7062                                                                                    // failed when invoked from a config that had run getopts earlier.
7063                                                                                    // zshrs keeps the counter in the $OPTIND param plus the ZOPTIND/
7064                                                                                    // OPTCIND trackers getopts syncs against; reset all three.
7065    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
7066        crate::ported::params::setiparam("OPTIND", 1); // c:5967 zoptind = 1
7067        crate::ported::builtin::ZOPTIND.store(1, Ordering::Relaxed);
7068        crate::ported::builtin::OPTCIND.store(0, Ordering::Relaxed); // c:5968 optcind = 0
7069    }
7070
7071    // c:5911-5914 — `memcpy(funcsave->opts, opts, sizeof(opts));`
7072    //
7073    // C copies 186 bytes. The port used to clone the whole option table
7074    // as a `HashMap<String, bool>` — a heap allocation per option name,
7075    // on every call of every function, whether or not the body touches
7076    // an option. `opt_state_store` is C's array, so this is C's memcpy.
7077    let funcsave_opts = crate::ported::options::opt_state_store::save();
7078
7079    // c:5974-5975 — `funcsave->emulation = emulation;
7080    //                funcsave->sticky = sticky;`
7081    let funcsave_emulation = crate::ported::options::emulation.load(Ordering::Relaxed);
7082    let funcsave_emulation_live = crate::ported::options::EMULATION.load(Ordering::Relaxed);
7083    let funcsave_fully = crate::ported::options::FULLY_EMULATING.load(Ordering::Relaxed);
7084    let funcsave_sticky = sticky
7085        .lock()
7086        .unwrap_or_else(|e| e.into_inner())
7087        .as_ref()
7088        .map(|b| sticky_emulation_dup(b, 0));
7089
7090    // c:5977-6012 —
7091    //     if (sticky_emulation_differs(shfunc->sticky)) {
7092    //         sticky = sticky_emulation_dup(shfunc->sticky, 1);
7093    //         emulation = sticky->emulation;
7094    //         funcsave->restore_sticky = 1;
7095    //         installemulation(emulation, opts);
7096    //         if (sticky->n_on_opts) { ... opts[*onptr] = 1; }
7097    //         if (sticky->n_off_opts) { ... opts[*offptr] = 0; }
7098    //         /* All emulations start with pattern disables clear */
7099    //         clearpatterndisables();
7100    //     } else
7101    //         funcsave->restore_sticky = 0;
7102    //
7103    // This whole block was missing, so a function DEFINED inside
7104    // `emulate sh -c '...'` did not re-enter that emulation when
7105    // called (B07emulate.ztst:6,7,8,12,13,14).
7106    let mut funcsave_restore_sticky = 0; // c:6012
7107    if sticky_emulation_differs(shfunc.sticky.as_deref()) != 0 {
7108        // c:5978
7109        let newsticky = sticky_emulation_dup(shfunc.sticky.as_deref().unwrap(), 1); // c:5990
7110        let emu = newsticky.emulation; // c:5991
7111        *sticky.lock().unwrap_or_else(|e| e.into_inner()) = Some(newsticky);
7112        // C's single `emulation` global carries EMULATE_FULLY; the Rust
7113        // port splits it into EMULATION (base bits) + FULLY_EMULATING.
7114        let emu_base = emu & !crate::ported::zsh_h::EMULATE_FULLY;
7115        let emu_fully = (emu & crate::ported::zsh_h::EMULATE_FULLY) != 0;
7116        crate::ported::options::emulation.store(emu_base, Ordering::Relaxed); // c:5991
7117        crate::ported::options::EMULATION.store(emu_base, Ordering::Relaxed); // (port keeps 2 cells)
7118        crate::ported::options::FULLY_EMULATING.store(emu_fully, Ordering::Relaxed);
7119        funcsave_restore_sticky = 1; // c:5992
7120                                     // c:5993 — `installemulation(emulation, opts);`
7121        let mut new_opts = [-1i8; crate::ported::zsh_h::OPT_SIZE as usize];
7122        crate::ported::options::installemulation(emu, &mut new_opts); // c:5993
7123        for (optno, &v) in new_opts.iter().enumerate().skip(1) {
7124            if v >= 0 {
7125                opt_state_set(crate::ported::zsh_h::opt_name(optno as i32), v == 1);
7126            }
7127        }
7128        {
7129            let guard = sticky.lock().unwrap_or_else(|e| e.into_inner());
7130            if let Some(ref st) = *guard {
7131                // c:5995-6001 — `opts[*onptr] = 1;`
7132                for on in &st.on_opts {
7133                    opt_state_set(crate::ported::zsh_h::opt_name(*on as i32), true);
7134                }
7135                // c:6002-6008 — `opts[*offptr] = 0;`
7136                for off in &st.off_opts {
7137                    opt_state_set(crate::ported::zsh_h::opt_name(*off as i32), false);
7138                }
7139            }
7140        }
7141        // c:6010 — `clearpatterndisables();`
7142        crate::ported::pattern::clearpatterndisables();
7143    }
7144
7145    // c:5954-5960 — PM_TAGGED / PM_TAGGED_LOCAL turn XTRACE on for the
7146    // duration of the call:
7147    //     if (flags & (PM_TAGGED|PM_TAGGED_LOCAL))
7148    //         opts[XTRACE] = 1;
7149    //     else if (oflags & PM_TAGGED_LOCAL) {
7150    //         if (shfunc->node.nam == ANONYMOUS_FUNCTION_NAME)
7151    //             flags |= PM_TAGGED_LOCAL;
7152    //         else
7153    //             opts[XTRACE] = 0;
7154    //     }
7155    // This is what makes `functions -t f` / `-T f` trace a SINGLE function
7156    // without `setopt xtrace` globally. It was previously skipped, so both
7157    // flags parsed and stored correctly (builtin.rs c:3358-3365) but nothing
7158    // ever consulted them and the traced function ran silently.
7159    //
7160    // `-T` is the non-recursive form: a function called FROM a `-T` function
7161    // gets XTRACE turned back OFF unless it is itself tagged. C tracks the
7162    // caller's flags in the module-global `oflags`, saved per call at c:5970
7163    // (`funcsave->oflags = oflags;`) and restored on return — mirrored here by
7164    // FUNC_OFLAGS. Anonymous functions are the documented exception: they
7165    // INHERIT the local tag instead of clearing it, so `-T` tracing spans the
7166    // `(anon)` bodies inside the traced function.
7167    //
7168    // No restore of XTRACE is needed here: it is already in the
7169    // always-restored subset at the exit block below (c:6097-6101).
7170    // Bug #1058.
7171    let mut fn_flags = shfunc.node.flags as u32;
7172    let oflags_prev = FUNC_OFLAGS.load(Ordering::Relaxed);
7173    if (fn_flags & (PM_TAGGED | PM_TAGGED_LOCAL)) != 0 {
7174        opt_state_set("xtrace", true); // c:5955
7175    } else if (oflags_prev & PM_TAGGED_LOCAL) != 0 {
7176        // c:5956
7177        if shfunc.node.nam == ANONYMOUS_FUNCTION_NAME {
7178            fn_flags |= PM_TAGGED_LOCAL; // c:5958
7179        } else {
7180            opt_state_set("xtrace", false); // c:5960
7181        }
7182    }
7183    // c:6022-6029 — `functions -W` (PM_WARNNESTED) is the per-function
7184    // form of WARN_NESTED_VAR, with the same anonymous-function inheritance
7185    // as `-T` above:
7186    //     if (flags & PM_WARNNESTED)
7187    //         opts[WARNNESTEDVAR] = 1;
7188    //     else if (oflags & PM_WARNNESTED) {
7189    //         if (shfunc->node.nam == ANONYMOUS_FUNCTION_NAME)
7190    //             flags |= PM_WARNNESTED;
7191    //         else
7192    //             opts[WARNNESTEDVAR] = 0;
7193    //     }
7194    // WARNNESTEDVAR is in the always-restored subset at the exit block
7195    // (c:6161).
7196    if (fn_flags & PM_WARNNESTED) != 0 {
7197        opt_state_set("warnnestedvar", true); // c:6023
7198    } else if (oflags_prev & PM_WARNNESTED) != 0 {
7199        // c:6024
7200        if shfunc.node.nam == ANONYMOUS_FUNCTION_NAME {
7201            fn_flags |= PM_WARNNESTED; // c:6026
7202        } else {
7203            opt_state_set("warnnestedvar", false); // c:6028
7204        }
7205    }
7206    FUNC_OFLAGS.store(fn_flags, Ordering::Relaxed); // c:5970
7207
7208    // c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
7209    // for inner commands; outer flag restored on exit.
7210    opt_state_set("printexitvalue", false);
7211
7212    // c:5978-5998 — pparams swap. C reads doshargs and constructs the
7213    // function's positional-param array. First arg is the function
7214    // name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
7215    let funcsave_argv0: Option<String> = if !doshargs.is_empty() {
7216        // c:5978
7217        // c:5982-5985 — `pparams = x = zshcalloc(...)`.
7218        let positionals: Vec<String> = if doshargs.len() > 1 {
7219            doshargs[1..].to_vec()
7220        } else {
7221            Vec::new()
7222        };
7223        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7224            *pp = positionals;
7225        }
7226        // c:5984-5987 — FUNCTIONARGZERO: save argzero, install
7227        // doshargs[0] (the function name).
7228        if isset(FUNCTIONARGZERO) {
7229            // c:5984
7230            let prev = crate::ported::utils::argzero();
7231            crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
7232            prev
7233        } else {
7234            None
7235        }
7236    } else {
7237        // c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
7238        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7239            *pp = Vec::new();
7240        }
7241        if isset(FUNCTIONARGZERO) {
7242            // c:5994
7243            let prev = crate::ported::utils::argzero();
7244            crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
7245            prev
7246        } else {
7247            None
7248        }
7249    };
7250
7251    // c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
7252    // since zshrs tracks function-call depth there.
7253    //
7254    // Plus the canonical startparamscope (c:6194 inside runshfunc).
7255    // zshrs's body_runner replaces runshfunc's `execode` call so the
7256    // startparamscope/endparamscope pair must wrap body_runner here,
7257    // not inside the closure. inc_locallevel is exactly startparamscope.
7258    inc_locallevel();
7259
7260    // c:6000-6004 — FUNCNEST check + `goto undoshfunc` on overflow.
7261    // Skip the runtime check (the zshrs fusevm doesn't recurse via
7262    // real stack frames so the depth limit is less critical), but
7263    // keep the comment so the C label `undoshfunc:` target is
7264    // visible — `goto undoshfunc;` here would jump straight to the
7265    // epilogue at the `undoshfunc:` label below.
7266
7267    // c:6005-6019 — funcstack frame push. The full C block:
7268    //   funcsave->fstack.name      = dupstring(name);
7269    //   funcsave->fstack.caller    = funcstack ? funcstack->name :
7270    //                                 dupstring(argv0 ? argv0 : argzero);
7271    //   funcsave->fstack.lineno    = lineno;
7272    //   funcsave->fstack.prev      = funcstack;
7273    //   funcsave->fstack.tp        = FS_FUNC;
7274    //   funcstack                  = &funcsave->fstack;
7275    //   funcsave->fstack.flineno   = shfunc->lineno;
7276    //   funcsave->fstack.filename  = getshfuncfile(shfunc);
7277    // c:6013 — `funcsave->fstack.lineno = lineno;` C has ONE lineno
7278    // global (params.c:123); zshrs mirrors it in both input::lineno
7279    // and lex::LEX_LINENO. The lex mirror is the one driven by
7280    // BUILTIN_SET_LINENO per statement AND zeroed for the duration
7281    // of a function body (see set_lineno(0) below), so it is the
7282    // one that matches C's value at call time: a call made INSIDE a
7283    // caller's body records the caller-relative line (0 for a
7284    // single-line fn), giving `$functrace` entries like `g:0`.
7285    // input::lineno stayed parked at the script-wide line and
7286    // produced `g:1`.
7287    let lineno_now = crate::ported::lex::lineno() as i64;
7288    let (caller, prev_tp): (Option<String>, Option<i32>) = {
7289        let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7290        if let Some(p) = stk.last() {
7291            (Some(p.name.clone()), Some(p.tp))
7292        } else {
7293            // c:6011-6012 — outermost: argv0 (saved) or argzero global.
7294            let z = funcsave_argv0
7295                .clone()
7296                .or_else(crate::ported::utils::argzero);
7297            (z, None)
7298        }
7299    };
7300    // c:6018-6019 — flineno: shfunc->lineno (function def line)
7301    let mut flineno = shfunc.lineno;
7302    // c:6079 — `funcsave->fstack.filename = getshfuncfile(shfunc);`, NOT a raw
7303    // read of `shfunc->filename`. `getshfuncfile` (c:Src/hashtable.c:1058-1068)
7304    // appends `/name` when PM_LOADDIR is set:
7305    //     if (shf->node.flags & PM_LOADDIR)
7306    //         return zhtricat(shf->filename, "/", shf->node.nam);
7307    // An fpath autoload stores the DIRECTORY in `filename` (loadautofnsetfile,
7308    // c:5657), so reading the field directly rendered a funcstack frame as the
7309    // directory — `$funcfiletrace` showed `/Users/…/.zshrs/functions:117`
7310    // where zsh has `/…/functions/_complete:117`.
7311    //
7312    // The `.or_else(Some(String::new()))` coercion is DELIBERATELY kept: C's
7313    // `getshfuncfile` can return NULL and c:5675 tests `!funcstack->filename`
7314    // for it, but several readers here assume non-`None`, so making that
7315    // distinction expressible is a separate change.
7316    let mut filename = if (shfunc.node.flags as u32 & crate::ported::zsh_h::PM_LOADDIR) != 0 {
7317        // c:1061-1062
7318        shfunc
7319            .filename
7320            .as_ref()
7321            .map(|d| format!("{}/{}", d, shfunc.node.nam))
7322            .or_else(|| Some(String::new()))
7323    } else {
7324        // c:1063-1066
7325        shfunc.filename.clone().or_else(|| Some(String::new()))
7326    };
7327    // !!! WARNING: RUST-ONLY HELPER !!!
7328    //
7329    // c:6019 — `funcsave->fstack.filename = getshfuncfile(shfunc);`, which
7330    // reads the shfunc's OWN `filename`, set by `loadautofnsetfile`
7331    // (c:5657, called from `loadautofn` at c:5735 / c:5757) when the
7332    // function was autoloaded out of `$fpath`.
7333    //
7334    // zshrs has no C counterpart for a function that exists ONLY as a Rust
7335    // port: `_main_complete`, `_normal`, `_dispatch`, … are dispatched from
7336    // `compsys::router::try_rust_dispatch` and the autoload prelude is
7337    // skipped entirely (vm_helper.rs — "no upstream shell function to
7338    // load"), so nothing ever records a `$fpath` file for the name and the
7339    // synthesized shfunc handed to us carries the caller's `scriptfilename`
7340    // ("zsh") or nothing at all. `$funcsourcetrace` / `$funcfiletrace` then
7341    // reported `zsh:1` for every completer frame where zsh names the
7342    // `$fpath` file — and `funcfiletrace` is read by real completion code
7343    // (`_git` derives its git-completion.bash search path from
7344    // `${funcsourcetrace[1]%:*}`).
7345    //
7346    // Stand in for `loadautofnsetfile` in exactly that case by resolving the
7347    // defining file out of `$fpath` the way `getfpfunc` (c:6219) does for a
7348    // real autoload. An autoload-installed function gets `shf->lineno == 0`
7349    // (c:5384-5388 only stamps a `name() { … }` STATEMENT), so the def line
7350    // is 0, matching zsh's `<fpath-file>:0` in `$funcsourcetrace`.
7351    //
7352    // The gate is `try_rust_dispatch` ALONE. That predicate is the same one
7353    // `vm_helper::dispatch_function_call` and `compcore::callcompfunc` use to
7354    // pick the body_runner, so `Some` means the frame being pushed IS the
7355    // port standing in for the stock `$fpath` file — and the router has
7356    // already refused every name a shell definition owns
7357    // (`has_fpath_override` / `has_shfunc_override`, router.rs:53,62), so
7358    // there is nothing left here to shadow.
7359    //
7360    // An additional "…and the name has no shfunctab node" clause used to
7361    // guard this, which meant it never fired in practice: `compinit` writes
7362    // one bare `autoload -Uz <every completer>` line into its dump
7363    // (Completion/compdump:113), so EVERY completer has a
7364    // PM_UNDEFINED stub by the time Tab is pressed. A bare-name stub records
7365    // neither `filename` nor PM_LOADDIR (`add_autoload_function`,
7366    // Src/builtin.c:3278-3334 — only the `/abs/dir/name` arm and the
7367    // inherited-loaddir arm set them), so `getshfuncfile` returns NULL for
7368    // it and the fallback chain in vm_helper landed on `scriptfilename`.
7369    // That is how `$funcsourcetrace` read `zsh:1` for `_vars`, `_dispatch`,
7370    // `_normal`, `_complete` where zsh reads `/usr/share/zsh/5.9/functions/
7371    // _vars:0` etc.
7372    //
7373    // Memoised, and deliberately never invalidated: C stamps `shf->filename`
7374    // ONCE, at autoload time, and a later `fpath=(…)` does not restamp it —
7375    // so a permanent per-name answer is what matches zsh, not a re-scan. It
7376    // also keeps `doshfunc` off the filesystem on the completion hot path.
7377    if crate::compsys::router::try_rust_dispatch(&name).is_some() {
7378        static RUST_PORT_FILE: std::sync::OnceLock<
7379            std::sync::Mutex<std::collections::HashMap<String, Option<String>>>,
7380        > = std::sync::OnceLock::new();
7381        let cache = RUST_PORT_FILE.get_or_init(|| std::sync::Mutex::new(Default::default()));
7382        let cached = cache
7383            .lock()
7384            .unwrap_or_else(|e| e.into_inner())
7385            .get(&name)
7386            .cloned();
7387        let resolved = match cached {
7388            Some(hit) => hit,
7389            None => {
7390                let mut fdir: Option<String> = None;
7391                let mut dump: Option<(eprog, i32)> = None;
7392                // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
7393                // autoloads.rkyv already records which `$fpath` directory
7394                // holds this name; take it from there instead of walking
7395                // `$fpath` for every native completer a new shell calls.
7396                let fpath = crate::ported::params::getaparam("fpath").unwrap_or_default();
7397                let from_shard = crate::autoload_cache::try_source_dir(&name, &fpath);
7398                // c:6219 `getfpfunc(s, ksh, test, fdir, 1)` with test_only —
7399                // a pure probe: it fills `*fdir` (c:6240 `*fdir = *pp;`)
7400                // without parsing the file.
7401                let hit = from_shard
7402                    .or_else(|| getfpfunc(&name, &mut fdir, None, 1, &mut dump).and(fdir))
7403                    // c:1061 (Src/hashtable.c, getshfuncfile) — a PM_LOADDIR
7404                    // filename renders as
7405                    // `zhtricat(shf->filename, "/", shf->node.nam)`.
7406                    .map(|d| {
7407                        if d.is_empty() {
7408                            name.clone()
7409                        } else {
7410                            format!("{d}/{name}")
7411                        }
7412                    });
7413                cache
7414                    .lock()
7415                    .unwrap_or_else(|e| e.into_inner())
7416                    .insert(name.clone(), hit.clone());
7417                hit
7418            }
7419        };
7420        if let Some(file) = resolved {
7421            filename = Some(file);
7422            flineno = 0; // c:5384-5388 — never runs for an autoload stub
7423        }
7424    }
7425    {
7426        let frame = crate::ported::zsh_h::funcstack {
7427            prev: None,             // c:6014 (Vec-stack: index encodes link)
7428            name: dupstring(&name), // c:6005
7429            filename,               // c:6019
7430            caller,                 // c:6011
7431            flineno,                // c:6018
7432            lineno: lineno_now,     // c:6013
7433            tp: FS_FUNC,            // c:6015
7434        };
7435        let _ = prev_tp; // c:6011 (informational)
7436        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7437        stack.push(frame); // c:6016 funcstack = &funcsave->fstack
7438    }
7439
7440    // c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
7441    // zshrs delegates to the body_runner closure (typically a fusevm
7442    // sub-VM run from the bridge). The closure returns the body's
7443    // exit status which becomes lastval.
7444    //
7445    // c:Src/exec.c:1251-1266 — push "shfunc" onto zsh_eval_context
7446    // so the body sees `${zsh_eval_context[*]}` containing the call
7447    // chain context. The execode-based path (c:1245-1282 port at
7448    // exec.rs:7092) already did this, but the fusevm body_runner
7449    // path skipped doshfunc's body_runner invocation without the
7450    // push. Bug #262 in docs/BUGS.md.
7451    //
7452    // Push BOTH the static `zsh_eval_context` (matches C's variable)
7453    // AND the paramtab array entry (what `${zsh_eval_context[*]}`
7454    // reads). Pop on every return path via the guard struct so
7455    // panics / early returns don't leak the entry. Inlined here
7456    // (sole caller) — `zsh_eval_context` is this module's own static.
7457    //
7458    // c:Src/exec.c:1251-1266 — `zsh_eval_context[*]` shell-visible
7459    // mirror: the array entry holds the stack; the scalar
7460    // `ZSH_EVAL_CONTEXT` holds the `:`-joined form. Both written via
7461    // the PM_READONLY bypass (`u_arr`/`u_str` direct), the same shape
7462    // the binary's `-c` ZSH_EVAL_CONTEXT init uses (bins/zshrs.rs).
7463    // `EvalContextFrame` owns both halves (push + paramtab sync, then
7464    // pop + sync on Drop) and is shared with the other C sites that
7465    // reach `execode` with a label — `bin_eval` ("eval", builtin.c:6209)
7466    // and the autoload body run ("loadautofunc", exec.c:5626).
7467    let _eval_ctx_guard = EvalContextFrame::push("shfunc");
7468    // c:Src/exec.c — function bodies execute with `lineno` reset to
7469    // the relative line within the body (incremented per WC_PIPE
7470    // from the wordcode-encoded lineno). zsh's zerrmsg
7471    // (Src/utils.c:301) emits the lineno prefix only when lineno
7472    // is non-zero AND (!SHINSTDIN || locallevel != 0). For an
7473    // inline single-line function like `f() { x=1 }`, the body's
7474    // WC_PIPE encodes lineno=1, exec sets `lineno = lineno - 1 =
7475    // 0`, and the zerrmsg path falls through to space-only ("f: ").
7476    //
7477    // zshrs's compiler doesn't thread WC_PIPE_LINENO into the
7478    // bytecode, so the global lineno stays at the script-wide
7479    // value (1 for inline `-c`). Suppress the line-number prefix
7480    // inside function bodies by saving lineno on entry and forcing
7481    // it to 0 during body execution; restore on exit. This makes
7482    // warnings inside functions emit `f: ...` matching zsh's
7483    // single-line-function format. Bug #54/#74/#86 in docs/BUGS.md.
7484    //
7485    // NOT for a PM_UNDEFINED function. Its `prog` is the `mkautofn` stub
7486    // (c:Src/builtin.c:3662, zsh-5.9.1), which carries no line marker, so in C the
7487    // `loadautofn` -> `getfpfunc` lookup it runs (c:5626-5632) still sees the
7488    // CALLER's `lineno`: a corrupt digest on `$fpath` is reported as
7489    // `_main_complete:218: invalid zwc file:` (c:Src/parse.c:3288 `zwarnnam`),
7490    // and zeroing it here printed `_main_complete: invalid zwc file:`. The load
7491    // runs inside `body_runner`; `run_function_body_only` zeroes `lineno` once
7492    // the definition is loaded, just before the body itself runs.
7493    let saved_lineno = crate::ported::lex::lineno();
7494    if (flags as u32 & PM_UNDEFINED) == 0 {
7495        crate::ported::lex::set_lineno(0);
7496    }
7497    // c:Src/exec.c:6173-6175 + c:6196-6198 — `runshfunc` saves
7498    // zunderscore before the body runs and restores it after, so
7499    // `$_` reads outside the function continue to reflect the
7500    // function CALL's last arg (set by setunderscore at c:3491
7501    // before doshfunc enters). Without this, commands inside the
7502    // body (`:`, `echo`, etc.) update `$_` to their own last arg,
7503    // and the post-call `echo "[$_]"` sees the body's residue
7504    // instead of the call's arg. Bug surfaced via
7505    // test_dollar_underscore_after_function_call.
7506    let saved_zunderscore = crate::ported::params::getsparam("_").unwrap_or_default();
7507    // c:6042 — `runshfunc(prog, wrappers, funcsave->fstack.name);` — run
7508    // the body through the module-registered wrapper chain, starting at
7509    // its head (`wrappers` == [`BODY_WRAPPERS`] index 0). `prog` is `None`
7510    // because the body arrives as `body_runner`; see [`runshfunc`] for the
7511    // split of C's c:6166-6202 between it and this function.
7512    let body_status = runshfunc(None, 0, &name, &mut body_runner);
7513    crate::ported::params::set_zunderscore(std::slice::from_ref(&saved_zunderscore));
7514    crate::ported::lex::set_lineno(saved_lineno);
7515    LASTVAL.store(body_status, Ordering::Relaxed);
7516
7517    // c:6043 — `doneshfunc:` label. The C `runshfunc` happy-path
7518    // falls through here from c:6042.
7519    // c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
7520    {
7521        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7522        stack.pop();
7523    }
7524    // c:Src/exec.c:6260 — `endparamscope();` inside `runshfunc`, i.e.
7525    // BEFORE control returns to doshfunc's epilogue at c:6103. zshrs
7526    // hoists runshfunc's `startparamscope()` (c:6254) up to
7527    // `inc_locallevel()` above because the body arrives as a closure,
7528    // so its partner must be dropped here — at the same point in the
7529    // sequence C reaches it — not at the tail of the epilogue.
7530    //
7531    // The whole epilogue below therefore runs with the callee's locals
7532    // already popped, which is what C's own comment at c:6195-6196
7533    // asserts ("The endparamscope() has already happened, hence the
7534    // `+1` here" for the exit_pending test at c:6201). Three orderings
7535    // depend on it and all three were wrong while this call sat at the
7536    // tail:
7537    //   * `endtrapscope()` (c:6174) dispatches a function-scoped EXIT
7538    //     trap. In C the trap body runs in the CALLER's param scope:
7539    //       X=5; f(){ local X=1; trap 'print IN=$X; X=99' EXIT; }; f
7540    //       zsh -> IN=5 then X==99 outside
7541    //     With endparamscope last, zshrs printed `IN=1` and the trap's
7542    //     `X=99` landed in the doomed local shadow (outer X stayed 5).
7543    //   * the OPTIND/optcind restore (c:6120-6122) must write the
7544    //     CALLER's parameter, so it has to follow this pop — see the
7545    //     restore site below.
7546    //   * `foo() { exit 7; }; foo` needs the decrement before the
7547    //     exit_pending comparison or `exit_level >= locallevel+1` is
7548    //     off by one and the shell exits 0.
7549    endparamscope();
7550
7551    // c:6045 — `undoshfunc:` label. Reached either by fall-through
7552    // from c:6044 or by `goto undoshfunc;` from the FUNCNEST check
7553    // at c:6003. Tail epilogue follows.
7554
7555    // c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
7556    // runshfunc) lives at c:6157 below as `endparamscope()`. Removed
7557    // the dec here so locallevel only decrements once per
7558    // function-call frame; double-dec was purging level-0 globals on
7559    // function exit (the `f() { x=foo; }; f; echo $x` regression).
7560
7561    // c:6047-6053 — retflag clear. C clears retflag and restores
7562    // outer breaks if a `return` fired.
7563    if RETFLAG.load(Ordering::SeqCst) != 0 {
7564        // c:6047
7565        RETFLAG.store(0, Ordering::SeqCst); // c:6051
7566        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
7567    }
7568
7569    // c:6054-6058 — pparams + argv0 restore.
7570    if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7571        *pp = pptab; // c:6059 pparams = pptab
7572    }
7573    if let Some(saved) = funcsave_argv0 {
7574        // c:6055
7575        crate::ported::utils::set_argzero(Some(saved)); // c:6057
7576    }
7577
7578    // c:6120-6123 — `if (!isset(POSIXBUILTINS)) { zoptind =
7579    // funcsave->zoptind; optcind = funcsave->optcind; }`.
7580    //
7581    // Placement is load-bearing in both directions, which is why this
7582    // sits exactly where C has it — after the pparams/argv0 restore
7583    // (c:6114-6119) and before the option restore (c:6129-6162):
7584    //
7585    //  * AFTER `endparamscope()` (already run above). In C `zoptind`
7586    //    is a plain global int (c:47); in zshrs `$OPTIND` is an
7587    //    ordinary paramtab entry, so "restore OPTIND" means "write the
7588    //    visible parameter". Written before the scope pop it landed in
7589    //    the CALLEE's own `local OPTIND` shadow, which the pop then
7590    //    discarded — re-exposing the caller's parameter that the entry
7591    //    reset at c:5967 had already clobbered to 1:
7592    //        g() { local OPTIND OPTARG; }
7593    //        f() { local OPTIND=9; g; print $OPTIND }
7594    //        zsh -> 9        zshrs (before) -> 1
7595    //    A callee with no `local OPTIND` was unaffected, which is why
7596    //    it hid for so long; it reached every compsys port that
7597    //    declares OPTIND local (`_describe` and thus every completer
7598    //    calling it).
7599    //
7600    //  * BEFORE `endtrapscope()` (c:6174). A function-scoped EXIT trap
7601    //    that assigns OPTIND must win, because in C it runs after the
7602    //    restore:
7603    //        t1(){ trap 'OPTIND=99' EXIT; }; OPTIND=5; t1; print $OPTIND
7604    //        zsh -> 99
7605    //    Restoring after endtrapscope clobbered the trap's write to 5.
7606    //
7607    //  * BEFORE the option restore (c:6129-6162), so `isset(POSIXBUILTINS)`
7608    //    reads the CALLEE's setting, matching the entry-side reset at
7609    //    c:5966 which is likewise evaluated inside the function's option
7610    //    scope:
7611    //        OPTIND=5; f(){ setopt localoptions posixbuiltins; OPTIND=42 }
7612    //        f; print $OPTIND    zsh -> 42
7613    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
7614        if let Some(saved) = funcsave_optind {
7615            // c:6121 `zoptind = funcsave->zoptind;`
7616            if let Ok(n) = saved.parse::<i64>() {
7617                crate::ported::params::setiparam("OPTIND", n);
7618                crate::ported::builtin::ZOPTIND.store(n as i32, Ordering::Relaxed);
7619            } else {
7620                crate::ported::params::setsparam("OPTIND", &saved);
7621            }
7622        }
7623        // c:6122 `optcind = funcsave->optcind;`
7624        crate::ported::builtin::OPTCIND.store(funcsave_optcind, Ordering::Relaxed);
7625    }
7626
7627    // c:6064 — `scriptname = funcsave->scriptname;`
7628    crate::ported::utils::set_scriptname(funcsave_scriptname);
7629
7630    // c:6067 — `endpatternscope();`
7631    crate::ported::pattern::endpatternscope();
7632
7633    // c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
7634    // localoptions was set inside the body.
7635    // c:6129 — `if (isset(LOCALOPTIONS) || funcsave->restore_sticky)`.
7636    // A sticky-emulation call ALWAYS restores the full option set on
7637    // return, whether or not LOCAL_OPTIONS is on — otherwise the
7638    // emulation the call installed leaks into the caller.
7639    if crate::ported::options::opt_state_get("localoptions").unwrap_or(false)
7640        || funcsave_restore_sticky != 0
7641    {
7642        // c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
7643        let mut restore = funcsave_opts.clone();
7644        // c:6089 — `funcsave->opts[PRIVILEGED] = opts[PRIVILEGED];`.
7645        // PRIVILEGED is carved out of the restore in C: a function that
7646        // dropped privileges does not get them back on return. The port
7647        // restored it with everything else.
7648        restore.carry_privileged_from_live();
7649        restore.restore();
7650        // c:6136 / c:6153 — `emulation = funcsave->emulation;`
7651        crate::ported::options::emulation.store(funcsave_emulation, Ordering::Relaxed);
7652        crate::ported::options::EMULATION.store(funcsave_emulation_live, Ordering::Relaxed);
7653        crate::ported::options::FULLY_EMULATING.store(funcsave_fully, Ordering::Relaxed);
7654        // c:6137 — `sticky = funcsave->sticky;` (restore_sticky arm only)
7655        if funcsave_restore_sticky != 0 {
7656            *sticky.lock().unwrap_or_else(|e| e.into_inner()) = funcsave_sticky;
7657        }
7658    } else {
7659        // c:6097-6101 — non-LOCALOPTIONS: restore only the always-
7660        // restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
7661        // LOCALLOOPS / WARNNESTEDVAR).
7662        for opt in [
7663            "xtrace",
7664            "printexitvalue",
7665            "localoptions",
7666            "localloops",
7667            "warnnestedvar",
7668        ] {
7669            if let Some(v) = funcsave_opts.saved_get(opt) {
7670                opt_state_set(opt, v);
7671            }
7672        }
7673    }
7674
7675    // c:5970 counterpart — `oflags = funcsave->oflags;` restores the caller's
7676    // attribute set so a sibling call after this one sees the right `-T`
7677    // inheritance. Bug #1058.
7678    FUNC_OFLAGS.store(oflags_prev, Ordering::Relaxed);
7679
7680    // c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
7681    // breaks/contflag/loops snapshot.
7682    if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
7683        // c:6106-6108 —
7684        //     if (contflag) zwarn("`continue' active at end of function scope");
7685        //     if (breaks)   zwarn("`break' active at end of function scope");
7686        if CONTFLAG.load(Ordering::SeqCst) != 0 {
7687            crate::ported::utils::zwarn("`continue' active at end of function scope");
7688            // c:6106
7689        }
7690        if BREAKS.load(Ordering::SeqCst) != 0 {
7691            crate::ported::utils::zwarn("`break' active at end of function scope");
7692            // c:6107
7693        }
7694        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
7695        CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
7696        LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
7697    }
7698
7699    // c:6174 — `endtrapscope();`
7700    //
7701    // Bug #80 in docs/BUGS.md: zshrs used to run endtrapscope while
7702    // locallevel was still at the function's own level, so savetrap
7703    // entries tagged `local == current_function_level` failed the
7704    // `local > locallevel` pop condition and nested EXIT traps never
7705    // restored — outer EXIT traps fired at script exit instead. That
7706    // was worked around by bracketing this call with a manual
7707    // decrement/re-bump of locallevel. The workaround is gone: the
7708    // real `endparamscope()` now runs at its C position above, so the
7709    // pop loop observes the genuine post-decrement level, and the
7710    // trap body executes in the caller's param scope as C does.
7711    //
7712    // The function body's "shfunc" context was pushed by runshfunc's
7713    // execode (c:Src/exec.c:5960 → c:1251) and popped when that execode
7714    // returned, before doshfunc reaches endtrapscope; a function-scoped
7715    // EXIT trap therefore sees the CALLER's context plus its own "trap".
7716    drop(_eval_ctx_guard);
7717    crate::ported::signals::endtrapscope();
7718
7719    // c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
7720    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
7721        // c:6116
7722        TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
7723    }
7724
7725    // c:6118 — `ret = lastval;`
7726    let ret = LASTVAL.load(Ordering::Relaxed);
7727
7728    // c:6119 — `noerrexit = funcsave->noerrexit;`
7729    noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
7730
7731    // c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
7732    // the function for side-effects only; outer lastval/pipestats
7733    // should reflect the PRE-call state.
7734    if noreturnval {
7735        // c:6120
7736        LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
7737        if let Some(saved_ps) = funcsave_pipestats {
7738            let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
7739            if let Ok(mut nguard) = n.lock() {
7740                *nguard = funcsave_numpipestats; // c:6122
7741            }
7742            let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
7743            if let Ok(mut pguard) = p.lock() {
7744                for (i, v) in saved_ps.iter().enumerate() {
7745                    if i < pguard.len() {
7746                        pguard[i] = *v; // c:6123 memcpy
7747                    }
7748                }
7749            }
7750        }
7751    }
7752
7753    // c:6128 — `unqueue_signals();`
7754    unqueue_signals();
7755
7756    // c:6135-6155 — exit_pending branch: when an `exit` was queued
7757    // inside the function body and we've unwound enough scopes for
7758    // it to take effect, either keep unwinding (still inside a
7759    // nested function) or actually exit the shell.
7760    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
7761    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
7762    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
7763    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
7764    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
7765    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
7766        // c:6141
7767        if cur_locallevel > cur_forklevel {
7768            // c:6143 — still inside a nested function: keep unwinding.
7769            RETFLAG.store(1, Ordering::Relaxed); // c:6144
7770            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
7771        } else {
7772            // c:6151 — out of all functions: exit for real.
7773            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
7774            let val = EXIT_VAL.load(Ordering::Relaxed);
7775            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
7776            // c:6152
7777        }
7778    }
7779
7780    ret // c:6157 return ret
7781}
7782
7783// ═══════════════════════════════════════════════════════════════════════
7784// `(eval)` funcstack frame — the `FS_EVAL` half of the funcstack/functrace/
7785// funcfiletrace subsystem whose `FS_FUNC` half is `doshfunc` above.
7786// ═══════════════════════════════════════════════════════════════════════
7787
7788/// !!! WARNING: RUST-ONLY HELPER !!!
7789///
7790/// C keeps `struct funcstack fstack;` as an automatic in `eval()`
7791/// (`Src/builtin.c:6148`) and unwinds it with a plain
7792/// `funcstack = funcstack->prev;` on the single return path
7793/// (`Src/builtin.c:6210`). zshrs's `FUNCSTACK` is a `Mutex<Vec<funcstack>>`
7794/// and its eval entry points have MANY early-return paths (errflag
7795/// containment, recursion backstop, `?` from `execute_script`), so the pop
7796/// is carried by `Drop` instead. There is no C counterpart for this type;
7797/// the pushed frame itself is a line-by-line port of `Src/builtin.c:6155-6193`.
7798///
7799/// Every caller that stands in for C's `eval()` — the `BUILTIN_EVAL`
7800/// handler and `eval_string` below — must hold one of these for the
7801/// duration of the eval'd body, or `$funcstack` / `$functrace` /
7802/// `$funcfiletrace` lose a frame relative to zsh.
7803pub struct EvalFuncstackFrame {
7804    /// c:6191/6193 `fpushed` — whether the frame was actually pushed.
7805    fpushed: bool,
7806    /// c:6147 — `int oineval = ineval, fpushed;`. Restored at c:6215
7807    /// (`ineval = oineval;`) by this guard's Drop.
7808    oineval: i32,
7809}
7810
7811impl EvalFuncstackFrame {
7812    /// Port of the funcstack prologue of `eval(char **argv)` from
7813    /// `Src/builtin.c:6155-6193`.
7814    ///
7815    /// ```c
7816    /// ineval = !isset(EVALLINENO);
7817    /// if (!ineval) {
7818    ///     scriptname = "(eval)";
7819    ///     fstack.prev = funcstack;
7820    ///     fstack.name = scriptname;
7821    ///     fstack.caller = funcstack ? funcstack->name : dupstring(argzero);
7822    ///     fstack.lineno = lineno;
7823    ///     fstack.tp = FS_EVAL;
7824    ///     if (!funcstack || funcstack->tp == FS_SOURCE) {
7825    ///         fstack.flineno = fstack.lineno;
7826    ///         fstack.filename = fstack.caller;
7827    ///     } else {
7828    ///         fstack.flineno = funcstack->flineno + lineno;
7829    ///         if (funcstack->tp == FS_EVAL)
7830    ///             fstack.flineno--;
7831    ///         fstack.filename = funcstack->filename;
7832    ///         if (!fstack.filename)
7833    ///             fstack.filename = "";
7834    ///     }
7835    ///     funcstack = &fstack;
7836    ///     fpushed = 1;
7837    /// } else
7838    ///     fpushed = 0;
7839    /// ```
7840    ///
7841    /// `scriptname` (c:6157) is left to the caller: the `BUILTIN_EVAL`
7842    /// handler saves/restores it around the whole builtin, which is where
7843    /// C's `oscriptname` (c:6146) / restore (c:6222) live.
7844    pub fn push() -> Self {
7845        use crate::ported::modules::parameter::FUNCSTACK;
7846        use crate::ported::zsh_h::{FS_EVAL, FS_SOURCE};
7847        // c:6147 — `int oineval = ineval, fpushed;`
7848        let oineval = crate::ported::builtin::ineval.load(std::sync::atomic::Ordering::Relaxed);
7849        // c:6155 — `ineval = !isset(EVALLINENO);`
7850        let ineval = !crate::ported::zsh_h::isset(crate::ported::zsh_h::EVALLINENO);
7851        // The global drives `Src/exec.c`'s three `!ineval` lineno gates
7852        // (c:1355 / c:1451 / c:2056); zshrs reads it in the
7853        // `BUILTIN_SET_LINENO` handler. Only the funcstack half of this
7854        // prologue was ported, so `unsetopt evallineno; eval 'print
7855        // $LINENO'` counted lines inside the eval string (1) instead of
7856        // reporting the caller's line (E01options.ztst:29).
7857        crate::ported::builtin::ineval.store(
7858            if ineval { 1 } else { 0 },
7859            std::sync::atomic::Ordering::Relaxed,
7860        );
7861        if ineval {
7862            return EvalFuncstackFrame {
7863                fpushed: false, // c:6193 fpushed = 0
7864                oineval,
7865            };
7866        }
7867        // c:6161 — `fstack.lineno = lineno;`. zshrs mirrors C's single
7868        // `lineno` global (params.c:123) in `lex::LEX_LINENO`, the one
7869        // `BUILTIN_SET_LINENO` drives per statement — the same mirror
7870        // `doshfunc` reads for its `FS_FUNC` frame (see c:6013 there).
7871        let lineno = crate::ported::lex::lineno() as i64;
7872        let frame = {
7873            let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7874            // c:6160 — `fstack.caller = funcstack ? funcstack->name
7875            //                                     : dupstring(argzero);`
7876            let caller = match stk.last() {
7877                Some(f) => Some(f.name.clone()),
7878                None => crate::ported::utils::argzero(),
7879            };
7880            // c:6174-6188 — flineno/filename deduction. Identical logic to
7881            // `funcfiletracegetfn` (Src/Modules/parameter.c:724-762): an eval
7882            // is an inlined call from a tracing perspective.
7883            let (flineno, filename) = match stk.last() {
7884                // c:6174 — `if (!funcstack || funcstack->tp == FS_SOURCE)`
7885                None => (lineno, caller.clone()), // c:6175-6176
7886                Some(p) if p.tp == FS_SOURCE => (lineno, caller.clone()), // c:6175-6176
7887                Some(p) => {
7888                    // c:6178 — `fstack.flineno = funcstack->flineno + lineno;`
7889                    let mut flineno = p.flineno + lineno;
7890                    // c:6183-6184 — `if (funcstack->tp == FS_EVAL) fstack.flineno--;`
7891                    // Line numbers in eval start from 1, not zero, so offset
7892                    // by one to get line in file.
7893                    if p.tp == FS_EVAL {
7894                        flineno -= 1;
7895                    }
7896                    // c:6185-6187 — `fstack.filename = funcstack->filename;
7897                    //                if (!fstack.filename) fstack.filename = "";`
7898                    (
7899                        flineno,
7900                        Some(p.filename.clone().unwrap_or_else(String::new)),
7901                    )
7902                }
7903            };
7904            crate::ported::zsh_h::funcstack {
7905                prev: None,                 // c:6158 (Vec-stack: index encodes link)
7906                name: "(eval)".to_string(), // c:6159 fstack.name = scriptname
7907                filename,                   // c:6176 / c:6185
7908                caller,                     // c:6160
7909                flineno,                    // c:6175 / c:6178
7910                lineno,                     // c:6161
7911                tp: FS_EVAL,                // c:6162
7912            }
7913        };
7914        FUNCSTACK
7915            .lock()
7916            .unwrap_or_else(|e| e.into_inner())
7917            .push(frame); // c:6189 funcstack = &fstack
7918        EvalFuncstackFrame {
7919            fpushed: true, // c:6191 fpushed = 1
7920            oineval,
7921        }
7922    }
7923
7924    /// c:6191/6193 `fpushed` — whether `EVALLINENO` was set and a frame
7925    /// really went on. Callers mirror C's `if (!ineval) scriptname =
7926    /// "(eval)";` (c:6157) off the same test.
7927    pub fn pushed(&self) -> bool {
7928        self.fpushed
7929    }
7930}
7931
7932impl Drop for EvalFuncstackFrame {
7933    /// c:6209-6210 — `if (fpushed) funcstack = funcstack->prev;`
7934    /// c:6215 — `ineval = oineval;`
7935    fn drop(&mut self) {
7936        crate::ported::builtin::ineval.store(self.oineval, std::sync::atomic::Ordering::Relaxed); // c:6215
7937        if self.fpushed {
7938            crate::ported::modules::parameter::FUNCSTACK
7939                .lock()
7940                .unwrap_or_else(|e| e.into_inner())
7941                .pop();
7942        }
7943    }
7944}
7945
7946/// `TRAP_STATE_PRIMED` — re-exported from the canonical enum port rather
7947/// than redefined here.
7948///
7949/// This was a LOCAL `const … = 2`, which is `TRAP_STATE_FORCE_RETURN`'s value:
7950/// `enum trap_state { TRAP_STATE_INACTIVE, TRAP_STATE_PRIMED,
7951/// TRAP_STATE_FORCE_RETURN }` (zsh.h:2947-2960) makes PRIMED 1, and
7952/// zsh_h.rs:4501-4505 already carries all three with the right values. With
7953/// the wrong number, doshfunc's two trap_return adjustments (c:5866-5867 on
7954/// entry, c:6116-6117 on exit) never fired for a primed trap, so trap_return
7955/// stayed at -1 instead of reaching the -2 sentinel bin_break tests
7956/// (builtin.c:5845). `return N` inside a TRAPxxx function therefore never
7957/// promoted to TRAP_STATE_FORCE_RETURN:
7958///     TRAPINT() { print CAUGHT; return 130 }; kill -INT $$; print DONE
7959///     zsh  : CAUGHT
7960///     zshrs: CAUGHT DONE
7961use crate::ported::zsh_h::TRAP_STATE_PRIMED;
7962
7963/// Port of `execfuncdef()` from `Src/exec.c:5309` — C decl `execfuncdef(Estate state, Eprog redir_prog)`.
7964/// Define a shell function: extract
7965/// name(s)+body from the wordcode payload, allocate the Shfunc,
7966/// install into `shfunctab` (named), or execute immediately (anon).
7967#[allow(non_snake_case)]
7968pub fn execfuncdef(state: &mut estate, mut redir_prog: Option<crate::ported::zsh_h::Eprog>) -> i32 {
7969    use crate::ported::hashtable::{dircache_set, shfunctab_lock};
7970    use crate::ported::jobs::{getsigidx, removetrapnode};
7971    use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
7972    use crate::ported::signals::settrap;
7973    use crate::ported::utils::scriptfilename_get;
7974    use crate::ported::zsh_h::{
7975        eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, Patprog,
7976        EC_DUPTOK as _, EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED,
7977        PM_TAGGED_LOCAL, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
7978    };
7979    // c:5311 — `Shfunc shf;`
7980    let mut shf: Box<shfunc_t>;
7981    // c:5312 — `char *s = NULL;`
7982    let mut s: Option<String> = None;
7983    // c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
7984    let mut signum: i32;
7985    let nprg: i32;
7986    let sbeg: i32;
7987    let nstrs: i32;
7988    let npats: i32;
7989    let do_tracing: i32;
7990    let len: i32;
7991    let plen: i32;
7992    // `i` — C loop counter for pp stamp; Rust uses .map().collect().
7993    let mut htok: i32 = 0;
7994    let mut ret: i32 = 0;
7995    // c:5314 — `int anon_func = 0;`
7996    let mut anon_func: i32 = 0;
7997    // c:5315 — `Wordcode beg = state->pc, end;`
7998    let _beg: usize = state.pc;
7999    let mut end: usize;
8000    // c:5316 — `Eprog prog;`
8001    // (allocated inline per-iter below; no upfront binding needed)
8002    // c:5317 — `Patprog *pp;` — handled by Vec construction.
8003    // c:5318 — `LinkList names;`
8004    let names: Vec<String>;
8005    // c:5319 — `int tracing_flags;`
8006    let tracing_flags: i32;
8007
8008    // c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
8009    end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
8010    // c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
8011    let num = state.prog.prog[state.pc] as usize;
8012    state.pc += 1;
8013    names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
8014    // c:5323 — `sbeg = *state->pc++;`
8015    sbeg = state.prog.prog[state.pc] as i32;
8016    state.pc += 1;
8017    // c:5324 — `nstrs = *state->pc++;`
8018    nstrs = state.prog.prog[state.pc] as i32;
8019    state.pc += 1;
8020    // c:5325 — `npats = *state->pc++;`
8021    npats = state.prog.prog[state.pc] as i32;
8022    state.pc += 1;
8023    // c:5326 — `do_tracing = *state->pc++;`
8024    do_tracing = state.prog.prog[state.pc] as i32;
8025    state.pc += 1;
8026
8027    // c:5328 — `nprg = (end - state->pc);`
8028    nprg = end.saturating_sub(state.pc) as i32;
8029    // c:5329 — `plen = nprg * sizeof(wordcode);`
8030    plen = nprg.saturating_mul(size_of::<wordcode>() as i32);
8031    // c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
8032    len = plen + npats.saturating_mul(size_of::<usize>() as i32) + nstrs;
8033    // c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
8034    tracing_flags = if do_tracing != 0 {
8035        PM_TAGGED_LOCAL as i32
8036    } else {
8037        0
8038    };
8039
8040    // c:5333-5339 — htok name substitution.
8041    let mut names_mut: Vec<String> = names;
8042    if htok != 0 && !names_mut.is_empty() {
8043        execsubst(&mut names_mut); // c:5334
8044        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8045            // c:5335
8046            state.pc = end; // c:5336
8047            return 1; // c:5337
8048        }
8049    }
8050
8051    // c:5341-5342 DPUTS — debug assertion (anon + redir simultaneously).
8052    // Not portable as panic; left as comment.
8053
8054    // c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
8055    // num==0 → anon (no names); else iterate names.
8056    let mut names_iter = names_mut.into_iter();
8057    loop {
8058        let no_names = num == 0;
8059        if !no_names {
8060            // c:5343 — `s = ugetnode(names)`; break when list exhausted.
8061            match names_iter.next() {
8062                Some(nm) => s = Some(nm),
8063                None => break,
8064            }
8065        }
8066        // c:5344-5374 — Eprog alloc.
8067        let prog: Box<eprog_t>;
8068        let dump_present = state.prog.dump.is_some();
8069        let make_pat = || -> Patprog {
8070            // c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
8071            Box::new(patprog_t {
8072                startoff: 0,
8073                size: 0,
8074                mustoff: 0,
8075                patmlen: 0,
8076                globflags: 0,
8077                globend: 0,
8078                flags: 0,
8079                patnpar: 0,
8080                patstartch: 0,
8081            })
8082        };
8083        if no_names {
8084            // c:5345-5346 — `zhalloc`, `nref = -1`.
8085            // c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
8086            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8087            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
8088            // c:5365 — `prog->strs = state->strs + sbeg;`
8089            let strs_tail = state.strs.as_ref().map(|t| {
8090                let off = (sbeg as usize).min(t.len());
8091                t[off..].to_string()
8092            });
8093            prog = Box::new(eprog_t {
8094                flags: EF_HEAP,
8095                len,
8096                npats,
8097                nref: -1, // c:5346
8098                pats,
8099                prog: prog_words,
8100                strs: strs_tail,
8101                shf: None,  // c:5377
8102                dump: None, // c:5356
8103                // c:5365 — `prog->strs = state->strs + sbeg;` — the tail is
8104                // the SAME pool, so it keeps the same encoding.
8105                strs_metafied: state.prog.strs_metafied,
8106            });
8107        } else if dump_present {
8108            // c:5358-5363 — EF_MAP path: refcount the dump, allocate
8109            // pats permanent, reuse `state->pc` slice in place.
8110            if let Some(dp) = state.prog.dump.as_deref() {
8111                incrdumpcount(dp); // c:5360
8112            }
8113            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8114            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
8115            let strs_tail = state.strs.as_ref().map(|t| {
8116                let off = (sbeg as usize).min(t.len());
8117                t[off..].to_string()
8118            });
8119            prog = Box::new(eprog_t {
8120                flags: EF_MAP, // c:5359
8121                len,
8122                npats,
8123                nref: 1, // c:5349
8124                pats,
8125                prog: prog_words,
8126                strs: strs_tail,
8127                shf: None,                               // c:5377
8128                dump: state.prog.dump.clone(),           // c:5361
8129                strs_metafied: state.prog.strs_metafied, // pool copied verbatim — carry provenance
8130            });
8131        } else {
8132            // c:5366-5374 — EF_REAL: copy wordcode + strs into a
8133            // freshly-owned eprog (no shared dump backing).
8134            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8135            let pc_end = state.pc + nprg as usize;
8136            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
8137            // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
8138            let strs_copy = state.strs.as_ref().map(|t| {
8139                let off = (sbeg as usize).min(t.len());
8140                let n_avail = t.len().saturating_sub(off);
8141                let take = (nstrs as usize).min(n_avail);
8142                t[off..off + take].to_string()
8143            });
8144            prog = Box::new(eprog_t {
8145                flags: EF_REAL, // c:5367
8146                len,
8147                npats,
8148                nref: 1, // c:5349
8149                pats,
8150                prog: prog_words,
8151                strs: strs_copy,
8152                shf: None,  // c:5377
8153                dump: None, // c:5371
8154                // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);` —
8155                // a byte copy of the source pool, so the same encoding.
8156                strs_metafied: state.prog.strs_metafied,
8157            });
8158        }
8159
8160        // c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
8161        shf = Box::new(shfunc_t {
8162            node: hashnode {
8163                next: None,
8164                nam: String::new(),
8165                flags: tracing_flags,
8166            },
8167            filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
8168            // c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
8169            // else just lineno.
8170            lineno: {
8171                let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
8172                if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
8173                    if let Some(top) = stk.last() {
8174                        if top.tp == FS_FUNC || top.tp == FS_EVAL {
8175                            top.flineno + cur_lineno
8176                        } else {
8177                            cur_lineno
8178                        }
8179                    } else {
8180                        cur_lineno
8181                    }
8182                } else {
8183                    cur_lineno
8184                }
8185            },
8186            funcdef: Some(prog), // c:5380
8187            redir: None,
8188            sticky: None,
8189            body: None,
8190            redir_text: None,
8191        });
8192        // c:5396-5401 — redir_prog ownership.
8193        // C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
8194        // else `shf->redir = redir_prog; redir_prog = 0;`
8195        // "nonempty(names)" means there's a NEXT name still to consume —
8196        // i.e. peek the iterator.
8197        if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
8198            // c:5397 — dupe so each earlier name gets its own copy; the
8199            // last name (when iterator drains) gets the original.
8200            if let Some(rp) = redir_prog.as_deref() {
8201                shf.redir = Some(Box::new(dupeprog(rp, false)));
8202            }
8203        } else {
8204            // c:5399-5400 — last name (or anon) takes original.
8205            shf.redir = redir_prog.take();
8206        }
8207        // c:5402 — `shfunc_set_sticky(shf);`
8208        shfunc_set_sticky(&mut shf);
8209
8210        if no_names {
8211            // c:5404-5457 — anonymous function: execute immediately.
8212            // `LinkList args;` c:5409
8213            let mut args: Vec<String>;
8214
8215            anon_func = 1; // c:5411
8216            shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
8217
8218            state.pc = end; // c:5414
8219                            // c:5415 — `end += *state->pc++;`
8220            end += state.prog.prog[state.pc] as usize;
8221            state.pc += 1;
8222            // c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
8223            let arg_count = state.prog.prog[state.pc] as usize;
8224            state.pc += 1;
8225            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
8226
8227            // c:5418-5429 — htok arg subst + cleanup-on-error.
8228            if htok != 0 && !args.is_empty() {
8229                execsubst(&mut args); // c:5419
8230                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8231                    // c:5421 — `freeeprog(shf->funcdef);`
8232                    if let Some(mut fd) = shf.funcdef.take() {
8233                        freeeprog(&mut fd);
8234                    }
8235                    if shf.redir.is_some() {
8236                        // c:5422-5423 — "shouldn't be" anon+redir, but free if so.
8237                        if let Some(mut rd) = shf.redir.take() {
8238                            freeeprog(&mut rd);
8239                        }
8240                    }
8241                    dircache_set(&mut shf.filename, None); // c:5424
8242                    drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
8243                    state.pc = end; // c:5426
8244                    return 1; // c:5427
8245                }
8246            }
8247
8248            // c:5431-5432 — `setunderscore` to last arg (or "").
8249            let under_val = if !args.is_empty() {
8250                args.last().cloned().unwrap_or_default()
8251            } else {
8252                String::new()
8253            };
8254            setunderscore(&under_val);
8255
8256            // c:5434-5435 — `if (!args) args = newlinklist();`
8257            // (Rust Vec is never null; no-op.)
8258            shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
8259                                                                // c:5437 — `pushnode(args, shf->node.nam);` — prepend.
8260            args.insert(0, shf.node.nam.clone());
8261
8262            execshfunc(&mut shf, &mut args); // c:5439
8263            ret = LASTVAL.load(Ordering::Relaxed); // c:5440
8264
8265            // c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
8266            if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
8267                eprintln!("zsh: exit {}", ret); // c:5445/5447
8268            }
8269
8270            // c:5452-5456 — cleanup.
8271            if let Some(mut fd) = shf.funcdef.take() {
8272                freeeprog(&mut fd);
8273            }
8274            if let Some(mut rd) = shf.redir.take() {
8275                // c:5453-5454 — "shouldn't be" but free if present.
8276                freeeprog(&mut rd);
8277            }
8278            dircache_set(&mut shf.filename, None); // c:5455
8279            drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
8280            break; // c:5457
8281        } else {
8282            // c:5458-5484 — named function path.
8283            let nm = s.as_deref().unwrap_or("");
8284            // c:5460-5475 — TRAP* signal-trap install.
8285            // BUGS.md #1114 — the TRAPxxx() FUNCTION-trap form is zsh-only.
8286            // bash, ksh and dash have no such concept: there `TRAPINT` is an
8287            // ordinary function whose name merely begins with TRAP, and SIGINT
8288            // keeps its default disposition. Recognising it in a drop-in mode
8289            // also SILENTLY DESTROYS an already-installed list trap for that
8290            // signal, since the two forms are alternatives in zsh.
8291            // posix_faithful() is raised only for a bare drop-in flag, so
8292            // --zsh, native zshrs and `emulate sh` are untouched.
8293            if !crate::extensions::dash_mode::posix_faithful() && nm.len() > 4 && nm.starts_with("TRAP") {
8294                if let Some(sn) = getsigidx(&nm[4..]) {
8295                    signum = sn;
8296                    // c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
8297                    if settrap(signum, None, ZSIG_FUNC) != 0 {
8298                        if let Some(mut fd) = shf.funcdef.take() {
8299                            freeeprog(&mut fd); // c:5463
8300                        }
8301                        dircache_set(&mut shf.filename, None); // c:5464
8302                        drop(shf); // c:5465
8303                        state.pc = end; // c:5466
8304                        return 1; // c:5467
8305                    }
8306                    // c:5474 — `removetrapnode(signum);`
8307                    removetrapnode(signum);
8308                    // c:Src/signals.c::settrap → unsettrap →
8309                    // removetrap also clears sigfuncs[sig] (the C
8310                    // string-form trap slot). zshrs's port stores
8311                    // string-form bodies in a separate
8312                    // `traps_table` HashMap not touched by
8313                    // removetrap. Drop the string-form entry here
8314                    // so dotrap's fallback doesn't double-dispatch
8315                    // when a TRAPxxx function REPLACES an
8316                    // existing `trap '...' SIG` registration. Bug
8317                    // #541 in docs/BUGS.md.
8318                    if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
8319                        t.remove(&nm[4..]);
8320                    }
8321                }
8322            }
8323            // c:5477-5482 — re-define-self trace flag propagate.
8324            if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
8325                if let Some(top) = stk.last() {
8326                    if top.tp == FS_FUNC && top.name == nm {
8327                        // c:5479 — `Shfunc old = shfunctab->getnode(s);`
8328                        if let Ok(rd) = shfunctab_lock().read() {
8329                            if let Some(old) = rd.get(nm) {
8330                                // c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
8331                                shf.node.flags |=
8332                                    old.node.flags & (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
8333                            }
8334                        }
8335                    }
8336                }
8337            }
8338            // c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
8339            shf.node.nam = nm.to_string();
8340            // Lineage tap: the definition is a function's origin, and a
8341            // second definition of the same name is a `redefine` op.
8342            if crate::provenance::active() {
8343                crate::provenance::on_func_define(
8344                    nm,
8345                    shf.body.as_deref(),
8346                    shf.filename.as_deref(),
8347                    shf.lineno,
8348                );
8349            }
8350            if let Ok(mut wr) = shfunctab_lock().write() {
8351                wr.add(*shf);
8352            }
8353        }
8354    }
8355    // c:5486-5487 — `if (!anon_func) setunderscore("");`
8356    if anon_func == 0 {
8357        setunderscore("");
8358    }
8359    // c:5488-5491 — leftover redir cleanup ("shouldn't happen").
8360    if let Some(mut rd) = redir_prog.take() {
8361        freeeprog(&mut rd);
8362    }
8363    // c:5492 — `state->pc = end;`
8364    state.pc = end;
8365    // c:5493 — `return ret;`
8366    ret
8367}
8368
8369/// Port of `execsimple()` from `Src/exec.c:1290` — C decl `execsimple(Estate state)`.
8370/// Fast-path for single-Simple commands that bypasses the full
8371/// `execcmd_exec` machinery.
8372pub fn execsimple(state: &mut estate) -> i32 {
8373    // c:1292 — `wordcode code = *state->pc++;`
8374    let mut code = state.prog.prog[state.pc];
8375    state.pc += 1;
8376    // c:1295-1296 — `if (errflag) return (lastval = 1);`
8377    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
8378    // the C line cited above tests the WHOLE errflag, so masking here let an
8379    // interrupted shell keep going where zsh stops.
8380    if errflag.load(Ordering::Relaxed) != 0 {
8381        LASTVAL.store(1, Ordering::Relaxed);
8382        return 1;
8383    }
8384    // c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
8385    if !isset(crate::ported::zsh_h::EXECOPT) {
8386        LASTVAL.store(0, Ordering::Relaxed);
8387        return 0;
8388    }
8389    // c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
8390    // In evaluated traps, don't modify the line number (the trap
8391    // dispatcher restores it). `code` here is the wordcode-encoded
8392    // line number from the WC_SIMPLE entry at state.pc-1.
8393    if !crate::ported::zsh_h::IN_EVAL_TRAP()
8394        && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
8395        && code != 0
8396    {
8397        crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
8398    }
8399    // c:1306 — `code = wc_code(*state->pc++);`
8400    code = wc_code(state.prog.prog[state.pc]);
8401    state.pc += 1;
8402    // c:1311-1312 — `otj = thisjob; thisjob = -1;`
8403    let otj = *THISJOB
8404        .get_or_init(|| std::sync::Mutex::new(-1))
8405        .lock()
8406        .unwrap();
8407    *THISJOB
8408        .get_or_init(|| std::sync::Mutex::new(-1))
8409        .lock()
8410        .unwrap() = -1;
8411    use crate::ported::zsh_h::{
8412        WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
8413        WC_WHILE,
8414    };
8415    use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
8416    let lv = if code == WC_ASSIGN {
8417        // c:1315-1319 — assignment-only simple cmd path.
8418        // cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
8419        addvars(state, state.pc.saturating_sub(1), 0);
8420        setunderscore(""); // c:1317
8421        if isset(XTRACE) {
8422            eprintln!();
8423        }
8424        let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
8425        if ef != 0 {
8426            ef
8427        } else {
8428            0
8429        }
8430    } else {
8431        // c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
8432        let q = queue_signal_level();
8433        dont_queue_signals();
8434        let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8435            ERRFLAG_ERROR
8436        } else if code == WC_FUNCDEF {
8437            execfuncdef(state, None)
8438        } else {
8439            // c:5499 execfuncs[] table inlined — match the WC_* tag.
8440            match code {
8441                WC_CURSH => execcursh(state, 0),
8442                WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
8443                WC_FOR => execfor(state, 0),
8444                WC_SELECT => execselect(state, 0),
8445                WC_CASE => execcase(state, 0),
8446                WC_IF => execif(state, 0),
8447                WC_WHILE => execwhile(state, 0),
8448                WC_REPEAT => execrepeat(state, 0),
8449                WC_TIMED => exectime(state, 0),
8450                WC_COND => execcond(state, 0),
8451                WC_ARITH => execarith(state, 0),
8452                WC_TRY => exectry(state, 0),
8453                _ => 0,
8454            }
8455        };
8456        restore_queue_signals(q);
8457        result
8458    };
8459    // c:1334 — `thisjob = otj;`
8460    *THISJOB
8461        .get_or_init(|| std::sync::Mutex::new(-1))
8462        .lock()
8463        .unwrap() = otj;
8464    LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
8465    lv
8466}
8467
8468/// Port of `execlist()` from `Src/exec.c:1349` — C decl `execlist(Estate state, int dont_change_job, int exiting)`.
8469/// Walks WC_LIST entries, dispatches each
8470/// sublist (WC_SUBLIST chain inlined per c:1525-1625, same as C —
8471/// there's no separate execsublist function), handles signal-trap
8472/// dispatch + ERREXIT propagation.
8473///
8474/// Body ports the structural skeleton faithfully (WC_LIST walk,
8475/// per-iteration breaks/retflag/errflag guards, ltype dispatch on
8476/// Z_END/Z_SYNC/Z_ASYNC, donetrap handling). The full signal queue
8477/// + DEBUGBEFORECMD trap machinery from c:1357-1500 is preserved
8478/// in shape with TODO-citations where dependent primitives aren't
8479/// yet ported.
8480pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) -> i32 {
8481    let mut last_status: i32 = 0;
8482    let mut donetrap: i32 = 0; // c:1352 — `static int donetrap;`
8483    let cj = *THISJOB
8484        .get_or_init(|| std::sync::Mutex::new(-1))
8485        .lock()
8486        .unwrap(); // c:1364 — `cj = thisjob;`
8487    let _ = dont_change_job; // c:1361 — restored on exit if nonzero.
8488                             // c:1380 — `code = *state->pc++;`
8489    if state.pc >= state.prog.prog.len() {
8490        return last_status;
8491    }
8492    let mut code = state.prog.prog[state.pc];
8493    state.pc += 1;
8494    // c:1382-1384 — empty list returns lastval = 0.
8495    if wc_code(code) != WC_LIST {
8496        LASTVAL.store(0, Ordering::Relaxed);
8497        return 0;
8498    }
8499    use crate::ported::zsh_h::{WC_LIST_SKIP, WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC};
8500    // c:1385-1499 — main WC_LIST loop.
8501    while wc_code(code) == WC_LIST
8502        && BREAKS.load(Ordering::SeqCst) == 0
8503        && RETFLAG.load(Ordering::SeqCst) == 0
8504        // c:1390 — `while (wc_code(code) == WC_LIST && !breaks && !retflag &&
8505        // !errflag)`: the WHOLE errflag. A user interrupt sets ERRFLAG_INT and
8506        // never ERRFLAG_ERROR (signals.c:457), so the mask let the rest of the
8507        // list run after ^C or an interrupting trap:
8508        //   TRAPINT() { print T; return 1 }
8509        //   f() { print A; kill -INT $$; print C }; f; print B
8510        //   zsh: A T      zshrs: A T B
8511        && errflag.load(Ordering::Relaxed) == 0
8512    {
8513        let ltype = WC_LIST_TYPE(code) as i32;
8514        // c:1396 — `csp = cmdsp;` — snapshot cmdstack depth at start
8515        // of this WC_LIST iteration; restored at end so partial
8516        // cmdpush sequences (e.g. from execcond, execfuncs) don't
8517        // leak into the next sublist.
8518        let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len());
8519        // c:1502-1509 — Z_SIMPLE fast-path.
8520        if (ltype & Z_SIMPLE as i32) != 0 {
8521            let next_pc = state.pc + WC_LIST_SKIP(code) as usize;
8522            let s = execsimple(state);
8523            last_status = s;
8524            state.pc = next_pc;
8525        } else {
8526            // c:1513-1523 — sublist chain.
8527            if state.pc >= state.prog.prog.len() {
8528                break;
8529            }
8530            code = state.prog.prog[state.pc];
8531            state.pc += 1;
8532            // c:1525-1625 — sublist chain (&&/|| operators) inlined.
8533            use crate::ported::zsh_h::{
8534                WC_SUBLIST_AND, WC_SUBLIST_END, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SIMPLE,
8535                WC_SUBLIST_SKIP,
8536            };
8537            let mut sub_code = code;
8538            let _ = dont_change_job;
8539            while wc_code(sub_code) == WC_SUBLIST {
8540                let flags = WC_SUBLIST_FLAGS(sub_code);
8541                let next = state.pc + WC_SUBLIST_SKIP(sub_code) as usize;
8542                let sl_type = WC_SUBLIST_TYPE(sub_code) as i32;
8543                let last1 = if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
8544                    exiting
8545                } else {
8546                    0
8547                };
8548                if flags == WC_SUBLIST_SIMPLE {
8549                    last_status = execsimple(state); // c:1605
8550                } else {
8551                    let _ = execpline(state, sub_code, sl_type, last1); // c:1607
8552                    last_status = LASTVAL.load(Ordering::Relaxed);
8553                }
8554                // c:1612 — `WC_SUBLIST_NOT` inverts status.
8555                if (flags & WC_SUBLIST_NOT) != 0 {
8556                    last_status = if last_status == 0 { 1 } else { 0 };
8557                    LASTVAL.store(last_status, Ordering::Relaxed);
8558                }
8559                state.pc = next;
8560                if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
8561                    break;
8562                }
8563                if state.pc >= state.prog.prog.len() {
8564                    break;
8565                }
8566                // c:1617-1623 — short-circuit on && / ||.
8567                if sl_type == WC_SUBLIST_AND as i32 && last_status != 0 {
8568                    while state.pc < state.prog.prog.len() {
8569                        let c = state.prog.prog[state.pc];
8570                        if wc_code(c) != WC_SUBLIST {
8571                            break;
8572                        }
8573                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
8574                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
8575                            break;
8576                        }
8577                    }
8578                    break;
8579                }
8580                if sl_type == WC_SUBLIST_OR as i32 && last_status == 0 {
8581                    while state.pc < state.prog.prog.len() {
8582                        let c = state.prog.prog[state.pc];
8583                        if wc_code(c) != WC_SUBLIST {
8584                            break;
8585                        }
8586                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
8587                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
8588                            break;
8589                        }
8590                    }
8591                    break;
8592                }
8593                sub_code = state.prog.prog[state.pc];
8594                state.pc += 1;
8595            }
8596        }
8597        // c:1593 — `cmdsp = csp;` — restore cmdstack depth to the
8598        // snapshot taken at start of iteration. Reverses any cmdpush
8599        // calls made by nested execcond / execfuncs / execcmd_exec
8600        // that didn't pop cleanly.
8601        crate::ported::prompt::CMDSTACK.with(|s| {
8602            let mut g = s.borrow_mut();
8603            if g.len() > csp {
8604                g.truncate(csp);
8605            }
8606        });
8607        // c:1626-1634 — donetrap is reset between sublists.
8608        donetrap = 0;
8609        // c:1640-1645 — fetch next WC_LIST header (or break out).
8610        if state.pc >= state.prog.prog.len() {
8611            break;
8612        }
8613        let next_code = state.prog.prog[state.pc];
8614        if wc_code(next_code) != WC_LIST {
8615            break;
8616        }
8617        state.pc += 1;
8618        code = next_code;
8619        // c:1389 — z_end means last sublist, exiting becomes 1 for tail-exec.
8620        if (ltype & Z_END as i32) != 0 {
8621            exiting = 1;
8622        }
8623    }
8624    // c:1659-1664 — cleanup: restore thisjob if dont_change_job, this_noerrexit=1.
8625    if dont_change_job != 0 {
8626        *THISJOB
8627            .get_or_init(|| std::sync::Mutex::new(-1))
8628            .lock()
8629            .unwrap() = cj;
8630    }
8631    let _ = donetrap;
8632    this_noerrexit.store(1, Ordering::Relaxed);
8633    LASTVAL.store(last_status, Ordering::Relaxed);
8634    last_status
8635}
8636
8637// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
8638// 1625`, the C source likewise inlines it — there's no `execsublist`
8639// function in zsh C).
8640
8641/// Port of `execcmd_getargs()` from `Src/exec.c:2791` — C decl `static void execcmd_getargs(LinkList preargs, LinkList args, int expand)`.
8642/// Transfer the first node of `args`
8643/// to `preargs`, performing `prefork` (singleton-list expansion) on
8644/// the way if `expand` is set. Used by `execcmd_exec` to pull the
8645/// command head one word at a time so prefix-modifier walking
8646/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
8647pub fn execcmd_getargs(preargs: &mut LinkList<String>, args: &mut LinkList<String>, expand: i32) {
8648    // c:2791
8649    if args.firstnode().is_none() {
8650        // c:2793 — `if (!firstnode(args)) return;`
8651        return;
8652    } else if expand != 0 {
8653        // c:2795
8654        // c:2796-2797 — `local_list0(svl); init_list0(svl);` —
8655        // stack-local single-bucket list. Rust uses a fresh
8656        // LinkList<String> per call.
8657        let mut svl: LinkList<String> = Default::default();
8658        // c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
8659        if let Some(idx) = args.firstnode() {
8660            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
8661                svl.push_back(head);
8662            }
8663        }
8664        // c:2801 — `prefork(&svl, 0, NULL);`
8665        let mut rf = 0i32;
8666        prefork(&mut svl, 0, &mut rf);
8667        // c:2802 — `joinlists(preargs, &svl);`
8668        crate::ported::linklist::joinlists(preargs, &mut svl);
8669    } else {
8670        // c:2803-2804 — no-expand path: move head verbatim.
8671        if let Some(idx) = args.firstnode() {
8672            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
8673                preargs.push_back(head);
8674            }
8675        }
8676    }
8677}
8678
8679/// Port of `execcmd_fork()` from `Src/exec.c:2810` — C decl `execcmd_fork(Estate state, int how, int type, Wordcode varspc, LinkList *filelistp, char *text, int oautocont, int close_if_forked)`.
8680/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
8681/// int close_if_forked)` from `Src/exec.c:2810-2893`.
8682///
8683/// Fork the current command into a child process: parent records
8684/// the pid + STTY env scan + addproc; child enters subshell, writes
8685/// `entersubsh_ret` back to parent through `synch` pipe, and returns
8686/// 0 so the caller can continue with the body.
8687///
8688/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
8689/// only in the child branch (so the parent's `filelist` stays
8690/// untouched). Rust sig keeps the same C contract.
8691pub fn execcmd_fork(
8692    state: &mut estate,
8693    how: i32,
8694    typ: i32,
8695    varspc: Option<usize>,
8696    filelistp: &mut Vec<jobfile>,
8697    text: &str,
8698    oautocont: i32,
8699    close_if_forked: i32,
8700) -> i32 {
8701    use crate::ported::signals::sigtrapped as sigtrapped_static;
8702    use crate::ported::signals_h::SIGEXIT;
8703    use crate::ported::zsh_h::{
8704        AUTOCONTINUE, BGNICE, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
8705        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
8706        WC_SUBSH as ZWC_SUBSH, ZSIG_IGNORED, Z_ASYNC,
8707    };
8708    // c:2810
8709    let pid: libc::pid_t; // c:2814
8710    let mut synch: [i32; 2] = [-1, -1]; // c:2815
8711    let flags: i32; // c:2815
8712    let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
8713                                                               // c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
8714                                                               // for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
8715    let mut bgtime = ZshTimespec::default();
8716
8717    child_block(); // c:2819
8718    esret.gleader = -1; // c:2820
8719    esret.list_pipe_job = -1; // c:2821
8720
8721    // c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
8722    if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
8723        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
8724        return -1; // c:2825
8725    }
8726    // c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
8727    pid = zfork(Some(&mut bgtime));
8728    if pid == -1 {
8729        unsafe {
8730            libc::close(synch[0]); // c:2827
8731            libc::close(synch[1]); // c:2828
8732        }
8733        LASTVAL.store(1, Ordering::Relaxed); // c:2829
8734        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
8735        return -1; // c:2831
8736    }
8737    if pid != 0 {
8738        // c:2833 — parent.
8739        unsafe { libc::close(synch[1]) }; // c:2834
8740                                          // c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
8741        let mut buf = [0u8; size_of::<entersubsh_ret>()];
8742        let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
8743        // entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
8744        if buf.len() >= 8 {
8745            esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
8746            esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
8747        }
8748        unsafe { libc::close(synch[0]) }; // c:2836
8749        if (how & Z_ASYNC as i32) != 0 {
8750            // c:2837 — `lastpid = (zlong) pid;`
8751            crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
8752        } else {
8753            // c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
8754            let thisjob_idx = {
8755                if let Some(m) = THISJOB.get() {
8756                    *m.lock().unwrap()
8757                } else {
8758                    -1
8759                }
8760            };
8761            // Examine the jobtab entry under lock.
8762            let stty_already = if thisjob_idx >= 0 {
8763                if let Some(jt) = JOBTAB.get() {
8764                    let guard = jt.lock().unwrap();
8765                    guard
8766                        .get(thisjob_idx as usize)
8767                        .map(|j| j.stty_in_env != 0)
8768                        .unwrap_or(true)
8769                } else {
8770                    true
8771                }
8772            } else {
8773                true
8774            };
8775            if !stty_already && varspc.is_some() {
8776                // c:2841-2851 — walk varspc looking for STTY=...
8777                let mut p = varspc.unwrap();
8778                loop {
8779                    if p >= state.prog.prog.len() {
8780                        break;
8781                    }
8782                    let ac = state.prog.prog[p];
8783                    if wc_code(ac) != ZWC_ASSIGN {
8784                        break;
8785                    }
8786                    // c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
8787                    let name = ecrawstr(&state.prog, p + 1, None);
8788                    if name == "STTY" {
8789                        // c:2846 — `jobtab[thisjob].stty_in_env = 1;`
8790                        if let Some(jt) = JOBTAB.get() {
8791                            let mut guard = jt.lock().unwrap();
8792                            if let Some(j) = guard.get_mut(thisjob_idx as usize) {
8793                                j.stty_in_env = 1;
8794                            }
8795                        }
8796                        break; // c:2847
8797                    }
8798                    p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
8799                        3 // c:2849
8800                    } else {
8801                        (ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
8802                    };
8803                }
8804            }
8805        }
8806        // c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
8807        if let Some(jt) = JOBTAB.get() {
8808            let mut guard = jt.lock().unwrap();
8809            let tj = {
8810                if let Some(m) = THISJOB.get() {
8811                    *m.lock().unwrap()
8812                } else {
8813                    -1
8814                }
8815            };
8816            if tj >= 0 {
8817                if let Some(j) = guard.get_mut(tj as usize) {
8818                    crate::ported::jobs::addproc(
8819                        j,
8820                        pid,
8821                        text,
8822                        false,
8823                        Some(std::time::Instant::now()),
8824                        esret.gleader,
8825                        esret.list_pipe_job,
8826                    );
8827                }
8828            }
8829        }
8830        // c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
8831        if oautocont >= 0 {
8832            opt_state_set("autocontinue", oautocont != 0);
8833            let _ = AUTOCONTINUE; // const referenced for parity
8834        }
8835        // c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
8836        if let Some(jt) = JOBTAB.get() {
8837            let mut guard = jt.lock().unwrap();
8838            let tj = {
8839                if let Some(m) = THISJOB.get() {
8840                    *m.lock().unwrap()
8841                } else {
8842                    -1
8843                }
8844            };
8845            if tj >= 0 {
8846                if let Some(j) = guard.get_mut(tj as usize) {
8847                    crate::ported::jobs::pipecleanfilelist(j, true);
8848                }
8849            }
8850        }
8851        return pid; // c:2857
8852    }
8853
8854    // c:2860 — pid == 0 (child).
8855    unsafe { libc::close(synch[0]) }; // c:2861
8856    flags = (if (how & Z_ASYNC as i32) != 0 {
8857        esub::ASYNC
8858    } else {
8859        0
8860    }) | esub::PGRP; // c:2862
8861    let mut flags = flags;
8862    if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
8863        flags |= esub::KEEPTRAP; // c:2864
8864    }
8865    if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
8866        flags |= esub::JOB_CONTROL; // c:2866
8867    }
8868    // c:2867 — `*filelistp = jobtab[thisjob].filelist;`
8869    if let Some(jt) = JOBTAB.get() {
8870        let mut guard = jt.lock().unwrap();
8871        let tj = {
8872            if let Some(m) = THISJOB.get() {
8873                *m.lock().unwrap()
8874            } else {
8875                -1
8876            }
8877        };
8878        if tj >= 0 {
8879            if let Some(j) = guard.get_mut(tj as usize) {
8880                *filelistp = std::mem::take(&mut j.filelist);
8881            }
8882        }
8883    }
8884    entersubsh(flags, Some(&mut esret)); // c:2868
8885                                         // c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
8886    let mut buf = [0u8; 8];
8887    buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
8888    buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
8889    if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
8890        zerr(&format!(
8891            "Failed to send entersubsh_ret report: {}",
8892            std::io::Error::last_os_error()
8893        ));
8894        return -1; // c:2871
8895    }
8896    unsafe { libc::close(synch[1]) }; // c:2873
8897    let _ = zclose(close_if_forked); // c:2874
8898
8899    // c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
8900    let sigint_state = {
8901        let guard = sigtrapped_static.lock().unwrap();
8902        guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
8903    };
8904    if (sigint_state & ZSIG_IGNORED) != 0 {
8905        crate::ported::signals::holdintr(); // c:2877
8906    }
8907    // c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
8908    {
8909        let mut guard = sigtrapped_static.lock().unwrap();
8910        if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
8911            *slot = 0;
8912        }
8913    }
8914    // c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
8915    // Per-platform errno setter+reader: __error() on macOS,
8916    // __errno_location() on Linux. Without cfg gating Linux CI breaks.
8917    if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
8918        #[cfg(target_os = "macos")]
8919        unsafe {
8920            *libc::__error() = 0;
8921            if libc::nice(5) == -1 && *libc::__error() != 0 {
8922                zwarn(&format!(
8923                    "nice(5) failed: {}",
8924                    std::io::Error::last_os_error()
8925                ));
8926            }
8927        }
8928        #[cfg(target_os = "linux")]
8929        unsafe {
8930            *libc::__errno_location() = 0;
8931            if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
8932                zwarn(&format!(
8933                    "nice(5) failed: {}",
8934                    std::io::Error::last_os_error()
8935                ));
8936            }
8937        }
8938    }
8939    0 // c:2892
8940}
8941
8942/// Port of `execcmd_analyse()` from `Src/exec.c:2733` — C decl `execcmd_analyse(Estate state, Execcmd_params eparams)`.
8943/// Pre-execcmd_exec analysis pass:
8944/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
8945/// without expanding (no prefork, no globbing), and fills `eparams`
8946/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
8947/// can branch on the command type before the real work.
8948pub fn execcmd_analyse(state: &mut estate, eparams: &mut crate::ported::zsh_h::execcmd_params) {
8949    use crate::ported::zsh_h::{
8950        WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
8951        WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
8952        WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
8953    };
8954    // c:2733
8955    let mut code: wordcode; // c:2735
8956    let mut i: i32; // c:2736
8957    let _ = i;
8958
8959    // c:2738 — `eparams->beg = state->pc;`
8960    eparams.beg = state.pc;
8961    // c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
8962    eparams.redir =
8963        if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_REDIR {
8964            Some(crate::ported::parse::ecgetredirs(state))
8965        } else {
8966            None
8967        };
8968    // c:2741-2748 — varspc walk (WC_ASSIGN chain).
8969    if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
8970        cmdoutval.store(0, Ordering::Relaxed); // c:2742
8971        eparams.varspc = Some(state.pc); // c:2743
8972                                         // c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
8973        loop {
8974            if state.pc >= state.prog.prog.len() {
8975                break;
8976            }
8977            code = state.prog.prog[state.pc];
8978            if wc_code(code) != ZWC_ASSIGN {
8979                break;
8980            }
8981            state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
8982                3 // c:2745
8983            } else {
8984                (WC_ASSIGN_NUM(code) + 2) as usize // c:2746
8985            };
8986        }
8987    } else {
8988        eparams.varspc = None; // c:2748
8989    }
8990
8991    // c:2750 — `code = *state->pc++;`
8992    if state.pc >= state.prog.prog.len() {
8993        eparams.args = None;
8994        eparams.assignspc = None;
8995        eparams.typ = 0;
8996        eparams.postassigns = 0;
8997        eparams.htok = 0;
8998        return;
8999    }
9000    code = state.prog.prog[state.pc];
9001    state.pc += 1;
9002
9003    // c:2752 — `eparams->type = wc_code(code);`
9004    eparams.typ = wc_code(code) as i32;
9005    // c:2753 — `eparams->postassigns = 0;`
9006    eparams.postassigns = 0;
9007
9008    // c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
9009    // per the comment at c:2755-2757.
9010    match eparams.typ as wordcode {
9011        x if x == ZWC_SIMPLE => {
9012            // c:2759-2763
9013            let mut htok = 0;
9014            let argc = ZWC_SIMPLE_ARGC(code) as usize;
9015            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
9016            eparams.htok = htok;
9017            eparams.assignspc = None;
9018        }
9019        x if x == ZWC_TYPESET => {
9020            // c:2765-2777
9021            let mut htok = 0;
9022            let argc = ZWC_TYPESET_ARGC(code) as usize;
9023            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
9024            eparams.htok = htok;
9025            // c:2768 — `eparams->postassigns = *state->pc++;`
9026            if state.pc < state.prog.prog.len() {
9027                eparams.postassigns = state.prog.prog[state.pc] as i32;
9028                state.pc += 1;
9029            }
9030            // c:2769 — `eparams->assignspc = state->pc;`
9031            eparams.assignspc = Some(state.pc);
9032            // c:2770-2776 — walk past the postassigns.
9033            let mut k = 0i32;
9034            while k < eparams.postassigns {
9035                if state.pc >= state.prog.prog.len() {
9036                    break;
9037                }
9038                code = state.prog.prog[state.pc];
9039                // c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
9040                state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
9041                    3 // c:2774
9042                } else {
9043                    (WC_ASSIGN_NUM(code) + 2) as usize // c:2775
9044                };
9045                k += 1;
9046            }
9047        }
9048        _ => {
9049            // c:2779-2783 default.
9050            eparams.args = None;
9051            eparams.assignspc = None;
9052            eparams.htok = 0;
9053        }
9054    }
9055}
9056
9057/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
9058/// Stack of `"context"` labels used by `eval`-style nested execution:
9059/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
9060/// ..., "context")` pushes its label and pops on return.
9061///
9062/// Storage is [`crate::thread_shell_state::ThreadMutex`], not a plain
9063/// `Mutex`: like `funcstack`, the stack describes where the ONE thread of
9064/// execution is, so a hook function running on a pool worker must not push
9065/// `"shfunc"` onto the shell thread's `$ZSH_EVAL_CONTEXT`.
9066pub static zsh_eval_context: crate::thread_shell_state::ThreadMutex<Vec<String>> =
9067    crate::thread_shell_state::ThreadMutex::new(Vec::new(), Vec::new);
9068
9069/// RAII form of C's `execode` context push/pop (`Src/exec.c:1265-1266`
9070/// and the matching `zsh_eval_context[alen] = NULL` at c:1281).
9071///
9072/// zshrs does not route every nested execution through `execode`, so
9073/// the callers that C reaches via `execode(prog, …, "label")` push
9074/// their label with this guard instead. Dropping it pops, on every
9075/// return path including a panic — C relies on the plain assignment at
9076/// c:1281 being reached, which the guard reproduces more robustly.
9077pub struct EvalContextFrame {
9078    pushed: bool,
9079}
9080
9081impl EvalContextFrame {
9082    /// c:1265-1266 — `zsh_eval_context[alen] = context;`
9083    pub fn push(context: &str) -> Self {
9084        let mut pushed = false;
9085        if let Ok(mut ctx) = zsh_eval_context.lock() {
9086            ctx.push(context.to_string());
9087            Self::sync(&ctx);
9088            pushed = true;
9089        }
9090        Self { pushed }
9091    }
9092
9093    /// Publish the `zsh_eval_context` stack into the shell-visible
9094    /// params. C keeps ONE `char **zsh_eval_context` that the parameter
9095    /// table points at directly (`IPDEF8`/`IPDEF9`, params.c:401/431),
9096    /// so a push is instantly visible as `$zsh_eval_context` /
9097    /// `$ZSH_EVAL_CONTEXT`. The Rust params own their storage, so every
9098    /// mutation of the static has to be mirrored — through the
9099    /// `u_arr`/`u_str` fields directly because both names are
9100    /// `PM_READONLY_SPECIAL`.
9101    fn sync(stack: &[String]) {
9102        // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
9103        // C has one `char **zsh_eval_context` and one thread, so the
9104        // publish below cannot be reached from anywhere but the frame that
9105        // pushed. zshrs's param table is ONE shared table that
9106        // `async_precmd` hooks deliberately write into, so a worker's push
9107        // would overwrite the shell thread's `$ZSH_EVAL_CONTEXT` with the
9108        // hook's context. The worker still tracks its own stack (the
9109        // `ThreadMutex` above); only the shell-visible parameters stay the
9110        // shell thread's.
9111        if !crate::thread_shell_state::is_shell_thread() {
9112            return;
9113        }
9114        let joined = stack.join(":");
9115        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
9116            if let Some(pm) = tab.get_mut("zsh_eval_context") {
9117                pm.u_arr = Some(stack.to_vec());
9118                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
9119            }
9120            if let Some(pm) = tab.get_mut("ZSH_EVAL_CONTEXT") {
9121                pm.u_str = Some(joined);
9122                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
9123            }
9124        }
9125    }
9126}
9127
9128impl Drop for EvalContextFrame {
9129    fn drop(&mut self) {
9130        // c:1281 — `zsh_eval_context[alen] = NULL;`
9131        if self.pushed {
9132            if let Ok(mut ctx) = zsh_eval_context.lock() {
9133                ctx.pop();
9134                Self::sync(&ctx);
9135            }
9136        }
9137    }
9138}
9139
9140/// Port of `static int donetrap;` from `Src/exec.c:1351`. Tracks
9141/// whether the ZERR trap has already fired for the current sublist.
9142/// C source resets to 0 at sublist start (c:1455) and sets to 1
9143/// after `dotrap(SIGZERR)` (c:1602). The check
9144/// `if (!this_noerrexit && !donetrap && !this_donetrap)` at c:1598
9145/// suppresses re-firing within the same sublist AND, crucially,
9146/// carries the "already fired" state across a function-call return
9147/// boundary so the outer caller's post-command check doesn't fire
9148/// ZERR a second time for the same logical error. Bug #303 in
9149/// docs/BUGS.md.
9150///
9151/// Reset at each top-level statement boundary via
9152/// `BUILTIN_DONETRAP_RESET` emitted by `compile_list`. Set after
9153/// `dotrap(SIGZERR)` fires inside `BUILTIN_ERREXIT_CHECK`.
9154pub static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9155
9156/// Port of `save_params()` from `Src/exec.c:4410` — C decl `save_params(Estate state, Wordcode pc, LinkList *restore_p, LinkList *remove_p)`.
9157/// Walk WC_ASSIGN
9158/// chain at `pc`, snapshot each existing param into `restore_p` (so
9159/// the builtin/shfunc can restore them on return) and enqueue every
9160/// touched name in `remove_p` (so we know what to unset).
9161pub fn save_params(
9162    state: &mut estate,
9163    pc: usize,
9164    restore_p: &mut Vec<crate::ported::zsh_h::param>,
9165    remove_p: &mut Vec<String>,
9166) {
9167    use crate::ported::zsh_h::{
9168        PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
9169        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
9170    };
9171    // c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
9172    // c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
9173    let mut p = pc;
9174    // c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
9175    loop {
9176        if p >= state.prog.prog.len() {
9177            break;
9178        }
9179        let ac = state.prog.prog[p];
9180        if wc_code(ac) != WC_ASSIGN {
9181            break;
9182        }
9183        // c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
9184        let s = ecrawstr(&state.prog, p + 1, None);
9185        // c:4421 — `pm = paramtab->getnode(paramtab, s)`
9186        let pm_clone: Option<crate::ported::zsh_h::param> = {
9187            let tab = paramtab().read().unwrap();
9188            tab.get(&s).map(|b| (**b).clone())
9189        };
9190        if let Some(pm) = pm_clone {
9191            // c:4423-4424 — `if (pm->env) delenv(pm);`
9192            if pm.env.is_some() {
9193                crate::ported::params::delenv(&s);
9194            }
9195            // c:4425-4448 — copy if not readonly-special.
9196            if (pm.node.flags & PM_SPECIAL as i32) == 0 {
9197                // c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
9198                let mut tpm = pm.clone();
9199                tpm.node.nam = s.clone();
9200                // copyparam with fakecopy=0 already done by the clone()
9201                // (Clone derives a deep copy of param fields).
9202                restore_p.push(tpm); // c:4451
9203            } else if (pm.node.flags & PM_READONLY as i32) == 0
9204            {
9205                // c:4439-4448 — special-but-not-readonly: fakecopy=1.
9206                let mut tpm = pm.clone();
9207                tpm.node.nam = pm.node.nam.clone();
9208                restore_p.push(tpm); // c:4451
9209            }
9210            // c:4449 — `addlinknode(*remove_p, dupstring(s));`
9211            remove_p.push(s.clone());
9212        } else {
9213            // c:4453 — `addlinknode(*remove_p, dupstring(s));`
9214            remove_p.push(s.clone());
9215        }
9216        // c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
9217        p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
9218            3
9219        } else {
9220            (ZWC_ASSIGN_NUM(ac) + 2) as usize
9221        };
9222    }
9223}
9224
9225/// Port of `restore_params()` from `Src/exec.c:4464` — C decl `restore_params(LinkList restorelist, LinkList removelist)`.
9226/// After the builtin/shfunc returns,
9227/// unset every name in removelist, then for each saved param in
9228/// restorelist re-install its values (PM_SPECIAL go through gsu
9229/// setfn; regular params re-enter paramtab as-is).
9230pub fn restore_params(restorelist: Vec<crate::ported::zsh_h::param>, removelist: Vec<String>) {
9231    use crate::ported::zsh_h::{PM_READONLY, PM_SPECIAL};
9232    // c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
9233    for s in &removelist {
9234        // c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
9235        let flags = {
9236            let tab = paramtab().read().unwrap();
9237            tab.get(s).map(|p| p.node.flags)
9238        };
9239        if let Some(f) = flags {
9240            if (f & PM_SPECIAL as i32) == 0 {
9241                // !!! LOCK DISCIPLINE — NO GSU CALLBACK MAY RUN UNDER THE GUARD !!!
9242                // Same rule, and the same reasoning, as the long note in
9243                // `assignnparam` (params.rs). C holds no lock at all: c:4474
9244                // hands `unsetparam_pm` a pointer to the live node, so the
9245                // callee may reach anything, and this one does — its
9246                // `if (pm->env) delenv(pm)` arm (c:Src/params.c:3872) lands in
9247                // a `delenv` that re-takes `paramtab().write()`, and its
9248                // read-only rejection (c:Src/params.c:3852) calls `zerr`,
9249                // which is not a leaf either (zwarning →
9250                // zleentry(ZLE_CMD_TRASH) → zrefresh →
9251                // getaparam("zle_highlight") → this same lock). `paramtab()`
9252                // is a `std::sync::RwLock` and is NOT reentrant.
9253                //
9254                // Neither arm can fire from HERE today, and neither is
9255                // disarmed by the callee being a leaf — both are disarmed by
9256                // this caller's preconditions, one line apart:
9257                //   * the `zerr` arm, by c:4473 clearing PM_READONLY
9258                //     immediately below;
9259                //   * the `delenv` arm, by `save_params` having already run
9260                //     `if (pm->env) delenv(pm)` (c:4423-4424) over this very
9261                //     name on the way in, which left `pm.env` None.
9262                // That is a guarantee held by a different function, for its
9263                // own reasons. `bin_ztie` had the identical shape with no such
9264                // guarantee and hung outright. The previous spelling here even
9265                // said `// Drop write guard before calling unsetparam_pm.` and
9266                // then re-acquired the guard on the next line, so the dispatch
9267                // ran under it anyway.
9268                //
9269                // Detach under the guard, release, dispatch, publish — C's
9270                // order. `unsetparam_pm` mutates only the node it is handed
9271                // and never unlinks it (its own comment records that the
9272                // c:3853-3935 removenode postlude is unported), so
9273                // republishing that node is the whole of the write-back.
9274                let staged = {
9275                    // c:4473 — `pm->node.flags &= ~PM_READONLY;`
9276                    let mut tab = paramtab().write().unwrap();
9277                    tab.get_mut(s).map(|pm_mut| {
9278                        pm_mut.node.flags &= !(PM_READONLY as i32);
9279                        (**pm_mut).clone()
9280                    })
9281                };
9282                // ---- guard released; none is held across the dispatch ----
9283                if let Some(mut pm) = staged {
9284                    let _ = crate::ported::params::unsetparam_pm(&mut pm, 0, 0); // c:4474
9285                    let mut tab = paramtab().write().unwrap();
9286                    // Only if the node is still there — re-adding one the
9287                    // dispatch removed would resurrect a parameter C dropped.
9288                    if let Some(slot) = tab.get_mut(s) {
9289                        **slot = pm;
9290                    }
9291                }
9292            }
9293        }
9294    }
9295    // c:4478-4523 — restore saved params.
9296    for pm in restorelist {
9297        // c:4481-4520 — PM_SPECIAL: route through gsu setfn.
9298        // c:4521-4523 — non-special: re-install via paramtab.
9299        if (pm.node.flags & PM_SPECIAL as i32) != 0 {
9300            // c:4537 — `tpm = paramtab->getnode(paramtab, pm->node.nam);`
9301            // c:4542-4543 — `if (!pm->env && tpm->env) delenv(tpm);`
9302            if pm.env.is_none() {
9303                let live_env = {
9304                    let tab = paramtab().read().unwrap();
9305                    tab.get(&pm.node.nam).and_then(|p| p.env.clone())
9306                };
9307                if live_env.is_some() {
9308                    crate::ported::params::delenv(&pm.node.nam);
9309                }
9310            }
9311            // c:4544 — `tpm->node.flags = pm->node.flags;`
9312            {
9313                let mut tab = paramtab().write().unwrap();
9314                if let Some(slot) = tab.get_mut(&pm.node.nam) {
9315                    slot.node.flags = pm.node.flags;
9316                }
9317            }
9318            // c:4545-4563 — the value goes back through the LIVE node's own
9319            // setfn, so the special's write hook fires: `PATH=/tmp cmd` has
9320            // to leave `pathsetfn` re-deriving `$path` and the command hash
9321            // from the restored string. The previous spelling overwrote the
9322            // paramtab entry instead, which restored `$PATH` but left every
9323            // global the setfn derives sitting at the prefix assignment's
9324            // value.
9325            match crate::ported::zsh_h::PM_TYPE(pm.node.flags as u32) {
9326                crate::ported::zsh_h::PM_INTEGER => {
9327                    // c:4551 — `tpm->gsu.i->setfn(tpm, pm->u.val);`
9328                    crate::ported::params::setiparam(&pm.node.nam, pm.u_val);
9329                }
9330                crate::ported::zsh_h::PM_EFLOAT | crate::ported::zsh_h::PM_FFLOAT => {
9331                    // c:4555 — `tpm->gsu.f->setfn(tpm, pm->u.dval);`
9332                    crate::ported::params::setnparam(
9333                        &pm.node.nam,
9334                        crate::ported::zsh_h::mnumber {
9335                            l: 0,
9336                            d: pm.u_dval,
9337                            type_: crate::ported::zsh_h::MN_FLOAT,
9338                        },
9339                    );
9340                }
9341                crate::ported::zsh_h::PM_ARRAY => {
9342                    // c:4558 — `tpm->gsu.a->setfn(tpm, pm->u.arr);`
9343                    crate::ported::params::setaparam(
9344                        &pm.node.nam,
9345                        pm.u_arr.clone().unwrap_or_default(),
9346                    );
9347                }
9348                crate::ported::zsh_h::PM_HASHED => {
9349                    // c:4561 — `tpm->gsu.h->setfn(tpm, pm->u.hash);`. zshrs
9350                    // keeps an association's pairs OUTSIDE the Param, in
9351                    // `paramtab_hashed_storage` (see `stdunsetfn`'s note at
9352                    // params.rs:11044), so they are not in `pm` to hand over
9353                    // here; the caller that saved them restores them.
9354                }
9355                _ => {
9356                    // c:4548 — `tpm->gsu.s->setfn(tpm, pm->u.str);`
9357                    crate::ported::params::setsparam(
9358                        &pm.node.nam,
9359                        pm.u_str.as_deref().unwrap_or(""),
9360                    );
9361                }
9362            }
9363        } else {
9364            // c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
9365            let mut tab = paramtab().write().unwrap();
9366            tab.insert(pm.node.nam.clone(), Box::new(pm));
9367        }
9368    }
9369}
9370
9371/// Port of `execode` from `Src/exec.c:1245` — C decl `execode(Eprog p, int dont_change_job, int exiting, char *context)`. Rust fn `execode_wordcode` is the wordcode form, driving the ported `execlist` interpreter; `execode` at exec.rs:8413 is the fusevm-pipeline entry that carries the C name.
9372/// Set up an `estate`
9373/// around the given Eprog and run `execlist`. Maintains the
9374/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
9375/// call chain.
9376///
9377/// NOTE: this is the WORDCODE form (drives the ported `execlist`
9378/// interpreter). zshrs's live execution pipeline is fusevm
9379/// (`compile_zsh` → VM), so the top-level REPL `loop()` and most call
9380/// sites run through [`execode`] (the ZshProgram/fusevm form below)
9381/// instead. This wordcode entry is retained for the internal
9382/// function-body callers (`doshfunc` / autoload) that already hold an
9383/// `Eprog`.
9384pub fn execode_wordcode(
9385    p: crate::ported::zsh_h::Eprog,
9386    dont_change_job: i32,
9387    exiting: i32,
9388    context: &str,
9389) {
9390    // c:1245
9391    let prog_ref = *p;
9392    // c:1247 — `struct estate s;`
9393    let mut s = estate {
9394        prog: Box::new(prog_ref.clone()),
9395        // c:1269 — `s.pc = p->prog;` — start at index 0.
9396        pc: 0,
9397        // c:1270 — `s.strs = p->strs;`
9398        strs: prog_ref.strs.clone(),
9399        strs_offset: 0,
9400    };
9401    // c:1251-1266 — push context onto zsh_eval_context.
9402    let pushed = {
9403        if let Ok(mut ctx) = zsh_eval_context.lock() {
9404            ctx.push(context.to_string());
9405            true
9406        } else {
9407            false
9408        }
9409    };
9410    // c:1271 — `useeprog(p);`
9411    crate::ported::parse::useeprog(&mut s.prog);
9412    // c:1273 — `execlist(&s, dont_change_job, exiting);`
9413    execlist(&mut s, dont_change_job, exiting);
9414    // c:1275 — `freeeprog(p);`
9415    crate::ported::parse::freeeprog(&mut s.prog);
9416    // c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
9417    if pushed {
9418        if let Ok(mut ctx) = zsh_eval_context.lock() {
9419            ctx.pop();
9420        }
9421    }
9422}
9423
9424thread_local! {
9425    /// !!! WARNING: RUST-ONLY CARRIER !!! The VM opens a simple command's
9426    /// redirections itself and hands `execcmd_exec` an empty `redir` list
9427    /// for a run-time-expanded head (fusevm_bridge BUILTIN_EXEC_DYNAMIC).
9428    /// Set for that call when the command HAD redirections, so `trycd`
9429    /// still sees c:3596 `(!redir || empty(redir))` as false.
9430    pub(crate) static REDIRS_PREAPPLIED: std::cell::Cell<bool> =
9431        const { std::cell::Cell::new(false) };
9432}
9433
9434thread_local! {
9435    /// The long-lived interactive executor for the top-level `loop()`
9436    /// REPL (the `zsh_main` path). Set once by the bin before
9437    /// `zsh_main`; [`execode`] runs each parsed program through it.
9438    /// Persists for the whole session so variables/functions survive
9439    /// across prompts.
9440    static SESSION_EXECUTOR: std::cell::Cell<Option<*mut crate::vm_helper::ShellExecutor>> =
9441        const { std::cell::Cell::new(None) };
9442}
9443
9444/// !!! WARNING: RUST-ONLY HELPER !!!
9445///
9446/// No function of this name exists in `Src/exec.c`. This helper stands in for
9447/// installing the process-lifetime fusevm executor that `execode`
9448/// (exec.rs:8413) runs programs on. C has no analogue: its interpreter state
9449/// is the `estate` built per call in `execode` (c:1245).
9450/// Register the persistent session executor used by [`execode`] for the
9451/// interactive `loop()` REPL. The pointer must outlive the session (the
9452/// bin keeps the executor alive until `zsh_main` exits the process).
9453pub fn install_session_executor(exec: &mut crate::vm_helper::ShellExecutor) {
9454    SESSION_EXECUTOR.with(|c| c.set(Some(exec as *mut crate::vm_helper::ShellExecutor)));
9455    // Mirror the pointer into the bridge so `with_session_context` can
9456    // establish a VM context for startup rc-sourcing (run_init_scripts,
9457    // c:1914) before the loop's first execode enters one.
9458    crate::fusevm_bridge::register_session_executor(exec);
9459}
9460
9461/// Port of `execode()` from `Src/exec.c:1245` — C decl `execode(Eprog p, int dont_change_job, int exiting, char *context)`. DIVERGENCE: this entry drives the fusevm pipeline over a `ZshProgram`; the faithful wordcode walker is `execode_wordcode` (exec.rs:8338).
9462/// zshrs `execode` — run an already-parsed `ZshProgram` (Src/exec.c:1245
9463/// `execode(prog, ...)`, called from `loop()`). This is the **exec.rs
9464/// exception** to the line-by-line port: rather than walk wordcode via
9465/// `execlist`, it drives zshrs's live engine — compile the program with
9466/// `compile_zsh` and run it on the session executor's fusevm VM. The
9467/// faithful `loop()` in init.rs calls this exactly as C calls execode.
9468/// Returns `$?` (0 when no session executor is installed).
9469pub fn execode(
9470    program: &crate::parse::ZshProgram,
9471    _dont_change_job: i32,
9472    _exiting: i32,
9473    context: &str,
9474) -> i32 {
9475    // c:1319 — `zsh_eval_context_push(context)`. The wordcode twin
9476    // (`execode_wordcode`) already does this; this fusevm variant is the
9477    // one `loop()` (Src/init.c:220) calls with "toplevel", so dropping the
9478    // push left `$zsh_eval_context` / `$ZSH_EVAL_CONTEXT` empty for the
9479    // whole interactive session (a `-c` shell got "cmdarg" from its own
9480    // caller and looked correct, hiding this).
9481    // c:1334 — the matching `zsh_eval_context_pop()` runs when this
9482    // guard drops, i.e. after execution, exactly as in C.
9483    let _ctx_frame = EvalContextFrame::push(context); // c:1319
9484    SESSION_EXECUTOR.with(|c| match c.get() {
9485        // SAFETY: set by install_session_executor to an executor that
9486        // lives for the whole single-threaded interactive session;
9487        // loop() runs only after the bin installs it.
9488        Some(ptr) => unsafe { (*ptr).execute_program(program) },
9489        None => 0,
9490    })
9491}
9492
9493// =========================================================================
9494// Live-executor accessors (former `exec_hooks` OnceLock layer).
9495//
9496// These are the **exec.rs exception**: src/ported/ code reaches the
9497// live fusevm `ShellExecutor` (param store, function dispatch, nested
9498// script/cmdsubst execution) through these thin wrappers instead of the
9499// deleted `exec_hooks` fn-pointer registry. Each delegates to
9500// `fusevm_bridge::try_with_executor` — `Some` when a VM execution
9501// context is in scope, `None` in unit-test / compsys contexts with no
9502// bridge running — and reproduces the exact per-call fallback the old
9503// `exec_hooks` wrappers used when no hook was installed. Behavior is
9504// byte-for-byte identical to the OnceLock path; only the indirection is
9505// gone. See `feedback_no_shellexecutor_in_ported` /
9506// `feedback_no_exec_script_from_ported`: the bridge belongs in exec.rs
9507// (the sanctioned exception), not scattered through src/ported.
9508// =========================================================================
9509
9510/// !!! WARNING: RUST-ONLY HELPER !!!
9511///
9512/// No function of this name exists in `Src/exec.c`. This helper stands in for
9513/// reading an array parameter through the live fusevm executor, falling back
9514/// to `params::getaparam`. C reads `paramtab` directly wherever it needs
9515/// this.
9516/// Array param value via the live executor; falls back to the direct
9517/// param table (`params::getaparam`) when no executor is in scope, so
9518/// compsys / unit-test environments still observe shell-side arrays.
9519pub fn array(name: &str) -> Option<Vec<String>> {
9520    // bash special arrays (PIPESTATUS / FUNCNAME / BASH_VERSINFO) alias the
9521    // zsh-native specials in --bash mode. Checked before the stored-array
9522    // lookup so a user array of the same name can still shadow it (rare), but
9523    // after the bash-mode gate so --zsh is untouched. Guard against the
9524    // PIPESTATUS→pipestatus self-alias recursing by only aliasing uppercase.
9525    if name.starts_with(|c: char| c.is_ascii_uppercase()) {
9526        if let Some(v) = crate::dash_mode::bash_special_array(name) {
9527            return Some(v);
9528        }
9529    }
9530    if let Some(Some(v)) = crate::fusevm_bridge::try_with_executor(|exec| exec.array(name)) {
9531        return Some(v);
9532    }
9533    crate::ported::params::getaparam(name)
9534}
9535
9536/// !!! WARNING: RUST-ONLY HELPER !!!
9537///
9538/// No function of this name exists in `Src/exec.c`. This helper stands in for
9539/// reading an associative parameter through the live fusevm executor. C reads
9540/// `paramtab` directly.
9541/// Associative-array param value via the live executor (`None` when no
9542/// executor / not set).
9543pub fn assoc(name: &str) -> Option<indexmap::IndexMap<String, String>> {
9544    crate::fusevm_bridge::try_with_executor(|exec| exec.assoc(name)).flatten()
9545}
9546
9547/// !!! WARNING: RUST-ONLY HELPER !!!
9548///
9549/// No function of this name exists in `Src/exec.c`. This helper stands in for
9550/// writing an array parameter through the live fusevm executor. C calls
9551/// `setaparam` directly.
9552/// Store an array param into the live executor (no-op without one).
9553pub fn set_array(name: &str, val: Vec<String>) {
9554    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_array(name.to_string(), val));
9555}
9556
9557/// !!! WARNING: RUST-ONLY HELPER !!!
9558///
9559/// No function of this name exists in `Src/exec.c`. This helper stands in for
9560/// writing an associative parameter through the live fusevm executor. C calls
9561/// `sethparam` directly.
9562/// Store an associative-array param into the live executor (no-op
9563/// without one).
9564pub fn set_assoc(name: &str, val: indexmap::IndexMap<String, String>) {
9565    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_assoc(name.to_string(), val));
9566}
9567
9568/// !!! WARNING: RUST-ONLY HELPER !!!
9569///
9570/// No function of this name exists in `Src/exec.c`. This helper stands in for
9571/// unsetting a scalar parameter through the live fusevm executor. C calls
9572/// `unsetparam` directly.
9573/// Unset a scalar param in the live executor (no-op without one).
9574pub fn unset_scalar(name: &str) {
9575    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_scalar(name));
9576}
9577
9578/// !!! WARNING: RUST-ONLY HELPER !!!
9579///
9580/// No function of this name exists in `Src/exec.c`. This helper stands in for
9581/// unsetting an array parameter through the live fusevm executor. C calls
9582/// `unsetparam` directly.
9583/// Unset an array param in the live executor (no-op without one).
9584pub fn unset_array(name: &str) {
9585    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_array(name));
9586}
9587
9588/// !!! WARNING: RUST-ONLY HELPER !!!
9589///
9590/// No function of this name exists in `Src/exec.c`. This helper stands in for
9591/// unsetting an associative parameter through the live fusevm executor. C
9592/// calls `unsetparam` directly.
9593/// Unset an associative-array param in the live executor (no-op
9594/// without one).
9595pub fn unset_assoc(name: &str) {
9596    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_assoc(name));
9597}
9598
9599/// !!! WARNING: RUST-ONLY HELPER !!!
9600///
9601/// No function of this name exists in `Src/exec.c`. This helper stands in for
9602/// calling a shell function by name on the live fusevm executor, with the
9603/// full `doshfunc` (c:5823) scope wrap. C reaches the function through
9604/// `execshfunc` (c:5540) off a wordcode `Shfunc`.
9605/// Dispatch a shell-function call by name through the live executor
9606/// (full doshfunc scope wrap). `None` when no executor / not a
9607/// function.
9608pub fn dispatch_function_call(name: &str, args: &[String]) -> Option<i32> {
9609    // c:3490-3492 — `if (type != WC_FUNCDEF) setunderscore((args &&
9610    // nonempty(args)) ? getdata(lastnode(args)) : "")`. C's `args` list
9611    // carries the command word, so a bare `_pre` leaves `$_` == "_pre".
9612    // The compsys ports call shell functions through this shim rather
9613    // than through a parsed command, so those calls never reached
9614    // execcmd's write and the callee saw whatever `$_` the caller left:
9615    // zsh reports `_post` inside comppostfuncs where zshrs reported the
9616    // stale value.
9617    {
9618        let last = args.last().cloned().unwrap_or_else(|| name.to_string());
9619        crate::ported::params::set_zunderscore(std::slice::from_ref(&last)); // c:3491
9620    }
9621    let __ft = crate::ftime::start(name);
9622    if let Some(r) =
9623        crate::fusevm_bridge::try_with_executor(|exec| exec.dispatch_function_call(name, args))
9624    {
9625        crate::ftime::stop(__ft);
9626        return r;
9627    }
9628    // No active VM context: this is the loop()/zsh_main exit path where
9629    // `zexit` fires a `TRAPEXIT() { ... }` (via dotrap, Src/builtin.c:6043)
9630    // or the `zshexit` hook after `loop()` returned. Enter the installed
9631    // session executor so the handler runs in the shell. SAFETY per execode.
9632    SESSION_EXECUTOR.with(|c| match c.get() {
9633        Some(ptr) => {
9634            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9635            unsafe { (*ptr).dispatch_function_call(name, args) }
9636        }
9637        None => None,
9638    })
9639}
9640
9641/// !!! WARNING: RUST-ONLY HELPER !!!
9642///
9643/// No function of this name exists in `Src/exec.c`. This helper stands in for
9644/// running a function BODY on the live fusevm executor with no scope wrap,
9645/// for callers that already entered `doshfunc` (c:5823).
9646/// Body-only function dispatch (no doshfunc scope wrap) — call as the
9647/// `body_runner` of a direct `doshfunc(...)` invocation to avoid the
9648/// double-wrap of going back through [`dispatch_function_call`]. `None`
9649/// when no executor.
9650pub fn run_function_body(name: &str, args: &[String]) -> Option<i32> {
9651    if let Some(r) =
9652        crate::fusevm_bridge::try_with_executor(|exec| exec.run_function_body_only(name, args))
9653    {
9654        return r;
9655    }
9656    // Session-executor fallback for the no-VM-context exit path (e.g. the
9657    // `zshexit` hook fired by `zexit` from zsh_main). SAFETY per execode.
9658    SESSION_EXECUTOR.with(|c| match c.get() {
9659        Some(ptr) => {
9660            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9661            unsafe { (*ptr).run_function_body_only(name, args) }
9662        }
9663        None => None,
9664    })
9665}
9666
9667/// !!! WARNING: RUST-ONLY HELPER !!!
9668///
9669/// No function of this name exists in `Src/exec.c`. This helper stands in for
9670/// running a source string on the live fusevm executor. The C path is
9671/// `execstring` (c:1228) -> `parse_string` (c:283) -> `execode` (c:1245).
9672/// Run a script source string on the live executor. `Ok(0)` when no
9673/// executor is in scope.
9674pub fn execute_script(src: &str) -> Result<i32, String> {
9675    // c:1228 execstring → execode: no end-of-script hooks (see
9676    // ShellExecutor::execute_string_without_exit_hooks).
9677    if let Some(r) =
9678        crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_without_exit_hooks(src))
9679    {
9680        return r;
9681    }
9682    // No active VM context: the loop()/zsh_main exit path where `zexit`
9683    // runs a `trap '...' EXIT` raw body (Src/builtin.c:6043) after `loop()`
9684    // returned. Enter the installed session executor so the trap body runs
9685    // in the shell instead of silently no-op'ing. SAFETY per execode.
9686    SESSION_EXECUTOR.with(|c| match c.get() {
9687        Some(ptr) => {
9688            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9689            unsafe { (*ptr).execute_string_without_exit_hooks(src) }
9690        }
9691        None => Ok(0),
9692    })
9693}
9694
9695/// !!! WARNING: RUST-ONLY HELPER !!!
9696///
9697/// No function of this name exists in `Src/exec.c`. This helper stands in for
9698/// running a source string through the fusevm zsh pipeline. Same C path as
9699/// above: `execstring` (c:1228).
9700/// Run a script source string through the live executor's zsh pipeline.
9701/// `Ok(0)` when no executor is in scope.
9702///
9703/// Executor ladder, identical to [`execute_script`] above and to
9704/// `fusevm_bridge::source_file_per_command`: the innermost ACTIVE executor
9705/// when one is in scope, the installed session executor otherwise, `Ok(0)`
9706/// with neither.
9707///
9708/// The session-executor rung is what makes `execstring` (c:1228) behave like
9709/// C, where the interpreter is plain process globals and the string ALWAYS
9710/// runs. Every caller that fires between commands rather than inside one
9711/// reaches this with `CURRENT_EXECUTOR` unset — `checksched`'s
9712/// `execstring(sch->cmd, 0, 0, "sched")` (c:Src/Builtins/sched.c:122), fired
9713/// from the pre-prompt hook or from the `addtimedfn` deadline in ZLE's read
9714/// loop, and `dotrap`'s re-parse of a non-function trap action
9715/// (signals.rs). Without it, `try_with_executor` returned `None`, the
9716/// `.unwrap_or(Ok(0))` reported success, and the command was dropped on the
9717/// floor: `sched +1 'print hi'` listed the entry, dequeued it on time, and
9718/// printed nothing.
9719pub fn execute_script_zsh_pipeline(src: &str) -> Result<i32, String> {
9720    if let Some(r) =
9721        crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_without_exit_hooks(src))
9722    {
9723        return r;
9724    }
9725    SESSION_EXECUTOR.with(|c| match c.get() {
9726        // SAFETY: set by install_session_executor to an executor that lives
9727        // for the whole single-threaded interactive session. Reached only
9728        // when `CURRENT_EXECUTOR` is unset, i.e. no chunk is running on this
9729        // thread (`run_chunk` holds an `ExecutorContext` for the whole of
9730        // `vm.run()`), so this cannot alias a live `&mut ShellExecutor`.
9731        Some(ptr) => {
9732            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9733            unsafe { (*ptr).execute_string_without_exit_hooks(src) }
9734        }
9735        None => Ok(0),
9736    })
9737}
9738
9739/// !!! WARNING: RUST-ONLY HELPER !!!
9740///
9741/// No function of this name exists in `Src/exec.c`. This helper stands in for
9742/// running a `$(...)` body in-process on the live fusevm executor. C forks
9743/// and calls `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` inside `getoutput`
9744/// (c:4713); the in-process form applies that subshell state with a guard
9745/// instead.
9746/// Run a `$(...)` command substitution on the live executor, returning
9747/// captured stdout. Empty string when no executor is in scope.
9748///
9749/// The body runs IN-PROCESS (no fork), so the subshell state that
9750/// `Src/exec.c:4781`'s `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` would
9751/// have installed in the forked child is applied here by `SubshStateGuard`
9752/// and unwound when the substitution returns. Every caller of this funnel —
9753/// `getoutput` above and the p10k custom-segment renderer
9754/// (`extensions/p10k/segments_core.rs:917`, which is `content="$(eval
9755/// $command)"`) — is a genuine command substitution and wants it.
9756pub fn run_command_substitution(cmd: &str) -> String {
9757    // c:4781 — `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL);` in the child
9758    // arm of getoutput. Fork-safe subset only; see SubshStateGuard for the
9759    // applied/skipped breakdown.
9760    let _subsh_state = SubshStateGuard::enter();
9761    if let Some(r) =
9762        crate::fusevm_bridge::try_with_executor(|exec| exec.run_command_substitution(cmd))
9763    {
9764        return r;
9765    }
9766    // Session-executor fallback for no-VM-context callers — the native
9767    // p10k custom_* segments (p10k:1698 `content="$(eval $command)"`)
9768    // render at preprompt time, before any ExecutorContext is entered,
9769    // so try_with_executor alone returned "" and every custom segment
9770    // rendered empty. Same pattern as run_function_body above; SAFETY
9771    // per execode.
9772    SESSION_EXECUTOR.with(|c| match c.get() {
9773        Some(ptr) => {
9774            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9775            unsafe { (*ptr).run_command_substitution(cmd) }
9776        }
9777        None => String::new(),
9778    })
9779}
9780
9781/// !!! WARNING: RUST-ONLY HELPER !!!
9782///
9783/// No function of this name exists in `Src/exec.c`. This helper stands in for
9784/// reading the positional parameters off the live fusevm executor. C reads
9785/// the `pparams` global directly (Src/params.c).
9786/// Positional parameters ($1..$N) from the live executor; empty without
9787/// one.
9788pub fn pparams() -> Vec<String> {
9789    crate::fusevm_bridge::try_with_executor(|exec| exec.pparams()).unwrap_or_default()
9790}
9791
9792/// !!! WARNING: RUST-ONLY HELPER !!!
9793///
9794/// No function of this name exists in `Src/exec.c`. This helper stands in for
9795/// replacing the positional parameters on the live fusevm executor. C assigns
9796/// the `pparams` global directly (Src/params.c).
9797/// Replace the positional parameters in the live executor (no-op
9798/// without one).
9799pub fn set_pparams(v: Vec<String>) {
9800    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_pparams(v));
9801}
9802
9803/// !!! WARNING: RUST-ONLY HELPER !!!
9804///
9805/// No function of this name exists in `Src/exec.c`. This helper stands in for
9806/// dropping a function from the fusevm executor's compiled-chunk and source
9807/// maps. C removes the node from `shfunctab` directly.
9808/// Drop a function from both the compiled-chunk and source maps in the
9809/// live executor. Returns true if either entry existed; false when no
9810/// executor.
9811pub fn unregister_function(name: &str) -> bool {
9812    crate::fusevm_bridge::try_with_executor(|exec| {
9813        let a = exec.functions_compiled.remove(name).is_some();
9814        let b = exec.function_source.remove(name).is_some();
9815        a || b
9816    })
9817    .unwrap_or(false)
9818}
9819
9820/// !!! WARNING: RUST-ONLY HELPER !!!
9821///
9822/// No function of this name exists in `Src/exec.c`. This helper stands in for
9823/// reading the saved outer stdout fd of an in-progress `$(...)` capture. C
9824/// has no such stack: the capture happens in a forked child (`getoutput`,
9825/// c:4713) so the parent fd is never rebound.
9826/// Saved outer stdout fd for an in-progress `$(...)` capture (top of
9827/// the bridge's CMDSUBST_OUTER_FDS stack), or `None` when not inside a
9828/// cmdsub. Used by the trap dispatcher to route a trap body's stdout to
9829/// the parent terminal instead of the cmdsub-bound pipe (Bug #56).
9830pub fn cmdsubst_outer_stdout() -> Option<i32> {
9831    crate::fusevm_bridge::cmdsubst_outer_stdout()
9832}
9833
9834/// Port of `execautofn_basic()` from `Src/exec.c:5608` — C decl `execautofn_basic(Estate state, UNUSED(int do_exec))`.
9835/// Run a pre-loaded autoload function body
9836/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
9837/// the call so `%N` / `%x` reflect the autoload target during
9838/// execution.
9839pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
9840    // c:5608
9841    // c:5613 — `shf = state->prog->shf;`
9842    let shf = match state.prog.shf.as_deref() {
9843        Some(s) => s.clone(),
9844        None => return LASTVAL.load(Ordering::Relaxed),
9845    };
9846
9847    // c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
9848    // top-of-stack tracking is in modules::parameter::FUNCSTACK.
9849    {
9850        let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
9851        if let Some(top) = stk.last_mut() {
9852            if top.filename.is_none() {
9853                // c:5620 — `funcstack->filename = getshfuncfile(shf);`
9854                top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
9855            }
9856        }
9857    }
9858
9859    // c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
9860    let oldscriptname = crate::ported::utils::scriptname_get();
9861    let oldscriptfilename = crate::ported::utils::scriptfilename_get();
9862    // c:5624 — `scriptname = dupstring(shf->node.nam);`
9863    crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
9864    // c:5625 — `scriptfilename = getshfuncfile(shf);`
9865    crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
9866        &shf.node.nam,
9867    ));
9868    // c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
9869    if let Some(funcdef) = shf.funcdef.clone() {
9870        execode_wordcode(funcdef, 1, 0, "loadautofunc");
9871    }
9872    // c:5627-5628 — restore.
9873    crate::ported::utils::set_scriptname(oldscriptname);
9874    crate::ported::utils::set_scriptfilename(oldscriptfilename);
9875
9876    LASTVAL.load(Ordering::Relaxed) // c:5630
9877}
9878
9879/// Port of `execautofn()` from `Src/exec.c:5635` — C decl `execautofn(Estate state, UNUSED(int do_exec))`.
9880/// The autoload-aware dispatch entry
9881/// for `WC_AUTOFN`: fault the function body in via `loadautofn`,
9882/// then hand off to `execautofn_basic` to actually run it.
9883///
9884/// C body:
9885/// ```c
9886/// static int
9887/// execautofn(Estate state, UNUSED(int do_exec))
9888/// {
9889///     Shfunc shf;
9890///     if (!(shf = loadautofn(state->prog->shf, 1, 0, 0)))
9891///         return 1;
9892///     state->prog->shf = shf;
9893///     return execautofn_basic(state, 0);
9894/// }
9895/// ```
9896///
9897/// Rust port: `loadautofn` mutates the `shfunc` in place via a raw
9898/// pointer and returns 0/1 (success/failure), so the explicit
9899/// `state->prog->shf = shf` assignment in C is implicit here.
9900pub fn execautofn(state: &mut estate, _do_exec: i32) -> i32 {
9901    // c:5638-5640 — `if (!(shf = loadautofn(state->prog->shf, 1, 0, 0))) return 1;`
9902    let shf_ptr: *mut shfunc = match state.prog.shf.as_mut() {
9903        Some(b) => &mut **b as *mut shfunc,
9904        None => return 1,
9905    };
9906    if loadautofn(shf_ptr, 1, 0, 0) != 0 {
9907        return 1;
9908    }
9909    // c:5643 — `return execautofn_basic(state, 0);`
9910    execautofn_basic(state, 0)
9911}
9912
9913/// Port of `execpline2()` from `Src/exec.c:1991` — C decl `execpline2(Estate state, wordcode pcode, int how, int input, int output, int last1)`.
9914/// Recursive
9915/// multi-stage pipe walker: at each step, analyse the current
9916/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
9917/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
9918/// its input fd.
9919pub fn execpline2(
9920    state: &mut estate,
9921    pcode: wordcode,
9922    how: i32,
9923    input: i32,
9924    output: i32,
9925    last1: i32,
9926) {
9927    use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
9928    use crate::ported::zsh_h::{
9929        execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
9930        WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
9931    };
9932    // c:1991
9933    let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
9934
9935    // c:1996-1997 — `if (breaks || retflag) return;`
9936    if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
9937        return;
9938    }
9939
9940    // c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
9941    //                  lineno = WC_PIPE_LINENO(pcode) - 1;`
9942    if !crate::ported::zsh_h::IN_EVAL_TRAP()
9943        && INEVAL.load(Ordering::SeqCst) == 0
9944        && ZWC_PIPE_LINENO(pcode) != 0
9945    {
9946        let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
9947        crate::ported::input::lineno.with(|l| l.set(new_lineno));
9948    }
9949
9950    // c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
9951    if pline_level.load(Ordering::Relaxed) == 1 {
9952        // c:2003
9953        if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
9954            // c:2004 — `(how & Z_ASYNC) || !sfcontext`
9955            // c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
9956            //   state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
9957            let pc_for_text = state.pc
9958                + if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
9959                    0
9960                } else {
9961                    1
9962                };
9963            let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
9964            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
9965                *lpt = text;
9966            }
9967        } else {
9968            // c:2010 — `list_pipe_text[0] = '\0';`
9969            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
9970                lpt.clear();
9971            }
9972        }
9973    }
9974
9975    if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
9976        // c:2012-2014 — terminal stage: analyse + exec directly.
9977        execcmd_analyse(state, &mut eparams); // c:2013
9978        execcmd_exec(
9979            state,
9980            &mut eparams,
9981            input,
9982            output,
9983            how,
9984            if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
9985            -1,                             // c:2014 close_if_forked = -1
9986        );
9987    } else {
9988        // c:2015-2039 — non-terminal stage: pipe + fork + recurse.
9989        let mut pipes: [i32; 2] = [-1, -1]; // c:2016
9990        let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
9991                                                               // c:2018 — `Wordcode next = state->pc + (*state->pc);`
9992        let next = if state.pc < state.prog.prog.len() {
9993            state.pc + state.prog.prog[state.pc] as usize
9994        } else {
9995            state.pc
9996        };
9997        // c:2020 — `++state->pc;`
9998        if state.pc < state.prog.prog.len() {
9999            state.pc += 1;
10000        }
10001        execcmd_analyse(state, &mut eparams); // c:2021
10002
10003        if mpipe(&mut pipes) < 0 {
10004            // c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
10005        }
10006
10007        // c:2027 — `addfilelist(NULL, pipes[0]);`
10008        // C uses the current thisjob's filelist; Rust port wires through JOBTAB.
10009        if let Some(jt) = JOBTAB.get() {
10010            let mut guard = jt.lock().unwrap();
10011            let tj = {
10012                if let Some(m) = THISJOB.get() {
10013                    *m.lock().unwrap()
10014                } else {
10015                    -1
10016                }
10017            };
10018            if tj >= 0 {
10019                if let Some(j) = guard.get_mut(tj as usize) {
10020                    crate::ported::jobs::addfilelist(j, None, pipes[0]);
10021                }
10022            }
10023        }
10024
10025        // c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
10026        execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
10027        let _ = zclose(pipes[1]); // c:2029
10028        state.pc = next; // c:2030
10029
10030        // c:2034 — `cmdpush(CS_PIPE);`
10031        cmdpush(CS_PIPE as u8);
10032        // c:2035 — `list_pipe = 1;`
10033        list_pipe.store(1, Ordering::Relaxed);
10034        // c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
10035        let next_pcode = if state.pc < state.prog.prog.len() {
10036            state.prog.prog[state.pc]
10037        } else {
10038            0
10039        };
10040        if state.pc < state.prog.prog.len() {
10041            state.pc += 1;
10042        }
10043        execpline2(state, next_pcode, how, pipes[0], output, last1);
10044        // c:2037 — `list_pipe = old_list_pipe;`
10045        list_pipe.store(old_list_pipe, Ordering::Relaxed);
10046        // c:2038 — `cmdpop();`
10047        cmdpop();
10048    }
10049}
10050
10051/// Port of `execpline()` from `Src/exec.c:1668` — C decl `execpline(Estate state, wordcode slcode, int how, int last1)`.
10052/// Full faithful port: allocates a job-table
10053/// entry via `initjob`, sets up coproc mpipes, drives the whole
10054/// (multi-stage) pipeline through `execpline2` (which performs the real
10055/// per-stage mpipe/fork/exec), then either spawns the job asynchronously
10056/// (`Z_ASYNC` -> `spawnjob`/`deletejob`) or waits synchronously
10057/// (`waitjobs`), including the `list_pipe` stop/continue fork machinery
10058/// (SUBJOB/SUPERJOB linkage) that re-forks the shell to keep an
10059/// interactively-suspended right-hand pipeline stage running.
10060///
10061/// Divergences from C, each forced by the Rust substrate and cited
10062/// inline at the point of use:
10063///   * `initjob` here grows the `Vec`-backed jobtab on demand and does
10064///     not return -1, so C's per-pipeline table-full bailout (c:1756-1760)
10065///     is inert on THIS path. That bailout is zsh's universal recursion
10066///     backstop (`initjob` caps the table at `MAX_MAXJOBS` → `zerr("job
10067///     table full or recursion limit exceeded")`), which bounds recursion
10068///     through paths FUNCNEST does not count (sourced files, `eval` — the
10069///     doshfunc funcnest check at c:5684 counts FS_FUNC frames only). The
10070///     fusevm runtime that actually executes pipelines does not allocate a
10071///     job per pipeline, so that backstop was missing entirely — runaway
10072///     `source`/`eval` recursion overflowed the (large but finite)
10073///     main-thread stack → SIGBUS. It is reinstated inline (same ceiling,
10074///     total FUNCSTACK depth as the proxy for held job slots) at the
10075///     FS_SOURCE re-entry (init.rs::source) and FS_EVAL re-entry
10076///     (builtin.rs eval).
10077///   * `errbrk_saved` / `prev_errflag` / `prev_breaks` (jobs.c:128
10078///     globals) are only *read* here; their setter lives in the
10079///     not-yet-ported jobs.c reaping path, so they stay 0 and the
10080///     `if (errbrk_saved)` restore (c:1998-2003) is a faithful no-op.
10081pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
10082    use crate::ported::builtin::{BREAKS, LOOPS, RETFLAG};
10083    use crate::ported::init::zleentry;
10084    use crate::ported::jobs::stat as jst;
10085    use crate::ported::jobs::{
10086        addproc, clearoldjobtab, deletejob, hasprocs, initjob, makerunning, pipecleanfilelist,
10087        printjob, spawnjob, waitjobs, CURJOB, LASTVAL2, PREVJOB,
10088    };
10089    use crate::ported::modules::clone::{coprocin, coprocout};
10090    use crate::ported::signals::killjb;
10091    use crate::ported::signals_h::{
10092        queue_signal_level, queue_signals, restore_queue_signals, unqueue_signals,
10093    };
10094    use crate::ported::utils::read_loop;
10095    use crate::ported::zsh_h::{
10096        jobbing, INTERACTIVE, LONGLISTJOBS, STAT_SUBJOB_ORPHANED, WC_PIPE, WC_PIPE_END,
10097        WC_PIPE_TYPE, WC_SUBLIST_COPROC, WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, ZLE_CMD_TRASH, Z_ASYNC,
10098        Z_DISOWN, Z_TIMED,
10099    };
10100
10101    // c:1731 — `static int lastwj, lpforked;` (persist across calls).
10102    static LASTWJ: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10103    static LPFORKED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10104    // c:465 (exec.c) — `static struct timespec list_pipe_start;`. Used as
10105    // the addproc bgtime for the re-forked super-job leader (c:1841).
10106    static LIST_PIPE_START: std::sync::Mutex<Option<std::time::Instant>> =
10107        std::sync::Mutex::new(None);
10108    // c:128 (jobs.c) — `int prev_errflag, prev_breaks, errbrk_saved;`. The
10109    // setter is in the not-yet-ported reaping path, so these stay 0 here.
10110    static ERRBRK_SAVED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10111    static PREV_ERRFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10112    static PREV_BREAKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10113
10114    let jt = JOBTAB.get_or_init(|| std::sync::Mutex::new(Vec::new()));
10115    // Read/write the shared thisjob slot.
10116    let thisjob_set = |v: i32| {
10117        if let Some(m) = THISJOB.get() {
10118            *m.lock().unwrap() = v;
10119        }
10120    };
10121    let thisjob_get = || THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10122
10123    let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:1728
10124    let mut slflags = WC_SUBLIST_FLAGS(slcode); // c:1729
10125    let code = state.prog.prog[state.pc]; // c:1730 `wordcode code = *state->pc++;`
10126    state.pc += 1;
10127
10128    // c:1733-1736 — non-pipe, non-timed sublist short-circuits with the
10129    // negated-empty status.
10130    if wc_code(code) != WC_PIPE && (how & Z_TIMED) == 0 {
10131        let r = i32::from((slflags & WC_SUBLIST_NOT) != 0);
10132        LASTVAL.store(r, Ordering::Relaxed);
10133        return r;
10134    }
10135    let mut last1 = last1;
10136    if (slflags & WC_SUBLIST_NOT) != 0 {
10137        last1 = 0; // c:1736
10138    }
10139    let mut how = how;
10140
10141    queue_signals(); // c:1744
10142
10143    let pj = thisjob_get(); // c:1746 — `pj = thisjob;`
10144    let mut ipipe: [i32; 2] = [0, 0]; // c:1747
10145    let mut opipe: [i32; 2] = [0, 0];
10146    child_block(); // c:1748
10147
10148    // c:1755 — `thisjob = newjob = initjob();` (Rust jobtab grows on
10149    // demand, so initjob never fails; the -1 bailout is unreachable).
10150    let newjob = {
10151        let mut g = jt.lock().unwrap();
10152        initjob(&mut g)
10153    };
10154    thisjob_set(newjob as i32);
10155    if (how & Z_TIMED) != 0 {
10156        // c:1760-1761
10157        let mut g = jt.lock().unwrap();
10158        g[newjob].stat |= jst::TIMED;
10159    }
10160
10161    if (slflags & WC_SUBLIST_COPROC) != 0 {
10162        // c:1763-1782
10163        how = Z_ASYNC; // c:1764
10164        if coprocin.load(Ordering::Relaxed) >= 0 {
10165            zclose(coprocin.load(Ordering::Relaxed)); // c:1766
10166            zclose(coprocout.load(Ordering::Relaxed)); // c:1767
10167        }
10168        if mpipe(&mut ipipe) < 0 {
10169            // c:1769-1771
10170            coprocin.store(-1, Ordering::Relaxed);
10171            coprocout.store(-1, Ordering::Relaxed);
10172            slflags &= !WC_SUBLIST_COPROC;
10173        } else if mpipe(&mut opipe) < 0 {
10174            // c:1772-1776
10175            unsafe {
10176                libc::close(ipipe[0]);
10177                libc::close(ipipe[1]);
10178            }
10179            coprocin.store(-1, Ordering::Relaxed);
10180            coprocout.store(-1, Ordering::Relaxed);
10181            slflags &= !WC_SUBLIST_COPROC;
10182        } else {
10183            // c:1777-1781
10184            coprocin.store(ipipe[0], Ordering::Relaxed);
10185            coprocout.store(opipe[1], Ordering::Relaxed);
10186            fdtable_set(ipipe[0], FDT_UNUSED);
10187            fdtable_set(opipe[1], FDT_UNUSED);
10188        }
10189    }
10190
10191    // c:1788-1793 — `if (!pline_level++) { ... }`.
10192    let prev_pline = pline_level.fetch_add(1, Ordering::Relaxed);
10193    if prev_pline == 0 {
10194        list_pipe_pid.store(0, Ordering::Relaxed); // c:1789
10195        nowait.store(0, Ordering::Relaxed); // c:1790
10196        simple_pline.store(
10197            i32::from(WC_PIPE_TYPE(code) == WC_PIPE_END),
10198            Ordering::Relaxed,
10199        ); // c:1791
10200        list_pipe_job.store(newjob as i32, Ordering::Relaxed); // c:1792
10201    }
10202    LASTWJ.store(0, Ordering::Relaxed); // c:1794
10203    LPFORKED.store(0, Ordering::Relaxed);
10204    execpline2(state, code, how, opipe[0], ipipe[1], last1); // c:1795
10205    pline_level.fetch_sub(1, Ordering::Relaxed); // c:1796
10206
10207    if (how & Z_ASYNC) != 0 {
10208        // c:1797-1818
10209        clearoldjobtab(); // c:1798
10210        LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1799
10211
10212        if thisjob_get() == list_pipe_job.load(Ordering::Relaxed) {
10213            list_pipe_job.store(0, Ordering::Relaxed); // c:1801-1802
10214        }
10215        {
10216            let mut g = jt.lock().unwrap();
10217            let tj = thisjob_get();
10218            if tj >= 0 {
10219                g[tj as usize].stat |= jst::NOSTTY; // c:1803
10220            }
10221        }
10222        if (slflags & WC_SUBLIST_COPROC) != 0 {
10223            zclose(ipipe[1]); // c:1805
10224            zclose(opipe[0]); // c:1806
10225        }
10226        if (how & Z_DISOWN) != 0 {
10227            // c:1808-1812
10228            let tj = thisjob_get();
10229            if tj >= 0 {
10230                let mut g = jt.lock().unwrap();
10231                pipecleanfilelist(&mut g[tj as usize], false); // c:1809
10232                deletejob(&mut g[tj as usize], true); // c:1810
10233            }
10234            thisjob_set(-1); // c:1811
10235        } else {
10236            spawnjob(); // c:1814 (locks JOBTAB internally — no guard held)
10237        }
10238        child_unblock(); // c:1815
10239        unqueue_signals(); // c:1816
10240        LASTVAL.store(0, Ordering::Relaxed); // c:1818 `return lastval = 0;`
10241        return 0;
10242    }
10243
10244    // c:1819-2033 — synchronous branch.
10245    if newjob as i32 != LASTWJ.load(Ordering::Relaxed) {
10246        // c:1820
10247        let mut jn_idx = newjob; // `Job jn = jobtab + newjob;`
10248
10249        // c:1824-1825 — a list_pipe sub-shell child exits here.
10250        if newjob as i32 == list_pipe_job.load(Ordering::Relaxed)
10251            && list_pipe_child.load(Ordering::Relaxed) != 0
10252        {
10253            unsafe { libc::_exit(0) };
10254        }
10255
10256        LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1826
10257        thisjob_set(newjob as i32);
10258
10259        // c:1828-1830 — suppress the job announcement in nested pipes.
10260        {
10261            let mut g = jt.lock().unwrap();
10262            let noprint = list_pipe.load(Ordering::Relaxed) != 0
10263                || (pline_level.load(Ordering::Relaxed) != 0
10264                    && (how & Z_TIMED) == 0
10265                    && (g[jn_idx].stat & jst::NOSTTY) == 0);
10266            if noprint {
10267                g[jn_idx].stat |= jst::NOPRINT;
10268            }
10269        }
10270
10271        if nowait.load(Ordering::Relaxed) != 0 {
10272            // c:1832-1882
10273            if pline_level.load(Ordering::Relaxed) == 0 {
10274                // c:1833-1875
10275                *CURJOB
10276                    .get_or_init(|| std::sync::Mutex::new(-1))
10277                    .lock()
10278                    .unwrap() = newjob as i32; // c:1836
10279                                               // c:1838 — DPUTS(!list_pipe_pid, "invalid list_pipe_pid").
10280                                               // c:1840-1841 — record the re-forked leader in the super-job.
10281                {
10282                    let txt = LIST_PIPE_TEXT.lock().map(|s| s.clone()).unwrap_or_default();
10283                    let bgt = *LIST_PIPE_START.lock().unwrap();
10284                    let mut g = jt.lock().unwrap();
10285                    addproc(
10286                        &mut g[jn_idx],
10287                        list_pipe_pid.load(Ordering::Relaxed),
10288                        &txt,
10289                        false,
10290                        bgt,
10291                        -1,
10292                        -1,
10293                    );
10294                }
10295                {
10296                    let mut g = jt.lock().unwrap();
10297                    // c:1845 — `if (!jn->procs->next || lpforked == 2)`.
10298                    if g[jn_idx].procs.len() <= 1 || LPFORKED.load(Ordering::Relaxed) == 2 {
10299                        g[jn_idx].gleader = list_pipe_pid.load(Ordering::Relaxed); // c:1845
10300                        g[jn_idx].stat |= jst::SUBLEADER; // c:1847
10301                                                          // c:1852-1861 — adopt any orphaned subjob; we
10302                                                          // become its super-job.
10303                        for jobsub in 1..g.len() {
10304                            if (g[jobsub].stat & STAT_SUBJOB_ORPHANED) != 0 {
10305                                g[jn_idx].other = jobsub as i32; // c:1855
10306                                g[jn_idx].stat |= jst::SUPERJOB; // c:1856
10307                                g[jobsub].stat &= !STAT_SUBJOB_ORPHANED; // c:1857
10308                                g[jobsub].other = list_pipe_pid.load(Ordering::Relaxed);
10309                                // c:1858
10310                            }
10311                        }
10312                    }
10313                    // c:1863-1869 — copy a stopped proc status from the
10314                    // subjob onto our last proc.
10315                    let other = g[jn_idx].other as usize;
10316                    let stopped = if other < g.len() {
10317                        g[other]
10318                            .procs
10319                            .iter()
10320                            .find(|p| p.is_stopped())
10321                            .map(|p| p.status)
10322                    } else {
10323                        None
10324                    };
10325                    if let Some(st) = stopped {
10326                        if let Some(last) = g[jn_idx].procs.last_mut() {
10327                            last.status = st;
10328                        }
10329                    }
10330                    // c:1870-1872
10331                    g[jn_idx].stat &= !(jst::DONE | jst::NOPRINT);
10332                    g[jn_idx].stat |= jst::STOPPED | jst::CHANGED | jst::LOCKED | jst::INUSE;
10333                }
10334                // c:1875 — printjob(jn, !!isset(LONGLISTJOBS), 1).
10335                {
10336                    let g = jt.lock().unwrap();
10337                    let cur = *CURJOB
10338                        .get_or_init(|| std::sync::Mutex::new(-1))
10339                        .lock()
10340                        .unwrap();
10341                    let prev = *PREVJOB
10342                        .get_or_init(|| std::sync::Mutex::new(-1))
10343                        .lock()
10344                        .unwrap();
10345                    let s = printjob(
10346                        &g[jn_idx],
10347                        jn_idx,
10348                        i32::from(isset(LONGLISTJOBS)),
10349                        if cur >= 0 { Some(cur as usize) } else { None },
10350                        if prev >= 0 { Some(prev as usize) } else { None },
10351                        false,
10352                    );
10353                    if !s.is_empty() {
10354                        eprintln!("{}", s);
10355                    }
10356                }
10357            } else if newjob as i32 != list_pipe_job.load(Ordering::Relaxed) {
10358                let mut g = jt.lock().unwrap();
10359                deletejob(&mut g[jn_idx], false); // c:1878
10360            } else {
10361                LASTWJ.store(-1, Ordering::Relaxed); // c:1879
10362            }
10363        }
10364
10365        ERRBRK_SAVED.store(0, Ordering::Relaxed); // c:1883
10366                                                  // c:1884-2015 — `for (; !nowait;)` wait / continue-fork loop.
10367        loop {
10368            if nowait.load(Ordering::Relaxed) != 0 {
10369                break;
10370            }
10371            if list_pipe_child.load(Ordering::Relaxed) != 0 {
10372                // c:1886-1887
10373                let mut g = jt.lock().unwrap();
10374                g[jn_idx].stat |= jst::NOPRINT;
10375                makerunning(&mut g, jn_idx);
10376            }
10377            // c:1889-1894 — wait unless the job is LOCKED.
10378            let locked = {
10379                let g = jt.lock().unwrap();
10380                (g[jn_idx].stat & jst::LOCKED) != 0
10381            };
10382            let updated;
10383            if !locked {
10384                let tj = thisjob_get();
10385                {
10386                    let mut g = jt.lock().unwrap();
10387                    updated = hasprocs(&g, tj as usize); // c:1890
10388                    waitjobs(&mut g, tj as usize); // c:1891
10389                }
10390                child_block(); // c:1892
10391            } else {
10392                updated = false; // c:1894
10393            }
10394            // c:1895-1902 — nudge the signal queue when the LHS job is
10395            // still running but we saw no update.
10396            let lpj = list_pipe_job.load(Ordering::Relaxed);
10397            let nudge = !updated && lpj != 0 && {
10398                let g = jt.lock().unwrap();
10399                (lpj as usize) < g.len()
10400                    && hasprocs(&g, lpj as usize)
10401                    && (g[lpj as usize].stat & jst::STOPPED) == 0
10402            };
10403            if nudge {
10404                let q = queue_signal_level();
10405                child_unblock();
10406                child_block();
10407                dont_queue_signals();
10408                restore_queue_signals(q);
10409            }
10410            // c:1903-1907 — forward a fatal signal from a done child.
10411            let jn_done = {
10412                let g = jt.lock().unwrap();
10413                (g[jn_idx].stat & jst::DONE) != 0
10414            };
10415            if list_pipe_child.load(Ordering::Relaxed) != 0
10416                && jn_done
10417                && (LASTVAL2.load(Ordering::Relaxed) & 0o200) != 0
10418            {
10419                unsafe {
10420                    libc::killpg(
10421                        mypgrp.load(Ordering::Relaxed),
10422                        LASTVAL2.load(Ordering::Relaxed) & !0o200,
10423                    );
10424                }
10425            }
10426            // c:1908-1921 — a pipeline with the shell running the RHS was
10427            // stopped; fork to let it continue.
10428            let stop_fork = list_pipe_child.load(Ordering::Relaxed) == 0
10429                && LPFORKED.load(Ordering::Relaxed) == 0
10430                && subsh.load(Ordering::Relaxed) == 0
10431                && jobbing()
10432                && (list_pipe.load(Ordering::Relaxed) != 0
10433                    || last1 != 0
10434                    || pline_level.load(Ordering::Relaxed) != 0)
10435                && {
10436                    let g = jt.lock().unwrap();
10437                    (g[jn_idx].stat & jst::STOPPED) != 0
10438                        || (lpj != 0
10439                            && pline_level.load(Ordering::Relaxed) != 0
10440                            && (lpj as usize) < g.len()
10441                            && (g[lpj as usize].stat & jst::STOPPED) != 0)
10442                };
10443            if stop_fork {
10444                let mut synch: [i32; 2] = [0, 0]; // c:1913
10445                let mut bgtime = ZshTimespec {
10446                    tv_sec: 0,
10447                    tv_nsec: 0,
10448                };
10449                let mut pid: libc::pid_t = 0;
10450                let pipe_failed = unsafe { libc::pipe(synch.as_mut_ptr()) } < 0; // c:1922
10451                if !pipe_failed {
10452                    pid = zfork(Some(&mut bgtime)); // c:1922
10453                }
10454                if pipe_failed || pid == -1 {
10455                    // c:1923-1935 — failure: can't suspend, resume the job.
10456                    if pid < 0 {
10457                        unsafe {
10458                            libc::close(synch[0]);
10459                            libc::close(synch[1]);
10460                        }
10461                    } else {
10462                        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
10463                        // c:1926
10464                    }
10465                    let _ = zleentry(ZLE_CMD_TRASH); // c:1929
10466                    eprintln!("zsh: job can't be suspended"); // c:1930
10467                    {
10468                        let mut g = jt.lock().unwrap();
10469                        makerunning(&mut g, jn_idx); // c:1932
10470                    }
10471                    killjb(jn_idx, libc::SIGCONT); // c:1933
10472                    thisjob_set(newjob as i32); // c:1934
10473                } else if pid != 0 {
10474                    // c:1936-1973 — parent: job control lives here.
10475                    let gl = {
10476                        let g = jt.lock().unwrap();
10477                        g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0)
10478                    };
10479                    LPFORKED.store(
10480                        if unsafe { libc::killpg(gl, 0) } == -1 {
10481                            2
10482                        } else {
10483                            1
10484                        },
10485                        Ordering::Relaxed,
10486                    ); // c:1951-1952
10487                    list_pipe_pid.store(pid, Ordering::Relaxed); // c:1953
10488                    *LIST_PIPE_START.lock().unwrap() = Some(std::time::Instant::now()); // c:1954
10489                    nowait.store(1, Ordering::Relaxed); // c:1955
10490                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:1956
10491                    BREAKS.store(LOOPS.load(Ordering::SeqCst), Ordering::SeqCst); // c:1957
10492                    unsafe { libc::close(synch[1]) }; // c:1958
10493                    let mut dummy = [0u8; 1];
10494                    let _ = read_loop(synch[0], &mut dummy); // c:1959
10495                    unsafe { libc::close(synch[0]) }; // c:1960
10496                                                      // c:1962-1970 — link super/sub jobs if we're still live.
10497                    let jn_done2 = {
10498                        let g = jt.lock().unwrap();
10499                        (g[jn_idx].stat & jst::DONE) != 0
10500                    };
10501                    if !jn_done2 {
10502                        let mut g = jt.lock().unwrap();
10503                        g[lpj as usize].other = newjob as i32; // c:1964
10504                        g[lpj as usize].stat |= jst::SUPERJOB; // c:1965
10505                        g[jn_idx].stat |= jst::SUBJOB | jst::NOPRINT; // c:1966
10506                        g[jn_idx].other = list_pipe_pid.load(Ordering::Relaxed); // c:1967
10507                        if hasprocs(&g, lpj as usize) {
10508                            g[jn_idx].gleader = g[lpj as usize].gleader; // c:1968-1969
10509                        }
10510                    }
10511                    // c:1971-1972 — stop the LHS group so the whole pipe
10512                    // suspends together.
10513                    let (do_kill, gl2) = {
10514                        let g = jt.lock().unwrap();
10515                        (
10516                            (list_pipe.load(Ordering::Relaxed) != 0 || last1 != 0)
10517                                && hasprocs(&g, lpj as usize),
10518                            g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0),
10519                        )
10520                    };
10521                    if do_kill {
10522                        unsafe { libc::killpg(gl2, libc::SIGSTOP) };
10523                    }
10524                    break; // c:1973
10525                } else {
10526                    // c:1975-2004 — child: become our own group, stop, then
10527                    // continue as the RHS sub-shell.
10528                    unsafe { libc::close(synch[0]) }; // c:1976
10529                    entersubsh(esub::ASYNC, None); // c:1977
10530                    let mypid = unsafe { libc::getpid() };
10531                    mypgrp.store(mypid, Ordering::Relaxed);
10532                    unsafe { libc::setpgid(0, mypid) }; // c:1992 setpgrp
10533                    unsafe { libc::close(synch[1]) }; // c:1993
10534                    unsafe { libc::kill(mypid, libc::SIGSTOP) }; // c:1994
10535                    list_pipe.store(0, Ordering::Relaxed); // c:1995
10536                    list_pipe_child.store(1, Ordering::Relaxed); // c:1996
10537                    dosetopt(INTERACTIVE, 0, 0); // c:1997 `opts[INTERACTIVE] = 0`
10538                    if ERRBRK_SAVED.load(Ordering::Relaxed) != 0 {
10539                        // c:1998-2003 — restore saved break/errflag state.
10540                        errflag.store(
10541                            PREV_ERRFLAG.load(Ordering::Relaxed)
10542                                | (errflag.load(Ordering::Relaxed) & ERRFLAG_INT),
10543                            Ordering::Relaxed,
10544                        );
10545                        BREAKS.store(PREV_BREAKS.load(Ordering::Relaxed), Ordering::SeqCst);
10546                    }
10547                    break;
10548                }
10549            } else if subsh.load(Ordering::Relaxed) != 0 && {
10550                let g = jt.lock().unwrap();
10551                (g[jn_idx].stat & jst::STOPPED) != 0
10552            } {
10553                // c:2008-2012
10554                if thisjob_get() == newjob as i32 {
10555                    let mut g = jt.lock().unwrap();
10556                    makerunning(&mut g, jn_idx);
10557                } else {
10558                    thisjob_set(newjob as i32);
10559                }
10560            } else {
10561                break; // c:2015
10562            }
10563        }
10564
10565        child_unblock(); // c:2017
10566        unqueue_signals(); // c:2018
10567
10568        // c:2020-2026 — a signal-killed list_pipe: drop this job and
10569        // forward the signal to the enclosing job's group.
10570        let lastval_now = LASTVAL.load(Ordering::Relaxed);
10571        let drop_and_signal =
10572            list_pipe.load(Ordering::Relaxed) != 0 && (lastval_now & 0o200) != 0 && pj >= 0 && {
10573                let g = jt.lock().unwrap();
10574                (g[jn_idx].stat & jst::INUSE) == 0 || (g[jn_idx].stat & jst::DONE) != 0
10575            };
10576        if drop_and_signal {
10577            {
10578                let mut g = jt.lock().unwrap();
10579                deletejob(&mut g[jn_idx], false); // c:2022
10580            }
10581            jn_idx = pj as usize; // c:2023 `jn = jobtab + pj;`
10582            let gl = {
10583                let g = jt.lock().unwrap();
10584                g[jn_idx].gleader
10585            };
10586            if gl != 0 {
10587                killjb(jn_idx, lastval_now & !0o200); // c:2025
10588            }
10589        }
10590        // c:2027-2030 — final cleanup deletejob for a done/child job.
10591        let final_delete = list_pipe_child.load(Ordering::Relaxed) != 0 || {
10592            let g = jt.lock().unwrap();
10593            (g[jn_idx].stat & jst::DONE) != 0
10594                && (list_pipe.load(Ordering::Relaxed) != 0
10595                    || (pline_level.load(Ordering::Relaxed) != 0
10596                        && (g[jn_idx].stat & jst::SUBJOB) == 0))
10597        };
10598        if final_delete {
10599            let mut g = jt.lock().unwrap();
10600            deletejob(&mut g[jn_idx], false); // c:2030
10601        }
10602        thisjob_set(pj); // c:2031
10603    } else {
10604        unqueue_signals(); // c:2034
10605    }
10606
10607    // c:2035-2036 — apply `!` negation to the pipeline status.
10608    if (slflags & WC_SUBLIST_NOT) != 0
10609        && errflag.load(Ordering::Relaxed) == 0
10610        && RETFLAG.load(Ordering::SeqCst) == 0
10611    {
10612        let lv = LASTVAL.load(Ordering::Relaxed);
10613        LASTVAL.store(i32::from(lv == 0), Ordering::Relaxed);
10614    }
10615
10616    if pline_level.load(Ordering::Relaxed) == 0 {
10617        simple_pline.store(old_simple_pline, Ordering::Relaxed); // c:2039
10618    }
10619    LASTVAL.load(Ordering::Relaxed) // c:2040 `return lastval;`
10620}
10621
10622// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
10623// inlined at every call site (execsimple, execpline) as the match
10624// expression that selects the right execX function. There's no
10625// separate Rust fn for it because:
10626//   - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
10627//     occupies the canonical name (handling precommand modifiers).
10628//   - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
10629//     a table lookup at exec.c:5499 — not a separate function.
10630#[cfg(any())]
10631mod _execcmd_tail_doc_anchor {
10632    // c:2901-3700 — see inlined match in execpline + execsimple above.
10633    // c:5499 — execfuncs[] table inlined as the same match.
10634}
10635
10636
10637/// Port of `execcmd_exec()` from `Src/exec.c:2901` — C decl `execcmd_exec(Estate state, Execcmd_params eparams, int input, int output, int how, int last1, int close_if_forked)`.
10638/// int input, int output, int how, int last1, int close_if_forked)`
10639/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
10640/// level of the hierarchy.
10641///
10642/// Line-by-line port of the full 1500-line C body. Sections:
10643///   c:2904-2916  — locals
10644///   c:2917-2924  — eparams field unpacking
10645///   c:2934-2939  — Z_TIMED + doneps4 reset
10646///   c:2945-2960  — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
10647///   c:2962-2986  — %job head rewrite + AUTORESUME prefix match
10648///   c:2988-3011  — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
10649///   c:3013-3283  — precommand-modifier walk (BINF_PREFIX strip)
10650///                  + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
10651///   c:3285-3307  — prefork substitutions + magic_assign
10652///   c:3309-3406  — empty-command branch (redir / nullexec / BINF_COMMAND)
10653///   c:3409-3466  — main resolution loop (shfunc / builtin / autocd)
10654///   c:3468-3479  — errflag bail-out
10655///   c:3480-3492  — text fetch + setunderscore
10656///   c:3494-3524  — rm * safety prompt
10657///   c:3526-3591  — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
10658///   c:3593-3632  — external resolution (cmdnamtab, hashcmd, AUTOCD)
10659///   c:3634-3697  — fork decision
10660///   c:3700-3955  — redir loop + multio + addfd + xpandredir
10661///   c:3957-3961  — multio close (`mfds[i].ct >= 2` → closemn)
10662///   c:3963-3995  — nullexec branch
10663///   c:3996-4327  — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
10664///                  execbuiltin / execshfunc / execute)
10665///   c:4330-4365  — `err:` label: forked-child fd cleanup, fixfds
10666///   c:4366-4403  — `done:` label: POSIX special-builtin error escalation,
10667///                  shelltime stop, newxtrerr close, AUTOCONTINUE restore
10668///
10669/// **Substrate stubs (declared inside this fn citing home C file):**
10670///   - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
10671///   - `restore_params(restorelist, removelist)` → Src/exec.c:4463
10672///   - `isreallycom(cn)` → Src/exec.c:2670
10673///   - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
10674///   - `execautofn_basic(state, do_exec)` → Src/exec.c:5608
10675///   - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
10676///
10677/// **NOT routed through fusevm.** This canonical port targets the
10678/// tree-walker dispatcher; the fusevm bytecode VM uses
10679/// `execcmd_compile_head` + `compile_simple` instead. No call
10680/// site yet — the port closes the substrate gap so future
10681/// wordcode-walker code can use it.
10682#[allow(non_snake_case)]
10683#[allow(clippy::too_many_arguments)]
10684#[allow(clippy::redundant_field_names)]
10685#[allow(unused_assignments)]
10686#[allow(unused_variables)]
10687#[allow(unused_mut)]
10688#[allow(unused_imports)]
10689#[allow(unreachable_code)]
10690#[allow(dead_code)]
10691pub fn execcmd_exec(
10692    state: &mut estate,
10693    eparams: &mut crate::ported::zsh_h::execcmd_params,
10694    input: i32,
10695    output: i32,
10696    mut how: i32,
10697    mut last1: i32,
10698    close_if_forked: i32,
10699) {
10700    use crate::ported::zsh_h::{
10701        Star, ASG_ARRAY, ASG_KEY_VALUE, AUTOCD, AUTOCONTINUE, AUTORESUME, BGNICE,
10702        BINF_ASSIGN as BINF_ASSIGN_FLAG, BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_MAGICEQUALS,
10703        BINF_NOGLOB, BINF_PREFIX, BINF_PSPECIAL, CSHNULLCMD, ERRFLAG_INT, EXECOPT, FDT_EXTERNAL,
10704        FDT_INTERNAL, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHCMDS, HFILE_USE_OPTIONS,
10705        IS_APPEND_REDIR, IS_DASH, IS_ERROR_REDIR, MAGICEQUALSUBST, NOTIFY, PM_READONLY, PM_SPECIAL,
10706        POSIXBUILTINS, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET,
10707        PRINTEXITVALUE, RCS, REDIR_CLOSE, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN,
10708        REDIR_MERGEOUT, REDIR_OUTPIPE, REDIR_READ, REDIR_READWRITE, RMSTARSILENT, SHINSTDIN,
10709        SHNULLCMD, STAT_BUILTIN, STAT_CURSH, STAT_DONE, STAT_NOPRINT, WC_ASSIGN as ZWC_ASSIGN,
10710        WC_ASSIGN_INC as ZWC_ASSIGN_INC, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
10711        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
10712        WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2, WC_AUTOFN, WC_CURSH, WC_FUNCDEF, WC_REDIR, WC_SIMPLE,
10713        WC_SUBSH, WC_TIMED, WC_TYPESET, XTRACE, Z_ASYNC, Z_DISOWN, Z_SYNC, Z_TIMED,
10714    };
10715
10716    // c:2900
10717
10718    // c:2904-2916 — locals.
10719    let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
10720    let mut filelist: Vec<jobfile> = Vec::new(); // c:2905 LinkList filelist = NULL
10721                                                 // c:2906 LinkNode node; (loop locals)
10722                                                 // c:2907 Redir fn;       (loop locals)
10723    let mut mfds: [Option<Box<multio>>; 10] =                              // c:2908 struct multio *mfds[10]
10724        [None, None, None, None, None, None, None, None, None, None];
10725    let mut text: Option<String> = None; // c:2909 char *text
10726    let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
10727    let mut fil: i32; // c:2911 int fil
10728    let mut dfil: i32 = 0; // c:2911 int dfil
10729    let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
10730    let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
10731    let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
10732    let mut i: i32; // c:2911 int i
10733    let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
10734    let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
10735    let mut forked: i32 = 0; // c:2912 int forked = 0
10736    let mut old_lastval: i32; // c:2912 int old_lastval
10737    let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
10738    let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
10739    let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
10740    let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
10741                                  // c:2914 — `Various flags to the command.`
10742    let mut cflags: u32 = 0; // c:2915 int cflags = 0
10743    let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
10744    let mut checked: i32 = 0; // c:2915 int checked = 0
10745    let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
10746                                 // c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;`. newxtrerr
10747                                 // is only made while xtrerr == stderr (c:3767), so the
10748                                 // c:4449 restore to oxtrerr is a restore to stderr.
10749    let mut newxtrerr: Option<i32> = None; // c:2916
10750
10751    // c:2917-2924 — eparams field unpacking. `args` / `redir` are
10752    // pulled into mutable locals so the body can mutate them
10753    // independently of the eparams struct.
10754    let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
10755    let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
10756    let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
10757    let typ: i32 = eparams.typ; // c:2924 int type
10758                                // c:2925-2929 — `preargs comes from expanding the head of the args
10759                                // list in order to check for prefix commands.` declared later.
10760
10761    // c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
10762    // + struct timespec then. Rust port keeps the names so the shelltime
10763    // start+stop calls map directly. Use jobs.rs's existing types.
10764    let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
10765    let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
10766    let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
10767    if (how & Z_TIMED as i32) != 0 {
10768        // c:2936
10769        crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0);
10770        // c:2937
10771    }
10772
10773    doneps4.store(0, Ordering::Relaxed); // c:2939
10774
10775    // c:2941-2947 — `If assignment but no command get the status from
10776    // variable assignment.`
10777    old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
10778    if args.is_none() && varspc.is_some() {
10779        // c:2946
10780        let ef = errflag.load(Ordering::Relaxed);
10781        LASTVAL.store(
10782            if ef != 0 {
10783                ef
10784            } else {
10785                cmdoutval.load(Ordering::Relaxed)
10786            },
10787            Ordering::Relaxed,
10788        ); // c:2947
10789    }
10790    // c:2948-2954 — `If there are arguments, we should reset the status
10791    // for the command before execution---unless we are using the result
10792    // of a command substitution...`
10793    use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
10794
10795    // c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
10796    // Already initialised above via array literals; preserved as
10797    // comment for parity. The C loop maps to a no-op in Rust.
10798
10799    // c:2962-2973 — `%job` head rewrite.
10800    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
10801        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
10802        && args.as_ref().unwrap()[0].starts_with('%')
10803    {
10804        // c:2964-2965
10805        if (how & Z_DISOWN as i32) != 0 {
10806            // c:2966
10807            oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) {
10808                1
10809            } else {
10810                0
10811            }; // c:2967
10812            opt_state_set("autocontinue", true); // c:2968
10813        }
10814        // c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
10815        let head = if (how & Z_DISOWN as i32) != 0 {
10816            "disown".to_string()
10817        } else if (how & Z_ASYNC as i32) != 0 {
10818            "bg".to_string()
10819        } else {
10820            "fg".to_string()
10821        };
10822        if let Some(ref mut v) = args {
10823            v.insert(0, head);
10824        }
10825        how = Z_SYNC as i32; // c:2972
10826    }
10827
10828    // c:2975-2986 — AUTORESUME prefix match against jobtab.
10829    if isset(AUTORESUME)
10830        && typ == WC_SIMPLE as i32
10831        && (how & Z_SYNC as i32) != 0
10832        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
10833        && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
10834        && input == 0
10835        && args.as_ref().unwrap().len() == 1
10836    {
10837        // c:2979-2981
10838        if unset(NOTIFY) {
10839            // c:2982 — `scanjobs();` via the canonical port
10840            // (Src/jobs.c:1993).
10841            if let Some(jt) = JOBTAB.get() {
10842                let mut guard = jt.lock().unwrap();
10843                crate::ported::jobs::scanjobs(&mut guard);
10844            }
10845        }
10846        // c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
10847        let head = args.as_ref().unwrap()[0].clone();
10848        let maxjob = JOBTAB
10849            .get()
10850            .map(|m| m.lock().unwrap().len() as i32)
10851            .unwrap_or(0);
10852        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10853        // c:2982 — `findjobnam(s)`. Canonical port at
10854        // jobs.rs::findjobnam matches against `proc.text`, which is
10855        // the command text actually saved into the job at fork —
10856        // matching C exactly. Returns the job index if any non-
10857        // SUBJOB jobtab entry's first-proc text starts with `s`.
10858        let found = if let Some(jt) = JOBTAB.get() {
10859            let guard = jt.lock().unwrap();
10860            crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
10861        } else {
10862            false
10863        };
10864        if found {
10865            // c:2985 — `pushnode(args, dupstring("fg"));`
10866            if let Some(ref mut v) = args {
10867                v.insert(0, "fg".to_string());
10868            }
10869        }
10870    }
10871
10872    // ====================================================================
10873    // SUBSTRATE STUBS — same-named locals citing their home C file per
10874    // [[feedback_no_shortcuts_in_porting]]. Each stub mirrors the C
10875    // signature and returns a degenerate value that keeps the body
10876    // executing while the real port lands.
10877    // ====================================================================
10878    // save_params + restore_params — top-level ports in exec.rs
10879    // (c:4410 / c:4464). Both bridged via `use` below.
10880    use crate::ported::exec::{restore_params, save_params};
10881    // isreallycom — top-level port at exec.rs (c:972). Bridges the
10882    // local shadow that this fn body used pre-port.
10883    use crate::ported::exec::isreallycom;
10884    // execautofn_basic — top-level port at exec.rs (c:5608).
10885    use crate::ported::exec::execautofn_basic;
10886    // C `execerr` macro (c:2700) was a goto-equivalent:
10887    //   errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
10888    // Rust expansion: each call site inlines the errflag+LASTVAL set
10889    // and then `break`s out of the enclosing redir loop. The loop's
10890    // post-loop errflag check at c:3949 routes to execcmd_exec_err_path
10891    // for the cleanup tail. No macro needed.
10892
10893    // c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
10894    // fork-immediately fast path.
10895    if (how & Z_ASYNC as i32) != 0
10896        || output != 0
10897        || (last1 == 2 && input != 0 && {
10898            // c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
10899            // EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
10900            // ported `emulation` static at options.rs:1044 holds the
10901            // current bit; compare against EMULATE_SH (zsh_h:2883).
10902            (crate::ported::options::emulation.load(Ordering::Relaxed)
10903                & crate::ported::zsh_h::EMULATE_SH)
10904                != 0
10905        })
10906    {
10907        // c:2988
10908        // c:2999 — `text = getjobtext(state->prog, eparams->beg);`
10909        text = Some(crate::ported::text::getjobtext(
10910            state.prog.clone(),
10911            Some(eparams.beg),
10912        ));
10913        // c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
10914        let mut filelist_for_fork = filelist.clone();
10915        let pid = execcmd_fork(
10916            state,
10917            how,
10918            typ,
10919            varspc,
10920            &mut filelist_for_fork,
10921            text.as_deref().unwrap_or(""),
10922            oautocont,
10923            close_if_forked,
10924        );
10925        match pid {
10926            -1 => {
10927                // c:3002-3003 — `goto fatal;` — fall through to fatal:
10928                // label at c:4377. We model this with a flag.
10929                redir_err = 1; // pretend redir error to trigger fatal arm
10930                               // Continue to done label by setting forked + jumping forward.
10931                               // Simplified: just bail with status 1 + fatal handling at
10932                               // the bottom of the fn.
10933                return execcmd_exec_done_path(
10934                    redir_err,
10935                    oautocont,
10936                    how,
10937                    &mut shti,
10938                    &mut chti,
10939                    &mut then_ts,
10940                    forked,
10941                    &mut newxtrerr,
10942                    cflags,
10943                    orig_cflags,
10944                    is_cursh,
10945                    do_exec,
10946                );
10947            }
10948            0 => {
10949                // c:3004 — child returned 0; continue with the body.
10950            }
10951            _ => {
10952                // c:3007 — parent: `return;` — but first restore AUTOCONTINUE
10953                // and shelltime stop. Inline the done-tail equivalent.
10954                if oautocont >= 0 {
10955                    opt_state_set("autocontinue", oautocont != 0);
10956                }
10957                if (how & Z_TIMED as i32) != 0 {
10958                    crate::ported::jobs::shelltime(
10959                        Some(&mut shti),
10960                        Some(&mut chti),
10961                        Some(&mut then_ts),
10962                        1,
10963                    );
10964                }
10965                return;
10966            }
10967        }
10968        last1 = 1; // c:3009
10969        forked = 1; // c:3009
10970    } else {
10971        // c:3010-3011
10972        text = None;
10973    }
10974
10975    // ====================================================================
10976    // c:3013-3283 — precommand-modifier walk.
10977    //
10978    // The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
10979    // BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
10980    // (above this fn). Call into it to keep DRY, then convert the
10981    // returned dispatch struct's fields into the locals C uses
10982    // (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
10983    // exec_argv0, precmd_skip).
10984    //
10985    // Per [[feedback_true_port_pattern]] the C function does this
10986    // walk inline. Reusing the existing port is acceptable because
10987    // `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
10988    // citations there match. The C tree-walker and the fusevm
10989    // compile-time walker arrive at identical dispatch decisions
10990    // from the same input.
10991    // ====================================================================
10992    let mut preargs: Vec<String> = Vec::new();
10993    let mut exec_argv0: Option<String> = None;
10994    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
10995        // c:3018
10996        let head_args: Vec<String> = args.as_ref().unwrap().clone();
10997        let dispatch = execcmd_compile_head(&head_args, typ as u32);
10998        // Pull fields into local mirror of C state.
10999        cflags = dispatch.cflags;
11000        if dispatch.is_builtin {
11001            is_builtin = 1;
11002        }
11003        if dispatch.is_shfunc {
11004            is_shfunc = 1;
11005        }
11006        if dispatch.use_defpath {
11007            use_defpath = 1;
11008        }
11009        exec_argv0 = dispatch.exec_argv0;
11010        // c:3061 — `orig_cflags |= cflags;` accumulator path; for
11011        // BINF_PREFIX walks orig_cflags tracks each step's pre-mask
11012        // bits. execcmd_compile_head doesn't surface orig_cflags
11013        // separately, so approximate as the post-strip cflags.
11014        orig_cflags = cflags;
11015        // c:3030-3086 — strip the precmd-modifier prefix from args.
11016        // In C, the walk pulls one arg at a time from `args` into
11017        // `preargs` via execcmd_getargs, then uremnodes each
11018        // BINF_PREFIX modifier. At loop exit C's `preargs` holds the
11019        // dispatch target (1 element) and `args` holds whatever's
11020        // left; `joinlists(preargs, args)` (c:3305-3306) splices the
11021        // target back onto the head. The net effect is `args` with
11022        // the precmd modifiers stripped. We compute that final shape
11023        // directly and leave `preargs` empty so the joinlists arm
11024        // below is a no-op. Without this, preargs=head_args[skip..]
11025        // plus a non-draining args was double-counting every word
11026        // when both held the same suffix.
11027        if let Some(ref mut v) = args {
11028            v.drain(0..dispatch.precmd_skip);
11029            // c:3154 — `pushnode(preargs, "command");` /* Leave everything
11030            // alone, dispatch to whence. We need to put the name back in
11031            // the list. */  `command -v`/`-V` keeps its whole word list and
11032            // dispatches to the `command` builtin (bin_whence with
11033            // BIN_COMMAND), so the name goes back on the front after the
11034            // precommand strip. `execcmd_compile_head` only REPORTS this
11035            // via `has_command_vv` (the fusevm compiler acts on it); the
11036            // tree-walker has to perform it. Without this the head after
11037            // the strip was `-v`, which resolves to nothing —
11038            // `x=command; $x -v cat` printed nothing.
11039            if dispatch.has_command_vv {
11040                v.insert(0, "command".to_string()); // c:3154
11041            } else if dispatch.use_defpath {
11042                // c:3165 — `if (pnode) uremnode(preargs, pnode);` /* We
11043                // don't need this node as we're not treating "command" as a
11044                // builtin this time. */  The `-p` word is consumed here,
11045                // same reason as above: compile_head drops it from its own
11046                // local list only.
11047                if v.first()
11048                    .map(|w| {
11049                        let b = w.as_bytes();
11050                        b.len() >= 2
11051                            && IS_DASH(b[0] as char)
11052                            && w[1..].chars().all(|c| c == 'p' || c == 'v' || c == 'V')
11053                    })
11054                    .unwrap_or(false)
11055                {
11056                    v.remove(0); // c:3165
11057                }
11058                // c:3176-3177 — `if (IS_DASH(argdata[0]) && IS_DASH(argdata[1])
11059                // && !argdata[2]) uremnode(preargs, argnode);`
11060                if v.first().map(|w| w == "--").unwrap_or(false) {
11061                    v.remove(0); // c:3177
11062                }
11063            }
11064        }
11065        let _ = head_args;
11066        preargs.clear();
11067        // c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
11068        // — surface via cflags check: if a typeset-family builtin
11069        // landed, BINF_MAGICEQUALS is in its flags and dispatch
11070        // surfaces it via cflags.
11071        if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
11072            magic_assign = 1;
11073        }
11074        // c:3056 — C's precmd walk sets `hn = builtintab->getnode(...)`
11075        // for the dispatch target before breaking at c:3064. The
11076        // Rust port's execcmd_compile_head returns is_builtin but
11077        // not the entry pointer, and the second resolution loop
11078        // below short-circuits on `is_builtin != 0` (c:3423-3426)
11079        // without re-resolving. Look up the dispatch target now so
11080        // `hn` is non-null at the execbuiltin call (c:4233 /
11081        // exec.rs:10177); otherwise execbuiltin returns 1 silently
11082        // on a null `bn`.
11083        hn = None;
11084        if dispatch.has_command_vv {
11085            // c:3209 — `hn = &commandbn.node;`. The `command -v`/`-V` form
11086            // dispatches through the dedicated descriptor (bin_whence /
11087            // BIN_COMMAND / optstr "pvV"), NOT through builtintab's
11088            // handler-less `BIN_PREFIX("command", …)` row — looking the name
11089            // up there gave a null handler and `command -v cat` printed
11090            // nothing.
11091            hn = Some(&*commandbn as *const builtin as *mut builtin); // c:3209
11092        } else if is_builtin != 0 {
11093            if let Some(target) = args.as_ref().and_then(|v| v.first()) {
11094                if let Some(entry) = BUILTINS.iter().find(|b| b.node.nam == *target) {
11095                    hn = Some(entry as *const builtin as *mut builtin);
11096                }
11097            }
11098        }
11099    } else {
11100        // c:3282-3283 — `else preargs = NULL;`
11101        // We use an empty preargs to model NULL — C's `preargs` is
11102        // only iterated if `nonempty(preargs)` in this branch.
11103    }
11104
11105    // c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
11106    // Sets the file-static `esprefork` (exec.rs:267) so any downstream
11107    // execsubst() call inside this command's expansion uses the same
11108    // prefork flags. Also keep a local copy for the immediate
11109    // prefork(args, esprefork, NULL) below.
11110    let esprefork_v: i32 =
11111        if magic_assign != 0 || (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32) {
11112            PREFORK_TYPESET // c:3300
11113        } else {
11114            0
11115        };
11116    esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
11117
11118    // c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
11119    if args.is_some() && eparams.htok != 0 {
11120        // c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
11121        let mut as_linklist: LinkList<String> = Default::default();
11122        if let Some(ref v) = args {
11123            for s in v {
11124                as_linklist.push_back(s.clone());
11125            }
11126        }
11127        let mut rf = 0i32;
11128        prefork(&mut as_linklist, esprefork_v, &mut rf);
11129        // Move back into args.
11130        let mut out: Vec<String> = Vec::new();
11131        while let Some(s) = as_linklist.pop_front() {
11132            out.push(s);
11133        }
11134        args = Some(out);
11135    }
11136    if !preargs.is_empty() {
11137        // c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
11138        let mut joined = preargs.clone();
11139        if let Some(ref v) = args {
11140            joined.extend(v.iter().cloned());
11141        }
11142        args = Some(joined);
11143    }
11144
11145    // c:3309-3406 — main resolution loop + empty-command branch.
11146    if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
11147        let mut unglobbed: i32 = 0; // c:3310
11148
11149        // c:3312 — `for (;;)` — main resolution loop.
11150        loop {
11151            // c:3315-3318 — globbing or untokenise sweep.
11152            if (cflags & BINF_NOGLOB) == 0 {
11153                while checked == 0
11154                    && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
11155                    && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
11156                    && crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
11157                {
11158                    // c:3318 — `zglob(args, firstnode(args), 0);`
11159                    // zglob takes &mut Vec<String>; isolate the head element
11160                    // by splitting args into [head] and [tail], then re-merging.
11161                    let mut head_vec: Vec<String> = Vec::new();
11162                    if let Some(ref mut v) = args {
11163                        head_vec.push(v.remove(0));
11164                    }
11165                    crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
11166                    if let Some(ref mut v) = args {
11167                        // Re-merge the globbed head ahead of the tail. Inserting
11168                        // one element at a time (`v.insert(i, s)`) memmoves the
11169                        // whole remaining tail on every element, so a head word
11170                        // that globs to K matches costs O(K*n) and a completion
11171                        // sweep over a large directory spins. Splicing the batch
11172                        // in at position 0 shifts the tail exactly once and is
11173                        // order-identical: element i lands at index i.
11174                        v.splice(0..0, head_vec);
11175                    }
11176                }
11177            } else if unglobbed == 0 {
11178                // c:3319-3322
11179                if let Some(ref mut v) = args {
11180                    for s in v.iter_mut() {
11181                        *s = untokenize(s); // c:3321
11182                    }
11183                }
11184                unglobbed = 1; // c:3322
11185            }
11186
11187            // c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
11188            if (cflags & BINF_EXEC) != 0 && last1 != 0 {
11189                do_exec = 1; // c:3328
11190            }
11191
11192            // c:3331-3407 — empty-command branch.
11193            if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
11194                // c:3331 — `if (!args || empty(args))`
11195                if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
11196                    // c:3332 — `if (redir && nonempty(redir))`
11197                    if do_exec != 0 {
11198                        // c:3333 — `Was this "exec < foobar"?`
11199                        nullexec = 1; // c:3335
11200                        break;
11201                    } else if varspc.is_some() {
11202                        // c:3337
11203                        nullexec = 2; // c:3338
11204                        break;
11205                    } else if {
11206                        // c:3340-3341 — `if (!nullcmd || !*nullcmd ||
11207                        //   opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
11208                        let nc = getsparam("NULLCMD");
11209                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
11210                        nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
11211                    } {
11212                        // c:3342 — `zerr("redirection with no command");`
11213                        zerr("redirection with no command");
11214                        LASTVAL.store(1, Ordering::Relaxed); // c:3343
11215                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
11216                        if forked != 0 {
11217                            // c:3345-3346
11218                            crate::ported::builtin::_realexit();
11219                        }
11220                        if (how & Z_TIMED as i32) != 0 {
11221                            // c:3347-3348
11222                            crate::ported::jobs::shelltime(
11223                                Some(&mut shti),
11224                                Some(&mut chti),
11225                                Some(&mut then_ts),
11226                                1,
11227                            );
11228                        }
11229                        return; // c:3349
11230                    } else if {
11231                        // c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
11232                        let nc = getsparam("NULLCMD");
11233                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
11234                        nc_empty || isset(SHNULLCMD)
11235                    } {
11236                        // c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
11237                        if args.is_none() {
11238                            args = Some(Vec::new());
11239                        }
11240                        args.as_mut().unwrap().push(":".to_string()); // c:3353
11241                    } else if {
11242                        // c:3354-3356 — `readnullcmd && *readnullcmd &&
11243                        //   peekfirst(redir).type == REDIR_READ &&
11244                        //   !nextnode(firstnode(redir))`
11245                        let rnc = getsparam("READNULLCMD");
11246                        let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
11247                        rnc_nonempty
11248                            && redir.as_ref().unwrap().len() == 1
11249                            && redir.as_ref().unwrap()[0].typ == REDIR_READ
11250                    } {
11251                        // c:3357-3359
11252                        if args.is_none() {
11253                            args = Some(Vec::new());
11254                        }
11255                        let rnc = getsparam("READNULLCMD").unwrap_or_default();
11256                        args.as_mut().unwrap().push(rnc); // c:3359
11257                    } else {
11258                        // c:3360-3364 — default: nullcmd as command.
11259                        if args.is_none() {
11260                            args = Some(Vec::new());
11261                        }
11262                        let nc = getsparam("NULLCMD").unwrap_or_default();
11263                        args.as_mut().unwrap().push(nc); // c:3363
11264                    }
11265                } else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
11266                    // c:3365 — bare `command`: lastval=0, return.
11267                    LASTVAL.store(0, Ordering::Relaxed); // c:3366
11268                    if forked != 0 {
11269                        crate::ported::builtin::_realexit(); // c:3367-3368
11270                    }
11271                    if (how & Z_TIMED as i32) != 0 {
11272                        crate::ported::jobs::shelltime(
11273                            Some(&mut shti),
11274                            Some(&mut chti),
11275                            Some(&mut then_ts),
11276                            1,
11277                        ); // c:3369-3370
11278                    }
11279                    return; // c:3371
11280                } else {
11281                    // c:3372-3406 — no arguments default arm.
11282                    // c:3378-3385 — badcshglob == 1 → no match.
11283                    if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
11284                        zerr("no match"); // c:3379
11285                        LASTVAL.store(1, Ordering::Relaxed); // c:3380
11286                        if forked != 0 {
11287                            crate::ported::builtin::_realexit(); // c:3381-3382
11288                        }
11289                        if (how & Z_TIMED as i32) != 0 {
11290                            crate::ported::jobs::shelltime(
11291                                Some(&mut shti),
11292                                Some(&mut chti),
11293                                Some(&mut then_ts),
11294                                1,
11295                            ); // c:3383-3384
11296                        }
11297                        return; // c:3385
11298                    }
11299                    // c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
11300                    cmdoutval.store(
11301                        if use_cmdoutval.load(Ordering::Relaxed) != 0 {
11302                            LASTVAL.load(Ordering::Relaxed)
11303                        } else {
11304                            0
11305                        },
11306                        Ordering::Relaxed,
11307                    );
11308                    if varspc.is_some() {
11309                        // c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
11310                        LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
11311                        addvars(state, varspc.unwrap_or(0), 0); // c:3391
11312                    }
11313                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11314                        // c:3393
11315                        LASTVAL.store(1, Ordering::Relaxed); // c:3394
11316                    } else {
11317                        // c:3395-3396
11318                        LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
11319                    }
11320                    if isset(XTRACE) {
11321                        // c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
11322                        // xtrerr accessor is stub; rely on the existing
11323                        // stderr writer in compile_zsh tracing path.
11324                        eprintln!();
11325                    }
11326                    if forked != 0 {
11327                        crate::ported::builtin::_realexit(); // c:3401-3402
11328                    }
11329                    if (how & Z_TIMED as i32) != 0 {
11330                        crate::ported::jobs::shelltime(
11331                            Some(&mut shti),
11332                            Some(&mut chti),
11333                            Some(&mut then_ts),
11334                            1,
11335                        ); // c:3403-3404
11336                    }
11337                    return; // c:3405
11338                }
11339            }
11340
11341            // c:3423-3426 — `if (errflag || checked || is_builtin ||
11342            //   (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
11343            // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11344            // the C line cited above tests the WHOLE errflag, so masking here let an
11345            // interrupted shell keep going where zsh stops.
11346            if errflag.load(Ordering::Relaxed) != 0
11347                || checked != 0
11348                || is_builtin != 0
11349                || if isset(POSIXBUILTINS) {
11350                    (cflags & BINF_EXEC) != 0
11351                } else {
11352                    (cflags & BINF_COMMAND) != 0
11353                }
11354            {
11355                // c:3423
11356                break; // c:3426
11357            }
11358
11359            // c:3428 — `cmdarg = (char *) peekfirst(args);`
11360            let cmdarg = args.as_ref().unwrap()[0].clone();
11361
11362            // c:3429-3433 — shfunc lookup.
11363            // c:3485 — `(hn = shfunctab->getnode(shfunctab, cmdarg))`.
11364            // Same misport as the compile-time head walk above: C's
11365            // `shfunctab->getnode` is `gethashnode`
11366            // (Src/hashtable.c:821), an O(1) probe that skips DISABLED
11367            // nodes, not a full-table walk.
11368            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
11369                let in_shfunctab = shfunctab_lock()
11370                    .read()
11371                    .map(|t| !t.getnode(cmdarg.as_str()).is_null())
11372                    .unwrap_or(false);
11373                if in_shfunctab {
11374                    is_shfunc = 1; // c:3431
11375                    break; // c:3432
11376                }
11377            }
11378            // c:3434-3447 — builtintab lookup.
11379            let builtin_entry: Option<&'static builtin> = BUILTINS
11380                .iter()
11381                .find(|b| b.node.nam.as_str() == cmdarg.as_str());
11382            if builtin_entry.is_none() {
11383                if (cflags & BINF_BUILTIN) != 0 {
11384                    // c:3435 — `zwarn("no such builtin: %s", cmdarg);`
11385                    zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
11386                    LASTVAL.store(1, Ordering::Relaxed); // c:3437
11387                    if oautocont >= 0 {
11388                        // c:3438-3439
11389                        opt_state_set("autocontinue", oautocont != 0);
11390                    }
11391                    if forked != 0 {
11392                        crate::ported::builtin::_realexit(); // c:3440-3441
11393                    }
11394                    if (how & Z_TIMED as i32) != 0 {
11395                        crate::ported::jobs::shelltime(
11396                            Some(&mut shti),
11397                            Some(&mut chti),
11398                            Some(&mut then_ts),
11399                            1,
11400                        ); // c:3442-3443
11401                    }
11402                    return; // c:3444
11403                }
11404                break; // c:3446
11405            }
11406            let entry = builtin_entry.unwrap();
11407            // c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
11408            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
11409                is_builtin = 1; // c:3449
11410                                // c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
11411                                // module autoload check. zshrs's BUILTINS table is
11412                                // static and pre-resolved; treat resolvebuiltin as
11413                                // pass-through.
11414                hn = Some(entry as *const builtin as *mut builtin);
11415                break; // c:3459
11416            }
11417            // c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
11418            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
11419            cflags |= entry.node.flags as u32;
11420            if let Some(ref mut v) = args {
11421                v.remove(0); // c:3463 uremnode(args, firstnode(args))
11422            }
11423            hn = None; // c:3464
11424        }
11425    }
11426
11427    // c:3468-3478 — errflag bail-out.
11428    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11429    // the C line cited above tests the WHOLE errflag, so masking here let an
11430    // interrupted shell keep going where zsh stops.
11431    if errflag.load(Ordering::Relaxed) != 0 {
11432        // c:3468
11433        if LASTVAL.load(Ordering::Relaxed) == 0 {
11434            // c:3469
11435            LASTVAL.store(1, Ordering::Relaxed); // c:3470
11436        }
11437        if oautocont >= 0 {
11438            opt_state_set("autocontinue", oautocont != 0);
11439            // c:3472
11440        }
11441        if forked != 0 {
11442            crate::ported::builtin::_realexit(); // c:3473-3474
11443        }
11444        if (how & Z_TIMED as i32) != 0 {
11445            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
11446            // c:3475-3476
11447        }
11448        return; // c:3477
11449    }
11450
11451    // c:3480-3483 — `Get the text associated with this command.`
11452    if text.is_none()
11453        && sfcontext.load(Ordering::Relaxed) == 0
11454        && (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
11455    {
11456        // c:3481-3482
11457        text = Some(crate::ported::text::getjobtext(
11458            state.prog.clone(),
11459            Some(eparams.beg),
11460        )); // c:3483
11461    }
11462
11463    // c:3485-3492 — `Set up special parameter $_`.
11464    if typ != WC_FUNCDEF as i32 {
11465        // c:3490
11466        let last_str = args
11467            .as_ref()
11468            .and_then(|v| v.last())
11469            .cloned()
11470            .unwrap_or_default();
11471        setunderscore(&last_str); // c:3491-3492
11472    }
11473
11474    // c:3494-3524 — `Warn about "rm *"`.
11475    if typ == WC_SIMPLE as i32
11476        && crate::ported::zsh_h::interact()
11477        && unset(RMSTARSILENT)
11478        && isset(SHINSTDIN)
11479        && args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
11480        && args.as_ref().unwrap()[0] == "rm"
11481    {
11482        // c:3495-3497
11483        let args_v = args.as_ref().unwrap().clone();
11484        for s in args_v.iter().skip(1) {
11485            // c:3500
11486            // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11487            // the C line cited above tests the WHOLE errflag, so masking here let an
11488            // interrupted shell keep going where zsh stops.
11489            if errflag.load(Ordering::Relaxed) != 0 {
11490                break;
11491            }
11492            let l = s.len();
11493            // c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
11494            if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
11495                let pwd = getsparam("PWD").unwrap_or_default();
11496                if !crate::ported::utils::checkrmall(&pwd) {
11497                    // c:3506
11498                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
11499                    break; // c:3508
11500                }
11501            } else if l >= 2 {
11502                // c:3510 — `s[l-2] == '/' && s[l-1] == Star`
11503                let bytes = s.as_bytes();
11504                if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
11505                    let prefix = if l == 2 {
11506                        "/".to_string()
11507                    } else {
11508                        String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
11509                    };
11510                    if !crate::ported::utils::checkrmall(&prefix) {
11511                        // c:3518
11512                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
11513                        break; // c:3520
11514                    }
11515                }
11516            }
11517        }
11518    }
11519
11520    // c:3526-3580 — type-specific dispatch prep.
11521    if typ == WC_FUNCDEF as i32 {
11522        // c:3526
11523        if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
11524            // c:3535 — `Nonymous, don't do redirections here`
11525            redir = None; // c:3537
11526        }
11527    } else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
11528        // c:3539
11529        // c:3540-3559 — shfunc / autoload preload.
11530        if is_shfunc != 0 {
11531            // c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
11532        } else {
11533            // c:3543-3559 — autoload preload.
11534            if let Some(ref mut sh) = state.prog.shf {
11535                let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
11536                let r = loadautofn(shf_ptr, 1, 0, 0);
11537                if r != 0 {
11538                    // c:3551 — `lastval = 1;`
11539                    LASTVAL.store(1, Ordering::Relaxed);
11540                    if oautocont >= 0 {
11541                        opt_state_set("autocontinue", oautocont != 0);
11542                    }
11543                    if forked != 0 {
11544                        crate::ported::builtin::_realexit();
11545                    }
11546                    if (how & Z_TIMED as i32) != 0 {
11547                        crate::ported::jobs::shelltime(
11548                            Some(&mut shti),
11549                            Some(&mut chti),
11550                            Some(&mut then_ts),
11551                            1,
11552                        );
11553                    }
11554                    return; // c:3558
11555                }
11556            }
11557        }
11558        // c:3561-3579 — shf->redir append: a function definition can
11559        // carry extra redirs (`f() { ... } < file`), captured as a
11560        // separate Eprog in shf->redir. Walk that Eprog with a temp
11561        // estate, extract its redirs with ecgetredirs, then merge
11562        // into the live `redir` list.
11563        // Resolve shfunc by name (hn is *mut builtin so we go through
11564        // shfunctab as in the dispatch site at c:4102).
11565        let shfn_name = args
11566            .as_ref()
11567            .and_then(|v| v.first())
11568            .cloned()
11569            .unwrap_or_default();
11570        let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
11571            if let Ok(tab) = shfunctab_lock().read() {
11572                tab.get(&shfn_name).and_then(|s| s.redir.clone())
11573            } else {
11574                None
11575            }
11576        };
11577        if let Some(red_eprog) = shf_redir_eprog {
11578            // c:3566-3571 — build temp estate from shf->redir.
11579            let mut tmp_state = estate {
11580                prog: red_eprog.clone(),
11581                pc: 0,
11582                strs: red_eprog.strs.clone(),
11583                strs_offset: 0,
11584            };
11585            // c:3572 — `redir2 = ecgetredirs(&s);`
11586            let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
11587            // c:3573-3578 — merge into existing redir.
11588            if redir.is_none() {
11589                redir = Some(redir2); // c:3574
11590            } else if let Some(ref mut r) = redir {
11591                // c:3576-3577 — append.
11592                for n in redir2 {
11593                    r.push(n);
11594                }
11595            }
11596        }
11597    }
11598
11599    // c:3582-3591 — errflag bail-out (2).
11600    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11601    // the C line cited above tests the WHOLE errflag, so masking here let an
11602    // interrupted shell keep going where zsh stops.
11603    if errflag.load(Ordering::Relaxed) != 0 {
11604        // c:3582
11605        LASTVAL.store(1, Ordering::Relaxed); // c:3583
11606        if oautocont >= 0 {
11607            opt_state_set("autocontinue", oautocont != 0);
11608            // c:3584-3585
11609        }
11610        if forked != 0 {
11611            crate::ported::builtin::_realexit(); // c:3586-3587
11612        }
11613        if (how & Z_TIMED as i32) != 0 {
11614            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
11615            // c:3588-3589
11616        }
11617        return; // c:3590
11618    }
11619
11620    // c:3593-3632 — external resolution + AUTOCD.
11621    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
11622        // c:3593
11623        // Consumed here so a nested command dispatched from this one
11624        // (a function body) does not inherit it.
11625        let redirs_preapplied = REDIRS_PREAPPLIED.with(|c| c.replace(false));
11626        let trycd = isset(AUTOCD)
11627            && isset(SHINSTDIN)
11628            && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
11629            && !redirs_preapplied
11630            && args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
11631            && !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
11632        let mut external_found = false;
11633        if hn.is_none() {
11634            // c:3600
11635            let cmdarg = args.as_ref().unwrap()[0].clone();
11636            let mut dohashcmd = isset(HASHCMDS); // c:3604
11637                                                 // c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
11638            let mut have_cmdnam: Option<cmdnam> = {
11639                let tab = cmdnamtab_lock().read().ok();
11640                tab.and_then(|t| {
11641                    t.iter()
11642                        .find(|(k, _)| k.as_str() == cmdarg.as_str())
11643                        .map(|(_, v)| v.clone())
11644                })
11645            };
11646            if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
11647                // c:3607
11648                // c:3608-3611 — `if (!(hn->flags & HASHED)) { checkpath = path; dohashcmd = 1; }`
11649                if (have_cmdnam.as_ref().unwrap().node.flags & crate::ported::zsh_h::HASHED) == 0 {
11650                    dohashcmd = true;
11651                }
11652                // c:3612-3614 — removenode + freenode; `hn = NULL`.
11653                if let Ok(mut tab) = cmdnamtab_lock().write() {
11654                    tab.remove(&cmdarg);
11655                }
11656                have_cmdnam = None;
11657            }
11658            if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
11659                // c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
11660                let has_slash = cmdarg.contains('/'); // c:3617-3618
11661                if !has_slash {
11662                    // c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
11663                    let path_dirs = getsparam("PATH").unwrap_or_default();
11664                    let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
11665                    have_cmdnam = hashcmd(&cmdarg, &dirs);
11666                }
11667            }
11668            // C keeps the resolved command in `hn` (c:3619), which the
11669            // AUTOCD test below reads as "found": `hn` here only carries
11670            // builtins and functions, so the external lookup is carried
11671            // separately. Dropped, a found external such as `mysql` was
11672            // treated as not found, and with AUTOCD + CDABLEVARS
11673            // `${=ZPWR_LEARN_COMMAND} 2>>log` ran `cd -- /var/empty`
11674            // (user mysql's home) and printed `~mysql`.
11675            external_found = have_cmdnam.is_some();
11676        }
11677
11678        // c:3625-3631 — AUTOCD: command not found, try directory.
11679        if hn.is_none() && !external_found && trycd {
11680            let cmdarg = args.as_ref().unwrap()[0].clone();
11681            if let Some(s) = cancd(&cmdarg) {
11682                // c:3625
11683                args.as_mut().unwrap()[0] = s; // c:3626
11684                args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
11685                args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
11686                                                                    // c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
11687                let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
11688                if let Some(cd) = cd_entry {
11689                    hn = Some(cd as *const builtin as *mut builtin);
11690                    is_builtin = 1; // c:3630
11691                }
11692            }
11693        }
11694    }
11695
11696    // c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
11697    is_cursh =
11698        (is_builtin != 0 || is_shfunc != 0 || nullexec != 0 || typ >= WC_CURSH as i32) as i32;
11699
11700    // c:3659-3697 — fork decision.
11701    if forked == 0 {
11702        // c:3659
11703        if do_exec == 0
11704            && (((is_builtin != 0 || is_shfunc != 0) && output != 0)
11705                || (is_cursh == 0
11706                    && (last1 != 1
11707                        || crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
11708                        || JOBTAB
11709                            .get()
11710                            .map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
11711                            .unwrap_or(false)
11712                        || false/* fdtable_flocks — substrate stub */)))
11713        {
11714            // c:3660-3663
11715            let mut filelist_for_fork = filelist.clone();
11716            let pid = execcmd_fork(
11717                state,
11718                how,
11719                typ,
11720                varspc,
11721                &mut filelist_for_fork,
11722                text.as_deref().unwrap_or(""),
11723                oautocont,
11724                close_if_forked,
11725            );
11726            match pid {
11727                -1 => {
11728                    // c:3666-3667 — goto fatal.
11729                    redir_err = 1;
11730                    return execcmd_exec_done_path(
11731                        redir_err,
11732                        oautocont,
11733                        how,
11734                        &mut shti,
11735                        &mut chti,
11736                        &mut then_ts,
11737                        forked,
11738                        &mut newxtrerr,
11739                        cflags,
11740                        orig_cflags,
11741                        is_cursh,
11742                        do_exec,
11743                    );
11744                }
11745                0 => {
11746                    // c:3668 — child continues.
11747                }
11748                _ => {
11749                    // c:3670-3671 — parent returns.
11750                    if oautocont >= 0 {
11751                        opt_state_set("autocontinue", oautocont != 0);
11752                    }
11753                    if (how & Z_TIMED as i32) != 0 {
11754                        crate::ported::jobs::shelltime(
11755                            Some(&mut shti),
11756                            Some(&mut chti),
11757                            Some(&mut then_ts),
11758                            1,
11759                        );
11760                    }
11761                    return;
11762                }
11763            }
11764            forked = 1; // c:3673
11765        } else if is_cursh != 0 {
11766            // c:3674
11767            // c:3678-3682 — set jobtab[thisjob] stat bits.
11768            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
11769            if thisjob >= 0 {
11770                if let Some(jt) = JOBTAB.get() {
11771                    let mut guard = jt.lock().unwrap();
11772                    if let Some(j) = guard.get_mut(thisjob as usize) {
11773                        j.stat |= STAT_CURSH; // c:3678
11774                                              // c:3679-3680 — `if (!jobtab[thisjob].procs)
11775                                              //                  jobtab[thisjob].stat |= STAT_NOPRINT;`
11776                                              // Suppress the "[N] done" print for jobs that
11777                                              // never forked a real process (cursh / builtin /
11778                                              // null exec).
11779                        if j.procs.is_empty() {
11780                            j.stat |= STAT_NOPRINT; // c:3680
11781                        }
11782                        if is_builtin != 0 {
11783                            j.stat |= STAT_BUILTIN; // c:3682
11784                        }
11785                    }
11786                }
11787            }
11788        } else {
11789            // c:3683-3697 — external exec (real or fake).
11790            is_exec = 1; // c:3687
11791                         // c:3695 — `if (type == WC_SUBSH) forked = 1;`
11792            if typ == WC_SUBSH as i32 {
11793                forked = 1; // c:3696
11794            }
11795        }
11796    }
11797
11798    // c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
11799    if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
11800        // c:3700
11801        let mut oargs: LinkList<String> = Default::default();
11802        if let Some(ref v) = args {
11803            for s in v {
11804                oargs.push_back(s.clone());
11805            }
11806        }
11807        globlist(&mut oargs, 0); // c:3702
11808        let mut out: Vec<String> = Vec::new();
11809        while let Some(s) = oargs.pop_front() {
11810            out.push(s);
11811        }
11812        args = Some(out);
11813    }
11814    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11815        // c:3705
11816        LASTVAL.store(1, Ordering::Relaxed); // c:3706
11817        return execcmd_exec_err_path(
11818            forked,
11819            &mut save,
11820            &mut mfds,
11821            oautocont,
11822            how,
11823            &mut shti,
11824            &mut chti,
11825            &mut then_ts,
11826            &mut newxtrerr,
11827            cflags,
11828            orig_cflags,
11829            is_cursh,
11830            do_exec,
11831            redir_err,
11832        );
11833    }
11834
11835    // c:3765-3773 — "Make a copy of stderr for xtrace output before
11836    // redirecting". The fusevm runtime reaches the same
11837    // port when it opens a simple command's redirect scope.
11838    crate::fusevm_bridge::xtrerr_flush(); // c:3766 fflush(xtrerr)
11839    if isset(XTRACE)
11840        && crate::ported::utils::xtrerr.load(Ordering::Relaxed) == 2
11841        && (typ < WC_SUBSH as i32 || typ == WC_TIMED as i32)
11842    {
11843        // c:3767-3768
11844        let fd = crate::ported::utils::movefd(unsafe { libc::dup(2) }); // c:3769
11845        if fd >= 0 {
11846            crate::ported::utils::xtrerr.store(fd, Ordering::Relaxed); // c:3770
11847            crate::ported::utils::fdtable_set(fd, FDT_XTRACE); // c:3771
11848            newxtrerr = Some(fd);
11849        }
11850    }
11851
11852    // c:3720-3724 — pipeline input/output to mfds.
11853    if input != 0 {
11854        addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
11855    }
11856    if output != 0 {
11857        addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
11858    }
11859
11860    // c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
11861    if let Some(ref mut r) = redir {
11862        spawnpipes(r.as_mut_slice(), nullexec);
11863    }
11864
11865    // c:3731-3955 — io redirection loop. Faithful per-redir match.
11866    while let Some(redir_list) = redir.as_mut() {
11867        // c:3731 — `while (redir && nonempty(redir))`
11868        if redir_list.is_empty() {
11869            break;
11870        }
11871        let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
11872                                            // c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
11873        if fn_.typ == REDIR_INPIPE {
11874            // c:3736
11875            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
11876                // c:3737
11877                if fn_.fd2 != -1 {
11878                    let _ = zclose(fn_.fd2); // c:3738-3739
11879                }
11880                closemnodes(&mut mfds); // c:3740
11881                fixfds(&save); // c:3741
11882                {
11883                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
11884                    LASTVAL.store(1, Ordering::Relaxed);
11885                } // c:3742
11886                break;
11887            }
11888            // c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
11889            addfd(
11890                forked,
11891                &mut save,
11892                &mut mfds,
11893                fn_.fd1,
11894                fn_.fd2,
11895                0,
11896                fn_.varid.as_deref(),
11897            );
11898        } else if fn_.typ == REDIR_OUTPIPE {
11899            // c:3745
11900            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
11901                // c:3746
11902                if fn_.fd2 != -1 {
11903                    let _ = zclose(fn_.fd2); // c:3747-3748
11904                }
11905                closemnodes(&mut mfds); // c:3749
11906                fixfds(&save); // c:3750
11907                {
11908                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
11909                    LASTVAL.store(1, Ordering::Relaxed);
11910                } // c:3751
11911                break;
11912            }
11913            // c:3753
11914            addfd(
11915                forked,
11916                &mut save,
11917                &mut mfds,
11918                fn_.fd1,
11919                fn_.fd2,
11920                1,
11921                fn_.varid.as_deref(),
11922            );
11923        } else {
11924            // c:3754 — non-pipe redir branch.
11925            let mut closed: i32; // c:3755
11926                                 // c:3756-3757 — xpandredir glob/brace.
11927            if fn_.typ != REDIR_HERESTR {
11928                // Put fn_ back temporarily so xpandredir can mutate
11929                // around it; not implemented identically — xpandredir
11930                // signature in zshrs differs (takes &mut redir + ctx).
11931                // c:3756 — `if (xpandredir(fn, redir)) continue;`
11932                // Pragmatic: skip xpandredir (it handles brace/glob in
11933                // redir paths — uncommon, ports to follow-up).
11934            }
11935            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11936                // c:3758
11937                closemnodes(&mut mfds); // c:3759
11938                fixfds(&save); // c:3760
11939                {
11940                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
11941                    LASTVAL.store(1, Ordering::Relaxed);
11942                } // c:3761
11943                break;
11944            }
11945            if !isset(EXECOPT) {
11946                // c:3763 — `if (unset(EXECOPT)) continue;`
11947                continue;
11948            }
11949            let fil_local: i32;
11950            match fn_.typ {
11951                t if t == REDIR_HERESTR => {
11952                    // c:3766
11953                    if checkclobberparam(&fn_) == 0 {
11954                        fil_local = -1; // c:3768
11955                    } else {
11956                        fil_local = getherestr(&fn_); // c:3770
11957                    }
11958                    if fil_local == -1 {
11959                        // c:3771
11960                        let e = std::io::Error::last_os_error();
11961                        let raw = e.raw_os_error().unwrap_or(0);
11962                        if raw != 0 && raw != libc::EINTR {
11963                            zwarn(&format!("can't create temp file for here document: {}", e));
11964                            // c:3772-3774
11965                        }
11966                        closemnodes(&mut mfds); // c:3775
11967                        fixfds(&save); // c:3776
11968                        {
11969                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
11970                            LASTVAL.store(1, Ordering::Relaxed);
11971                        } // c:3777
11972                        break;
11973                    }
11974                    // c:3779
11975                    addfd(
11976                        forked,
11977                        &mut save,
11978                        &mut mfds,
11979                        fn_.fd1,
11980                        fil_local,
11981                        0,
11982                        fn_.varid.as_deref(),
11983                    );
11984                }
11985                t if t == REDIR_READ || t == REDIR_READWRITE => {
11986                    // c:3781-3782
11987                    if checkclobberparam(&fn_) == 0 {
11988                        fil_local = -1; // c:3784
11989                    } else {
11990                        let name = fn_.name.clone().unwrap_or_default();
11991                        let unmeta_name = unmeta(&name);
11992                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
11993                            Ok(c) => c,
11994                            Err(_) => {
11995                                closemnodes(&mut mfds);
11996                                fixfds(&save);
11997                                {
11998                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
11999                                    LASTVAL.store(1, Ordering::Relaxed);
12000                                }
12001                                break;
12002                            }
12003                        };
12004                        if fn_.typ == REDIR_READ {
12005                            // c:3786
12006                            fil_local = unsafe {
12007                                libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
12008                            };
12009                        } else {
12010                            // c:3788-3789
12011                            fil_local = unsafe {
12012                                libc::open(
12013                                    cstr.as_ptr(),
12014                                    libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
12015                                    0o666,
12016                                )
12017                            };
12018                        }
12019                    }
12020                    if fil_local == -1 {
12021                        // c:3790
12022                        closemnodes(&mut mfds); // c:3791
12023                        fixfds(&save); // c:3792
12024                        let e = std::io::Error::last_os_error();
12025                        if e.raw_os_error().unwrap_or(0) != libc::EINTR {
12026                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
12027                            // c:3793-3794
12028                        }
12029                        {
12030                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12031                            LASTVAL.store(1, Ordering::Relaxed);
12032                        } // c:3795
12033                        break;
12034                    }
12035                    // c:3797
12036                    addfd(
12037                        forked,
12038                        &mut save,
12039                        &mut mfds,
12040                        fn_.fd1,
12041                        fil_local,
12042                        0,
12043                        fn_.varid.as_deref(),
12044                    );
12045                    // c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
12046                    if nullexec == 1
12047                        && fn_.fd1 == 0
12048                        && fn_.varid.is_none()
12049                        && isset(SHINSTDIN)
12050                        && isset(INTERACTIVE)
12051                    {
12052                        // c:3801 — `!zleactive` check ommitted (zleactive
12053                        // accessor lives in zle module; fusevm bypasses ZLE).
12054                        crate::ported::init::init_io(None); // c:3802
12055                    }
12056                }
12057                t if t == REDIR_CLOSE => {
12058                    // c:3804
12059                    // c:3805 — `if (fn->varid) { parse fd from variable }`
12060                    let mut fd1_local = fn_.fd1;
12061                    if let Some(varname) = fn_.varid.as_deref() {
12062                        // c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
12063                        // with varid. The C path resolves the named param
12064                        // to its integer-string value, parses as base-10
12065                        // (or base#NN), and rejects readonly / non-numeric
12066                        // / shell-owned-fd values.
12067                        //
12068                        //   bad=1  → "parameter %s does not contain a file descriptor"
12069                        //   bad=2  → "can't close file descriptor from readonly parameter %s"
12070                        //   bad=3  → "file descriptor %d used by shell, not closed"
12071                        //
12072                        // Substrate now available: getsparam for value,
12073                        // paramtab read for PM_READONLY, MAX_ZSH_FD +
12074                        // fdtable_get for shell-owned guard.
12075                        let mut bad: u8 = 0;
12076                        let value_opt = getsparam(varname);
12077                        let is_ro = paramtab()
12078                            .read()
12079                            .ok()
12080                            .and_then(|t| {
12081                                t.get(varname)
12082                                    .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
12083                            })
12084                            .unwrap_or(false);
12085                        if value_opt.is_none() {
12086                            bad = 1; // c:3811 getvalue failed
12087                        } else if is_ro {
12088                            bad = 2; // c:3813 PM_READONLY
12089                        } else {
12090                            let s = value_opt.as_deref().unwrap_or("");
12091                            match s.trim().parse::<i32>() {
12092                                Ok(n) => {
12093                                    fd1_local = n;
12094                                    fn_.fd1 = n;
12095                                    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
12096                                    if n >= 10
12097                                        && n <= max_fd
12098                                        && (fdtable_get(n) & FDT_TYPE_MASK) == FDT_INTERNAL
12099                                    {
12100                                        // c:3835 shell-owned-fd reject
12101                                        bad = 3;
12102                                    }
12103                                }
12104                                Err(_) => {
12105                                    bad = 1; // c:3823 strtol failure
12106                                }
12107                            }
12108                        }
12109                        if bad != 0 {
12110                            // c:3840-3849
12111                            match bad {
12112                                3 => zwarn(&format!(
12113                                    "file descriptor {} used by shell, not closed",
12114                                    fn_.fd1
12115                                )),
12116                                2 => zwarn(&format!(
12117                                    "can't close file descriptor from readonly parameter {}",
12118                                    varname
12119                                )),
12120                                _ => zwarn(&format!(
12121                                    "parameter {} does not contain a file descriptor",
12122                                    varname
12123                                )),
12124                            }
12125                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12126                            LASTVAL.store(1, Ordering::Relaxed);
12127                            break;
12128                        }
12129                    }
12130                    // c:3852-3865 — `closed`: optional movefd save.
12131                    closed = 0;
12132                    if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
12133                        // c:3856
12134                        let mv = movefd(fd1_local); // c:3857
12135                        save[fd1_local as usize] = mv;
12136                        if mv >= 0 {
12137                            closed = 1; // c:3862-3863
12138                        }
12139                    }
12140                    if fd1_local < 10 {
12141                        // c:3866
12142                        closemn(&mut mfds, fd1_local, REDIR_CLOSE);
12143                        // c:3867
12144                    }
12145                    // c:3873-3876
12146                    let _ = &mut fd1_local;
12147                    if closed == 0 && zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
12148                        zwarn(&format!(
12149                            "failed to close file descriptor {}: {}",
12150                            fn_.fd1,
12151                            std::io::Error::last_os_error()
12152                        )); // c:3873-3875
12153                    }
12154                }
12155                t if t == REDIR_MERGEIN || t == REDIR_MERGEOUT => {
12156                    // c:3878-3879
12157                    if fn_.fd2 < 10 {
12158                        closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
12159                    }
12160                    if checkclobberparam(&fn_) == 0 {
12161                        fil_local = -1; // c:3883
12162                    } else if fn_.fd2 > 9 {
12163                        // c:3884-3897 — fd table check.
12164                        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
12165                        let cin = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
12166                        let cout = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
12167                        let in_table = if fn_.fd2 <= max_fd {
12168                            let kind = fdtable_get(fn_.fd2) & FDT_TYPE_MASK;
12169                            kind != FDT_UNUSED && kind != FDT_EXTERNAL
12170                        } else {
12171                            false
12172                        };
12173                        if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
12174                            fil_local = -1; // c:3896
12175                                            // Per-platform errno setter (c:3897 `errno = EBADF;`).
12176                            #[cfg(target_os = "macos")]
12177                            unsafe {
12178                                *libc::__error() = libc::EBADF;
12179                            }
12180                            #[cfg(target_os = "linux")]
12181                            unsafe {
12182                                *libc::__errno_location() = libc::EBADF;
12183                            }
12184                        } else {
12185                            let fd = if fn_.fd2 == -2 {
12186                                // c:3900-3901
12187                                if fn_.typ == REDIR_MERGEOUT {
12188                                    crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
12189                                } else {
12190                                    crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
12191                                }
12192                            } else {
12193                                fn_.fd2
12194                            };
12195                            // c:3902 — `fil = movefd(dup(fd));`
12196                            let dup_fd = unsafe { libc::dup(fd) };
12197                            fil_local = movefd(dup_fd);
12198                        }
12199                    } else {
12200                        let fd = if fn_.fd2 == -2 {
12201                            if fn_.typ == REDIR_MERGEOUT {
12202                                crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
12203                            } else {
12204                                crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
12205                            }
12206                        } else {
12207                            fn_.fd2
12208                        };
12209                        let dup_fd = unsafe { libc::dup(fd) };
12210                        fil_local = movefd(dup_fd);
12211                    }
12212                    if fil_local == -1 {
12213                        // c:3904
12214                        closemnodes(&mut mfds); // c:3907
12215                        fixfds(&save); // c:3908
12216                        if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
12217                            let desc = if fn_.fd2 == -2 {
12218                                "coprocess".to_string()
12219                            } else {
12220                                format!("{}", fn_.fd2)
12221                            };
12222                            zwarn(&format!("{}: {}", desc, std::io::Error::last_os_error()));
12223                            // c:3911-3913
12224                        }
12225                        {
12226                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12227                            LASTVAL.store(1, Ordering::Relaxed);
12228                        } // c:3914
12229                        break;
12230                    }
12231                    // c:3916-3917
12232                    let merge_is_out = if fn_.typ == REDIR_MERGEOUT { 1 } else { 0 };
12233                    addfd(
12234                        forked,
12235                        &mut save,
12236                        &mut mfds,
12237                        fn_.fd1,
12238                        fil_local,
12239                        merge_is_out,
12240                        fn_.varid.as_deref(),
12241                    );
12242                }
12243                _ => {
12244                    // c:3919 default — write/append/error_redir.
12245                    let mut dfil: i32;
12246                    if checkclobberparam(&fn_) == 0 {
12247                        fil_local = -1; // c:3921
12248                    } else if IS_APPEND_REDIR(fn_.typ) {
12249                        // c:3922
12250                        let name = fn_.name.clone().unwrap_or_default();
12251                        let unmeta_name = unmeta(&name);
12252                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
12253                            Ok(c) => c,
12254                            Err(_) => {
12255                                closemnodes(&mut mfds);
12256                                fixfds(&save);
12257                                {
12258                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12259                                    LASTVAL.store(1, Ordering::Relaxed);
12260                                }
12261                                break;
12262                            }
12263                        };
12264                        // c:3924-3927
12265                        let mode = if !isset(CLOBBER)
12266                            && !isset(crate::ported::zsh_h::APPENDCREATE)
12267                            && !IS_CLOBBER_REDIR(fn_.typ)
12268                        {
12269                            libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
12270                        } else {
12271                            libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
12272                        };
12273                        fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
12274                    } else {
12275                        // c:3929
12276                        fil_local = clobber_open(&fn_);
12277                    }
12278                    // c:3930-3933 — error_redir dup.
12279                    if fil_local != -1 && IS_ERROR_REDIR(fn_.typ) {
12280                        let dup_fd = unsafe { libc::dup(fil_local) };
12281                        dfil = movefd(dup_fd); // c:3931
12282                    } else {
12283                        dfil = 0; // c:3933
12284                    }
12285                    if fil_local == -1 || dfil == -1 {
12286                        // c:3934
12287                        if fil_local != -1 {
12288                            unsafe { libc::close(fil_local) }; // c:3935-3936
12289                        }
12290                        closemnodes(&mut mfds); // c:3937
12291                        fixfds(&save); // c:3938
12292                        let e = std::io::Error::last_os_error();
12293                        let raw = e.raw_os_error().unwrap_or(0);
12294                        if raw != 0 && raw != libc::EINTR {
12295                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
12296                            // c:3939-3940
12297                        }
12298                        {
12299                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12300                            LASTVAL.store(1, Ordering::Relaxed);
12301                        } // c:3941
12302                        break;
12303                    }
12304                    // c:3943
12305                    addfd(
12306                        forked,
12307                        &mut save,
12308                        &mut mfds,
12309                        fn_.fd1,
12310                        fil_local,
12311                        1,
12312                        fn_.varid.as_deref(),
12313                    );
12314                    if IS_ERROR_REDIR(fn_.typ) {
12315                        // c:3944-3945
12316                        addfd(forked, &mut save, &mut mfds, 2, dfil, 1, None);
12317                    }
12318                    let _ = &mut dfil;
12319                }
12320            }
12321            // c:3948-3952 — addfd errflag check.
12322            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12323                // c:3949
12324                closemnodes(&mut mfds); // c:3950
12325                fixfds(&save); // c:3951
12326                {
12327                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12328                    LASTVAL.store(1, Ordering::Relaxed);
12329                } // c:3952
12330                break;
12331            }
12332        }
12333    }
12334
12335    // c:3957-3961 — close multios with ct >= 2.
12336    i = 0;
12337    while i < 10 {
12338        // c:3959
12339        if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
12340            if m.ct >= 2 {
12341                closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
12342            }
12343        }
12344        i += 1;
12345    }
12346
12347    // c:3963-3995 — nullexec branch.
12348    if nullexec != 0 {
12349        // c:3963
12350        if let Some(vspc) = varspc {
12351            // c:3969
12352            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
12353            let mut removelist: Vec<String> = Vec::new();
12354            if !isset(POSIXBUILTINS) && nullexec != 2 {
12355                // c:3971-3972
12356                save_params(state, vspc, &mut restorelist, &mut removelist);
12357            }
12358            addvars(state, vspc, 0); // c:3973
12359            if !restorelist.is_empty() {
12360                // c:3974
12361                restore_params(restorelist, removelist); // c:3975
12362            }
12363        }
12364        let ef = errflag.load(Ordering::Relaxed);
12365        LASTVAL.store(
12366            if ef != 0 {
12367                ef
12368            } else {
12369                cmdoutval.load(Ordering::Relaxed)
12370            },
12371            Ordering::Relaxed,
12372        ); // c:3977
12373        if nullexec == 1 {
12374            // c:3978
12375            // c:3983-3985 — close save[i].
12376            i = 0;
12377            while i < 10 {
12378                if save[i as usize] != -2 {
12379                    let _ = zclose(save[i as usize]); // c:3985
12380                }
12381                i += 1;
12382            }
12383            // c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
12384            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12385            if thisjob >= 0 {
12386                if let Some(jt) = JOBTAB.get() {
12387                    let mut guard = jt.lock().unwrap();
12388                    if let Some(j) = guard.get_mut(thisjob as usize) {
12389                        j.stat |= STAT_DONE; // c:3989
12390                    }
12391                }
12392            }
12393            return execcmd_exec_done_path(
12394                redir_err,
12395                oautocont,
12396                how,
12397                &mut shti,
12398                &mut chti,
12399                &mut then_ts,
12400                forked,
12401                &mut newxtrerr,
12402                cflags,
12403                orig_cflags,
12404                is_cursh,
12405                do_exec,
12406            );
12407        }
12408        if isset(XTRACE) {
12409            // c:3992-3994
12410            eprintln!();
12411        }
12412    } else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
12413        // c:3996 — main dispatch branch.
12414        // c:3997 — `int q = queue_signal_level();`
12415        let _q = 0;
12416        // c:4003-4012 — entersubsh for is_exec.
12417        if is_exec != 0 {
12418            // c:4003
12419            let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
12420                esub::ASYNC
12421            } else {
12422                0
12423            } | esub::PGRP
12424                | esub::FAKE; // c:4004-4005
12425            if typ != WC_SUBSH as i32 {
12426                flags |= esub::KEEPTRAP; // c:4007
12427            }
12428            if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
12429                // c:4008-4009
12430                flags |= esub::REVERTPGRP; // c:4010
12431            }
12432            entersubsh(flags, None); // c:4011
12433        }
12434
12435        if typ == WC_FUNCDEF as i32 {
12436            // c:4013
12437            // c:4014-4036 — `redir_prog` setup from wordcode if no
12438            // redirs+WC_REDIR follows. Wire only when fusevm WC_REDIR
12439            // peek is in scope; for the tree-walker entry point we
12440            // approximate by passing None.
12441            let redir_prog: Option<crate::ported::zsh_h::Eprog> = None;
12442            // c:4039 — `lastval = execfuncdef(state, redir_prog);`
12443            let lv = execfuncdef(state, redir_prog);
12444            LASTVAL.store(lv, Ordering::Relaxed);
12445        } else if typ >= WC_CURSH as i32 {
12446            // c:4042
12447            if last1 == 1 {
12448                do_exec = 1; // c:4044
12449            }
12450            if typ == WC_AUTOFN as i32 {
12451                // c:4046
12452                let lv = execautofn_basic(state, do_exec); // c:4051
12453                LASTVAL.store(lv, Ordering::Relaxed);
12454            } else {
12455                // c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
12456                // dispatch_execfuncs ports the C `execfuncs[]` table
12457                // (Src/exec.c:268) by typ → exec{cursh,for,select,...}
12458                // direct call. See dispatch_execfuncs at end of file.
12459                let lv = dispatch_execfuncs(state, typ, do_exec);
12460                LASTVAL.store(lv, Ordering::Relaxed);
12461            }
12462        } else if is_builtin != 0 || is_shfunc != 0 {
12463            // c:4055
12464            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
12465            let mut removelist: Vec<String> = Vec::new();
12466            let mut do_save: i32 = 0; // c:4057
12467
12468            if forked == 0 {
12469                // c:4060
12470                if isset(POSIXBUILTINS) {
12471                    // c:4061
12472                    if is_shfunc != 0
12473                        || (hn.map(|p| unsafe { (*p).node.flags as u32 }).unwrap_or(0)
12474                            & (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
12475                            != 0
12476                    {
12477                        // c:4067
12478                        do_save = if (orig_cflags & BINF_COMMAND) != 0 {
12479                            1
12480                        } else {
12481                            0
12482                        };
12483                    } else {
12484                        do_save = 1; // c:4070
12485                    }
12486                } else {
12487                    // c:4071
12488                    if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
12489                        // c:4076
12490                        do_save = 1; // c:4077
12491                    }
12492                }
12493                if do_save != 0 {
12494                    if let Some(vspc) = varspc {
12495                        // c:4079
12496                        save_params(state, vspc, &mut restorelist, &mut removelist);
12497                    }
12498                }
12499            }
12500            if varspc.is_some() {
12501                // c:4082
12502                let mut addflags: i32 = 0; // c:4086
12503                if is_shfunc != 0 {
12504                    addflags |= ADDVAR_EXPORT; // c:4088
12505                }
12506                if !restorelist.is_empty() {
12507                    addflags |= ADDVAR_RESTORE; // c:4090
12508                }
12509                addvars(state, varspc.unwrap_or(0), addflags); // c:4092
12510                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12511                    // c:4093
12512                    if !restorelist.is_empty() {
12513                        restore_params(restorelist, removelist); // c:4094-4095
12514                    }
12515                    LASTVAL.store(1, Ordering::Relaxed); // c:4096
12516                    fixfds(&save); // c:4097
12517                    return execcmd_exec_done_path(
12518                        redir_err,
12519                        oautocont,
12520                        how,
12521                        &mut shti,
12522                        &mut chti,
12523                        &mut then_ts,
12524                        forked,
12525                        &mut newxtrerr,
12526                        cflags,
12527                        orig_cflags,
12528                        is_cursh,
12529                        do_exec,
12530                    );
12531                }
12532            }
12533
12534            if is_shfunc != 0 {
12535                // c:4102-4105
12536                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
12537                // c:4104 — `execshfunc((Shfunc) hn, args);` C casts
12538                // HashNode hn to Shfunc; zshrs's hn is *mut builtin so
12539                // we re-resolve the shfunc by name from shfunctab and
12540                // dispatch through the top-level execshfunc port at
12541                // exec.rs:4978 (which routes to runshfunc).
12542                let name = args
12543                    .as_ref()
12544                    .and_then(|v| v.first())
12545                    .cloned()
12546                    .unwrap_or_default();
12547                let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
12548                    tab.get(&name).cloned()
12549                } else {
12550                    None
12551                };
12552                if let Some(ref mut shf) = shf_clone {
12553                    execshfunc(shf, &mut a_vec);
12554                }
12555                // c:4105 — `pipecleanfilelist(filelist, 0);` — clean
12556                // out the proc_subst entries from the current job's
12557                // filelist after the shfunc body ran. Route through
12558                // `JOBTAB[thisjob]`.
12559                if let Some(jt) = JOBTAB.get() {
12560                    let mut guard = jt.lock().unwrap();
12561                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12562                    if tj >= 0 {
12563                        if let Some(j) = guard.get_mut(tj as usize) {
12564                            crate::ported::jobs::pipecleanfilelist(j, false);
12565                        }
12566                    }
12567                }
12568            } else {
12569                // c:4107 — builtin path.
12570                let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
12571                let postassigns = eparams.postassigns; // c:4109
12572                if forked != 0 {
12573                    closem(FDT_INTERNAL, 0); // c:4111
12574                }
12575                if postassigns != 0 {
12576                    // c:4112-4230 — typeset post-assignment processing.
12577                    use crate::ported::zsh_h::{
12578                        ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
12579                        PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
12580                        WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
12581                    };
12582                    let opc = state.pc; // c:4113
12583                    state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
12584                                                                      // c:4115 — `assigns = newlinklist();` — already declared above.
12585                    let mut pa_remaining = postassigns;
12586                    while pa_remaining > 0 {
12587                        // c:4116 — `while (postassigns--)`
12588                        pa_remaining -= 1;
12589                        let mut pa_htok: i32 = 0; // c:4117
12590                        if state.pc >= state.prog.prog.len() {
12591                            break;
12592                        }
12593                        let ac = state.prog.prog[state.pc]; // c:4118
12594                        state.pc += 1;
12595                        let mut name = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut pa_htok)); // c:4119
12596                                                                                            // c:4123-4124 DPUTS — debug assertion skipped.
12597                        if pa_htok != 0 {
12598                            // c:4126 — `init_list1(svl, name);`
12599                            let mut svl: LinkList<String> = Default::default();
12600                            svl.push_back(name.clone());
12601                            // c:4127-4166 — INC-scalar special case (typeset $ass form).
12602                            if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
12603                                && WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
12604                            {
12605                                // c:4141 — `(void)ecgetstr(...)` — dummy.
12606                                let mut dummy_htok: i32 = 0;
12607                                let _ = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut dummy_htok));
12608                                let mut rf = 0i32;
12609                                prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
12610                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12611                                    // c:4143
12612                                    state.pc = opc; // c:4144
12613                                    break;
12614                                }
12615                                let mut rf2 = 0i32;
12616                                globlist(&mut svl, rf2); // c:4147
12617                                let _ = &mut rf2;
12618                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12619                                    // c:4148
12620                                    state.pc = opc; // c:4149
12621                                    break;
12622                                }
12623                                // c:4152-4165 — drain svl into assigns.
12624                                while let Some(data) = svl.pop_front() {
12625                                    let (asg_name, asg_val): (String, Option<String>) =
12626                                        if let Some(eq_pos) = data.find('=') {
12627                                            // c:4156-4159
12628                                            (
12629                                                data[..eq_pos].to_string(),
12630                                                Some(data[eq_pos + 1..].to_string()),
12631                                            )
12632                                        } else {
12633                                            // c:4161-4162
12634                                            (data, None)
12635                                        };
12636                                    assigns.push(crate::ported::zsh_h::asgment {
12637                                        node: crate::ported::zsh_h::linknode {
12638                                            next: None,
12639                                            prev: None,
12640                                            dat: 0,
12641                                        },
12642                                        name: asg_name,
12643                                        flags: 0,
12644                                        scalar: asg_val,
12645                                        array: None,
12646                                    });
12647                                }
12648                                continue; // c:4166
12649                            }
12650                            // c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
12651                            let mut rf = 0i32;
12652                            prefork(&mut svl, PREFORK_SINGLE, &mut rf);
12653                            // c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
12654                            name = if svl.is_empty() {
12655                                String::new()
12656                            } else {
12657                                svl.pop_front().unwrap_or_default()
12658                            };
12659                        }
12660                        // c:4172 — `untokenize(name);`
12661                        // (untokenize is destructive on bytes; Rust untokenize
12662                        // returns a new String — call and rebind.)
12663                        name = untokenize(&name);
12664                        let mut asg = crate::ported::zsh_h::asgment {
12665                            node: crate::ported::zsh_h::linknode {
12666                                next: None,
12667                                prev: None,
12668                                dat: 0,
12669                            },
12670                            name,
12671                            flags: 0,
12672                            scalar: None,
12673                            array: None,
12674                        };
12675                        if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
12676                            // c:4175
12677                            let mut val_htok: i32 = 0;
12678                            let mut val = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut val_htok)); // c:4176
12679                            asg.flags = 0; // c:4177
12680                            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
12681                                // c:4178-4180 — fake assignment, no value.
12682                                asg.scalar = None;
12683                            } else {
12684                                if val_htok != 0 {
12685                                    // c:4183
12686                                    let mut svl: LinkList<String> = Default::default();
12687                                    svl.push_back(val.clone());
12688                                    let mut rf = 0i32;
12689                                    prefork(&mut svl, PREFORK_SINGLE | PREFORK_ASSIGN, &mut rf); // c:4184-4186
12690                                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12691                                        // c:4187
12692                                        state.pc = opc; // c:4188
12693                                        break;
12694                                    }
12695                                    // c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
12696                                    val = if svl.is_empty() {
12697                                        String::new()
12698                                    } else {
12699                                        svl.pop_front().unwrap_or_default()
12700                                    };
12701                                }
12702                                // c:4198 — `untokenize(val);`
12703                                asg.scalar = Some(untokenize(&val));
12704                            }
12705                        } else {
12706                            // c:4202 — array assignment.
12707                            asg.flags = ASG_ARRAY; // c:4202
12708                            let mut arr_htok: i32 = 0;
12709                            let arr_words = ecgetlist(
12710                                state,
12711                                WC_ASSIGN_NUM(ac) as usize,
12712                                ECDUPTOK_LOCAL,
12713                                Some(&mut arr_htok),
12714                            ); // c:4204
12715                            let mut arr_list: LinkList<String> = Default::default();
12716                            for s in arr_words {
12717                                arr_list.push_back(s);
12718                            }
12719                            if !arr_list.is_empty()
12720                                && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
12721                            {
12722                                // c:4209 — `int prefork_ret = 0;`
12723                                let mut prefork_ret = 0i32;
12724                                prefork(&mut arr_list, PREFORK_ASSIGN, &mut prefork_ret); // c:4210-4211
12725                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12726                                    // c:4212
12727                                    state.pc = opc; // c:4213
12728                                    break;
12729                                }
12730                                if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
12731                                    // c:4216
12732                                    asg.flags |= ASG_KEY_VALUE; // c:4217
12733                                }
12734                                globlist(&mut arr_list, prefork_ret); // c:4218
12735                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12736                                    // c:4220
12737                                    state.pc = opc; // c:4221
12738                                    break;
12739                                }
12740                            }
12741                            asg.array = Some(arr_list);
12742                        }
12743                        // c:4227 — `uaddlinknode(assigns, &asg->node);`
12744                        assigns.push(asg);
12745                    }
12746                    state.pc = opc; // c:4229
12747                }
12748                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
12749                    // c:4232
12750                    // c:Src/builtin.c:262 — `name = (char *) ugetnode(args);`
12751                    // C's execbuiltin consumes args[0] (the command name)
12752                    // at entry. zshrs's execbuiltin reads the name from
12753                    // `bn->node.nam` instead, so we strip args[0] here
12754                    // before the call to match C's post-ugetnode argv
12755                    // shape. Without this, e.g. `cmd=pwd; $cmd` reached
12756                    // execbuiltin with args=["pwd"] and pwd's
12757                    // maxargs=0 check rejected the empty call as
12758                    // "too many arguments".
12759                    let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
12760                    if !a_vec.is_empty() {
12761                        a_vec.remove(0);
12762                    }
12763                    let ret = crate::ported::builtin::execbuiltin(
12764                        a_vec,
12765                        assigns,
12766                        hn.unwrap_or(std::ptr::null_mut()),
12767                    ); // c:4233
12768                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
12769                        // c:4238
12770                        LASTVAL.store(ret, Ordering::Relaxed); // c:4239
12771                    }
12772                }
12773                if (do_save & BINF_COMMAND as i32) != 0 {
12774                    // c:4241
12775                    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
12776                }
12777                // c:4244 fflush(stdout) — Rust stdio auto-flushes.
12778                // c:4245-4251 — write-error check on save[1].
12779            }
12780            if isset(PRINTEXITVALUE)
12781                && isset(SHINSTDIN)
12782                && LASTVAL.load(Ordering::Relaxed) != 0
12783                && subsh.load(Ordering::Relaxed) == 0
12784            {
12785                // c:4253-4255
12786                eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
12787            }
12788
12789            if do_exec != 0 {
12790                // c:4263
12791                if subsh.load(Ordering::Relaxed) != 0 {
12792                    crate::ported::builtin::_realexit(); // c:4264-4265
12793                }
12794                if isset(RCS)
12795                    && crate::ported::zsh_h::interact()
12796                    && nohistsave.load(Ordering::Relaxed) == 0
12797                {
12798                    // c:4269
12799                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
12800                    // c:4270
12801                }
12802                crate::ported::builtin::realexit(); // c:4271
12803            }
12804            if !restorelist.is_empty() {
12805                // c:4273
12806                restore_params(restorelist, removelist); // c:4274
12807            }
12808        } else {
12809            // c:4276 — external command execute.
12810            if subsh.load(Ordering::Relaxed) == 0 {
12811                // c:4277
12812                if forked == 0 {
12813                    // c:4280 — `setiparam("SHLVL", --shlvl);`
12814                    let cur = getsparam("SHLVL")
12815                        .and_then(|s| s.parse::<i64>().ok())
12816                        .unwrap_or(1);
12817                    setiparam("SHLVL", cur - 1); // c:4281
12818                }
12819                if do_exec != 0
12820                    && isset(RCS)
12821                    && crate::ported::zsh_h::interact()
12822                    && nohistsave.load(Ordering::Relaxed) == 0
12823                {
12824                    // c:4285
12825                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
12826                    // c:4286
12827                }
12828            }
12829            if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
12830                // c:4288
12831                if varspc.is_some() {
12832                    // c:4289
12833                    let mut addflags: i32 = ADDVAR_EXPORT; // c:4290
12834                    if forked != 0 {
12835                        addflags |= ADDVAR_RESTORE; // c:4292
12836                    }
12837                    addvars(state, varspc.unwrap_or(0), addflags); // c:4293
12838                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12839                        // c:4294
12840                        std::process::exit(1); // c:4295
12841                    }
12842                }
12843                closem(FDT_INTERNAL, 0); // c:4297
12844                                         // c:4298-4305 — close coprocin/coprocout.
12845                let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
12846                if cpi != -1 {
12847                    let _ = zclose(cpi); // c:4299
12848                    crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed);
12849                    // c:4300
12850                }
12851                let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
12852                if cpo != -1 {
12853                    let _ = zclose(cpo); // c:4303
12854                    crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed);
12855                    // c:4304
12856                }
12857                if forked == 0 {
12858                    // c:4307
12859                    setlimits(""); // c:4308
12860                }
12861                if (how & Z_ASYNC as i32) != 0 {
12862                    // c:4310 — `zsfree(STTYval); STTYval = 0;`
12863                    let mut guard = STTYval.lock().unwrap();
12864                    *guard = None; // c:4311-4312
12865                }
12866                // c:4314 — `execute(args, cflags, use_defpath);`
12867                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
12868                execute(&mut a_vec, cflags, use_defpath); // c:4314
12869            } else {
12870                // c:4315 — `( ... )` — WC_SUBSH.
12871                list_pipe.store(0, Ordering::Relaxed); // c:4318
12872                                                       // c:4319 — `pipecleanfilelist(filelist, 0);` — clean
12873                                                       // proc-subst entries from the current job's filelist
12874                                                       // before recursing into the subshell body.
12875                if let Some(jt) = JOBTAB.get() {
12876                    let mut guard = jt.lock().unwrap();
12877                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12878                    if tj >= 0 {
12879                        if let Some(j) = guard.get_mut(tj as usize) {
12880                            crate::ported::jobs::pipecleanfilelist(j, false);
12881                        }
12882                    }
12883                }
12884                state.pc += 1; // c:4324 — `state->pc++;`
12885                let _ = execlist(state, 0, 1); // c:4325
12886            }
12887        }
12888    }
12889
12890    // c:4330-4404 — err: + done: + fatal:.
12891    return execcmd_exec_done_path(
12892        redir_err,
12893        oautocont,
12894        how,
12895        &mut shti,
12896        &mut chti,
12897        &mut then_ts,
12898        forked,
12899        &mut newxtrerr,
12900        cflags,
12901        orig_cflags,
12902        is_cursh,
12903        do_exec,
12904    );
12905}
12906
12907/// !!! WARNING: RUST-ONLY HELPER !!!
12908///
12909/// No function of this name exists in `Src/exec.c`. This helper stands in for
12910/// the `done:` label tail of `execcmd_exec` (c:4366-4403), which C reaches by
12911/// `goto` and Rust cannot.
12912/// Internal helper modelling the C `done:` label tail of
12913/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
12914/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
12915/// shelltime stop, and newxtrerr close.
12916#[allow(clippy::too_many_arguments)]
12917fn execcmd_exec_done_path(
12918    redir_err: i32,
12919    oautocont: i32,
12920    how: i32,
12921    shti: &mut crate::ported::jobs::timeinfo,
12922    chti: &mut crate::ported::jobs::timeinfo,
12923    then_ts: &mut std::time::Instant,
12924    forked: i32,
12925    newxtrerr: &mut Option<i32>,
12926    cflags: u32,
12927    orig_cflags: u32,
12928    is_cursh: i32,
12929    do_exec: i32,
12930) {
12931    use crate::ported::zsh_h::{
12932        AUTOCONTINUE, BINF_COMMAND, BINF_EXEC, BINF_PSPECIAL, INTERACTIVE, POSIXBUILTINS, Z_TIMED,
12933    };
12934    // c:4366
12935    // c:4367-4386 — POSIX special-builtin error escalation.
12936    if isset(POSIXBUILTINS)
12937        && (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
12938        && (orig_cflags & BINF_COMMAND) == 0
12939    {
12940        // c:4367-4369
12941        let _forked_or_subsh = forked | zsh_subshell.load(Ordering::Relaxed); // c:4376
12942                                                                              // fatal: label entry point — same handling.
12943        if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12944            // c:4378
12945            if !isset(INTERACTIVE) {
12946                // c:4379
12947                if _forked_or_subsh != 0 {
12948                    unsafe { libc::_exit(1) }; // c:4381
12949                } else {
12950                    std::process::exit(1); // c:4383
12951                }
12952            }
12953            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
12954        }
12955    }
12956    // c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
12957    if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
12958        crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1);
12959        // c:4389
12960    }
12961    // c:4445-4452 — newxtrerr close.
12962    if let Some(fd) = newxtrerr.take() {
12963        // c:4445
12964        crate::fusevm_bridge::xtrerr_flush(); // c:4448 fclose(newxtrerr)
12965        crate::ported::utils::xtrerr.store(2, Ordering::Relaxed); // c:4449 xtrerr = oxtrerr
12966        let _ = zclose(fd); // c:4451
12967    }
12968    // c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
12969    {
12970        let mut guard = STTYval.lock().unwrap();
12971        *guard = None;
12972    }
12973    // c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
12974    if oautocont >= 0 {
12975        opt_state_set("autocontinue", oautocont != 0);
12976    }
12977}
12978
12979/// !!! WARNING: RUST-ONLY HELPER !!!
12980///
12981/// No function of this name exists in `Src/exec.c`. This helper stands in for
12982/// the `err:` label tail of `execcmd_exec` (c:4330-4365), which C reaches by
12983/// `goto` and Rust cannot.
12984/// Internal helper modelling the C `err:` label tail of
12985/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
12986/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
12987/// to done:.
12988#[allow(clippy::too_many_arguments)]
12989fn execcmd_exec_err_path(
12990    forked: i32,
12991    save: &mut [i32; 10],
12992    mfds: &mut [Option<Box<multio>>; 10],
12993    oautocont: i32,
12994    how: i32,
12995    shti: &mut crate::ported::jobs::timeinfo,
12996    chti: &mut crate::ported::jobs::timeinfo,
12997    then_ts: &mut std::time::Instant,
12998    newxtrerr: &mut Option<i32>,
12999    cflags: u32,
13000    orig_cflags: u32,
13001    is_cursh: i32,
13002    do_exec: i32,
13003    redir_err: i32,
13004) {
13005    use crate::ported::zsh_h::FDT_UNUSED;
13006    // c:4330
13007    if forked != 0 {
13008        // c:4331
13009        // c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
13010        let mut i: i32 = 0;
13011        while i < 10 {
13012            if fdtable_get(i) != FDT_UNUSED {
13013                unsafe { libc::close(i) }; // c:4358
13014            }
13015            i += 1;
13016        }
13017        // c:4359 — `closem(FDT_UNUSED, 1);`
13018        closem(FDT_UNUSED, 1); // c:4359
13019                               // c:4360-4361 — `if (thisjob != -1) waitjobs();`
13020        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
13021        if thisjob != -1 {
13022            if let Some(jt) = JOBTAB.get() {
13023                let mut guard = jt.lock().unwrap();
13024                crate::ported::jobs::waitjobs(&mut guard, thisjob as usize); // c:4361
13025            }
13026        }
13027        crate::ported::builtin::_realexit(); // c:4362
13028    }
13029    fixfds(save); // c:4364
13030
13031    execcmd_exec_done_path(
13032        redir_err,
13033        oautocont,
13034        how,
13035        shti,
13036        chti,
13037        then_ts,
13038        forked,
13039        newxtrerr,
13040        cflags,
13041        orig_cflags,
13042        is_cursh,
13043        do_exec,
13044    );
13045}
13046
13047/// !!! WARNING: RUST-ONLY HELPER !!!
13048///
13049/// No function of this name exists in `Src/exec.c`. This helper stands in for
13050/// the `execfuncs[type - WC_CURSH]` function-pointer table dispatch (table at
13051/// c:268, call sites at c:1331 and c:4053), written as a `match` because Rust
13052/// has no equivalent table of `int (*)(Estate, int)`.
13053/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
13054/// `Src/exec.c:268`. Each branch maps to the ported wordcode-
13055/// walker function in `src/ported/exec.rs`.
13056fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
13057    use crate::ported::zsh_h::{
13058        WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
13059        WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
13060    };
13061    // Port of `static int (*const execfuncs[])(Estate, int)` dispatch
13062    // table at `Src/exec.c:268`. C indexes by `(type - WC_CURSH)`;
13063    // Rust matches on the WC_* tag directly.
13064    match typ as wordcode {
13065        x if x == WC_CURSH => execcursh(state, do_exec),
13066        x if x == WC_FOR => execfor(state, do_exec),
13067        x if x == WC_SELECT => execselect(state, do_exec),
13068        x if x == WC_WHILE => execwhile(state, do_exec),
13069        x if x == WC_REPEAT => execrepeat(state, do_exec),
13070        x if x == WC_CASE => execcase(state, do_exec),
13071        x if x == WC_IF => execif(state, do_exec),
13072        x if x == WC_COND => execcond(state, do_exec),
13073        x if x == WC_ARITH => execarith(state, do_exec),
13074        x if x == WC_TRY => exectry(state, do_exec),
13075        x if x == WC_FUNCDEF => execfuncdef(state, None),
13076        // c:272 — execfuncs[] table dispatches `WC_AUTOFN` to
13077        // `execautofn` (the loadautofn-then-basic wrapper), not
13078        // `execautofn_basic` directly.
13079        x if x == WC_AUTOFN => execautofn(state, do_exec),
13080        x if x == WC_TIMED => exectime(state, do_exec),
13081        x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
13082        _ => 0,
13083    }
13084}
13085
13086/// Port of `stripkshdef()` from `Src/exec.c:6292` — C decl `stripkshdef(Eprog prog, char *name)`.
13087/// Given an Eprog read from an autoload
13088/// file plus the function name being defined, check whether the
13089/// file consists of *exactly* one `function NAME { … }` definition
13090/// for that name. If so, return a new Eprog whose `prog`/`strs`/
13091/// `pats` slice out just the function body (so calling code can
13092/// invoke the body directly instead of re-parsing). Otherwise
13093/// return the input untouched.
13094///
13095/// Header word layout consumed (matches C `pc[…]` reads):
13096///   pc[0] = WC_LIST with `Z_SYNC|Z_END|Z_SIMPLE` flags
13097///   pc[1] = (sublist header, skipped)
13098///   pc[2] = WC_FUNCDEF
13099///   pc[3] = 1                       (single-name funcdef)
13100///   pc[4] = name-string slot        (compared to `name`)
13101///   pc[5] = sbeg  (offset into strs table)
13102///   pc[6] = nstrs (bytes of strs to copy)
13103///   pc[7] = npats (number of pattern slots to allocate)
13104///   pc[8] = WC_FUNCDEF_SKIP target  (end-of-funcdef pc)
13105///   pc[9] = (unused header word — `pc += 6` lands here as the
13106///           start of the body wordcode stream)
13107///
13108/// Returns `None` only when the input was `None` (matches C
13109/// `return NULL`). Equivalence between the original `prog` and a
13110/// successfully stripped `prog` is *not* preserved at the pointer
13111/// level (C may return the original Eprog when the file fails the
13112/// single-funcdef shape check; this Rust port does the same by
13113/// passing the box back through).
13114///
13115/// `EF_MAP` (`zcompile`d / mmap'd Eprog) path: C mutates the
13116/// existing Eprog in place, swapping its `prog` / `strs` /
13117/// `pats` to slice into the funcdef body. Rust mirrors this on
13118/// the moved-in `Box<eprog>` (no separate `free()` needed —
13119/// `Vec` drop handles the old `pats`).
13120pub fn stripkshdef(
13121    prog: Option<crate::ported::zsh_h::Eprog>,
13122    name: &str,
13123) -> Option<crate::ported::zsh_h::Eprog> {
13124    use crate::ported::parse::ecrawstr;
13125    use crate::ported::zsh_h::{
13126        wc_code, wordcode, Dash, EF_HEAP, EF_MAP, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_LIST,
13127        WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC,
13128    };
13129
13130    // c:6300 — `if (!prog) return NULL;`
13131    let mut prog = prog?;
13132
13133    // c:6302-6306 — first word must be WC_LIST with all of
13134    // Z_SYNC|Z_END|Z_SIMPLE set (i.e. the trivial "single simple
13135    // sublist" wrapper around the funcdef).
13136    if prog.prog.len() < 3 {
13137        return Some(prog);
13138    }
13139    let code0: wordcode = prog.prog[0];
13140    if wc_code(code0) != WC_LIST
13141        || (WC_LIST_TYPE(code0) & (Z_SYNC | Z_END | Z_SIMPLE) as wordcode)
13142            != (Z_SYNC | Z_END | Z_SIMPLE) as wordcode
13143    {
13144        return Some(prog);
13145    }
13146    // c:6307 — `pc++;` (skip the sublist header word at pc[1]).
13147    // c:6308 — `code = *pc++;` lands `code` on pc[2], leaving the
13148    // walking cursor at pc[3] which is read directly below.
13149    let code: wordcode = prog.prog[2];
13150    let pc_after_code: usize = 3;
13151    if wc_code(code) != WC_FUNCDEF || prog.prog[pc_after_code] != 1 {
13152        return Some(prog);
13153    }
13154
13155    // c:6320 — `ptr2 = ecrawstr(prog, pc + 1, NULL);` (note: C's
13156    // `pc` is already past `code`, so `pc + 1` lands on pc[4] —
13157    // the name-string slot).
13158    let name_slot = pc_after_code + 1; // == 4
13159    let name_in_def = ecrawstr(&prog, name_slot, None);
13160
13161    // c:6383-6388 — name match, tolerating Dash-tokenised hyphens
13162    // on either side. C walks bytes, and there the Dash token is ONE
13163    // byte; the port's String holds it as the char U+e19B (three UTF-8
13164    // bytes), so the walk must be per char. Walking `as_bytes()` put the
13165    // two sides out of step at the first hyphen, so a ksh-style
13166    // `foo-bar() { … }` autoload file was never recognised as its own
13167    // definition and the first call only defined the function.
13168    let n1: Vec<char> = name.chars().collect();
13169    let n2: Vec<char> = name_in_def.chars().collect();
13170    let mut i = 0usize;
13171    let mut j = 0usize;
13172    while i < n1.len() && j < n2.len() {
13173        let c1 = n1[i];
13174        let c2 = n2[j];
13175        if c1 != c2 && c1 != Dash && c1 != '-' && c2 != Dash && c2 != '-' {
13176            break;
13177        }
13178        i += 1;
13179        j += 1;
13180    }
13181    // c:6329 — `if (*ptr1 || *ptr2) return prog;` (any unmatched
13182    // tail on either side → not the right funcdef).
13183    if i < n1.len() || j < n2.len() {
13184        return Some(prog);
13185    }
13186
13187    // c:6332-6362 — slice the funcdef body out. Layout:
13188    //   sbeg  = pc[2] (in C, == prog.prog[pc_after_code + 2] == [5])
13189    //   nstrs = pc[3] (== [6])
13190    //   npats = pc[4] (== [7])
13191    //   end   = pc + WC_FUNCDEF_SKIP(code)   (== pc_after_code + skip)
13192    //   pc   += 6  (body wordcode begins at pc_after_code + 6 == [9])
13193    let sbeg = prog.prog[pc_after_code + 2] as usize;
13194    let nstrs = prog.prog[pc_after_code + 3] as usize;
13195    let npats = prog.prog[pc_after_code + 4] as i32;
13196    let skip = WC_FUNCDEF_SKIP(code) as usize;
13197    let end_pc = pc_after_code + skip;
13198    let body_start = pc_after_code + 6;
13199    if end_pc < body_start || end_pc > prog.prog.len() {
13200        // Defensive: malformed header — return input untouched so
13201        // the caller's parse-eprog fallback re-reads from source.
13202        return Some(prog);
13203    }
13204    let nprg = end_pc - body_start;
13205    let plen = nprg * size_of::<wordcode>();
13206    let len = plen + (npats as usize) * size_of::<usize>() + nstrs;
13207
13208    // Build the new pats slice — `dummy_patprog1` slots in C; the
13209    // Rust convention (mirrors `dupeprog` at parse.rs:2716) is to
13210    // synthesize zero-initialised patprog placeholders that
13211    // pattern compile-on-first-use will overwrite.
13212    let dummy_pat = || {
13213        Box::new(crate::ported::zsh_h::patprog {
13214            startoff: 0,
13215            size: 0,
13216            mustoff: 0,
13217            patmlen: 0,
13218            globflags: 0,
13219            globend: 0,
13220            flags: 0,
13221            patnpar: 0,
13222            patstartch: 0,
13223        })
13224    };
13225    let new_pats: Vec<crate::ported::zsh_h::Patprog> =
13226        (0..npats.max(0)).map(|_| dummy_pat()).collect();
13227
13228    // c:6353 — `ret->strs = prog->strs + sbeg;` (EF_MAP) or
13229    // c:6359 — `memcpy(ret->strs, prog->strs + sbeg, nstrs);` (heap).
13230    let strs_metafied = prog.strs_metafied; // pool sliced verbatim — carry provenance
13231    let old_strs = prog.strs.take().unwrap_or_default();
13232    let old_bytes = old_strs.as_bytes();
13233    let new_strs = if sbeg + nstrs <= old_bytes.len() {
13234        Some(String::from_utf8_lossy(&old_bytes[sbeg..sbeg + nstrs]).into_owned())
13235    } else {
13236        Some(String::new())
13237    };
13238
13239    let new_prog: Vec<wordcode> = prog.prog[body_start..end_pc].to_vec();
13240
13241    if (prog.flags & EF_MAP) != 0 {
13242        // c:6349-6354 — in-place EF_MAP path.
13243        prog.pats = new_pats;
13244        prog.prog = new_prog;
13245        prog.strs = new_strs;
13246        prog.len = len as i32;
13247        prog.npats = npats;
13248        prog.shf = None;
13249        return Some(prog);
13250    }
13251
13252    // c:6356-6361 — heap-allocated new Eprog.
13253    let ret = Box::new(eprog {
13254        flags: EF_HEAP,
13255        len: len as i32,
13256        npats,
13257        nref: -1, // c:6363 (heap path → never refcount-freed).
13258        pats: new_pats,
13259        prog: new_prog,
13260        strs: new_strs,
13261        shf: None, // c:6363
13262        dump: None,
13263        strs_metafied, // c:6353/6359 — the slice keeps the source pool's encoding
13264    });
13265    Some(ret)
13266}
13267
13268#[cfg(test)]
13269mod tests {
13270    use super::*;
13271
13272    // ─── zsh-corpus pins for pure exec helpers ─────────────────────
13273
13274    /// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
13275    #[test]
13276    fn exec_corpus_isrelative_empty_is_one() {
13277        let _g = crate::test_util::global_state_lock();
13278        assert_eq!(isrelative(""), 1, "empty path is relative");
13279    }
13280
13281    /// `isrelative("foo")` = 1 (no leading slash).
13282    #[test]
13283    fn exec_corpus_isrelative_bare_name_is_one() {
13284        let _g = crate::test_util::global_state_lock();
13285        assert_eq!(isrelative("foo"), 1);
13286        assert_eq!(isrelative("bin/cmd"), 1);
13287    }
13288
13289    /// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
13290    #[test]
13291    fn exec_corpus_isrelative_absolute_clean_is_zero() {
13292        let _g = crate::test_util::global_state_lock();
13293        assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
13294        assert_eq!(isrelative("/bin/ls"), 0);
13295        assert_eq!(isrelative("/"), 0, "root is absolute");
13296    }
13297
13298    /// `isrelative("/foo/../bar")` = 1 (contains `../` component).
13299    #[test]
13300    fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
13301        let _g = crate::test_util::global_state_lock();
13302        assert_eq!(
13303            isrelative("/foo/../bar"),
13304            1,
13305            "absolute path with ../ is still 'relative' per zsh"
13306        );
13307    }
13308
13309    /// `isrelative("/foo/./bar")` = 1 (contains `./` component).
13310    #[test]
13311    fn exec_corpus_isrelative_absolute_with_dot_is_one() {
13312        let _g = crate::test_util::global_state_lock();
13313        assert_eq!(
13314            isrelative("/./x"),
13315            1,
13316            "absolute with ./ component reported relative"
13317        );
13318    }
13319
13320    /// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
13321    #[test]
13322    fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
13323        assert_eq!(is_anonymous_function_name("(anon)"), 1);
13324    }
13325
13326    /// `is_anonymous_function_name("regular_name")` = 0.
13327    #[test]
13328    fn exec_corpus_is_anonymous_function_name_rejects_normal() {
13329        assert_eq!(is_anonymous_function_name("regular_name"), 0);
13330        assert_eq!(is_anonymous_function_name(""), 0);
13331        assert_eq!(
13332            is_anonymous_function_name("anon"),
13333            0,
13334            "plain 'anon' (no parens) is NOT the sentinel"
13335        );
13336    }
13337
13338    /// `iscom("/nonexistent/never_a_path")` = false.
13339    #[test]
13340    fn exec_corpus_iscom_missing_path_false() {
13341        assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
13342    }
13343
13344    /// `iscom("/tmp")` is a directory not a regular file → false.
13345    #[test]
13346    fn exec_corpus_iscom_directory_false() {
13347        assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
13348    }
13349
13350    /// `iscom("/bin/sh")` is true on POSIX systems.
13351    #[test]
13352    fn exec_corpus_iscom_known_binary_true() {
13353        // /bin/sh exists on all POSIX systems with X perms.
13354        if std::path::Path::new("/bin/sh").exists() {
13355            assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
13356        }
13357    }
13358
13359    // ─── stripkshdef (Src/exec.c:6292) early-return paths ──────────
13360
13361    /// `stripkshdef(None, "foo")` → `None` (matches C `if (!prog)
13362    /// return NULL;` at exec.c:6300).
13363    #[test]
13364    fn exec_corpus_stripkshdef_null_input_returns_none() {
13365        assert!(stripkshdef(None, "foo").is_none());
13366    }
13367
13368    /// `stripkshdef` on an empty/degenerate Eprog returns the same
13369    /// Eprog unchanged (no funcdef-shape to strip).
13370    #[test]
13371    fn exec_corpus_stripkshdef_empty_prog_returns_input() {
13372        let prog = Box::new(eprog {
13373            prog: vec![],
13374            ..Default::default()
13375        });
13376        let out = stripkshdef(Some(prog), "foo");
13377        assert!(out.is_some(), "empty prog → returned unchanged");
13378        assert!(out.unwrap().prog.is_empty(), "no mutation");
13379    }
13380
13381    /// `stripkshdef` on a non-WC_LIST head returns the input
13382    /// untouched (early return at exec.c:6304-6306).
13383    #[test]
13384    fn exec_corpus_stripkshdef_non_list_head_returns_input() {
13385        use crate::ported::zsh_h::{wc_bld, WC_SUBLIST};
13386        let prog = Box::new(eprog {
13387            prog: vec![wc_bld(WC_SUBLIST, 0), 0, 0],
13388            ..Default::default()
13389        });
13390        let out = stripkshdef(Some(prog), "foo");
13391        assert!(out.is_some());
13392        // first word is the WC_SUBLIST sentinel we passed in,
13393        // unchanged (the function bailed before doing any slicing).
13394        let p = out.unwrap();
13395        use crate::ported::zsh_h::wc_code;
13396        assert_eq!(
13397            wc_code(p.prog[0]),
13398            WC_SUBLIST,
13399            "header word preserved verbatim"
13400        );
13401    }
13402
13403    // ═══════════════════════════════════════════════════════════════════
13404    // C-parity tests pinning Src/exec.c. Tests that capture KNOWN
13405    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
13406    // ═══════════════════════════════════════════════════════════════════
13407
13408    /// `isrelative("/abs/path")` returns 0 (false = absolute path).
13409    /// C `Src/exec.c:996-1006` — leading `/` and no `.`/`..` components.
13410    #[test]
13411    fn isrelative_absolute_path_returns_zero() {
13412        let _g = crate::test_util::global_state_lock();
13413        assert_eq!(isrelative("/usr/local/bin"), 0);
13414    }
13415
13416    /// `isrelative("foo/bar")` returns 1 (no leading slash).
13417    #[test]
13418    fn isrelative_no_leading_slash_returns_one() {
13419        let _g = crate::test_util::global_state_lock();
13420        assert_eq!(isrelative("foo/bar"), 1);
13421    }
13422
13423    /// `isrelative("/foo/./bar")` returns 1 — contains `/./` walk.
13424    /// C c:1001 — `.` with prev `/` + next `/` triggers relative flag.
13425    #[test]
13426    fn isrelative_dot_component_returns_one() {
13427        let _g = crate::test_util::global_state_lock();
13428        assert_eq!(isrelative("/foo/./bar"), 1, "/./ in path → relative");
13429    }
13430
13431    /// `isrelative("/foo/../bar")` returns 1 — contains `/..` walk.
13432    #[test]
13433    fn isrelative_dotdot_component_returns_one() {
13434        let _g = crate::test_util::global_state_lock();
13435        assert_eq!(isrelative("/foo/../bar"), 1, "/../ in path → relative");
13436    }
13437
13438    /// `isrelative("")` returns 1 — empty input has no leading `/`.
13439    /// C c:998 — `*s != '/'` includes the NUL terminator case.
13440    #[test]
13441    fn isrelative_empty_returns_one() {
13442        let _g = crate::test_util::global_state_lock();
13443        assert_eq!(isrelative(""), 1, "empty string → not absolute");
13444    }
13445
13446    /// `isrelative("/a/.b")` returns 0 — `.b` is NOT a `/./` walk
13447    /// (followed by another non-`/` char `b`).
13448    #[test]
13449    fn isrelative_dotfile_in_path_returns_zero() {
13450        let _g = crate::test_util::global_state_lock();
13451        assert_eq!(
13452            isrelative("/usr/.config/zsh"),
13453            0,
13454            "dotfile name '.config' is NOT a relative walk"
13455        );
13456    }
13457
13458    /// `is_anonymous_function_name("(anon)")` returns 1 (true).
13459    /// C `Src/exec.c` — `!strcmp(name, ANONYMOUS_FUNCTION_NAME)`.
13460    #[test]
13461    fn is_anonymous_function_name_anon_returns_one() {
13462        let _g = crate::test_util::global_state_lock();
13463        assert_eq!(is_anonymous_function_name("(anon)"), 1);
13464    }
13465
13466    /// `is_anonymous_function_name("foo")` returns 0 (false).
13467    #[test]
13468    fn is_anonymous_function_name_normal_returns_zero() {
13469        let _g = crate::test_util::global_state_lock();
13470        assert_eq!(is_anonymous_function_name("foo"), 0);
13471        assert_eq!(is_anonymous_function_name(""), 0);
13472        assert_eq!(is_anonymous_function_name("(other)"), 0);
13473    }
13474
13475    /// `isgooderr(EACCES, "/no/such/dir")` returns true when the dir
13476    /// is not actually accessible. C `Src/exec.c:isgooderr` filters
13477    /// out "unreadable / not directory" errnos so caller doesn't
13478    /// emit spurious warnings.
13479    #[test]
13480    fn isgooderr_eacces_unreadable_dir_returns_false() {
13481        let _g = crate::test_util::global_state_lock();
13482        // /no/such/dir doesn't exist → access(X_OK) fails non-zero
13483        // → !access() is 0 (false) → returns false.
13484        assert!(
13485            !isgooderr(libc::EACCES, "/no/such/dir/zshrs_test"),
13486            "unreadable dir with EACCES should NOT be 'good error'"
13487        );
13488    }
13489
13490    // ═══════════════════════════════════════════════════════════════════
13491    // Additional C-parity tests for Src/exec.c basic accessors/predicates.
13492    // ═══════════════════════════════════════════════════════════════════
13493
13494    /// c:658 — `isgooderr(ENOENT, _)` always false (regardless of dir).
13495    /// Pin: ENOENT is NEVER a "good error" because the path itself
13496    /// doesn't exist — caller should suppress the warning.
13497    #[test]
13498    fn isgooderr_enoent_always_false() {
13499        let _g = crate::test_util::global_state_lock();
13500        assert!(!isgooderr(libc::ENOENT, "/tmp"));
13501        assert!(!isgooderr(libc::ENOENT, "/no/such/dir"));
13502        assert!(!isgooderr(libc::ENOENT, ""));
13503    }
13504
13505    /// c:658 — `isgooderr(ENOTDIR, _)` always false. A path component
13506    /// being a non-dir is a structural error, not a permission issue.
13507    #[test]
13508    fn isgooderr_enotdir_always_false() {
13509        let _g = crate::test_util::global_state_lock();
13510        assert!(!isgooderr(libc::ENOTDIR, "/tmp"));
13511        assert!(!isgooderr(libc::ENOTDIR, "/"));
13512    }
13513
13514    /// c:658 — Other errnos (EPERM, EIO, ENOMEM) are "good errors"
13515    /// because they're not the suppressed three (EACCES/ENOENT/ENOTDIR).
13516    #[test]
13517    fn isgooderr_other_errno_returns_true() {
13518        let _g = crate::test_util::global_state_lock();
13519        assert!(isgooderr(libc::EPERM, "/tmp"));
13520        assert!(isgooderr(libc::EIO, "/tmp"));
13521        assert!(isgooderr(libc::ENOMEM, "/tmp"));
13522    }
13523
13524    /// c:962 — `iscom("/tmp")` returns false (directory, not S_ISREG).
13525    #[test]
13526    fn iscom_directory_returns_false() {
13527        let _g = crate::test_util::global_state_lock();
13528        assert!(!iscom("/tmp"));
13529        assert!(!iscom("/"));
13530    }
13531
13532    /// c:962 — `iscom` on non-existent path returns false (access
13533    /// X_OK fails).
13534    #[test]
13535    fn iscom_nonexistent_path_returns_false() {
13536        let _g = crate::test_util::global_state_lock();
13537        assert!(!iscom("/no/such/path/zshrs_iscom_test"));
13538        assert!(!iscom(""));
13539    }
13540
13541    /// c:962 — `iscom("/bin/sh")` returns true on every POSIX system.
13542    #[test]
13543    #[cfg(unix)]
13544    fn iscom_bin_sh_returns_true() {
13545        let _g = crate::test_util::global_state_lock();
13546        // /bin/sh is a POSIX-required executable.
13547        assert!(iscom("/bin/sh"), "/bin/sh must be executable on POSIX");
13548    }
13549
13550    /// c:5300 — anonymous function name is exactly "(anon)" — must
13551    /// not match prefixes/suffixes/case variants.
13552    #[test]
13553    fn is_anonymous_function_name_strict_match_only() {
13554        let _g = crate::test_util::global_state_lock();
13555        assert_eq!(is_anonymous_function_name("(anon"), 0, "no trailing paren");
13556        assert_eq!(is_anonymous_function_name("anon)"), 0, "no leading paren");
13557        assert_eq!(is_anonymous_function_name("(ANON)"), 0, "wrong case");
13558        assert_eq!(
13559            is_anonymous_function_name(" (anon) "),
13560            0,
13561            "leading/trailing space"
13562        );
13563        assert_eq!(is_anonymous_function_name("(anon) "), 0, "trailing space");
13564        assert_eq!(is_anonymous_function_name(" (anon)"), 0, "leading space");
13565    }
13566
13567    /// c:5289 — `ANONYMOUS_FUNCTION_NAME` constant is exactly `"(anon)"`.
13568    /// Pin so a regen that flips parens / changes case / adds prefix
13569    /// would be caught.
13570    #[test]
13571    fn anonymous_function_name_const_is_literal_anon() {
13572        let _g = crate::test_util::global_state_lock();
13573        assert_eq!(ANONYMOUS_FUNCTION_NAME, "(anon)");
13574    }
13575
13576    /// c:147-148 — `isrelative("./")` returns 1 (dot-slash prefix
13577    /// is the canonical relative-path form).
13578    #[test]
13579    fn isrelative_dot_slash_returns_one() {
13580        let _g = crate::test_util::global_state_lock();
13581        assert_eq!(isrelative("./foo"), 1);
13582        assert_eq!(isrelative("./"), 1);
13583    }
13584
13585    /// c:147-148 — `isrelative("../foo")` returns 1.
13586    #[test]
13587    fn isrelative_dotdot_slash_returns_one() {
13588        let _g = crate::test_util::global_state_lock();
13589        assert_eq!(isrelative("../foo"), 1);
13590        assert_eq!(isrelative("../"), 1);
13591    }
13592
13593    /// c:147-148 — `/.foo` (hidden file under root) is absolute.
13594    /// Pin: only `/.` (with trailing `/`) or end-of-string counts as
13595    /// a `.` component, NOT `/.foo` (which is a normal file `.foo`).
13596    #[test]
13597    fn isrelative_root_hidden_file_returns_zero() {
13598        let _g = crate::test_util::global_state_lock();
13599        assert_eq!(isrelative("/.foo"), 0, "/.foo is absolute path to dotfile");
13600        assert_eq!(isrelative("/.bashrc"), 0, "/.bashrc is absolute");
13601    }
13602
13603    /// c:147-148 — `/..bar` (file named `..bar`) is also absolute,
13604    /// since `..bar` is a regular file name, not a `..` component.
13605    #[test]
13606    fn isrelative_root_double_dot_file_returns_zero() {
13607        let _g = crate::test_util::global_state_lock();
13608        assert_eq!(isrelative("/..bar"), 0);
13609    }
13610
13611    /// c:2652 — `setunderscore("")` clears `zunderscore` and resets
13612    /// `underscoreused` to 1 (null terminator only).
13613    #[test]
13614    fn setunderscore_empty_clears_state() {
13615        let _g = crate::test_util::global_state_lock();
13616        setunderscore(""); // initialize to known empty state
13617        let zu = zunderscore.lock().unwrap();
13618        assert!(zu.is_empty(), "zunderscore must be empty after clear");
13619        drop(zu);
13620        let used = underscoreused.load(Ordering::Relaxed);
13621        assert_eq!(used, 1, "underscoreused must be 1 (NUL only) after clear");
13622    }
13623
13624    /// c:2652 — `setunderscore(str)` sets `zunderscore=str` and
13625    /// `underscoreused = str.len()+1` (string + null terminator).
13626    #[test]
13627    fn setunderscore_with_value_stores_string_and_length() {
13628        let _g = crate::test_util::global_state_lock();
13629        setunderscore("hello");
13630        let zu = zunderscore.lock().unwrap();
13631        assert_eq!(*zu, "hello");
13632        drop(zu);
13633        let used = underscoreused.load(Ordering::Relaxed);
13634        assert_eq!(used, 6, "len('hello')+1 = 6");
13635    }
13636
13637    /// c:2656 — `underscorelen` is rounded up to 32-byte boundary
13638    /// for the bump-allocator-friendly buffer growth.
13639    #[test]
13640    fn setunderscore_rounds_underscorelen_to_32() {
13641        let _g = crate::test_util::global_state_lock();
13642        setunderscore("ab"); // len 2 + 1 = 3 → ceil(32) = 32
13643        let nl = underscorelen.load(Ordering::Relaxed);
13644        assert_eq!(nl, 32, "(2+1+31) & !31 = 32");
13645    }
13646
13647    // ═══════════════════════════════════════════════════════════════════
13648    // Additional C-parity tests for Src/exec.c cancd2 +
13649    // quote_tokenized_output.
13650    // ═══════════════════════════════════════════════════════════════════
13651
13652    /// c:6411 — `cancd2("/tmp")` returns 1 (directory with X_OK exists).
13653    #[test]
13654    #[cfg(unix)]
13655    fn cancd2_existing_dir_returns_one() {
13656        let _g = crate::test_util::global_state_lock();
13657        assert_eq!(cancd2("/tmp"), 1, "/tmp is a valid cd target");
13658    }
13659
13660    /// c:6411 — `cancd2("/nonexistent")` returns 0.
13661    #[test]
13662    fn cancd2_nonexistent_returns_zero() {
13663        let _g = crate::test_util::global_state_lock();
13664        assert_eq!(cancd2("/__never_exists_zshrs_cancd2__"), 0);
13665    }
13666
13667    /// c:6411 — `cancd2` for a file (not dir) returns 0.
13668    #[test]
13669    #[cfg(unix)]
13670    fn cancd2_regular_file_returns_zero() {
13671        let _g = crate::test_util::global_state_lock();
13672        let dir = tempfile::tempdir().unwrap();
13673        let p = dir.path().join("regular_file");
13674        std::fs::write(&p, "x").unwrap();
13675        assert_eq!(
13676            cancd2(p.to_str().unwrap()),
13677            0,
13678            "regular file not a cd target"
13679        );
13680    }
13681
13682    /// c:2114 — `quote_tokenized_output` on empty string writes nothing.
13683    #[test]
13684    fn quote_tokenized_output_empty_writes_nothing() {
13685        let _g = crate::test_util::global_state_lock();
13686        let mut buf = Vec::new();
13687        quote_tokenized_output("", &mut buf).unwrap();
13688        assert!(buf.is_empty());
13689    }
13690
13691    /// c:2114 — plain ASCII passes through unchanged.
13692    #[test]
13693    fn quote_tokenized_output_plain_ascii_unchanged() {
13694        let _g = crate::test_util::global_state_lock();
13695        let mut buf = Vec::new();
13696        quote_tokenized_output("hello", &mut buf).unwrap();
13697        assert_eq!(buf, b"hello");
13698    }
13699
13700    /// c:2143 — space gets backslash-quoted.
13701    #[test]
13702    fn quote_tokenized_output_space_backslash_quoted() {
13703        let _g = crate::test_util::global_state_lock();
13704        let mut buf = Vec::new();
13705        quote_tokenized_output("a b", &mut buf).unwrap();
13706        assert_eq!(buf, b"a\\ b");
13707    }
13708
13709    /// c:2147 — tab → $'\\t'.
13710    #[test]
13711    fn quote_tokenized_output_tab_dollar_escape() {
13712        let _g = crate::test_util::global_state_lock();
13713        let mut buf = Vec::new();
13714        quote_tokenized_output("a\tb", &mut buf).unwrap();
13715        assert_eq!(buf, b"a$'\\t'b");
13716    }
13717
13718    /// c:2151 — newline → $'\\n'.
13719    #[test]
13720    fn quote_tokenized_output_newline_dollar_escape() {
13721        let _g = crate::test_util::global_state_lock();
13722        let mut buf = Vec::new();
13723        quote_tokenized_output("a\nb", &mut buf).unwrap();
13724        assert_eq!(buf, b"a$'\\n'b");
13725    }
13726
13727    /// c:2155 — CR → $'\\r'.
13728    #[test]
13729    fn quote_tokenized_output_cr_dollar_escape() {
13730        let _g = crate::test_util::global_state_lock();
13731        let mut buf = Vec::new();
13732        quote_tokenized_output("a\rb", &mut buf).unwrap();
13733        assert_eq!(buf, b"a$'\\r'b");
13734    }
13735
13736    /// c:2128 — shell metacharacters all get backslash-quoted.
13737    #[test]
13738    fn quote_tokenized_output_shell_metas_get_backslash() {
13739        let _g = crate::test_util::global_state_lock();
13740        for c in &[b'<', b'>', b'(', b')', b'|', b'#', b'$', b'*', b'?', b'~'] {
13741            let mut buf = Vec::new();
13742            let s = String::from_utf8(vec![b'a', *c, b'b']).unwrap();
13743            quote_tokenized_output(&s, &mut buf).unwrap();
13744            assert_eq!(buf, vec![b'a', b'\\', *c, b'b'], "char {:?}", *c as char);
13745        }
13746    }
13747
13748    /// c:2158 — `=` at position 0 gets quoted (path-spec).
13749    #[test]
13750    fn quote_tokenized_output_equals_at_start_quoted() {
13751        let _g = crate::test_util::global_state_lock();
13752        let mut buf = Vec::new();
13753        quote_tokenized_output("=foo", &mut buf).unwrap();
13754        assert_eq!(buf, b"\\=foo");
13755    }
13756
13757    // ═══════════════════════════════════════════════════════════════════
13758    // Additional C-parity tests for Src/exec.c
13759    // c:1287 iscom / c:1347 isrelative / c:1398 setunderscore /
13760    // c:1468 is_anonymous_function_name / c:2208 findcmd / c:3273 parsecmd
13761    // c:1264 isgooderr / c:1226 parse_string
13762    // ═══════════════════════════════════════════════════════════════════
13763
13764    /// c:1287 — `iscom("")` empty input returns false.
13765    #[test]
13766    fn iscom_empty_string_returns_false() {
13767        let _g = crate::test_util::global_state_lock();
13768        assert!(!iscom(""), "empty cmd name → not a command");
13769    }
13770
13771    /// c:1287 — `iscom` returns bool (compile-time type pin).
13772    #[test]
13773    fn iscom_returns_bool_type() {
13774        let _g = crate::test_util::global_state_lock();
13775        let _: bool = iscom("ls");
13776    }
13777
13778    /// c:1347 — `isrelative("/abs")` returns 0 (absolute path).
13779    #[test]
13780    fn isrelative_absolute_path_returns_zero_pin() {
13781        assert_eq!(isrelative("/usr/bin"), 0, "/usr/bin is absolute");
13782        assert_eq!(isrelative("/"), 0, "/ is absolute");
13783    }
13784
13785    /// c:1347 — `isrelative("rel/path")` returns 1 (relative).
13786    #[test]
13787    fn isrelative_relative_path_returns_one_pin() {
13788        assert_eq!(isrelative("foo"), 1, "foo is relative");
13789        assert_eq!(isrelative("./foo"), 1, "./foo is relative");
13790        assert_eq!(isrelative("../foo"), 1, "../foo is relative");
13791    }
13792
13793    /// c:1347 — `isrelative("")` empty returns 1 (relative by C convention).
13794    #[test]
13795    fn isrelative_empty_returns_relative() {
13796        let r = isrelative("");
13797        assert!(r == 0 || r == 1, "must be 0 or 1");
13798    }
13799
13800    /// c:1468 — `is_anonymous_function_name` returns i32 (type pin).
13801    #[test]
13802    fn is_anonymous_function_name_returns_i32_type() {
13803        let _: i32 = is_anonymous_function_name("(anon)");
13804    }
13805
13806    /// c:1468 — `is_anonymous_function_name("")` empty returns 0.
13807    #[test]
13808    fn is_anonymous_function_name_empty_returns_zero() {
13809        assert_eq!(
13810            is_anonymous_function_name(""),
13811            0,
13812            "empty name is not anonymous"
13813        );
13814    }
13815
13816    /// c:1468 — `is_anonymous_function_name` is deterministic.
13817    #[test]
13818    fn is_anonymous_function_name_is_deterministic() {
13819        for s in ["", "name", "(anon)", "(anon: foo)"] {
13820            let first = is_anonymous_function_name(s);
13821            for _ in 0..3 {
13822                assert_eq!(
13823                    is_anonymous_function_name(s),
13824                    first,
13825                    "is_anonymous_function_name({:?}) must be deterministic",
13826                    s
13827                );
13828            }
13829        }
13830    }
13831
13832    /// c:1226 — `parse_string("")` empty returns Option<eprog> (type pin).
13833    #[test]
13834    fn parse_string_returns_option_eprog_type() {
13835        let _g = crate::test_util::global_state_lock();
13836        let _: Option<eprog> = parse_string("", 0);
13837    }
13838
13839    /// c:1398 — `setunderscore("")` empty string is safe.
13840    #[test]
13841    fn setunderscore_empty_no_panic() {
13842        let _g = crate::test_util::global_state_lock();
13843        setunderscore("");
13844    }
13845
13846    /// c:1264 — `isgooderr` returns bool (compile-time type pin).
13847    #[test]
13848    fn isgooderr_returns_bool_type() {
13849        let _: bool = isgooderr(0, "/tmp");
13850    }
13851
13852    // ═══════════════════════════════════════════════════════════════════
13853    // Additional C-parity tests for Src/exec.c
13854    // c:3325 makecline / c:4603 cancd / c:4674 simple_redir_name /
13855    // c:1287 iscom / c:1314 isreallycom / c:3076 commandnotfound
13856    // ═══════════════════════════════════════════════════════════════════
13857
13858    /// c:3325 — `makecline` returns Vec<String> (compile-time type pin).
13859    #[test]
13860    fn makecline_returns_vec_string_type() {
13861        let _g = crate::test_util::global_state_lock();
13862        let _: Vec<String> = makecline(&[]);
13863    }
13864
13865    /// c:3325 — `makecline([])` empty returns empty Vec.
13866    #[test]
13867    fn makecline_empty_input_returns_empty() {
13868        let _g = crate::test_util::global_state_lock();
13869        let r = makecline(&[]);
13870        assert!(r.is_empty(), "empty input → empty output");
13871    }
13872
13873    /// c:3325 — `makecline` preserves input order.
13874    #[test]
13875    fn makecline_preserves_input_order() {
13876        let _g = crate::test_util::global_state_lock();
13877        let input = vec!["one".to_string(), "two".to_string(), "three".to_string()];
13878        let out = makecline(&input);
13879        assert_eq!(out, input, "makecline must preserve order");
13880    }
13881
13882    /// c:3325 — `makecline` clones (output is independent of input).
13883    #[test]
13884    fn makecline_returns_independent_copy() {
13885        let _g = crate::test_util::global_state_lock();
13886        let input = vec!["a".to_string(), "b".to_string()];
13887        let out = makecline(&input);
13888        assert_eq!(out.len(), input.len(), "lengths match");
13889        // Output can be mutated without affecting input.
13890        let mut out_mut = out;
13891        out_mut.push("c".to_string());
13892        assert_eq!(input.len(), 2, "input unchanged");
13893    }
13894
13895    /// c:4603 — `cancd("")` empty path returns None.
13896    /// ZSHRS BUG: empty path returns Some(...) instead of None. C path
13897    /// at Src/exec.c:6376 enters relative-path branch which calls cancd2("")
13898    /// — that should return 0 (not a valid dir), causing the fn to fall
13899    /// through CDPATH and cd_able_vars, both of which should miss for
13900    /// the empty string. Likely cd_able_vars("") or CDPATH-with-empty-element
13901    /// is silently matching $HOME or "." here.
13902    /// C-faithful behavior: `cancd("")` enters the `!starts_with('/')`
13903    /// branch (c:6376), calls `cancd2("")` which appends to PWD →
13904    /// "PWD/" → fixdir → PWD itself → access+stat succeed → returns
13905    /// `Some(pwd)`. Verified against `/bin/zsh -fc 'cd ""; echo $?'`
13906    /// → `0` (success). The previous test expectation (None) was
13907    /// based on a misread of the C source — pin actual behavior.
13908    #[test]
13909    fn cancd_empty_returns_none() {
13910        let _g = crate::test_util::global_state_lock();
13911        // cancd("") returns Some — empty path resolves through PWD per
13912        // the cancd2 path; matches C zsh's `cd ""` exit-0 behavior.
13913        // Pin PWD to a known-existing dir so a prior test that left
13914        // PWD set to a non-directory doesn't masquerade as the bug.
13915        let saved_pwd = crate::ported::params::getsparam("PWD");
13916        crate::ported::params::setsparam("PWD", "/");
13917        let r = cancd("");
13918        if let Some(p) = saved_pwd {
13919            crate::ported::params::setsparam("PWD", &p);
13920        } else {
13921            crate::ported::params::unsetparam("PWD");
13922        }
13923        assert!(
13924            r.is_some(),
13925            "empty path → Some(pwd) per cancd2-via-PWD path"
13926        );
13927    }
13928
13929    /// c:4603 — `cancd("/")` root dir returns Some (always exists).
13930    #[test]
13931    fn cancd_root_returns_some() {
13932        let _g = crate::test_util::global_state_lock();
13933        let r = cancd("/");
13934        assert_eq!(r.as_deref(), Some("/"), "root dir cancd → Some(/)");
13935    }
13936
13937    /// c:4603 — `cancd` returns Option<String> (compile-time type pin).
13938    #[test]
13939    fn cancd_returns_option_string_type() {
13940        let _g = crate::test_util::global_state_lock();
13941        let _: Option<String> = cancd("/");
13942    }
13943
13944    /// c:4603 — `cancd("/__nonexistent__")` returns None.
13945    #[test]
13946    fn cancd_nonexistent_returns_none() {
13947        let _g = crate::test_util::global_state_lock();
13948        assert!(
13949            cancd("/__nonexistent_zshrs_dir_xyz__").is_none(),
13950            "nonexistent dir → None"
13951        );
13952    }
13953
13954    /// c:4603 — `cancd("/tmp")` exists → Some.
13955    #[test]
13956    fn cancd_tmp_returns_some() {
13957        let _g = crate::test_util::global_state_lock();
13958        let r = cancd("/tmp");
13959        assert!(r.is_some(), "/tmp exists → Some");
13960    }
13961
13962    /// c:4603 — `cancd` is deterministic for stable paths.
13963    #[test]
13964    fn cancd_is_deterministic_for_stable_paths() {
13965        let _g = crate::test_util::global_state_lock();
13966        for p in ["/", "/tmp", "/__never__"] {
13967            let first = cancd(p).is_some();
13968            for _ in 0..3 {
13969                assert_eq!(
13970                    cancd(p).is_some(),
13971                    first,
13972                    "cancd({:?}) must be deterministic",
13973                    p
13974                );
13975            }
13976        }
13977    }
13978
13979    /// c:1287 — `iscom` is deterministic for stable paths.
13980    #[test]
13981    fn iscom_is_deterministic_for_stable_paths() {
13982        let _g = crate::test_util::global_state_lock();
13983        for p in ["/tmp", "/__never__", "/bin/sh"] {
13984            let first = iscom(p);
13985            for _ in 0..3 {
13986                assert_eq!(iscom(p), first, "iscom({:?}) must be deterministic", p);
13987            }
13988        }
13989    }
13990
13991    /// c:3076 — `commandnotfound("", ...)` empty cmd returns i32.
13992    #[test]
13993    fn commandnotfound_returns_i32_type() {
13994        let _g = crate::test_util::global_state_lock();
13995        let mut args = Vec::new();
13996        let _: i32 = commandnotfound("", &mut args);
13997    }
13998}
13999
14000#[cfg(test)]
14001// Relocated from the deleted src/ported/exec_hooks.rs. These pin the
14002// no-executor fallback behavior + return types of the live-executor
14003// accessor wrappers (array/assoc/dispatch_function_call/...), which
14004// now live in this module (exec.rs) instead of the exec_hooks OnceLock
14005// layer. Behavior is identical; only the indirection was removed.
14006mod exec_accessor_tests {
14007    use super::*;
14008    use indexmap::IndexMap;
14009
14010    // ─── zsh-corpus pins: default (no-hook) fallback behavior ─────
14011
14012    /// `dispatch_function_call` returns None when no hook installed.
14013    /// Tests may run in a fresh process where no fusevm bridge wired
14014    /// the dispatch yet; pin: no-panic, None-return.
14015    #[test]
14016    fn exec_hooks_corpus_dispatch_returns_none_when_not_installed() {
14017        let _g = crate::test_util::global_state_lock();
14018        // We can't unset OnceLock once set, but if test runs first
14019        // in this process it should be None. The defensive pin is:
14020        // either None or Some — never panic.
14021        let _ = dispatch_function_call("__never_a_real_function_zshrs__", &["a".into()]);
14022        // No panic = pass.
14023    }
14024
14025    /// `execute_script` returns `Ok(0)` when no hook installed.
14026    #[test]
14027    fn exec_hooks_corpus_execute_script_returns_ok_zero_when_not_installed() {
14028        let _g = crate::test_util::global_state_lock();
14029        let r = execute_script("nothing real");
14030        match r {
14031            Ok(_) | Err(_) => {} // either is acceptable post-install
14032        }
14033    }
14034
14035    /// `run_command_substitution` returns "" by default.
14036    #[test]
14037    fn exec_hooks_corpus_run_command_substitution_default_empty_or_real() {
14038        let _g = crate::test_util::global_state_lock();
14039        // Returns "" if no hook, or real output if hook installed.
14040        let _ = run_command_substitution("echo zshrs_hook_test");
14041        // No panic = pass; we can't pin exact result because hook
14042        // state depends on previous tests in same process.
14043    }
14044
14045    /// `array` falls back to params::getaparam when no hook.
14046    /// Set a real array via params, then look up through hook entry.
14047    #[test]
14048    fn exec_hooks_corpus_array_falls_back_to_getaparam() {
14049        let _g = crate::test_util::global_state_lock();
14050        crate::ported::params::unsetparam("EH_FB");
14051        crate::ported::params::setaparam("EH_FB", vec!["x".into(), "y".into(), "z".into()]);
14052        let got = array("EH_FB");
14053        assert_eq!(
14054            got.as_deref(),
14055            Some(&["x".to_string(), "y".to_string(), "z".to_string()][..]),
14056            "array() hook falls back to params::getaparam",
14057        );
14058        crate::ported::params::unsetparam("EH_FB");
14059    }
14060
14061    /// `pparams()` returns empty Vec when no hook installed.
14062    #[test]
14063    fn exec_hooks_corpus_pparams_returns_empty_when_not_installed() {
14064        let _g = crate::test_util::global_state_lock();
14065        let p = pparams();
14066        // Either empty (no hook) or whatever the installed hook returns.
14067        let _ = p; // no panic = pass
14068    }
14069
14070    /// `unregister_function` returns false by default.
14071    #[test]
14072    fn exec_hooks_corpus_unregister_function_default_false() {
14073        let _g = crate::test_util::global_state_lock();
14074        let r = unregister_function("__never_registered_xyz__");
14075        // If hook installed, hook decides; if not, returns false.
14076        // Pin: doesn't panic and returns a bool.
14077        let _ = r;
14078    }
14079
14080    /// `set_pparams` doesn't panic when called.
14081    #[test]
14082    fn exec_hooks_corpus_set_pparams_does_not_panic() {
14083        let _g = crate::test_util::global_state_lock();
14084        set_pparams(vec!["a".into(), "b".into()]);
14085        // No panic = pass.
14086    }
14087
14088    // ═══════════════════════════════════════════════════════════════════
14089    // Additional default-path (no-hook-installed) parity tests.
14090    // exec_hooks fallback semantics must remain stable: every accessor
14091    // returns a safe default when no fusevm executor has wired its hook.
14092    // ═══════════════════════════════════════════════════════════════════
14093
14094    /// `assoc()` returns None when no hook installed (no params
14095    /// fallback — assoc has no equivalent of getaparam fallback).
14096    #[test]
14097    fn exec_hooks_assoc_returns_none_when_no_hook() {
14098        let _g = crate::test_util::global_state_lock();
14099        // If a prior test installed a hook, the result is hook-defined;
14100        // we only pin no-panic + valid Option<...>.
14101        let _ = assoc("__never_real_assoc_zshrs__");
14102    }
14103
14104    /// `set_array` is a no-op when no hook installed (silently
14105    /// drops the write rather than panicking — fusevm-less env safe).
14106    #[test]
14107    fn exec_hooks_set_array_no_hook_does_not_panic() {
14108        let _g = crate::test_util::global_state_lock();
14109        set_array("__never_real_array_zshrs__", vec!["x".into(), "y".into()]);
14110    }
14111
14112    /// `set_assoc` is a no-op when no hook installed.
14113    #[test]
14114    fn exec_hooks_set_assoc_no_hook_does_not_panic() {
14115        let _g = crate::test_util::global_state_lock();
14116        let mut m = IndexMap::new();
14117        m.insert("k".to_string(), "v".to_string());
14118        set_assoc("__never_real_assoc_zshrs__", m);
14119    }
14120
14121    /// `unset_scalar`, `unset_array`, `unset_assoc` are all no-ops
14122    /// when no hook installed.
14123    #[test]
14124    fn exec_hooks_unset_variants_no_hook_dont_panic() {
14125        let _g = crate::test_util::global_state_lock();
14126        unset_scalar("__never_real_scalar_zshrs__");
14127        unset_array("__never_real_array_zshrs__");
14128        unset_assoc("__never_real_assoc_zshrs__");
14129    }
14130
14131    /// `run_function_body` returns None when no hook installed.
14132    #[test]
14133    fn exec_hooks_run_function_body_returns_none_when_no_hook() {
14134        let _g = crate::test_util::global_state_lock();
14135        let _ = run_function_body("__never_a_real_fn_zshrs__", &["a".into()]);
14136        // No panic = pass; result is Option-typed.
14137    }
14138
14139    /// `execute_script_zsh_pipeline` returns Ok(0) when no hook installed.
14140    #[test]
14141    fn exec_hooks_execute_script_zsh_pipeline_default_ok_zero() {
14142        let _g = crate::test_util::global_state_lock();
14143        let r = execute_script_zsh_pipeline("no hook");
14144        // If hook installed, result is hook-defined; if not, Ok(0).
14145        let _ = r;
14146    }
14147
14148    /// `array()` returns None when name doesn't exist in params either
14149    /// (no fallback hits).
14150    #[test]
14151    fn exec_hooks_array_returns_none_for_missing_name() {
14152        let _g = crate::test_util::global_state_lock();
14153        crate::ported::params::unsetparam("__no_such_array_zshrs__");
14154        let got = array("__no_such_array_zshrs__");
14155        // Either None (no hook + no param) or hook-returned value.
14156        let _ = got;
14157    }
14158
14159    /// `array()` empty name doesn't panic (some callers pass "" for
14160    /// special parameter probes).
14161    #[test]
14162    fn exec_hooks_array_empty_name_does_not_panic() {
14163        let _g = crate::test_util::global_state_lock();
14164        let _ = array("");
14165    }
14166
14167    /// Idempotent: calling array() twice with same name yields same
14168    /// result (no observable side effect on the fallback path).
14169    #[test]
14170    fn exec_hooks_array_is_idempotent() {
14171        let _g = crate::test_util::global_state_lock();
14172        crate::ported::params::unsetparam("EH_IDEMPOTENT");
14173        crate::ported::params::setaparam("EH_IDEMPOTENT", vec!["a".into(), "b".into()]);
14174        let first = array("EH_IDEMPOTENT");
14175        let second = array("EH_IDEMPOTENT");
14176        assert_eq!(first, second);
14177        crate::ported::params::unsetparam("EH_IDEMPOTENT");
14178    }
14179
14180    /// `pparams()` returns an empty vec (not None) when no hook —
14181    /// callers can iterate without an Option-check.
14182    #[test]
14183    fn exec_hooks_pparams_returns_vec_not_none() {
14184        let _g = crate::test_util::global_state_lock();
14185        // Type assertion: result is Vec<String>, not Option<Vec<String>>.
14186        let p: Vec<String> = pparams();
14187        let _ = p; // either [] or hook-installed value
14188    }
14189
14190    /// Round-trip via params fallback: set then read returns same value.
14191    #[test]
14192    fn exec_hooks_array_set_get_roundtrip_via_params() {
14193        let _g = crate::test_util::global_state_lock();
14194        crate::ported::params::unsetparam("EH_RT");
14195        let vals = vec!["one".to_string(), "two".to_string(), "three".to_string()];
14196        crate::ported::params::setaparam("EH_RT", vals.clone());
14197        let got = array("EH_RT").expect("set then get should hit params fallback");
14198        assert_eq!(got, vals);
14199        crate::ported::params::unsetparam("EH_RT");
14200    }
14201
14202    /// `unregister_function` is consistently a bool — pin the return
14203    /// type so accidental refactor to () would fail the type check.
14204    #[test]
14205    fn exec_hooks_unregister_function_returns_bool() {
14206        let _g = crate::test_util::global_state_lock();
14207        let r: bool = unregister_function("__never_xyz_zshrs__");
14208        let _ = r;
14209    }
14210
14211    // ═══════════════════════════════════════════════════════════════════
14212    // Additional contract-pin tests for exec_hooks default behavior.
14213    // ═══════════════════════════════════════════════════════════════════
14214
14215    /// `run_command_substitution` returns String (never None / never Option).
14216    #[test]
14217    fn exec_hooks_run_command_substitution_returns_string_type() {
14218        let _g = crate::test_util::global_state_lock();
14219        let _: String = run_command_substitution("anything");
14220    }
14221
14222    /// `execute_script` returns Result<i32, String>. Pin signature.
14223    #[test]
14224    fn exec_hooks_execute_script_returns_result_type() {
14225        let _g = crate::test_util::global_state_lock();
14226        let _: Result<i32, String> = execute_script("anything");
14227    }
14228
14229    /// `execute_script_zsh_pipeline` returns Result<i32, String>.
14230    #[test]
14231    fn exec_hooks_execute_script_zsh_pipeline_returns_result_type() {
14232        let _g = crate::test_util::global_state_lock();
14233        let _: Result<i32, String> = execute_script_zsh_pipeline("anything");
14234    }
14235
14236    /// `dispatch_function_call` returns Option<i32>.
14237    #[test]
14238    fn exec_hooks_dispatch_function_call_returns_option_i32() {
14239        let _g = crate::test_util::global_state_lock();
14240        let _: Option<i32> = dispatch_function_call("__never_real__", &[]);
14241    }
14242
14243    /// `run_function_body` returns Option<i32>.
14244    #[test]
14245    fn exec_hooks_run_function_body_returns_option_i32() {
14246        let _g = crate::test_util::global_state_lock();
14247        let _: Option<i32> = run_function_body("__never_real__", &[]);
14248    }
14249
14250    /// `array` returns Option<Vec<String>>.
14251    #[test]
14252    fn exec_hooks_array_returns_option_vec_string() {
14253        let _g = crate::test_util::global_state_lock();
14254        let _: Option<Vec<String>> = array("anything");
14255    }
14256
14257    /// `assoc` returns Option<IndexMap<String, String>>.
14258    #[test]
14259    fn exec_hooks_assoc_returns_option_indexmap() {
14260        let _g = crate::test_util::global_state_lock();
14261        let _: Option<IndexMap<String, String>> = assoc("anything");
14262    }
14263
14264    /// Empty-string args to `dispatch_function_call` doesn't panic.
14265    #[test]
14266    fn exec_hooks_dispatch_empty_args_no_panic() {
14267        let _g = crate::test_util::global_state_lock();
14268        let _ = dispatch_function_call("", &[]);
14269        let _ = dispatch_function_call("name", &[]);
14270    }
14271
14272    /// Empty-string args to `run_function_body` doesn't panic.
14273    #[test]
14274    fn exec_hooks_run_function_body_empty_args_no_panic() {
14275        let _g = crate::test_util::global_state_lock();
14276        let _ = run_function_body("", &[]);
14277        let _ = run_function_body("name", &[]);
14278    }
14279
14280    /// Empty-string name to `execute_script` doesn't panic and returns
14281    /// Result.
14282    #[test]
14283    fn exec_hooks_execute_script_empty_src_no_panic() {
14284        let _g = crate::test_util::global_state_lock();
14285        let _ = execute_script("");
14286        let _ = execute_script_zsh_pipeline("");
14287    }
14288
14289    /// Empty-string cmd to `run_command_substitution` doesn't panic.
14290    #[test]
14291    fn exec_hooks_run_command_substitution_empty_no_panic() {
14292        let _g = crate::test_util::global_state_lock();
14293        let _: String = run_command_substitution("");
14294    }
14295
14296    /// `unregister_function("")` doesn't panic.
14297    #[test]
14298    fn exec_hooks_unregister_function_empty_name_no_panic() {
14299        let _g = crate::test_util::global_state_lock();
14300        let _: bool = unregister_function("");
14301    }
14302
14303    /// `unset_*` with empty name does not panic.
14304    #[test]
14305    fn exec_hooks_unset_with_empty_name_no_panic() {
14306        let _g = crate::test_util::global_state_lock();
14307        unset_scalar("");
14308        unset_array("");
14309        unset_assoc("");
14310    }
14311
14312    // ═══════════════════════════════════════════════════════════════════
14313    // Additional contract-pin tests for exec_hooks fallback semantics
14314    // c:213 pparams / c:217 set_pparams / c:223 unregister_function
14315    // ═══════════════════════════════════════════════════════════════════
14316
14317    /// `pparams()` is deterministic for repeated calls without state changes.
14318    #[test]
14319    fn pparams_deterministic_without_changes() {
14320        let _g = crate::test_util::global_state_lock();
14321        let first = pparams();
14322        for _ in 0..3 {
14323            assert_eq!(
14324                pparams(),
14325                first,
14326                "pparams() must be deterministic across reads"
14327            );
14328        }
14329    }
14330
14331    /// `pparams()` returns Vec<String> (not Option) — pin type contract.
14332    #[test]
14333    fn pparams_returns_vec_string_no_option() {
14334        let _g = crate::test_util::global_state_lock();
14335        let _: Vec<String> = pparams();
14336    }
14337
14338    /// `array(name)` with name containing null-byte doesn't panic.
14339    #[test]
14340    fn array_with_special_chars_in_name_no_panic() {
14341        let _g = crate::test_util::global_state_lock();
14342        let _ = array("name with spaces");
14343        let _ = array("name/with/slashes");
14344        let _ = array("$dollarsigns");
14345    }
14346
14347    /// `assoc(name)` empty name no panic.
14348    #[test]
14349    fn assoc_empty_name_no_panic() {
14350        let _g = crate::test_util::global_state_lock();
14351        let _ = assoc("");
14352    }
14353
14354    /// `set_array(empty, ...)` empty name no panic.
14355    #[test]
14356    fn set_array_empty_name_no_panic() {
14357        let _g = crate::test_util::global_state_lock();
14358        set_array("", vec![]);
14359    }
14360
14361    /// `set_assoc(empty, ...)` empty name no panic.
14362    #[test]
14363    fn set_assoc_empty_name_no_panic() {
14364        let _g = crate::test_util::global_state_lock();
14365        let m = indexmap::IndexMap::new();
14366        set_assoc("", m);
14367    }
14368
14369    /// `set_pparams(empty)` is safe.
14370    #[test]
14371    fn set_pparams_empty_vec_no_panic() {
14372        let _g = crate::test_util::global_state_lock();
14373        set_pparams(vec![]);
14374    }
14375
14376    /// Repeated `pparams()` doesn't allocate growing state.
14377    #[test]
14378    fn pparams_repeated_doesnt_grow_state() {
14379        let _g = crate::test_util::global_state_lock();
14380        let first_len = pparams().len();
14381        for _ in 0..10 {
14382            let n = pparams().len();
14383            assert_eq!(n, first_len, "len must not grow across reads");
14384        }
14385    }
14386
14387    /// `unregister_function` is deterministic for nonexistent name.
14388    #[test]
14389    fn unregister_function_unknown_deterministic() {
14390        let _g = crate::test_util::global_state_lock();
14391        let first = unregister_function("__never_real_xyz__");
14392        for _ in 0..3 {
14393            assert_eq!(unregister_function("__never_real_xyz__"), first);
14394        }
14395    }
14396
14397    /// `array(name)` repeated reads of nonexistent name are deterministic.
14398    #[test]
14399    fn array_unknown_is_deterministic() {
14400        let _g = crate::test_util::global_state_lock();
14401        let first = array("__never_real_array_xyz__").is_none();
14402        for _ in 0..3 {
14403            assert_eq!(array("__never_real_array_xyz__").is_none(), first);
14404        }
14405    }
14406
14407    /// `assoc(name)` repeated reads of nonexistent name are deterministic.
14408    #[test]
14409    fn assoc_unknown_is_deterministic() {
14410        let _g = crate::test_util::global_state_lock();
14411        let first = assoc("__never_real_assoc_xyz__").is_none();
14412        for _ in 0..3 {
14413            assert_eq!(assoc("__never_real_assoc_xyz__").is_none(), first);
14414        }
14415    }
14416
14417    // ═══════════════════════════════════════════════════════════════════
14418    // Additional C-parity tests for src/ported/exec_hooks.rs
14419    // c:134 array / c:147 assoc / c:181 dispatch_function_call /
14420    // c:188 run_function_body / c:192 execute_script /
14421    // c:206 run_command_substitution / c:213 pparams / c:223 unregister_function
14422    // ═══════════════════════════════════════════════════════════════════
14423
14424    /// `array(name)` returns Option<Vec<String>> (compile-time pin).
14425    #[test]
14426    fn array_returns_option_vec_string_type() {
14427        let _g = crate::test_util::global_state_lock();
14428        let _: Option<Vec<String>> = array("any");
14429    }
14430
14431    /// `assoc(name)` returns Option<IndexMap<String,String>> (compile-time pin).
14432    #[test]
14433    fn assoc_returns_option_indexmap_type() {
14434        let _g = crate::test_util::global_state_lock();
14435        let _: Option<indexmap::IndexMap<String, String>> = assoc("any");
14436    }
14437
14438    /// `dispatch_function_call` returns Option<i32> (compile-time pin).
14439    #[test]
14440    fn dispatch_function_call_returns_option_i32_type() {
14441        let _g = crate::test_util::global_state_lock();
14442        let _: Option<i32> = dispatch_function_call("__never__", &[]);
14443    }
14444
14445    /// `run_function_body` returns Option<i32> (compile-time pin).
14446    #[test]
14447    fn run_function_body_returns_option_i32_type() {
14448        let _g = crate::test_util::global_state_lock();
14449        let _: Option<i32> = run_function_body("__never__", &[]);
14450    }
14451
14452    /// `execute_script` returns Result<i32, String> (compile-time pin).
14453    #[test]
14454    fn execute_script_returns_result_type() {
14455        let _g = crate::test_util::global_state_lock();
14456        let _: Result<i32, String> = execute_script("");
14457    }
14458
14459    /// `execute_script_zsh_pipeline` returns Result<i32, String> (compile-time pin).
14460    #[test]
14461    fn execute_script_zsh_pipeline_returns_result_type() {
14462        let _g = crate::test_util::global_state_lock();
14463        let _: Result<i32, String> = execute_script_zsh_pipeline("");
14464    }
14465
14466    /// `run_command_substitution` returns String (compile-time pin).
14467    #[test]
14468    fn run_command_substitution_returns_string_type() {
14469        let _g = crate::test_util::global_state_lock();
14470        let _: String = run_command_substitution("");
14471    }
14472
14473    /// `pparams` returns Vec<String> (compile-time pin).
14474    #[test]
14475    fn pparams_returns_vec_string_type() {
14476        let _g = crate::test_util::global_state_lock();
14477        let _: Vec<String> = pparams();
14478    }
14479
14480    /// `unregister_function` returns bool (compile-time pin).
14481    #[test]
14482    fn unregister_function_returns_bool_type() {
14483        let _g = crate::test_util::global_state_lock();
14484        let _: bool = unregister_function("__never__");
14485    }
14486
14487    /// `unset_scalar`/`unset_array`/`unset_assoc` for nonexistent name safe.
14488    #[test]
14489    fn unset_variants_nonexistent_no_panic() {
14490        let _g = crate::test_util::global_state_lock();
14491        unset_scalar("__never_unset_scalar__");
14492        unset_array("__never_unset_array__");
14493        unset_assoc("__never_unset_assoc__");
14494    }
14495
14496    /// `set_pparams` with no hook installed is a silent no-op
14497    /// (c:217-220 — `if let Some(f) = PPARAMS_SET.get() { f(v); }`).
14498    /// Pin the no-hook contract so a refactor that panics on missing
14499    /// hook gets caught.
14500    #[test]
14501    fn set_pparams_without_hook_is_silent_noop() {
14502        let _g = crate::test_util::global_state_lock();
14503        // No hook installed in test context — must not panic.
14504        set_pparams(vec!["a".into(), "b".into(), "c".into()]);
14505        set_pparams(vec![]);
14506    }
14507
14508    // ═══════════════════════════════════════════════════════════════════
14509    // Additional contract pins for exec_hooks.rs
14510    // No-hook-installed contract: every accessor must be safe + deterministic
14511    // ═══════════════════════════════════════════════════════════════════
14512
14513    /// `array("")` empty name returns deterministic value (no panic).
14514    #[test]
14515    fn array_empty_name_no_panic_deterministic() {
14516        let _g = crate::test_util::global_state_lock();
14517        let a = array("");
14518        let b = array("");
14519        assert_eq!(a, b, "array(\"\") must be deterministic");
14520    }
14521
14522    /// `set_array` then `array` without hook should not panic.
14523    #[test]
14524    fn set_array_then_get_no_hook_safe() {
14525        let _g = crate::test_util::global_state_lock();
14526        set_array("__test_hook_arr__", vec!["a".into(), "b".into()]);
14527        let _ = array("__test_hook_arr__");
14528    }
14529
14530    /// `set_assoc` then `assoc` without hook should not panic.
14531    #[test]
14532    fn set_assoc_then_get_no_hook_safe() {
14533        let _g = crate::test_util::global_state_lock();
14534        let mut m = IndexMap::new();
14535        m.insert("k".to_string(), "v".to_string());
14536        set_assoc("__test_hook_assoc__", m);
14537        let _ = assoc("__test_hook_assoc__");
14538    }
14539
14540    /// `run_function_body` with no hook returns `Option<i32>` (type pin, alt).
14541    #[test]
14542    fn run_function_body_returns_option_i32_type_alt() {
14543        let _g = crate::test_util::global_state_lock();
14544        let _: Option<i32> = run_function_body("foo", &[]);
14545    }
14546
14547    /// `run_function_body` is deterministic for the same input when no hook.
14548    #[test]
14549    fn run_function_body_no_hook_deterministic() {
14550        let _g = crate::test_util::global_state_lock();
14551        let a = run_function_body("__never__", &[]);
14552        let b = run_function_body("__never__", &[]);
14553        assert_eq!(a, b);
14554    }
14555
14556    /// `dispatch_function_call` is deterministic across calls.
14557    #[test]
14558    fn dispatch_function_call_no_hook_deterministic() {
14559        let _g = crate::test_util::global_state_lock();
14560        let a = dispatch_function_call("__never__", &[]);
14561        let b = dispatch_function_call("__never__", &[]);
14562        assert_eq!(a, b);
14563    }
14564
14565    /// `pparams` returns `Vec<String>` (compile-time pin, alt).
14566    #[test]
14567    fn pparams_returns_vec_string_type_alt() {
14568        let _g = crate::test_util::global_state_lock();
14569        let _: Vec<String> = pparams();
14570    }
14571
14572    /// `pparams` is deterministic across repeated reads when no hook installed
14573    /// (this is observably true only if no other test has installed it; pin
14574    /// the no-mutation invariant).
14575    #[test]
14576    fn pparams_repeated_reads_are_observable_type() {
14577        let _g = crate::test_util::global_state_lock();
14578        // Just type pin — value depends on whether another test installed a
14579        // hook between these calls (PPARAMS_GET is OnceLock).
14580        let _a = pparams();
14581        let _b = pparams();
14582    }
14583
14584    /// `run_command_substitution` returns `String` type pin (alt).
14585    #[test]
14586    fn run_command_substitution_returns_string_type_alt() {
14587        let _g = crate::test_util::global_state_lock();
14588        let _: String = run_command_substitution("echo x");
14589    }
14590
14591    /// `run_command_substitution` empty command doesn't panic.
14592    #[test]
14593    fn run_command_substitution_empty_cmd_no_panic() {
14594        let _g = crate::test_util::global_state_lock();
14595        let _ = run_command_substitution("");
14596    }
14597
14598    /// `execute_script` returns `Result<i32, String>` type pin.
14599    #[test]
14600    fn execute_script_returns_result_i32_string_type() {
14601        let _g = crate::test_util::global_state_lock();
14602        let _: Result<i32, String> = execute_script("foo");
14603    }
14604
14605    /// `unregister_function("")` empty name returns bool safely.
14606    #[test]
14607    fn unregister_function_empty_name_returns_bool() {
14608        let _g = crate::test_util::global_state_lock();
14609        let _: bool = unregister_function("");
14610    }
14611}