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:5050` / `:5260`)
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::zsh_h::{
79    builtin, cmdnam, emulation_options, eprog, execstack, funcwrap, hashnode, isset, jobfile,
80    multio, redir, shfunc, unset, wc_code, Emulation_options, Inang, Inpar, Meta, Nularg, Outpar,
81    Pound, BINF_BUILTIN, BINF_CLEARENV, BINF_COMMAND, BINF_DASH, BINF_EXEC, BINF_PREFIX, CHASEDOTS,
82    CHASELINKS, CLOBBER, CLOBBEREMPTY, CS_CMDSUBST, ERRFLAG_INT, FDT_EXTERNAL, FDT_INTERNAL,
83    FDT_PROC_SUBST, FDT_SAVED_MASK, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHDIRS, INP_LINENO,
84    INTERACTIVE, IS_CLOBBER_REDIR, IS_DASH, JOBTEXTSIZE, MAX_PIPESTATS, MONITOR, MULTIOS,
85    MULTIOUNIT, PATHDIRS, PM_LOADDIR, PM_READONLY, PM_UNDEFINED, POSIXBUILTINS, POSIXJOBS,
86    POSIXTRAPS, REDIRF_FROM_HEREDOC, REDIR_CLOSE, REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE,
87    REDIR_OUTPIPE, USEZLE, VERBOSE, WC_LIST, WC_LIST_TYPE, WC_PIPE, WC_PIPE_END, WC_PIPE_TYPE,
88    WC_REDIR, WC_REDIR_TYPE, WC_REDIR_VARID, WC_SIMPLE, WC_SIMPLE_ARGC, WC_SUBLIST, WC_SUBLIST_END,
89    WC_SUBLIST_FLAGS, WC_SUBLIST_TYPE, WC_TYPESET, ZSIG_FUNC, ZSIG_IGNORED, Z_END,
90};
91use crate::ported::zsh_system_h::timespec as ZshTimespec;
92use crate::ported::ztype_h::{inull, itok};
93use crate::zsh_h::XTRACE;
94
95/// Port of the anonymous `enum { ... }` from `Src/exec.c:35-40`.
96/// Flag bits passed as the `addflags` argument to `addvars` /
97/// `addvarsfromargs`:
98///   - `ADDVAR_EXPORT`  (1<<0) — export each assignment for the
99///                                command `VAR=val cmd ...` form.
100///   - `ADDVAR_RESTORE` (1<<2) — the variable list is being restored
101///                                later (implicit local scope), so
102///                                suppress `ASSPM_WARN`.
103pub const ADDVAR_EXPORT: i32 = 1 << 0; // c:37 (Src/exec.c)
104/// `ADDVAR_RESTORE` constant.
105pub const ADDVAR_RESTORE: i32 = 1 << 2; // c:39 (Src/exec.c)
106
107/// Port of `int trap_state;` from `Src/exec.c:134`. Tracks whether
108/// a trap handler is currently being processed and, paired with
109/// `TRAP_RETURN` below, whether a `return` inside the trap should
110/// promote to `TRAP_STATE_FORCE_RETURN` to unwind the trap caller.
111///
112/// Values: `TRAP_STATE_INACTIVE = 0`, `TRAP_STATE_PRIMED = 1`,
113/// `TRAP_STATE_FORCE_RETURN = 2` (see `Src/zsh.h`).
114pub static TRAP_STATE: std::sync::atomic::AtomicI32 = // c:134 (Src/exec.c)
115    std::sync::atomic::AtomicI32::new(0);
116
117/// Port of `int trap_return;` from `Src/exec.c:155`. Carries the
118/// pending exit status from inside a trap; sentinel `-2` means
119/// "running an EXIT/DEBUG-style trap at the current level"
120/// (signals.c:1166). Promoted to the user's `return N` value by
121/// `bin_return` when POSIX-trap semantics apply (builtin.c:5852).
122pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = // c:155 (Src/exec.c)
123    std::sync::atomic::AtomicI32::new(0);
124
125/// Port of `int forklevel;` from `Src/exec.c:1052`. Records the
126/// `locallevel` at the most recent fork point (set at c:1221:
127/// `forklevel = locallevel;` inside `entersubsh()`). Used by:
128///   - `signals.c:808` SIGPIPE handler — `!forklevel` distinguishes
129///     the top-level shell from a forked subshell.
130///   - `exec.c:6146` — `if (locallevel > forklevel)` decides whether
131///     a function-defined trap should fire on this subshell exit.
132///   - `params.c:3724` — WARNCREATEGLOBAL nest-depth check.
133///
134/// Initialised to 0 (no fork has occurred yet). Set to `locallevel`
135/// at every `entersubsh()` entry per c:1221.
136pub static FORKLEVEL: std::sync::atomic::AtomicI32 = // c:1052 (Src/exec.c)
137    std::sync::atomic::AtomicI32::new(0);
138
139// =============================================================================
140// File-static globals from Src/exec.c. Bucket choices per PORT_PLAN.md:
141//   - Per-evaluator transient state → thread_local Cell (bucket 1)
142//   - Shell-wide shared state       → AtomicI32 / Mutex (bucket 2)
143// All names match C exactly. Surrounding doc-comments cite the C
144// declaration line.
145// =============================================================================
146
147/// Port of `int noerrexit;` from `Src/exec.c:72`. Bit-flags that
148/// suppress ERREXIT triggering on the next command(s). Bits:
149/// `NOERREXIT_EXIT` (in `if`/`while`/`until` test contexts),
150/// `NOERREXIT_RETURN` (after `return`), `NOERREXIT_UNTIL_EXEC`
151/// (until next exec'd command). Bucket-1 — per-evaluator (each
152/// recursive eval has its own suppression frame).
153pub static noerrexit: std::sync::atomic::AtomicI32 = // c:72 (Src/exec.c)
154    std::sync::atomic::AtomicI32::new(0);
155
156/// Port of `int this_noerrexit;` from `Src/exec.c:109`. When set,
157/// suppress ERREXIT for THIS one command only (consumed + cleared
158/// before the next command starts). Set by `execcursh` and the
159/// `((expr))` arith path so a 0-result doesn't trigger errexit.
160pub static this_noerrexit: std::sync::atomic::AtomicI32 = // c:109 (Src/exec.c)
161    std::sync::atomic::AtomicI32::new(0);
162
163/// Port of `mod_export int noerrs;` from `Src/exec.c:117`. When
164/// non-zero, suppress `zerr()` output (lex error reporting during
165/// `parse_string`, `parseopts` etc.). Saved/restored by
166/// `execsave`/`execrestore`.
167/// Port of `static char list_pipe_text[JOBTEXTSIZE]` from
168/// `Src/exec.c:463`. Holds the textual rendering of the in-flight
169/// pipe list; saved across nested execlist invocations at
170/// exec.c:1372-1380 (zeroed on entry, restored from
171/// `old_list_pipe_text` at c:1634-1638) and round-tripped through
172/// execsave/execrestore (c:6448 / c:6484). zshrs models it as a
173/// length-bounded String guarded by a Mutex — the C `char[80]` cap
174/// is a buffer-overflow guard, but matching length matters for the
175/// `jobs` builtin's pipe-list rendering.
176pub static LIST_PIPE_TEXT: std::sync::Mutex<String> = std::sync::Mutex::new(String::new()); // c:463 (Src/exec.c)
177
178pub static noerrs: std::sync::atomic::AtomicI32 = // c:117 (Src/exec.c)
179    std::sync::atomic::AtomicI32::new(0);
180
181/// Port of `int nohistsave;` from `Src/exec.c:122`. When non-zero,
182/// `addhistnode` no-ops so trap firings / `eval` invocations don't
183/// pollute `$HISTCMD`. Tracked alongside `noerrs` in the trap path.
184pub static nohistsave: std::sync::atomic::AtomicI32 = // c:122 (Src/exec.c)
185    std::sync::atomic::AtomicI32::new(0);
186
187/// Port of `int subsh;` from `Src/exec.c:160`. Subshell depth — bumped
188/// every time `entersubsh` forks a sub-shell, used by signal handling
189/// (different SIGINT semantics in subshells) and by `${$$}` (`$$`
190/// stays at the top-level pid).
191pub static subsh: std::sync::atomic::AtomicI32 = // c:160 (Src/exec.c)
192    std::sync::atomic::AtomicI32::new(0);
193
194/// Port of `mod_export int zsh_subshell;` from `Src/init.c:67`. Visible
195/// `$ZSH_SUBSHELL` parameter — incremented by `entersubsh()` each time
196/// the shell forks into a subshell (real or fake-exec). Distinct from
197/// `subsh` which records whether we ARE a subshell; `zsh_subshell` is
198/// the visible depth count.
199pub static zsh_subshell: std::sync::atomic::AtomicI32 = // c:67 (Src/init.c)
200    std::sync::atomic::AtomicI32::new(0);
201
202/// Port of `mod_export volatile int retflag;` from `Src/exec.c:165`.
203/// Set by `bin_return` to unwind the function-call stack. Cleared
204/// by `runshfunc` on entry, checked by `execlist`'s main loop.
205///
206/// Re-export alias of the canonical [`crate::ported::builtin::RETFLAG`] — C
207/// has ONE `retflag` (exec.c:165). All return-flow logic (execlist, the
208/// `return` builtin, signals) uses `RETFLAG`; this lowercase twin was never
209/// stored, so `zpty`/`system` module read-loops that check `exec::retflag`
210/// saw a permanent 0 and never aborted on a pending `return`.
211pub use crate::ported::builtin::RETFLAG as retflag; // c:165 (Src/exec.c)
212
213/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
214/// recent `$(cmd)` command-substitution child. Used by exit-status
215/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
216/// the pid `waitpid`-d for it.
217pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
218    std::sync::atomic::AtomicI32::new(0);
219
220/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
221/// Pid of the most recent process-substitution child (`<(cmd)` /
222/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
223/// jobs aren't wait-collected by the parent until the fd is closed.
224pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
225    std::sync::atomic::AtomicI32::new(0);
226
227/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
228/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
229/// runs alongside a substitution.
230pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
231    std::sync::atomic::AtomicI32::new(0);
232
233/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
234/// `lastval` is updated from `cmdoutval` after the command
235/// (i.e. the command had substitutions whose exit status matters).
236pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
237    std::sync::atomic::AtomicI32::new(0);
238
239/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
240/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
241/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
242/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
243/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
244/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
245pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
246    std::sync::atomic::AtomicI32::new(0);
247
248/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
249/// currently-executing pipeline is the long-running pipe-into-loop
250/// shape (`cat foo | while read a; do ... done`) — drives the
251/// super/sub-job tracking documented in the famous `Allen Edeln…`
252/// comment block above this declaration in C.
253pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
254    std::sync::atomic::AtomicI32::new(0);
255
256/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
257/// dispatch of a "simple" pipeline (single-stage / no shell-construct
258/// tail) so the `list_pipe` machinery short-circuits.
259pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
260    std::sync::atomic::AtomicI32::new(0);
261
262/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
263/// PID of the sub-shell created to host the loop-after-pipe pattern;
264/// passed up the recursive `execlist` stack so the cat-job's super-
265/// job entry can record it.
266pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
267    std::sync::atomic::AtomicI32::new(0);
268
269/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
270/// `execpline` doesn't wait for the pipeline; used during the
271/// list_pipe sub-shell fork bookkeeping.
272pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
273    std::sync::atomic::AtomicI32::new(0);
274
275/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
276/// pipeline depth (counts nested pipelines within the current
277/// `execlist` call chain).
278pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
279    std::sync::atomic::AtomicI32::new(0);
280
281/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
282/// Set in the child after the list_pipe fork so the child knows to
283/// continue executing the loop body (vs the parent which records
284/// the pid + returns).
285pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
286    std::sync::atomic::AtomicI32::new(0);
287
288/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
289/// table index of the pipeline's first-stage job (the `cat` in
290/// `cat foo | while ...`).
291pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
292    std::sync::atomic::AtomicI32::new(0);
293
294/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
295/// `printprompt4` has emitted the `$PS4` prefix for the current
296/// xtrace command — prevents double-printing when an inner sub-eval
297/// also wants to xtrace.
298pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
299    std::sync::atomic::AtomicI32::new(0);
300
301/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
302///
303/// File-static "execsubst parameters" — callers (execcmd_exec at
304/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
305/// uses them as the `flags` arg to prefork() and the gate on
306/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
307/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
308/// when the dispatched builtin has `BINF_NOGLOB`.
309pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
310    std::sync::atomic::AtomicI32::new(0);
311pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
312    std::sync::atomic::AtomicI32::new(1);
313
314/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
315/// of the linked exec-context save stack — `execsave` pushes a frame
316/// before signal-handler / trap dispatch; `execrestore` pops it
317/// afterwards so the interrupted command resumes with its state intact.
318pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
319    std::sync::Mutex::new(None);
320
321/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
322/// `stty` argument string captured by `addvars` when the command's
323/// inline env contains `STTY=...`. Applied by `execute` before fork
324/// + exec so the spawned program sees its tty configured. Reset to
325/// `None` after consumption to avoid infinite recursion.
326pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
327    std::sync::Mutex::new(None);
328
329/// Convert a here-document into a here-string. Line-by-line port of
330/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
331/// input stream via `hgetc()` until the terminator line is matched,
332/// returning the collected body as a string. `strp` is in/out: on
333/// entry the raw terminator (possibly with token markers + leading
334/// tabs); on return the munged terminator (after `quotesubst` +
335/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
336///
337/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
338/// Rust's `String` auto-grows so the OOM branch is effectively
339/// unreachable, but the return type stays `Option<String>` to mirror
340/// the C signature which can return NULL.
341///
342/// Port of `gethere(char **strp, int typ)` from `Src/exec.c:4573`.
343pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
344    // c:4573 (Src/exec.c)
345    let mut buf: String; // c:4575 char *buf
346    let mut bsiz: usize; // c:4576 int bsiz
347    let mut qt: i32 = 0; // c:4576 int qt = 0
348    let mut strip: i32 = 0; // c:4576 int strip = 0
349                            // c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
350                            // `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
351                            // C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
352                            // `s` (the loop iterator for the inull-scan) stays local to its
353                            // for-loop. `c` mirrors the C `char c`.
354    let mut t: usize; // c:4577 char *t
355    let mut c: Option<char>; // c:4577 char c
356    let mut str: String = strp.clone(); // c:4578 char *str = *strp
357
358    // c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
359    for s in str.bytes() {
360        if inull(s) {
361            // c:4581
362            qt = 1; // c:4582
363            break; // c:4583
364        }
365    }
366    str = quotesubst(&str); // c:4585
367    str = untokenize(&str); // c:4586
368    if typ == REDIR_HEREDOCDASH {
369        // c:4587
370        strip = 1; // c:4588
371                   // c:4589-4590 — while (*str == '\t') str++;
372        while str.starts_with('\t') {
373            str.remove(0);
374        }
375    }
376    *strp = str.clone(); // c:4592 *strp = str
377
378    // c:4593 — bptr = buf = zalloc(bsiz = 256);
379    bsiz = 256;
380    buf = String::with_capacity(bsiz);
381    let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
382
383    // c:4594 — for (;;)
384    loop {
385        t = buf.len(); // c:4595 t = bptr
386
387        // c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
388        loop {
389            c = hgetc();
390            if !(c == Some('\t') && strip != 0) {
391                break;
392            }
393        }
394
395        // c:4599 — for (;;) — inner body-read loop
396        loop {
397            // c:4600-4613 — buffer-growth realloc dance. Rust's
398            // String auto-grows; nothing to do.
399            // c:4614 — if (lexstop || c == '\n') break;
400            if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
401                break;
402            }
403            // c:4616 — if (!qt && c == '\\')
404            if qt == 0 && c == Some('\\') {
405                buf.push('\\'); // c:4617 *bptr++ = c
406                c = hgetc(); // c:4618
407                if c == Some('\n') {
408                    // c:4619
409                    buf.pop(); // c:4620 bptr--
410                    c = hgetc(); // c:4621
411                    continue; // c:4622
412                }
413            }
414            if let Some(ch) = c {
415                // c:4625 *bptr++ = c
416                buf.push(ch);
417            }
418            c = hgetc(); // c:4626
419        }
420        // c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
421
422        // c:4629-4630 — if (!strcmp(t, str)) break;
423        if &buf[t..] == str.as_str() {
424            break;
425        }
426        // c:4631-4634 — if (lexstop) { t = bptr; break; }
427        // C's ingetc sets `lexstop = 1` when input is exhausted
428        // (Src/input.c:289-292), so the flag check alone suffices
429        // there. zshrs's hgetc signals the same exhaustion by
430        // returning None WITHOUT setting LEX_LEXSTOP — treat both as
431        // the c:4631 condition, else an unterminated heredoc loops
432        // here forever appending '\n'. Gap #3 2026-06-12 (A04
433        // "Here-documents don't perform shell expansion" wedge).
434        if LEX_LEXSTOP.with(|f| f.get()) || c.is_none() {
435            t = buf.len();
436            break;
437        }
438        // c:4635 — *bptr++ = '\n';
439        buf.push('\n');
440    }
441    // c:4637 — *t = '\0';
442    buf.truncate(t);
443
444    // c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
445    // The C dance frees the realloc'd block and re-allocates via the
446    // string-heap allocator. Rust drops the old String when reassigned.
447    buf = dupstring(&buf);
448
449    if qt == 0 {
450        // c:4641
451        // c:4642 — int ef = errflag;
452        let ef = errflag.load(Ordering::Relaxed);
453        // c:4644 — parsestr(&buf);
454        if let Ok(parsed) = parsestr(&buf) {
455            buf = parsed;
456        }
457        // c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
458        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
459            let cur = errflag.load(Ordering::Relaxed);
460            errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
461        }
462    }
463    Some(buf) // c:4651 return buf
464}
465
466/// Port of `LinkList getoutput(char *cmd, int qt)` from
467/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
468/// active executor, then routes the captured stdout through
469/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
470///
471/// C return shape: `LinkList` of `char*`. Rust port returns
472/// `Vec<String>` (same shape, owned).
473///
474/// `qt` matches C exactly:
475///   - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
476///     entire output as a single-element vec. C c:4858-4862: if
477///     output empty, returns a single Nularg sentinel so callers
478///     see "empty value" rather than "no value".
479///   - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
480///     `spacesplit(buf, allownull=false)` per c:4865-4871.
481///
482/// Uses `with_executor` (panics on missing VM context), not
483/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
484/// `execpline` directly — there's no "no shell" code path. The
485/// silent-no-op pattern (return empty string when no executor) would
486/// mask catastrophic state corruption as "command produced no output",
487/// which is the failure mode the `subst.rs:496` warning block flags.
488/* $(...) */
489// c:4709
490/// `getoutput` — see implementation.
491pub fn getoutput(cmd: &str, qt: i32) -> Vec<String> {
492    // c:4713
493    // c:4715 — `Eprog prog;`
494    let prog: Option<eprog>;
495    // c:4716 — `int pipes[2];`  (collapsed: in-process executor; no fork)
496    // c:4717 — `pid_t pid;`     (collapsed)
497    let mut s: String; // c:4718
498                       // c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
499                       //                !sourcelevel && unset(INTERACTIVECOMMENTS));
500                       //                prog = parse_string(cmd, 0); nocomments = onc;`
501    let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
502    let new_nc = crate::ported::zsh_h::interact()
503        && crate::ported::init::sourcelevel.load(Ordering::Relaxed) == 0
504        && !isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
505    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
506    prog = parse_string(cmd, 0);
507    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
508
509    if prog.is_none() {
510        // c:4725
511        return Vec::new(); // c:4726 return NULL
512    }
513    let prog = prog.unwrap();
514
515    if !isset(crate::ported::zsh_h::EXECOPT) {
516        // c:4728
517        return Vec::new(); // c:4729 newlinklist()
518    }
519
520    // c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
521    if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
522        /* $(< word) */
523        // c:4732
524        s = red_name;
525        s = singsub(&s); // c:4737
526        if errflag.load(Ordering::Relaxed) != 0 {
527            return Vec::new(); // c:4739
528        }
529        let s = untokenize(&s); // c:4740
530        let path_meta = unmeta(&s); // c:4741 unmeta(s)
531        let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
532            Ok(c) => c,
533            Err(_) => return Vec::new(),
534        };
535        let stream = unsafe {
536            libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
537        };
538        if stream == -1 {
539            // c:4742 — `zwarn("%e: %s", errno, s);`
540            let errno = std::io::Error::last_os_error();
541            zerr(&format!("{}: {}", errno, s));
542            LASTVAL.store(1, Ordering::Relaxed);
543            cmdoutval.store(1, Ordering::Relaxed);
544            return Vec::new(); // c:4744
545        }
546        // c:4746 — `retval = readoutput(stream, qt, &readerror);`
547        let mut readerror: i32 = 0;
548        let retval = readoutput(stream, qt, &mut readerror); // c:4746
549        if readerror != 0 {
550            // c:4747
551            zerr(&format!(
552                "error when reading {}: {}", // c:4748
553                s,
554                std::io::Error::from_raw_os_error(readerror)
555            ));
556            LASTVAL.store(1, Ordering::Relaxed);
557            cmdoutval.store(1, Ordering::Relaxed);
558        }
559        return retval; // c:4751
560    }
561
562    // c:4753-4790 — Full fork path: mpipe + zfork + parent
563    // readoutput / waitforpid / child execode + _realexit. fusevm runs
564    // command substitution in-process, so the fork shape collapses to a
565    // synchronous executor call. C control points preserved as cites:
566    //   c:4753 mpipe       — handled by ShellExecutor pipe wiring
567    //   c:4758 child_block — no-op (no fork)
568    //   c:4760 zfork       — replaced by in-process exec
569    //   c:4768-4776 parent — equivalent to executor return
570    //   c:4778-4789 child  — entersubsh+execode+_realexit collapse
571    cmdoutval.store(0, Ordering::Relaxed); // c:4759
572    let buf = crate::ported::exec::run_command_substitution(cmd);
573    LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed); // c:4775
574
575    // c:4772 retval = readoutput — post-walk (c:4855-4871 tail) inlined.
576    let buf = buf.trim_end_matches('\n');
577    if qt != 0 {
578        if buf.is_empty() {
579            vec![String::from(Nularg)] // c:4859-4861
580        } else {
581            vec![buf.to_string()] // c:4863
582        }
583    } else {
584        crate::ported::utils::spacesplit(buf, false) // c:4865
585    }
586}
587
588/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
589/// int ignore_loaddir)` from `Src/exec.c:5050`. Walks `$fpath` for a
590/// file named `shf->node.nam`, reads it, installs the text body on
591/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
592///
593/// C body (abridged):
594///   1. `name = shf->node.nam`
595///   2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
596///   3. If !test_only && file found: parse → store eprog on
597///      `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
598///   4. Returns shf on success, NULL on failure.
599///
600/// Rust port: returns 0 = success, 1 = failure (matches the
601/// existing call-site convention in `bin_functions -c`). Stores
602/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
603/// `hashtable.rs:362`); the parser pass that converts text →
604/// Eprog runs lazily at first call site.
605/// Port of `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)` from `Src/exec.c:5682`.
606pub fn loadautofn(
607    shf: *mut shfunc, // c:5682 (Src/exec.c)
608    _ks: i32,
609    autol: i32,
610    _ignore_loaddir: i32,
611) -> i32 {
612    if shf.is_null() {
613        return 1;
614    }
615    // c:5054 — `name = shf->node.nam`.
616    let name = unsafe { (*shf).node.nam.clone() };
617    // c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
618    let mut dir_path: Option<String> = None;
619    let mut dump_hit: Option<(eprog, i32)> = None;
620    // A function autoloaded by ABSOLUTE PATH (`autoload -Uz /dir/name`) — or
621    // one already resolved via `autoload -r` — caches its directory in
622    // `shf.filename` with PM_LOADDIR set. Load-on-call must search THAT dir
623    // (`shf.filename/name`), not walk `$fpath` (which won't contain it).
624    // Without this, `autoload -Uz $fdir/.hist.*` (zsh-hist) named the
625    // functions correctly but every call died with "definition file not
626    // found". Mirrors C loadautofn's PM_LOADDIR spec-path branch.
627    let loaddir_spec: Option<Vec<String>> = {
628        let s = unsafe { &*shf };
629        if (s.node.flags as u32 & PM_LOADDIR) != 0 {
630            s.filename.clone().map(|d| vec![d])
631        } else {
632            None
633        }
634    };
635    let path = match getfpfunc(&name, &mut dir_path, loaddir_spec.as_deref(), 0, &mut dump_hit) {
636        Some(p) => p,
637        None => {
638            // !!! WARNING: RUST-ONLY BRANCH — NO DIRECT C COUNTERPART !!!
639            // compsys ships as native Rust functions (src/compsys/router.rs),
640            // so names like `_main_complete` have no definition file in
641            // $fpath. C zsh always loads them from files; zshrs must let
642            // `autoload +X -Uz _main_complete` (e.g. fzf-tab via zinit's
643            // :zinit-tmp-subst-autoload, zinit.zsh:356) succeed without one.
644            // Mark the stub loaded and return success — call-time dispatch
645            // short-circuits to the native fn (vm_helper.rs:2276), so no
646            // funcdef/body is needed.
647            if crate::compsys::router::is_intercepted(&name) {
648                unsafe {
649                    (*shf).node.flags &= !(PM_UNDEFINED as i32);
650                }
651                if let Ok(mut tab) = shfunctab_lock().write() {
652                    if let Some(existing) = tab.get_mut(&name) {
653                        existing.node.flags &= !(PM_UNDEFINED as i32);
654                    }
655                }
656                return 0;
657            }
658            // c:Src/exec.c:5713-5719 — file not found path. C:
659            //   `if (prog == &dummy_eprog) {
660            //        locallevel--;
661            //        zwarn("%s: function definition file not found",
662            //              shf->node.nam);
663            //        locallevel++;
664            //        popheap();
665            //        return NULL;
666            //    }`
667            // C's getfpfunc returns &dummy_eprog as the "not found"
668            // sentinel when test_only==0; loadautofn detects it and
669            // emits the diagnostic before returning NULL. Rust's
670            // getfpfunc returns Option::None for the same condition,
671            // so we emit the same diagnostic here. The locallevel
672            // dance is preserved as a comment because the Rust
673            // port's zwarn doesn't reference locallevel in the
674            // format string itself (the dance in C is only to keep
675            // the prefix line counter consistent with the function-
676            // body context). Bug #107 in docs/BUGS.md.
677            crate::ported::utils::zwarn(&format!("{}: function definition file not found", name));
678            return 1; // c:5719 NULL
679        }
680    };
681    let _ = autol;
682    // Previously the Rust port treated this parameter as
683    // "test_only" and early-returned when set, so the `+X`
684    // call from `eval_autoload` (`loadautofn(shf, mode, 1, d)`)
685    // never actually loaded the file. C's parameter is `autol`
686    // (autoload mode), NOT a test-only flag — the C body
687    // unconditionally loads/parses regardless of autol. autol=1
688    // controls the EF_RUN / map-flag dance for the wordcode prog
689    // (c:5725-5749), but the loaded-body / PM_UNDEFINED-clear
690    // path runs in all cases. Removing the early-return so
691    // `autoload -U +X funcname` actually loads the body and
692    // `type funcname` reports `function from /path/file` instead
693    // of `autoload shell function`. Bug #160 in docs/BUGS.md.
694    // c:5100-5140 — read the file. C uses zopen + read + parse_string +
695    // execsave; Rust port stores raw text on the ShFunc and defers
696    // parse-to-Eprog until the first call.
697    //
698    // c:Src/exec.c:6238 / parse.c:3833 — when getfpfunc resolved the
699    // function out of a compiled `.zwc` dump, there is no source file
700    // to read; the wordcode Eprog came back through `dump_hit`. C
701    // executes that wordcode directly (`shf->funcdef = stripkshdef(
702    // prog, ...)`, exec.c:5753-5755). zshrs executes function bodies
703    // through the fusevm bytecode pipeline which consumes source
704    // text, so bridge wordcode → text with the canonical text.c
705    // renderer (`getpermtext`, ported at text.rs:189) — the same
706    // walker `functions NAME` printing uses for wordcode-backed
707    // funcdefs (C hashtable.c:954). The downstream
708    // `autoload_register_source` step (vm_helper.rs:3162) performs
709    // the `stripkshdef` shape decision, matching c:5725-5760.
710    // c:5706-5710 — the ksh-mode precedence chain:
711    //   `if (ksh == 1) { ksh = fksh; if (ksh == 1)
712    //        ksh = PM_KSHSTORED ? 2 : PM_ZSHSTORED ? 0 : 1; }`
713    // The dump header flag (FDHF_KSHLOAD/FDHF_ZSHLOAD via `*ksh`
714    // from try_dump_file) outranks the stub's PM_*STORED bits, which
715    // are only consulted when the dump says 1 (no explicit style).
716    // zshrs's load/register split (vm_helper's
717    // `autoload_register_source` makes the c:5725 ksh-vs-zsh
718    // decision later, from the tab entry's flags + KSHAUTOLOAD) —
719    // fold a decisive dump flag into the PM bits so the downstream
720    // decision sees the same precedence.
721    let dump_ksh = dump_hit.as_ref().map(|(_, k)| *k);
722    let body = match dump_hit {
723        Some((prog, _ksh)) => crate::ported::text::getpermtext(Box::new(prog), None, 0),
724        None => match std::fs::read_to_string(&path) {
725            Ok(t) => t,
726            Err(_) => return 1,
727        },
728    };
729    // c:Src/exec.c:5735/5757 — `loadautofnsetfile(shf, fdir)`. The
730    // helper stamps PM_LOADDIR alongside the filename when fdir is
731    // present, so `whence -v NAME` later concatenates the directory
732    // with `/NAME` (PM_LOADDIR branch at hashtable.rs:1350). zshrs's
733    // prior `shf->filename = dir_path` assignment skipped the flag
734    // → `type colors` printed `from /path/to/functions` instead of
735    // `from /path/to/functions/colors`. Mirror C exactly.
736    unsafe {
737        loadautofnsetfile(&mut *shf, dir_path.as_deref().or(Some(&path)));
738    }
739    // c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
740    unsafe {
741        (*shf).node.flags &= !(PM_UNDEFINED as i32);
742    }
743    // c:5706-5710 fold (see comment above): decisive dump style wins
744    // over the stub's stored-style bits.
745    let (ksh_on, ksh_off): (i32, i32) = match dump_ksh {
746        Some(2) => (
747            crate::ported::zsh_h::PM_KSHSTORED as i32,
748            crate::ported::zsh_h::PM_ZSHSTORED as i32,
749        ),
750        Some(0) => (
751            crate::ported::zsh_h::PM_ZSHSTORED as i32,
752            crate::ported::zsh_h::PM_KSHSTORED as i32,
753        ),
754        _ => (0, 0),
755    };
756    unsafe {
757        (*shf).node.flags = ((*shf).node.flags | ksh_on) & !ksh_off;
758    }
759    // Sync the body string into the Rust-side ShFunc table so the
760    // lazy-parse path can find it later.
761    if let Ok(mut tab) = shfunctab_lock().write() {
762        if let Some(existing) = tab.get_mut(&name) {
763            existing.body = Some(body);
764            existing.node.flags = (existing.node.flags | ksh_on) & !ksh_off;
765            // c:5657 loadautofnsetfile — store the fpath DIRECTORY absolutized
766            // with PM_LOADDIR (not the raw relative `./fns`), so `whence -v`
767            // prints the absolute source. The `+X` (eval-autoload) path relies
768            // on this since it does not re-register through vm_helper.
769            loadautofnsetfile(existing, dir_path.as_deref());
770        } else {
771            let mut shf = shfunc {
772                node: hashnode {
773                    next: None,
774                    nam: name.clone(),
775                    flags: ksh_on, // c:5706-5710 dump-style fold
776                },
777                filename: None,
778                lineno: 0,
779                funcdef: None,
780                redir: None,
781                sticky: None,
782                body: Some(body),
783            };
784            loadautofnsetfile(&mut shf, dir_path.as_deref()); // c:5657
785            tab.add(shf);
786        }
787    }
788    0
789}
790
791/// Port of `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)` from Src/exec.c:6219. Walks `$fpath` (or the
792/// supplied `spec_path` slice) for a file named `name` and writes the
793/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
794/// Returns `Some(file_path)` on success, `None` when not found.
795///
796/// Per dir, the compiled-dump lookup runs FIRST (c:6238
797/// `try_dump_file(*pp, s, buf, ksh, test_only)`) — a directory
798/// digest `<dir>.zwc` or per-function `<dir>/<name>.zwc` wins over
799/// the plain file when newer (mtime logic inside try_dump_file,
800/// c:parse.c:3762-3784). On a dump hit the loaded program + ksh
801/// mode (C's `*ksh` out-param) are written through `dump_out` and
802/// the nominal `<dir>/<name>` path is returned; the caller must
803/// check `dump_out` before reading the returned path as a plain
804/// file.
805pub fn getfpfunc(
806    name: &str,
807    dir_path_out: &mut Option<String>, // c:6219 (Src/exec.c)
808    spec_path: Option<&[String]>,
809    test_only: i32,                      // c:6219 `int test_only`
810    dump_out: &mut Option<(eprog, i32)>, // c:6219 `int *ksh` + dump Eprog return
811) -> Option<String> {
812    // C reads $fpath via `getaparam("fpath")` (the param-table array form
813    // tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
814    // misses any in-script modification like `fpath=(/some/dir $fpath)`
815    // because that mutates the internal param table, not the inherited
816    // process env. Fall back to env only when the param table is empty
817    // (cold start before any param-table init).
818    let dirs: Vec<String> = match spec_path {
819        Some(s) => s.to_vec(),
820        None => crate::ported::params::getaparam("fpath")
821            .filter(|v| !v.is_empty())
822            .or_else(|| getsparam("FPATH").map(|v| v.split(':').map(String::from).collect()))
823            .or_else(|| {
824                std::env::var("FPATH")
825                    .ok()
826                    .map(|v| v.split(':').map(String::from).collect())
827            })
828            .unwrap_or_default(),
829    };
830    for dir in &dirs {
831        if dir.is_empty() {
832            continue;
833        }
834        let path = format!("{}/{}", dir, name); // c:6230 snprintf(buf, ..., "%s/%s", *pp, s)
835                                                // c:6238 — `if ((r = try_dump_file(*pp, s, buf, ksh, test_only)))`
836                                                // — the .zwc digest / per-function dump is tried BEFORE the
837                                                // plain file in each directory.
838        if let Some(hit) = crate::ported::parse::try_dump_file(dir, name, &path, test_only != 0) {
839            *dump_out = Some(hit);
840            *dir_path_out = Some(dir.clone()); // c:6240 `*fdir = *pp;`
841            return Some(path); // c:6241
842        }
843        if std::path::Path::new(&path).exists() {
844            // c:6245 access(buf, R_OK)
845            *dir_path_out = Some(dir.clone());
846            return Some(path);
847        }
848    }
849    None
850}
851
852/// Port of `resolvebuiltin(const char *cmdarg, HashNode hn)` from
853/// `Src/exec.c:2703`. Ensures that an autoload-stub builtin has its
854/// module loaded before the caller invokes its `handlerfunc`. If the
855/// stub has no handler, `ensurefeature` is asked to load the module
856/// and re-lookup the builtin node. C body (abridged):
857/// ```c
858/// if (!((Builtin) hn)->handlerfunc) {
859///     char *modname = dupstring(((Builtin) hn)->optstr);
860///     (void)ensurefeature(modname, "b:", ...);
861///     hn = builtintab->getnode(builtintab, cmdarg);
862///     if (!hn) { lastval=1; zerr(...); return NULL; }
863/// }
864/// return hn;
865/// ```
866///
867/// WARNING: zshrs's builtin table is the static `BUILTINS` array in
868/// `src/ported/builtin.rs`. Module autoload routes through
869/// `module::ensurefeature(MODULESTAB, modname, "b:", Some(cmdarg))`;
870/// after the module loads the handler should be wired into BUILTINS.
871pub fn resolvebuiltin<'a>(
872    cmdarg: &str, // c:2703 (Src/exec.c)
873    hn: &'a builtin,
874) -> Option<&'a builtin> {
875    // c:2705 — `if (!((Builtin) hn)->handlerfunc)`.
876    if hn.handlerfunc.is_none() {
877        // c:2706 — `modname = dupstring(((Builtin)hn)->optstr)`.
878        let modname = hn.optstr.clone().unwrap_or_default();
879        // c:2712 — `ensurefeature(modname, "b:", cmdarg)`.
880        let _ = {
881            let mut t = crate::ported::module::MODULESTAB.lock().unwrap();
882            crate::ported::module::ensurefeature(&mut t, &modname, "b:", Some(cmdarg))
883        };
884        // c:2715-2716 — re-lookup the now-(hopefully)-resolved builtin.
885        if let Some(re) = BUILTINS.iter().find(|b| b.node.nam == cmdarg) {
886            if re.handlerfunc.is_some() {
887                return Some(re); // c:2723
888            }
889        }
890        // c:2717-2721 — `lastval = 1; zerr(...)` + return NULL.
891        zerr(&format!(
892            "autoloading module {} failed to define builtin: {}",
893            modname, cmdarg
894        ));
895        return None; // c:2720
896    }
897    Some(hn) // c:2723
898}
899
900/// Dispatch decision returned by `execcmd_compile_head` — the
901/// fusevm-bytecode-time head resolver that mirrors the local-variable
902/// state the C `execcmd_exec` function carries through `c:2913-2916`
903/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
904/// precmd-modifier strip count. The fusevm bytecode compiler reads
905/// this to emit the correct dispatch opcode in
906/// `src/extensions/compile_zsh.rs::compile_simple`.
907///
908/// Not a C struct — invented to bridge the divergence between the
909/// C wordcode-walker (which mutates locals + falls through to
910/// invocation) and zshrs's split parse → compile → VM pipeline.
911#[allow(non_camel_case_types)]
912#[derive(Debug, Default, Clone)]
913pub struct execcmd_dispatch {
914    /// Number of `BINF_PREFIX` words to strip from the head of args.
915    /// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
916    pub precmd_skip: usize,
917    /// Set when the head (after strip) is a real builtin
918    /// (`Src/exec.c:3065 is_builtin = 1`).
919    pub is_builtin: bool,
920    /// Set when the head (after strip) is a shell function
921    /// (`Src/exec.c:3053 is_shfunc = 1`).
922    pub is_shfunc: bool,
923    /// `cflags` accumulator from `Src/exec.c:2915` — gathers
924    /// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
925    /// BINF_NOGLOB` bits encountered during the precommand-modifier
926    /// walk (c:3062 `cflags |= hn->flags`).
927    pub cflags: u32,
928    /// `command -p` requested: use the default `$PATH` for lookup
929    /// (`Src/exec.c:3160 use_defpath = 1`). NOT YET HONORED by the
930    /// fusevm compiler — flagged for follow-up.
931    pub use_defpath: bool,
932    /// `command -v` / `command -V` requested: the dispatch target
933    /// flips to `bin_whence` per `Src/exec.c:3149-3157`
934    /// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
935    /// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
936    /// instead of resolving the post-strip head.
937    pub has_command_vv: bool,
938    /// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
939    /// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
940    pub exec_argv0: Option<String>,
941    /// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
942    /// — the `else` arm of `if (redir && nonempty(redir))`). Covers
943    /// bare `exec` / `noglob` / `command`. Caller emits
944    /// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
945    /// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
946    /// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
947    /// returns 0 without complaining about missing redirs).
948    pub is_empty_command: bool,
949}
950
951/// !!! NOT A PORT OF C `execcmd_exec` !!!
952///
953/// This is a fusevm-bytecode-time head resolver invoked by
954/// `src/extensions/compile_zsh.rs::compile_simple` and the
955/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
956/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
957/// file under the C-faithful name `execcmd_exec`.
958///
959/// This helper mirrors the head section (`c:2904-3275`) of the C
960/// function — local initialisation, the precommand-modifier walk
961/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
962/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
963/// sub-option parsers — and returns the resulting dispatch
964/// decision via `execcmd_dispatch`. The fusevm compiler reads
965/// that struct to decide which `Op::CallBuiltin` /
966/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
967/// correct post-strip `argc`.
968///
969/// =================== WARNING — DIVERGENCE ====================
970///
971/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
972/// `multio` redirections, evaluates `varspc` assignments, then calls
973/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
974/// stops after the precmd-modifier walk and only returns the head
975/// decision; runtime dispatch is driven by the bytecode the fusevm
976/// compiler emits.
977///
978/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
979/// wordcode iterator state — zshrs doesn't traverse wordcode here,
980/// so the args list arrives already-expanded as a `&[String]`
981/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
982/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
983///
984/// =============================================================
985pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
986    // c:2900 (Src/exec.c)
987
988    // c:2904-2916 — locals.
989    let mut hn: Option<&'static builtin> = None; // c:2904
990    let mut is_shfunc = false; // c:2913
991    let mut is_builtin = false; // c:2913
992    let mut use_defpath = false; // c:2913
993    let mut cflags: u32 = 0; // c:2915
994    let mut orig_cflags: u32 = 0; // c:2915
995    let _ = orig_cflags;
996    // c:3263 — `char *exec_argv0 = NULL;` (declared inside the
997    // BINF_EXEC arm; hoisted here so the dispatch struct can carry it
998    // out after the loop terminates).
999    let mut exec_argv0: Option<String> = None;
1000    // c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
1001    // (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
1002    // compiler can emit `bin_whence` instead of resolving the head.
1003    let mut has_command_vv = false;
1004
1005    // c:2962-2973 — `%job` head: rewrite `%name` → `fg|bg|disown %name`.
1006    // Not in scope for the compile-time dispatch walk: jobspec
1007    // expansion happens at runtime in fusevm; the bytecode emits a
1008    // direct `fg`/`bg` call when it sees a leading `%`. Flagged for
1009    // follow-up when the canonical port lands.
1010
1011    // c:2975-2986 — AUTORESUME prefix-match against jobtab. Same
1012    // status as the %job head: runtime concern, deferred.
1013
1014    // c:3013-3091 — precommand-modifier walk.
1015    let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
1016    let mut precmd_skip: usize = 0;
1017
1018    // c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
1019    if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
1020        // c:3018
1021        // c:3029 — `while (nonempty(preargs))`.
1022        while precmd_skip < preargs.len() {
1023            // c:3029
1024            // c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
1025            let cmdarg = untokenize(&preargs[precmd_skip]);
1026            // c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
1027            // already performed prefork expansion on `preargs`, so
1028            // `has_token` is effectively false here; the C `break` on
1029            // unexpanded tokens is unreachable in this entry point.
1030
1031            // c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
1032            // even disabled builtins so the reserved-word form
1033            // (`integer x`, `local foo`) still dispatches to the
1034            // typeset family. The static `BUILTINS` array doesn't
1035            // expose a separate disabled-bit lookup; one path covers
1036            // both. Effect is identical for the precmd-modifier walk.
1037
1038            // c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
1039            // BINF_COMMAND)) && shfunctab->getnode(...))` — shell
1040            // function takes precedence unless a `builtin`/`command`
1041            // modifier preceded it.
1042            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
1043                // c:3051
1044                if shfunctab_lock()
1045                    .read()
1046                    .map(|t| t.iter().any(|(k, _)| k == &cmdarg))
1047                    .unwrap_or(false)
1048                {
1049                    is_shfunc = true; // c:3053
1050                    break; // c:3054
1051                }
1052            }
1053            // c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
1054            let entry = BUILTINS.iter().find(|b| b.node.nam == cmdarg);
1055            let Some(entry) = entry else {
1056                // c:3056-3058
1057                break;
1058            };
1059            hn = Some(entry);
1060            // c:3061-3063 — accumulate cflags.
1061            orig_cflags |= cflags;
1062            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
1063            cflags |= entry.node.flags as u32;
1064            // c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
1065            // builtin, stop.
1066            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
1067                // c:3064
1068                // WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
1069                // to autoload the builtin's module if its
1070                // `handlerfunc` is NULL. In zshrs, builtins live in
1071                // two places: the static `BUILTINS` table (which
1072                // mirrors C `handlerfunc`, often `None` for ports
1073                // dispatched through fusevm) AND fusevm's
1074                // `register_builtins` map (the actual runtime
1075                // dispatcher). A null `handlerfunc` in the static
1076                // table is NOT an autoload failure for us — it
1077                // means dispatch routes through fusevm. So we
1078                // skip the resolvebuiltin call here; the faithful
1079                // port remains available for future callers that
1080                // genuinely need module-autoload semantics.
1081                is_builtin = true; // c:3065
1082                break; // c:3077
1083            }
1084            // c:3086 — `uremnode(preargs, firstnode(preargs))`.
1085            precmd_skip += 1;
1086            // c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
1087            //   (...); if (!firstnode(preargs)) break; }`. zshrs has
1088            // no `execcmd_getargs` (args arrive pre-expanded); the
1089            // bounds-check at the top of `while precmd_skip <
1090            // preargs.len()` handles the empty case identically.
1091
1092            // c:3092-3177 — BINF_COMMAND sub-option parsing
1093            // (`command -p / -v / -V`).
1094            if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
1095                // c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
1096                //                int has_p = 0, has_vV = 0, has_other = 0;`
1097                let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
1098                let mut pnode: Option<usize> = None; // c:3102
1099                let mut has_p = false; // c:3104
1100                let mut has_vv = false; // c:3104
1101                let mut has_other = false; // c:3104
1102                                           // c:3107 — `while (IS_DASH(*argdata))`
1103                while argnode < preargs.len()
1104                    && IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
1105                {
1106                    let argdata = preargs[argnode].clone(); // c:3106
1107                    let bytes = argdata.as_bytes();
1108                    // c:3108-3111 — stop on bare `-` or `--`.
1109                    if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
1110                        // c:3109
1111                        break; // c:3111
1112                    }
1113                    // c:3112-3133 — scan flag chars.
1114                    for &c in &bytes[1..] {
1115                        // c:3112
1116                        match c as char {
1117                            'p' => {
1118                                // c:3114
1119                                has_p = true; // c:3122
1120                                pnode = Some(argnode); // c:3123
1121                            }
1122                            'v' | 'V' => {
1123                                // c:3125-3126
1124                                has_vv = true; // c:3127
1125                            }
1126                            _ => {
1127                                // c:3129
1128                                has_other = true; // c:3130
1129                            }
1130                        }
1131                    }
1132                    // c:3134-3138 — unknown flag → don't try, leave alone.
1133                    if has_other {
1134                        // c:3134
1135                        has_p = false; // c:3136
1136                        has_vv = false; // c:3136
1137                        break; // c:3137
1138                    }
1139                    // c:3140-3147 — advance to next arg.
1140                    argnode += 1; // c:3141 nextnode(argnode)
1141                    if argnode >= preargs.len() {
1142                        // c:3142 — execcmd_getargs (skipped: pre-expanded)
1143                        break; // c:3145
1144                    }
1145                }
1146                // c:3149-3157 — `-v`/`-V` → dispatch to whence.
1147                if has_vv {
1148                    // c:3149
1149                    // c:3154 `pushnode(preargs, "command")` — C re-inserts
1150                    // "command" so bin_whence sees it as argv[0]. zshrs
1151                    // surfaces this via `has_command_vv`; the fusevm
1152                    // compiler emits the equivalent whence call.
1153                    has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
1154                    is_builtin = true;
1155                    break; // c:3157
1156                } else if has_p {
1157                    // c:3158
1158                    use_defpath = true; // c:3160
1159                    if let Some(pn) = pnode {
1160                        // c:3165 — `uremnode(preargs, pnode)`. zshrs:
1161                        // remove the `-p`-bearing arg from preargs.
1162                        if pn < preargs.len() {
1163                            preargs.remove(pn);
1164                            // precmd_skip already accounts for the
1165                            // stripped `command` prefix; we just removed
1166                            // the `-p` flag which sat at preargs[pn].
1167                            // No precmd_skip change needed — the head
1168                            // remains where it was.
1169                        }
1170                    }
1171                }
1172                // c:3176-3177 — `--` trailing end-of-options strip.
1173                if argnode < preargs.len() {
1174                    let argdata = &preargs[argnode];
1175                    let b = argdata.as_bytes();
1176                    if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
1177                        // c:3176
1178                        preargs.remove(argnode); // c:3177
1179                    }
1180                }
1181            } else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
1182                // c:3178-3275 — BINF_EXEC sub-option parsing
1183                // (`exec -a NAME -l -c`).
1184                let mut argnode: usize = precmd_skip; // c:3185
1185                let mut error_done = false;
1186                // c:3196 — `while (argdata && IS_DASH(*argdata) &&
1187                //                  strlen(argdata) >= 2)`
1188                while argnode < preargs.len() {
1189                    let argdata = preargs[argnode].clone();
1190                    let bytes = argdata.as_bytes();
1191                    if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
1192                        break; // c:3196 loop guard
1193                    }
1194                    let oldnode = argnode; // c:3197
1195                    argnode += 1; // c:3198 nextnode(oldnode)
1196                                  // c:3203-3208 — empty next → error.
1197                    if argnode >= preargs.len() {
1198                        // c:3203
1199                        zerr(
1200                            // c:3204
1201                            "exec requires a command to execute",
1202                        );
1203                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
1204                        error_done = true;
1205                        break; // c:3207 goto done
1206                    }
1207                    // c:3209 — `uremnode(preargs, oldnode)`.
1208                    preargs.remove(oldnode);
1209                    argnode -= 1; // re-anchor — `argnode` was the post-removed slot
1210                                  // c:3210-3211 — `--` stops option scan.
1211                    if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
1212                        // c:3210
1213                        break; // c:3211
1214                    }
1215                    // c:3212-3258 — scan flag chars after the leading `-`.
1216                    let mut k = 1usize;
1217                    while k < bytes.len() && !error_done {
1218                        let cmdopt = bytes[k] as char; // c:3212
1219                        match cmdopt {
1220                            'a' => {
1221                                // c:3214 — `-a` ARGV0 override.
1222                                if k + 1 < bytes.len() {
1223                                    // c:3216 — `-aNAME` inline form.
1224                                    exec_argv0 =
1225                                        Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
1226                                    k = bytes.len(); // c:3219 position past end
1227                                } else {
1228                                    // c:3220 — `-a NAME` separate form.
1229                                    if argnode >= preargs.len() {
1230                                        // c:3230
1231                                        zerr(
1232                                            // c:3231
1233                                            "exec flag -a requires a parameter",
1234                                        );
1235                                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
1236                                        error_done = true;
1237                                        break; // c:3234 goto done
1238                                    }
1239                                    exec_argv0 = Some(preargs[argnode].clone()); // c:3236
1240                                    preargs.remove(argnode); // c:3239
1241                                }
1242                            }
1243                            'c' => {
1244                                // c:3242
1245                                cflags |= BINF_CLEARENV; // c:3243
1246                            }
1247                            'l' => {
1248                                // c:3245
1249                                cflags |= BINF_DASH; // c:3246
1250                            }
1251                            _ => {
1252                                // c:3248
1253                                zerr(
1254                                    // c:3249
1255                                    &format!("unknown exec flag -{}", cmdopt),
1256                                );
1257                                errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
1258                                error_done = true;
1259                                break; // c:3256
1260                            }
1261                        }
1262                        k += 1;
1263                    }
1264                    if error_done {
1265                        break;
1266                    }
1267                }
1268                // c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
1269                // the actual `setenv` to the fusevm compiler / external
1270                // exec path; we surface `exec_argv0` via the dispatch
1271                // struct so the caller can apply it before fork+exec.
1272                if let Some(ref a0) = exec_argv0 {
1273                    // c:3263 — `remnulargs + untokenize` then setenv.
1274                    let cleaned = untokenize(a0); // c:3266-3267
1275                    exec_argv0 = Some(cleaned);
1276                }
1277                if error_done {
1278                    return execcmd_dispatch {
1279                        precmd_skip,
1280                        is_builtin,
1281                        is_shfunc,
1282                        cflags,
1283                        use_defpath,
1284                        has_command_vv,
1285                        exec_argv0,
1286                        is_empty_command: false,
1287                    };
1288                }
1289            }
1290            // c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
1291            // unset(POSIXBUILTINS)) break;`. After processing a
1292            // `command` precmd modifier (and its -p/-v/-V flags), the
1293            // C loop exits with hn cleared so the dispatch falls
1294            // through to external lookup. Without this, the next
1295            // iteration would find `command print` → print's builtin
1296            // and dispatch to it; zsh's intentional behaviour is to
1297            // skip builtins under `command` (unless POSIXBUILTINS is
1298            // set, where the loop continues normally).
1299            if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
1300                hn = None; // c:3275 hn = NULL
1301                break; // c:3277
1302            }
1303        }
1304    }
1305
1306    // c:3309-3406 — "Empty command" branch. When the precmd-modifier
1307    // walk above strips every word with nothing left to dispatch
1308    // (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
1309    // C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
1310    //
1311    // - redir-present + do_exec       → nullexec=1 (continue to run)
1312    // - redir-present + varspc        → nullexec=2 (continue)
1313    // - redir-present + no nullcmd    → `zerr("redirection with no command")`
1314    //                                   lastval=1, return
1315    // - redir-present + SHNULLCMD     → args=[":"]
1316    // - redir-present + readnullcmd   → args=[readnullcmd]
1317    // - redir-present + default       → args=[nullcmd]
1318    // - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
1319    // - NO redir + default            → lastval=cmdoutval, return (c:3372-3406)
1320    //
1321    // zshrs's `execcmd_compile_head` doesn't receive `redir` (it
1322    // takes `args` only). The cases that DEPEND on redirs are handled by
1323    // `compile_zsh.rs::compile_redir` before this dispatch fires; the
1324    // remaining cases collapse into the single `is_empty_command`
1325    // flag below. Both NO-redir sub-cases produce the same observable
1326    // outcome (lastval=0, return without invoking anything), so a
1327    // single flag suffices.
1328    let is_empty_command = precmd_skip >= preargs.len();
1329
1330    // =================== WARNING — DIVERGENCE ====================
1331    // c:3285+: prefork-substitution, magic_assign decision, multio
1332    // setup, varspc evaluation, and the actual execbuiltin /
1333    // runshfunc / execute call. ~1300 lines of interpreter-only
1334    // code, entirely replaced by fusevm bytecode dispatch in
1335    // `src/extensions/compile_zsh.rs::compile_simple` and the
1336    // opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
1337    // The return value below feeds those compile-time decisions.
1338    // =============================================================
1339
1340    let _ = hn;
1341    execcmd_dispatch {
1342        precmd_skip,
1343        is_builtin,
1344        is_shfunc,
1345        cflags,
1346        use_defpath,
1347        has_command_vv,
1348        exec_argv0,
1349        is_empty_command,
1350    }
1351}
1352
1353// =============================================================================
1354// Leaf-function ports — c:283 (parse_string) and below. Added incrementally to
1355// chip at the ~5500 lines of exec.c still un-ported beyond the wordcode
1356// walker (execlist / execpline / execcmd which the fusevm bytecode VM
1357// replaces — see the WARNING block in execcmd_exec).
1358// =============================================================================
1359
1360/// Port of `parse_string(char *s, int reset_lineno)` from `Src/exec.c:283`.
1361///
1362/// C body:
1363/// ```c
1364/// Eprog p; zlong oldlineno;
1365/// zcontext_save();
1366/// inpush(s, INP_LINENO, NULL);
1367/// strinbeg(0);
1368/// oldlineno = lineno;
1369/// if (reset_lineno) lineno = 1;
1370/// p = parse_list();
1371/// lineno = oldlineno;
1372/// if (tok == LEXERR && !lastval) lastval = 1;
1373/// strinend();
1374/// inpop();
1375/// zcontext_restore();
1376/// return p;
1377/// ```
1378///
1379/// Parses an arbitrary string as a zsh command list, returning the
1380/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
1381/// `bin_eval` for `eval`, and the autoload path.
1382pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
1383    // c:285-286
1384    let p: Option<eprog>;
1385    let oldlineno: i64;
1386
1387    zcontext_save(); // c:288
1388    inpush(s, INP_LINENO, None); // c:289
1389    strinbeg(0); // c:290
1390    oldlineno = LEX_LINENO.get() as i64; // c:291
1391    if reset_lineno != 0 {
1392        // c:292
1393        LEX_LINENO.set(1); // c:293
1394    }
1395    p = parse_list(); // c:294
1396    LEX_LINENO.set(oldlineno as u64); // c:295
1397                                      // c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
1398    if tok() == LEXERR && LASTVAL.load(Ordering::Relaxed) == 0 {
1399        LASTVAL.store(1, Ordering::Relaxed);
1400    }
1401    strinend(); // c:298
1402    inpop(); // c:299
1403    zcontext_restore(); // c:300
1404    p // c:301
1405}
1406
1407/// Port of `int isgooderr(int e, char *dir)` from `Src/exec.c:652`.
1408///
1409/// C body:
1410/// ```c
1411/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
1412/// return ((e != EACCES || !access(dir, X_OK)) &&
1413///         e != ENOENT && e != ENOTDIR);
1414/// ```
1415///
1416/// errno classifier for `execve` failures during PATH search: if the
1417/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
1418/// it's "expected" (try next PATH entry); otherwise it's a real
1419/// failure worth surfacing.
1420pub fn isgooderr(e: i32, dir: &str) -> bool {
1421    // c:652
1422    // c:Src/exec.c:658-659 — `(e != EACCES || !access(dir, X_OK)) &&
1423    //   e != ENOENT && e != ENOTDIR`. C's `access(dir, X_OK)` returns
1424    //   0 on success / -1 on failure. The previous Rust port used
1425    //   `metadata().permissions().mode() & 0o111` which reports the
1426    //   X bit even when the path doesn't exist as the EFFECTIVE caller
1427    //   (root, ACLs, capabilities all flip access() vs raw mode).
1428    //   `/no/such/dir` metadata() fails → returned false for
1429    //   dir_x_ok, then `!false` = true, giving "good error" for a
1430    //   nonexistent path. Use libc::access directly to match C exactly.
1431    let unmeta_dir = unmeta(dir);
1432    let cstr = std::ffi::CString::new(unmeta_dir.as_bytes()).unwrap_or_default();
1433    let access_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1434    (e != libc::EACCES || access_ok) && e != libc::ENOENT && e != libc::ENOTDIR
1435}
1436
1437/// Port of `int iscom(char *s)` from `Src/exec.c:962`.
1438///
1439/// C body:
1440/// ```c
1441/// struct stat statbuf;
1442/// char *us = unmeta(s);
1443/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
1444///         S_ISREG(statbuf.st_mode));
1445/// ```
1446///
1447/// True iff `s` names an executable regular file (X-perm + S_IFREG).
1448/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
1449/// validate candidate paths before exec.
1450pub fn iscom(s: &str) -> bool {
1451    // c:962
1452    let us = unmeta(s); // c:965
1453                        // c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
1454    let cstr = match std::ffi::CString::new(us.as_str()) {
1455        Ok(c) => c,
1456        Err(_) => return false,
1457    };
1458    let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1459    if !x_ok {
1460        return false;
1461    }
1462    let meta = match std::fs::metadata(&us) {
1463        Ok(m) => m,
1464        Err(_) => return false,
1465    };
1466    meta.file_type().is_file()
1467}
1468
1469/// Port of `int isreallycom(Cmdnam cn)` from `Src/exec.c:972-987`.
1470///
1471/// Verify that a hashed/cached cmdnamtab entry still names a real
1472/// external command (X-perm + regular file). For HASHED entries
1473/// (`cn->u.cmd` carries the absolute path), test the path directly;
1474/// otherwise concatenate `name[0] + "/" + nam` and test that.
1475/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
1476/// turn into a failed `execve` syscall.
1477pub fn isreallycom(cn: &cmdnam) -> bool {
1478    // c:972
1479    let fullnam: String;
1480    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
1481        // c:977-978 — `strcpy(fullnam, cn->u.cmd);`
1482        fullnam = cn.cmd.clone().unwrap_or_default();
1483    } else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
1484        // c:979-980 — `if (!cn->u.name) return 0;`
1485        return false;
1486    } else {
1487        // c:982-984 — `strcpy + strcat("/") + strcat(nam)`
1488        let path0 = &cn.name.as_ref().unwrap()[0];
1489        fullnam = format!("{}/{}", path0, cn.node.nam);
1490    }
1491    iscom(&fullnam) // c:986
1492}
1493
1494/// Port of `int isrelative(char *s)` from `Src/exec.c:996`.
1495///
1496/// C body:
1497/// ```c
1498/// if (*s != '/') return 1;
1499/// for (; *s; s++)
1500///     if (*s == '.' && s[-1] == '/' &&
1501///         (s[1] == '/' || s[1] == '\0' ||
1502///          (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
1503///         return 1;
1504/// return 0;
1505/// ```
1506///
1507/// True iff `s` either doesn't start with `/` OR contains a `./` or
1508/// `../` component anywhere. Used by `cd` resolution and PATH-cache
1509/// invalidation to detect non-canonical paths.
1510pub fn isrelative(s: &str) -> i32 {
1511    // c:996
1512    let bytes = s.as_bytes();
1513    if bytes.is_empty() || bytes[0] != b'/' {
1514        // c:998
1515        return 1; // c:999
1516    }
1517    // c:1000-1004 — walk for `./` or `../` components.
1518    for i in 1..bytes.len() {
1519        let c = bytes[i];
1520        let prev = bytes[i - 1];
1521        if c == b'.' && prev == b'/' {
1522            let next = bytes.get(i + 1).copied().unwrap_or(0);
1523            if next == b'/' || next == 0 {
1524                // c:1002
1525                return 1;
1526            }
1527            if next == b'.' {
1528                let next2 = bytes.get(i + 2).copied().unwrap_or(0);
1529                if next2 == b'/' || next2 == 0 {
1530                    // c:1003
1531                    return 1;
1532                }
1533            }
1534        }
1535    }
1536    0 // c:1005
1537}
1538
1539/// Port of `void setunderscore(char *str)` from `Src/exec.c:2652`.
1540///
1541/// C body:
1542/// ```c
1543/// queue_signals();
1544/// if (str && *str) {
1545///     size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
1546///     if (nl > underscorelen || (underscorelen - nl) > 64) {
1547///         zfree(zunderscore, underscorelen);
1548///         zunderscore = (char *) zalloc(underscorelen = nl);
1549///     }
1550///     strcpy(zunderscore, str);
1551///     underscoreused = l;
1552/// } else {
1553///     ... reset zunderscore = "" ...
1554/// }
1555/// unqueue_signals();
1556/// ```
1557///
1558/// Sets the `$_` global to the last argument of the most recent
1559/// command. Called from `execcmd_exec` (c:3936) per `last_status`
1560/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
1561pub fn setunderscore(str: &str) {
1562    // c:2652
1563    queue_signals(); // c:2654
1564    if !str.is_empty() {
1565        // c:2655 `if (str && *str)`
1566        // c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
1567        let mut zu = zunderscore.lock().unwrap();
1568        *zu = str.to_string();
1569        let nl = (str.len() + 1 + 31) & !31; // c:2656
1570        underscorelen.store(nl, Ordering::Relaxed); // c:2660
1571        underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
1572    // c:2663
1573    } else {
1574        // c:2664
1575        let mut zu = zunderscore.lock().unwrap();
1576        zu.clear(); // c:2669 `*zunderscore = '\0';`
1577        underscoreused.store(1, Ordering::Relaxed); // c:2670
1578    }
1579    unqueue_signals(); // c:2672
1580}
1581
1582/// Port of `int mpipe(int *pp)` from `Src/exec.c:5160`.
1583///
1584/// C body:
1585/// ```c
1586/// if (pipe(pp) < 0) {
1587///     zerr("pipe failed: %e", errno);
1588///     return -1;
1589/// }
1590/// pp[0] = movefd(pp[0]);
1591/// pp[1] = movefd(pp[1]);
1592/// return 0;
1593/// ```
1594///
1595/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
1596/// fd range via `movefd`. Used by `getpipe` / `getproc` /
1597/// `spawnpipes` for process substitution and pipeline wiring.
1598pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
1599    // c:5160
1600    let mut fds: [libc::c_int; 2] = [-1; 2];
1601    if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
1602        // c:5162
1603        zerr(&format!(
1604            // c:5163
1605            "pipe failed: {}",
1606            std::io::Error::last_os_error()
1607        ));
1608        return -1; // c:5164
1609    }
1610    pp[0] = movefd(fds[0]); // c:5166
1611    pp[1] = movefd(fds[1]); // c:5167
1612    0 // c:5168
1613}
1614
1615/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
1616/// from `Src/exec.c:5289`. Anonymous-function name marker used by
1617/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
1618/// `() { ... }` anonymous function dispatch.
1619pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
1620
1621/// Port of `int is_anonymous_function_name(const char *name)` from
1622/// `Src/exec.c:5300`.
1623///
1624/// C body:
1625/// ```c
1626/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
1627/// ```
1628///
1629/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
1630/// reporting and `whence -v` to skip / annotate anonymous functions.
1631pub fn is_anonymous_function_name(name: &str) -> i32 {
1632    // c:5300
1633    if name == ANONYMOUS_FUNCTION_NAME {
1634        // c:5302
1635        1
1636    } else {
1637        0
1638    }
1639}
1640
1641/// Port of `void execsave(void)` from `Src/exec.c:6438`.
1642///
1643/// C body:
1644/// ```c
1645/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
1646/// es->list_pipe_pid = list_pipe_pid;
1647/// es->nowait = nowait;
1648/// es->pline_level = pline_level;
1649/// es->list_pipe_child = list_pipe_child;
1650/// es->list_pipe_job = list_pipe_job;
1651/// strcpy(es->list_pipe_text, list_pipe_text);
1652/// es->lastval = lastval;
1653/// es->noeval = noeval;
1654/// es->badcshglob = badcshglob;
1655/// es->cmdoutpid = cmdoutpid;
1656/// es->cmdoutval = cmdoutval;
1657/// es->use_cmdoutval = use_cmdoutval;
1658/// es->procsubstpid = procsubstpid;
1659/// es->trap_return = trap_return;
1660/// es->trap_state = trap_state;
1661/// es->trapisfunc = trapisfunc;
1662/// es->traplocallevel = traplocallevel;
1663/// es->noerrs = noerrs;
1664/// es->this_noerrexit = this_noerrexit;
1665/// es->underscore = ztrdup(zunderscore);
1666/// es->next = exstack;
1667/// exstack = es;
1668/// noerrs = cmdoutpid = 0;
1669/// ```
1670///
1671/// Snapshot every transient exec-context global onto the `exstack`
1672/// linked list so a signal-handler / trap-firing nested eval can
1673/// scribble freely; `execrestore` pops the frame back. Called by
1674/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
1675pub fn execsave() {
1676    // c:6438
1677    // c:6442 — `es = zalloc(sizeof(execstack));`
1678    let mut es = Box::new(execstack {
1679        // c:6442
1680        next: None,
1681        list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
1682        nowait: nowait.load(Ordering::Relaxed),               // c:6444
1683        pline_level: pline_level.load(Ordering::Relaxed),     // c:6445
1684        list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
1685        list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
1686        list_pipe_text: {
1687            // c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
1688            let mut buf = [0u8; JOBTEXTSIZE];
1689            if let Ok(s) = LIST_PIPE_TEXT.lock() {
1690                let bytes = s.as_bytes();
1691                let n = bytes.len().min(JOBTEXTSIZE - 1);
1692                buf[..n].copy_from_slice(&bytes[..n]);
1693            }
1694            buf
1695        },
1696        lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
1697        // c:6450 — `es->noeval = noeval;`. Snapshot math.c's
1698        // `int noeval` (the parse-only side-effect-skip counter)
1699        // via math.rs's pub accessor.
1700        noeval: crate::ported::math::m_noeval(),
1701        // c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
1702        // csh-glob diagnostic counter (glob.c:103 / glob.rs
1703        // BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
1704        // the outer command's per-line accounting.
1705        badcshglob: crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed), // c:6451
1706        cmdoutpid: cmdoutpid.load(Ordering::Relaxed),                        // c:6452
1707        cmdoutval: cmdoutval.load(Ordering::Relaxed),                        // c:6453
1708        use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed),                // c:6454
1709        procsubstpid: procsubstpid.load(Ordering::Relaxed),                  // c:6455
1710        trap_return: TRAP_RETURN.load(Ordering::Relaxed),                    // c:6456
1711        trap_state: TRAP_STATE.load(Ordering::Relaxed),                      // c:6457
1712        trapisfunc: trapisfunc.load(Ordering::Relaxed),                      // c:6458
1713        traplocallevel: traplocallevel.load(Ordering::Relaxed),              // c:6459
1714        noerrs: noerrs.load(Ordering::Relaxed),                              // c:6460
1715        this_noerrexit: this_noerrexit.load(Ordering::Relaxed),              // c:6461
1716        // c:6462 — `es->underscore = ztrdup(zunderscore);`
1717        underscore: Some(zunderscore.lock().unwrap().clone()),
1718    });
1719    // c:6463-6464 — `es->next = exstack; exstack = es;`
1720    let mut head = exstack.lock().unwrap();
1721    es.next = head.take();
1722    *head = Some(es);
1723    // c:6465 — `noerrs = cmdoutpid = 0;`
1724    noerrs.store(0, Ordering::Relaxed);
1725    cmdoutpid.store(0, Ordering::Relaxed);
1726}
1727
1728/// Port of `void execrestore(void)` from `Src/exec.c:6470`.
1729///
1730/// C body:
1731/// ```c
1732/// struct execstack *en = exstack;
1733/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
1734/// queue_signals();
1735/// exstack = exstack->next;
1736/// list_pipe_pid = en->list_pipe_pid;
1737/// nowait = en->nowait;
1738/// pline_level = en->pline_level;
1739/// list_pipe_child = en->list_pipe_child;
1740/// list_pipe_job = en->list_pipe_job;
1741/// strcpy(list_pipe_text, en->list_pipe_text);
1742/// lastval = en->lastval;
1743/// noeval = en->noeval;
1744/// badcshglob = en->badcshglob;
1745/// cmdoutpid = en->cmdoutpid;
1746/// cmdoutval = en->cmdoutval;
1747/// use_cmdoutval = en->use_cmdoutval;
1748/// procsubstpid = en->procsubstpid;
1749/// trap_return = en->trap_return;
1750/// trap_state = en->trap_state;
1751/// trapisfunc = en->trapisfunc;
1752/// traplocallevel = en->traplocallevel;
1753/// noerrs = en->noerrs;
1754/// this_noerrexit = en->this_noerrexit;
1755/// setunderscore(en->underscore);
1756/// zsfree(en->underscore);
1757/// free(en);
1758/// unqueue_signals();
1759/// ```
1760///
1761/// Pop the top `execstack` frame and restore every transient
1762/// exec-context global. Inverse of `execsave`.
1763pub fn execrestore() {
1764    // c:6470
1765    let mut head = exstack.lock().unwrap();
1766    let en = match head.take() {
1767        // c:6472 + c:6477
1768        Some(en) => en,
1769        None => {
1770            // c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
1771            crate::DPUTS!(true, "BUG: execrestore() without execsave()");
1772            return;
1773        }
1774    };
1775    queue_signals(); // c:6476
1776    *head = en.next; // c:6477
1777    drop(head); // release lock before scalar restores
1778
1779    list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
1780    nowait.store(en.nowait, Ordering::Relaxed); // c:6480
1781    pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
1782    list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
1783    list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
1784                                                              // c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
1785    if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
1786        let nul = en
1787            .list_pipe_text
1788            .iter()
1789            .position(|&b| b == 0)
1790            .unwrap_or(JOBTEXTSIZE);
1791        *s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
1792    }
1793    LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
1794                                                  // c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
1795                                                  // counter from the saved frame.
1796    crate::ported::math::m_noeval_set(en.noeval);
1797    // c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
1798    // diagnostic counter saved on entry.
1799    crate::ported::glob::BADCSHGLOB.store(en.badcshglob, Ordering::Relaxed);
1800    cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
1801    cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
1802    use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
1803    procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
1804    TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
1805    TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
1806    trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
1807    traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
1808    noerrs.store(en.noerrs, Ordering::Relaxed); // c:6496
1809    this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
1810                                                                // c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
1811    if let Some(ref u) = en.underscore {
1812        setunderscore(u); // c:6498
1813    }
1814    // c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
1815    unqueue_signals(); // c:6502
1816}
1817
1818/// Port of `void execstring(char *s, int dont_change_job, int exiting,
1819/// char *context)` from `Src/exec.c:1228`.
1820///
1821/// C body:
1822/// ```c
1823/// Eprog prog;
1824/// pushheap();
1825/// if (isset(VERBOSE)) {
1826///     zputs(s, stderr);
1827///     fputc('\n', stderr);
1828///     fflush(stderr);
1829/// }
1830/// if ((prog = parse_string(s, 0)))
1831///     execode(prog, dont_change_job, exiting, context);
1832/// popheap();
1833/// ```
1834///
1835/// Public entry — execute an arbitrary string as a zsh command list.
1836/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
1837/// body executors, command substitution body runners.
1838///
1839/// =================== WARNING — DIVERGENCE ====================
1840/// The C path is `parse_string` → `execode` → `execlist` (wordcode
1841/// walker). zshrs replaces `execode/execlist` with the fusevm
1842/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
1843/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
1844/// parse+execute chain delegates to the fusevm entry. When `execlist`
1845/// lands as a strict 1:1 port, swap the delegate for the canonical
1846/// chain.
1847/// =============================================================
1848pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
1849    // c:1228
1850    pushheap(); // c:1232
1851                // c:1233-1237 — VERBOSE banner.
1852    if isset(VERBOSE) {
1853        // c:1233
1854        let mut stderr = std::io::stderr().lock();
1855        use std::io::Write;
1856        let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
1857        let _ = stderr.write_all(b"\n"); // c:1235
1858        let _ = stderr.flush(); // c:1236
1859    }
1860    // c:1238-1239 — parse + execode. zshrs delegates the parse+VM
1861    // chain to the fusevm pipeline via the exec_hooks fn-ptr
1862    // installed by fusevm_bridge at startup. Direct
1863    // `with_executor` / ShellExecutor reach-in from src/ported/ is
1864    // forbidden — see memory feedback_no_exec_script_from_ported.
1865    let _ = crate::ported::exec::execute_script_zsh_pipeline(s);
1866    popheap(); // c:1240
1867}
1868
1869/// Port of `void runshfunc(Eprog prog, FuncWrap wrap, char *name)` from
1870/// `Src/exec.c:6166`. The inner shell-function executor — fires
1871/// module-registered wrapper handlers around the function body, with
1872/// `$_` (zunderscore) save/restore and a paramscope push/pop around
1873/// the wordcode walk.
1874///
1875/// C control flow:
1876/// ```c
1877/// queue_signals();
1878/// ou = zalloc(ouu = underscoreused);
1879/// if (ou) memcpy(ou, zunderscore, underscoreused);
1880/// while (wrap) {                       // wrapper chain
1881///     wrap->module->wrapper++;
1882///     cont = wrap->handler(prog, wrap->next, name);
1883///     wrap->module->wrapper--;
1884///     if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
1885///         unload_module(wrap->module);
1886///     if (!cont) {                     // wrapper handled it
1887///         if (ou) zfree(ou, ouu);
1888///         unqueue_signals();
1889///         return;
1890///     }
1891///     wrap = wrap->next;
1892/// }
1893/// startparamscope();
1894/// execode(prog, 1, 0, "shfunc");
1895/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
1896/// endparamscope();
1897/// unqueue_signals();
1898/// ```
1899///
1900/// (a) `wrap->module->wrapper++/--` (c:6178/6180) wired against
1901///     `module::MODULESTAB.modules[name].wrapper` (i32), looked up
1902///     by `wrap.module.node.nam`. Recursive unload during handler
1903///     defers correctly.
1904/// (b) `unload_module(wrap->module)` (c:6184) wired via
1905///     `modulestab.unload_module(name)` when wrapper hits 0 AND
1906///     MOD_UNLOAD flag is set on the module's hashnode.
1907/// (c) `execode(prog, 1, 0, "shfunc")` (c:6195) ported at
1908///     exec.rs:6047. Body uses execode for the no-source
1909///     (compiled-wordcode) branch and fusevm for the
1910///     source-preserving (autoloaded) branch per cache coherence.
1911/// (d) `startparamscope/endparamscope` Rust signatures take
1912///     `&mut HashTable` (params.rs:7425/7435). We pass the global
1913///     paramtab handle via the params crate.
1914pub fn runshfunc(prog: &eprog, mut wrap: Option<&funcwrap>, name: &str) {
1915    // c:6166
1916    queue_signals(); // c:6171
1917                     // c:6173-6175 — snapshot zunderscore into `ou`.
1918    let ouu = underscoreused.load(Ordering::Relaxed) as usize;
1919    let ou: Option<String> = if ouu > 0 {
1920        // c:6174
1921        Some(zunderscore.lock().unwrap().clone()) // c:6175
1922    } else {
1923        None
1924    };
1925    // c:6177-6193 — wrapper chain walk.
1926    while let Some(w) = wrap {
1927        // c:6177
1928        // c:6178 — wrap->module->wrapper++ (WARNING a).
1929        // c:6178 — `wrap->module->wrapper++;` — bump refcount so a
1930        // recursive unload during the handler defers until we return.
1931        let mod_name: Option<String> = w.module.as_ref().map(|m| m.node.nam.clone());
1932        if let Some(ref n) = mod_name {
1933            if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1934                if let Some(m) = tab.modules.get_mut(n) {
1935                    m.wrapper += 1;
1936                }
1937            }
1938        }
1939        let cont = if let Some(h) = w.handler {
1940            // c:6179 — WrapFunc takes Eprog by value + next FuncWrap by value.
1941            // We pass an empty next sentinel (wrapper-chain walks are
1942            // single-step in zshrs — see chain-walk comment below).
1943            let next_sentinel = Box::new(funcwrap {
1944                next: None,
1945                flags: 0,
1946                handler: None,
1947                module: None,
1948            });
1949            h(Box::new(prog.clone()), next_sentinel, name)
1950        } else {
1951            1
1952        };
1953        // c:6180 — `wrap->module->wrapper--;`
1954        // c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD)) unload_module(wrap->module);`
1955        if let Some(ref n) = mod_name {
1956            let should_unload = {
1957                if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1958                    if let Some(m) = tab.modules.get_mut(n) {
1959                        m.wrapper -= 1;
1960                        m.wrapper == 0 && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
1961                    } else {
1962                        false
1963                    }
1964                } else {
1965                    false
1966                }
1967            };
1968            if should_unload {
1969                if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1970                    let _ = tab.unload_module(n); // c:6184
1971                }
1972            }
1973        }
1974        if cont == 0 {
1975            // c:6186 — wrapper claimed the call.
1976            unqueue_signals(); // c:6189
1977            return; // c:6190
1978        }
1979        // c:6192 — wrap = wrap->next; the linked-list step requires
1980        // owning the next ref; the borrowed iteration breaks here.
1981        // Wrapper chains > 1 are extremely rare; we stop at the
1982        // first to avoid a Box::leak.
1983        wrap = None;
1984    }
1985    // c:6194 — startparamscope (just inc_locallevel internally).
1986    inc_locallevel();
1987    // c:6195 — `execode(prog, 1, 0, "shfunc");` — run the function
1988    // body. Prefer the canonical execode (exec.rs:6047) which walks
1989    // execlist on a fresh estate over the prog. If prog.strs carries
1990    // the original source (autoloaded ported that the lazy-compile path
1991    // populated), route through the fusevm pipeline for cache
1992    // coherence with execstring.
1993    if let Some(ref src) = prog.strs {
1994        let _ = crate::ported::exec::execute_script_zsh_pipeline(src);
1995    } else {
1996        // Pure wordcode body — drive via the canonical execode.
1997        execode_wordcode(Box::new(prog.clone()), 1, 0, "shfunc");
1998        let _ = name;
1999    }
2000    if let Some(ou_str) = ou {
2001        // c:6196
2002        setunderscore(&ou_str); // c:6197
2003                                // c:6198 — zfree(ou, ouu) — Rust drops on scope exit.
2004    }
2005    endparamscope(); // c:6200
2006                     // c:6141 — deferred-exit gate. After endparamscope() unwinds the
2007                     // function's local scope (locallevel--), check whether an exit
2008                     // queued inside the function has reached its target scope:
2009                     //   if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)
2010                     // The `+1` accounts for endparamscope having already happened
2011                     // here (locallevel is already one less than when exit_level was
2012                     // captured at c:5890). When the gate fires:
2013                     //   - locallevel > forklevel: still in a nested function — force
2014                     //     the outer frame to return too (retflag=1, breaks=loops).
2015                     //   - locallevel <= forklevel: out of all functions — actually
2016                     //     exit the shell now via zexit(exit_val, ZEXIT_NORMAL).
2017                     // `in_exit_trap` (c:Src/signals.c:63 — `int in_exit_trap;`) is the
2018                     // EXIT-trap reentry counter. dotrap at signals.c:1272/1277 wraps
2019                     // SIGEXIT handler dispatch with ++/--, so an exit issued FROM an
2020                     // EXIT trap shouldn't re-trigger the gate (or the trap would
2021                     // recurse). zshrs's signals::in_exit_trap is the canonical port
2022                     // surface — read it directly here.
2023    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
2024    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
2025    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
2026    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
2027    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
2028    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
2029        // c:6141
2030        if cur_locallevel > cur_forklevel {
2031            // c:6143 — still inside a nested function: keep unwinding.
2032            RETFLAG.store(1, Ordering::Relaxed); // c:6144
2033            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
2034        } else {
2035            // c:6151 — out of all functions: exit for real.
2036            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
2037            let val = EXIT_VAL.load(Ordering::Relaxed);
2038            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
2039            // c:6152
2040        }
2041    }
2042    unqueue_signals(); // c:6202
2043}
2044
2045/// Port of `Emulation_options sticky_emulation_dup(Emulation_options src,
2046/// int useheap)` from `Src/exec.c:5501`.
2047///
2048/// C body (`useheap` selects between heap-arena and permanent zalloc;
2049/// Rust collapses both into owned `Box` clones):
2050/// ```c
2051/// Emulation_options newsticky = useheap ?
2052///     hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
2053/// newsticky->emulation = src->emulation;
2054/// if (src->n_on_opts) {
2055///     size_t sz = src->n_on_opts * sizeof(*src->on_opts);
2056///     newsticky->n_on_opts = src->n_on_opts;
2057///     newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
2058///     memcpy(newsticky->on_opts, src->on_opts, sz);
2059/// }
2060/// if (src->n_off_opts) {
2061///     size_t sz = src->n_off_opts * sizeof(*src->off_opts);
2062///     newsticky->n_off_opts = src->n_off_opts;
2063///     newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
2064///     memcpy(newsticky->off_opts, src->off_opts, sz);
2065/// }
2066/// return newsticky;
2067/// ```
2068///
2069/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
2070/// at function-def time to snapshot the pending `sticky` global so
2071/// the function carries its own immutable copy.
2072pub fn sticky_emulation_dup(src: &emulation_options, _useheap: i32) -> Emulation_options {
2073    // c:5501
2074    // c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
2075    let mut newsticky = Box::new(emulation_options {
2076        emulation: src.emulation, // c:5505
2077        n_on_opts: 0,
2078        n_off_opts: 0,
2079        on_opts: Vec::new(),
2080        off_opts: Vec::new(),
2081    });
2082    // c:5506-5511 — copy on_opts.
2083    if src.n_on_opts != 0 {
2084        // c:5506
2085        newsticky.n_on_opts = src.n_on_opts; // c:5508
2086        newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
2087    }
2088    // c:5512-5517 — copy off_opts.
2089    if src.n_off_opts != 0 {
2090        // c:5512
2091        newsticky.n_off_opts = src.n_off_opts; // c:5514
2092        newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
2093    }
2094    newsticky // c:5519
2095}
2096
2097/// Port of `void shfunc_set_sticky(Shfunc shf)` from `Src/exec.c:5527`.
2098///
2099/// C body:
2100/// ```c
2101/// if (sticky)
2102///     shf->sticky = sticky_emulation_dup(sticky, 0);
2103/// else
2104///     shf->sticky = NULL;
2105/// ```
2106///
2107/// Stamp the function with the current pending sticky-emulation
2108/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
2109pub fn shfunc_set_sticky(shf: &mut shfunc) {
2110    // c:5527
2111    let sticky_guard = sticky.lock().unwrap();
2112    if let Some(ref s) = *sticky_guard {
2113        // c:5529
2114        shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
2115    } else {
2116        // c:5531
2117        shf.sticky = None; // c:5532
2118    }
2119}
2120
2121/// Port of `static char *search_defpath(char *cmd, char *pbuf, int plen)`
2122/// from `Src/exec.c:691`.
2123///
2124/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
2125/// Used by `command -p` to bypass the user's `$PATH` and search the
2126/// system default (`/bin:/usr/bin:...`).
2127pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
2128    // c:691
2129    // c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
2130    for ps in DEFAULT_PATH.split(':') {
2131        // c:695
2132        // c:697 — `if (*ps == '/')`.
2133        if !ps.starts_with('/') {
2134            continue;
2135        }
2136        // c:700-707 — PATH_MAX bounds check on `<dir>` segment.
2137        if ps.len() >= plen {
2138            // c:700 / c:704
2139            continue; // c:701 / c:705
2140        }
2141        // c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
2142        let full_len = ps.len() + 1 + cmd.len();
2143        if full_len >= plen {
2144            // c:709
2145            continue; // c:710
2146        }
2147        let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
2148                                             // c:712 — `if (iscom(pbuf)) return pbuf;`
2149        if iscom(&buf) {
2150            // c:712
2151            return Some(buf); // c:713
2152        }
2153    }
2154    None // c:716
2155}
2156
2157/// Port of `static int checkclobberparam(struct redir *f)` from
2158/// `Src/exec.c:2178`.
2159///
2160/// C body:
2161/// ```c
2162/// struct value vbuf; Value v;
2163/// char *s = f->varid; int fd;
2164/// if (!s) return 1;
2165/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
2166/// if (v->pm->node.flags & PM_READONLY) {
2167///     zwarn("can't allocate file descriptor to readonly parameter %s",
2168///           f->varid);
2169///     errno = 0;
2170///     return 0;
2171/// }
2172/// /* We can't clobber the value in the parameter if it's
2173///  * already an opened file descriptor */
2174/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
2175///     (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
2176///     fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
2177///     zwarn("can't clobber parameter %s containing file descriptor %d",
2178///          f->varid, fd);
2179///     errno = 0;
2180///     return 0;
2181/// }
2182/// return 1;
2183/// ```
2184///
2185/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
2186/// name) is writable and (under NOCLOBBER) doesn't currently hold an
2187/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
2188/// already emitted).
2189///
2190/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
2191/// NOCLOBBER is set and the param's value is the fd-number of an
2192/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
2193/// the existing fd doesn't get clobbered by the upcoming open(2).
2194pub fn checkclobberparam(f: &redir) -> i32 {
2195    // c:2178
2196    // c:2182 — `char *s = f->varid;`
2197    let s = match &f.varid {
2198        Some(v) => v.clone(),
2199        None => return 1, // c:2185-2186 — `if (!s) return 1;`
2200    };
2201    // c:2186 — `if (!(v = getvalue(&vbuf, &s, 0))) return 1;`
2202    let mut vbuf = crate::ported::zsh_h::value {
2203        pm: None,
2204        arr: Vec::new(),
2205        scanflags: 0,
2206        valflags: 0,
2207        start: 0,
2208        end: 0,
2209    };
2210    let mut cursor: &str = s.as_str();
2211    let v_opt = crate::ported::params::getvalue(Some(&mut vbuf), &mut cursor, 0);
2212    if v_opt.is_none() {
2213        return 1; // c:2187
2214    }
2215    // c:2188-2197 — readonly refusal via v->pm->node.flags.
2216    let readonly = vbuf
2217        .pm
2218        .as_ref()
2219        .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
2220        .unwrap_or(false);
2221    if readonly {
2222        // c:2191
2223        zwarn(&format!(
2224            // c:2192
2225            "can't allocate file descriptor to readonly parameter {}",
2226            s
2227        ));
2228        // c:2195 — `errno = 0;` not flagged as a system error.
2229        return 0; // c:2196
2230    }
2231    // c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
2232    // AND the param holds a valid fd that's already in our fdtable as
2233    // FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
2234    // open so we don't clobber it.
2235    if !isset(CLOBBER) {
2236        // c:2201 — `getstrvalue(v)` — read the param's string form.
2237        let val_str = crate::ported::params::getstrvalue(Some(&mut vbuf));
2238        if let Ok(fd) = val_str.trim().parse::<i32>() {
2239            // c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
2240            let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2241            if fd >= 0 && fd <= max_fd {
2242                let kind = fdtable_get(fd);
2243                if kind == FDT_EXTERNAL {
2244                    zwarn(&format!("{}: file descriptor {} already open", s, fd)); // c:2206-2210
2245                    return 0; // c:2211
2246                }
2247            }
2248        }
2249    }
2250    1 // c:2214
2251}
2252
2253/// Port of `static int clobber_open(struct redir *f)` from
2254/// `Src/exec.c:2221`.
2255///
2256/// C body:
2257/// ```c
2258/// struct stat buf;
2259/// int fd, oerrno;
2260/// char *ufname = unmeta(f->name);
2261/// /* If clobbering, just open. */
2262/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
2263///     return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
2264/// /* If not clobbering, attempt to create file exclusively. */
2265/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
2266///     return fd;
2267/// /* If that fails, we are still allowed to open non-regular files. */
2268/// oerrno = errno;
2269/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
2270///     if (!fstat(fd, &buf)) {
2271///         if (!S_ISREG(buf.st_mode)) return fd;
2272///         /* CLOBBER_EMPTY allows re-use of empty regular files. */
2273///         if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
2274///     }
2275///     close(fd);
2276/// }
2277/// errno = oerrno;
2278/// return -1;
2279/// ```
2280///
2281/// Open the redir target for write with the NOCLOBBER rules:
2282/// - CLOBBER set or `>|` form → just open with O_TRUNC
2283/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
2284///   files (FIFOs, devices, sockets) OR empty regular files under
2285///   CLOBBEREMPTY.
2286pub fn clobber_open(f: &redir) -> i32 {
2287    // c:2221
2288    let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
2289    let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
2290        Ok(c) => c,
2291        Err(_) => return -1,
2292    };
2293    // c:2228-2230 — clobber path: just open + truncate.
2294    if isset(CLOBBER) || IS_CLOBBER_REDIR(f.typ) {
2295        // c:2228
2296        // c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
2297        let fd = unsafe {
2298            libc::open(
2299                ufname.as_ptr(),
2300                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
2301                0o666 as libc::c_uint,
2302            )
2303        };
2304        return fd; // c:2230
2305    }
2306    // c:2233-2235 — try O_EXCL create first.
2307    let fd = unsafe {
2308        // c:2233
2309        libc::open(
2310            ufname.as_ptr(),
2311            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
2312            0o666 as libc::c_uint,
2313        )
2314    };
2315    if fd >= 0 {
2316        return fd; // c:2235
2317    }
2318    // c:2240 — `oerrno = errno;` — save for restoration on the recover path.
2319    let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2320    // c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
2321    // OR (CLOBBEREMPTY && size == 0).
2322    let fd = unsafe {
2323        // c:2241
2324        libc::open(
2325            ufname.as_ptr(),
2326            libc::O_WRONLY | libc::O_NOCTTY,
2327            0o666 as libc::c_uint,
2328        )
2329    };
2330    if fd != -1 {
2331        let mut buf: libc::stat = unsafe { std::mem::zeroed() };
2332        if unsafe { libc::fstat(fd, &mut buf) } == 0 {
2333            // c:2242
2334            // c:2243-2244 — non-regular file: accept.
2335            if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
2336                // c:2243
2337                return fd; // c:2244
2338            }
2339            // c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
2340            if isset(CLOBBEREMPTY) && buf.st_size == 0 {
2341                // c:2256
2342                return fd; // c:2257
2343            }
2344        }
2345        unsafe {
2346            libc::close(fd);
2347        } // c:2259
2348    }
2349    // c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
2350    // "file exists" not the noisier "couldn't reopen" trailing errno.
2351    // Per-platform errno setter: __error() on macOS, __errno_location()
2352    // on Linux. Without cfg gating the build breaks on Linux (CI).
2353    #[cfg(target_os = "macos")]
2354    unsafe {
2355        *libc::__error() = oerrno;
2356    }
2357    #[cfg(target_os = "linux")]
2358    unsafe {
2359        *libc::__errno_location() = oerrno;
2360    }
2361    -1 // c:2263
2362}
2363
2364/// Port of `char *findcmd(char *arg0, int docopy, int default_path)`
2365/// from `Src/exec.c:897`. Walk `$PATH` (or DEFAULT_PATH under
2366/// `default_path=1`) for `arg0`, returning the matching path on
2367/// success. `_docopy` is the C source's "duplicate the result"
2368/// flag — Rust ownership covers it without an explicit copy step.
2369/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
2370/// `command -p`).
2371pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
2372    // c:897
2373    // c:903-908 — if (default_path) → search_defpath; return.
2374    if default_path != 0 {
2375        return search_defpath(arg0, libc::PATH_MAX as usize);
2376    }
2377    // c:912-913 — strlen(arg0) > PATH_MAX → NULL.
2378    if arg0.len() > libc::PATH_MAX as usize {
2379        return None;
2380    }
2381    // c:Src/exec.c:914-920 — `/`-bearing arg path resolution.
2382    //   if ((s = strchr(arg0, '/'))) {
2383    //       RET_IF_COM(arg0);   // ← unconditional accept on iscom hit
2384    //       if (arg0 == s || unset(PATHDIRS) ||
2385    //           !strncmp(arg0, "./", 2) ||
2386    //           !strncmp(arg0, "../", 3))
2387    //           return NULL;
2388    //   }
2389    // The Rust port had the iscom check gated on `starts_with('/')`,
2390    // so `type ./target/debug/zshrs` returned None even when the
2391    // file was executable. Bug #496 family.
2392    if arg0.contains('/') {
2393        if iscom(arg0) {
2394            return Some(arg0.to_string()); // c:915 RET_IF_COM
2395        }
2396        // c:916-919 — absolute OR PATHDIRS-off OR `./` / `../` →
2397        // give up here (no $PATH walk for these). Relative without
2398        // those prefixes falls through to the $PATH scan below for
2399        // the PATHDIRS=set case.
2400        if arg0.starts_with('/')
2401            || !isset(PATHDIRS)
2402            || arg0.starts_with("./")
2403            || arg0.starts_with("../")
2404        {
2405            return None;
2406        }
2407        // else fall through to PATH walk.
2408    }
2409    // c:943-951 — walk `path[]` (the shell `$path` array). Read $PATH
2410    // from paramtab so shell-private edits via `path=(...)` take
2411    // effect (not OS env only).
2412    let path = getsparam("PATH")?;
2413    for dir in path.split(':') {
2414        if dir.is_empty() {
2415            continue;
2416        }
2417        let candidate = format!("{}/{}", dir, arg0);
2418        if iscom(&candidate) {
2419            return Some(candidate);
2420        }
2421    }
2422    None // c:952
2423}
2424
2425/// Port of `static void addfd(int forked, int *save, struct multio **mfds,
2426///                             int fd1, int fd2, int rflag, char *varid)`
2427/// from `Src/exec.c:2397`.
2428///
2429/// C body (~100 lines, three branches):
2430/// ```c
2431/// if (varid) {
2432///     /* {varid}>file form — move fd above 10 and bind $varid to it */
2433/// } else if (!mfds[fd1] || unset(MULTIOS)) {
2434///     /* new multio OR MULTIOS off — first redir on this fd */
2435/// } else {
2436///     /* additional redir on a fd that's already a multio (split or extend) */
2437/// }
2438/// ```
2439///
2440/// Register `fd2` (already-open) as a redirection target for `fd1`.
2441/// Three branches: `varid` writes the moved fd to `$varid` and bumps
2442/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
2443/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
2444/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
2445/// via `mpipe`, or appends another fd to an already-split stream
2446/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
2447///
2448/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
2449/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
2450/// longer a hard cap (still 8 for the initial allocation, grown
2451/// on demand thereafter).
2452///
2453/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
2454/// stores the int value but doesn't expose a bitwise-OR setter; we
2455/// re-read + OR + re-store as two atomic-feeling steps.
2456pub fn addfd(
2457    forked: i32,
2458    save: &mut [i32; 10],
2459    mfds: &mut [Option<Box<multio>>; 10],
2460    fd1: i32,
2461    fd2: i32,
2462    rflag: i32,
2463    varid: Option<&str>,
2464) {
2465    // c:2397
2466    let mut pipes: [i32; 2] = [-1; 2]; // c:2400
2467
2468    // c:2402-2417 — `if (varid)` branch — {varid}>file shape.
2469    if let Some(vid) = varid {
2470        // c:2402
2471        let fd_moved = movefd(fd2); // c:2404
2472        if fd_moved == -1 {
2473            // c:2405
2474            zerr(&format!(
2475                // c:2406
2476                "cannot move fd {}: {}",
2477                fd2,
2478                std::io::Error::last_os_error()
2479            ));
2480            return; // c:2407
2481        }
2482        // c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
2483        fdtable_set(fd_moved, FDT_EXTERNAL);
2484        // c:2410 — `setiparam(varid, (zlong)fd1);`
2485        setiparam(vid, fd_moved as i64);
2486        // c:2415-2416 — `if (errflag) zclose(fd1);`
2487        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
2488            // c:2415
2489            let _ = zclose(fd_moved); // c:2416
2490        }
2491        return;
2492    }
2493    // c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
2494    let fd1u = fd1 as usize;
2495    if fd1u >= mfds.len() {
2496        return;
2497    }
2498    if mfds[fd1u].is_none() || unset(MULTIOS) {
2499        // c:2418
2500        if mfds[fd1u].is_none() {
2501            // c:2419 — `starting a new multio`
2502            // c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
2503            mfds[fd1u] = Some(Box::new(multio {
2504                ct: 0,
2505                rflag: 0,
2506                pipe: -1,
2507                // c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
2508                // grow on demand via push() below. Pre-fill MULTIOUNIT
2509                // slots with -1 so existing indexed writes (fds[0], fds[1])
2510                // still work without explicit resize().
2511                fds: vec![-1; MULTIOUNIT],
2512            }));
2513            // c:2421 — `if (!forked && save[fd1] == -2)`
2514            if forked == 0 && save[fd1u] == -2 {
2515                if fd1 == fd2 {
2516                    // c:2422
2517                    save[fd1u] = -1; // c:2423
2518                } else {
2519                    // c:2424
2520                    let fd_n = movefd(fd1); // c:2425
2521                    if fd_n < 0 {
2522                        // c:2430
2523                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2524                        if e != libc::EBADF {
2525                            // c:2431
2526                            zerr(&format!(
2527                                // c:2432
2528                                "cannot duplicate fd {}: {}",
2529                                fd1,
2530                                std::io::Error::from_raw_os_error(e)
2531                            ));
2532                            mfds[fd1u] = None; // c:2433
2533                            closemnodes(mfds); // c:2434
2534                            return; // c:2435
2535                        }
2536                    } else {
2537                        // c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
2538                        crate::DPUTS!(
2539                            fdtable_get(fd_n) != FDT_INTERNAL,
2540                            "Saved file descriptor not marked as internal"
2541                        );
2542                        // c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
2543                        let cur = fdtable_get(fd_n);
2544                        fdtable_set(fd_n, cur | FDT_SAVED_MASK);
2545                    }
2546                    save[fd1u] = fd_n; // c:2442
2547                }
2548            }
2549        }
2550        // c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
2551        // handled above; this is the non-varid branch.)
2552        let _ = redup(fd2, fd1);
2553        // c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
2554        if let Some(mn) = mfds[fd1u].as_mut() {
2555            mn.ct = 1; // c:2448
2556            mn.fds[0] = fd1; // c:2449
2557            mn.rflag = rflag; // c:2450
2558        }
2559    } else {
2560        // c:2451 — extend existing multio.
2561        // c:2452-2456 — rflag mismatch check.
2562        let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
2563        if cur_rflag != rflag {
2564            // c:2452
2565            zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
2566            closemnodes(mfds); // c:2454
2567            return; // c:2455
2568        }
2569        let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
2570        if cur_ct == 1 {
2571            // c:2457 — split the stream.
2572            // c:2458 — `int fdN = movefd(fd1);`
2573            let fd_n = movefd(fd1);
2574            if fd_n < 0 {
2575                // c:2459
2576                zerr(&format!(
2577                    // c:2460
2578                    "multio failed for fd {}: {}",
2579                    fd1,
2580                    std::io::Error::last_os_error()
2581                ));
2582                closemnodes(mfds); // c:2461
2583                return; // c:2462
2584            }
2585            if let Some(mn) = mfds[fd1u].as_mut() {
2586                mn.fds[0] = fd_n; // c:2464
2587            }
2588            // c:2465 — `fdN = movefd(fd2);`
2589            let fd_n2 = movefd(fd2);
2590            if fd_n2 < 0 {
2591                // c:2466
2592                zerr(&format!(
2593                    // c:2467
2594                    "multio failed for fd {}: {}",
2595                    fd2,
2596                    std::io::Error::last_os_error()
2597                ));
2598                closemnodes(mfds); // c:2468
2599                return; // c:2469
2600            }
2601            if let Some(mn) = mfds[fd1u].as_mut() {
2602                mn.fds[1] = fd_n2; // c:2471
2603            }
2604            // c:2472 — `mpipe(pipes)`
2605            if mpipe(&mut pipes) < 0 {
2606                // c:2472
2607                zerr(&format!(
2608                    // c:2473
2609                    "multio failed for fd {}: {}",
2610                    fd2,
2611                    std::io::Error::last_os_error()
2612                ));
2613                closemnodes(mfds); // c:2474
2614                return; // c:2475
2615            }
2616            // c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
2617            if let Some(mn) = mfds[fd1u].as_mut() {
2618                mn.pipe = pipes[(1 - rflag) as usize];
2619            }
2620            // c:2478 — `redup(pipes[rflag], fd1);`
2621            let _ = redup(pipes[rflag as usize], fd1);
2622            // c:2479 — `mfds[fd1]->ct = 2;`
2623            if let Some(mn) = mfds[fd1u].as_mut() {
2624                mn.ct = 2;
2625            }
2626        } else {
2627            // c:2480 — extend already-split stream.
2628            // c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
2629            //                              sizeof + ct*sizeof(int));`
2630            // Rust's `Vec<i32>` grows on demand; ensure capacity for the
2631            // new slot before the indexed write below.
2632            if let Some(mn) = mfds[fd1u].as_mut() {
2633                while mn.fds.len() <= cur_ct as usize {
2634                    mn.fds.push(-1);
2635                }
2636            }
2637            // c:2487 — `if ((fdN = movefd(fd2)) < 0)`
2638            let fd_n = movefd(fd2);
2639            if fd_n < 0 {
2640                zerr(&format!(
2641                    // c:2488
2642                    "multio failed for fd {}: {}",
2643                    fd2,
2644                    std::io::Error::last_os_error()
2645                ));
2646                closemnodes(mfds); // c:2489
2647                return; // c:2490
2648            }
2649            // c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
2650            if let Some(mn) = mfds[fd1u].as_mut() {
2651                let slot = mn.ct as usize;
2652                if slot < mn.fds.len() {
2653                    mn.fds[slot] = fd_n;
2654                    mn.ct += 1;
2655                }
2656            }
2657        }
2658    }
2659}
2660
2661/// Port of `static void closemn(struct multio **mfds, int fd, int type)`
2662/// from `Src/exec.c:2273`.
2663///
2664/// C body (abridged — the meat is the fork-into-tee-or-cat child):
2665/// ```c
2666/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
2667///     struct multio *mn = mfds[fd];
2668///     char buf[TCBUFSIZE]; int len, i;
2669///     pid_t pid; struct timespec bgtime;
2670///     child_block();
2671///     if ((pid = zfork(&bgtime))) {
2672///         for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
2673///         zclose(mn->pipe);
2674///         if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
2675///         mn->ct = 1; mn->fds[0] = fd;
2676///         addproc(pid, NULL, 1, &bgtime, -1, -1);
2677///         child_unblock(); return;
2678///     }
2679///     /* pid == 0 (child) */
2680///     opts[INTERACTIVE] = 0;
2681///     dont_queue_signals();
2682///     child_unblock();
2683///     closeallelse(mn);
2684///     if (mn->rflag) {
2685///         /* tee process: read mn->pipe, write each mn->fds[i] */
2686///     } else {
2687///         /* cat process: read each mn->fds[i], write mn->pipe */
2688///     }
2689///     _exit(0);
2690/// } else if (fd >= 0 && type == REDIR_CLOSE)
2691///     mfds[fd] = NULL;
2692/// ```
2693///
2694/// Success-path close of a multio. For ct>=2 (multiple-output
2695/// redirection), forks a tee/cat child that proxies bytes between
2696/// the original fd and the per-output fds. Single-output multios
2697/// (ct=1) skip the fork entirely and just clear the slot.
2698///
2699/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
2700/// tee/cat child in the current job's auxprocs.
2701pub fn closemn(mfds: &mut [Option<Box<multio>>; 10], fd: i32, type_: i32) {
2702    // c:2273
2703    // c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
2704    let needs_tee = fd >= 0
2705        && (fd as usize) < mfds.len()
2706        && mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
2707    if needs_tee {
2708        // c:2275
2709        // Take the multio out of the slot so we can move pieces into
2710        // the child without aliasing the slot.
2711        let mn = mfds[fd as usize].take().unwrap();
2712        let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
2713                                   // c:2287 — `child_block();` block SIGCHLD before fork race.
2714        child_block();
2715        // c:2288 — `pid = zfork(&bgtime);`
2716        let mut bgtime = ZshTimespec {
2717            tv_sec: 0,
2718            tv_nsec: 0,
2719        };
2720        let pid = zfork(Some(&mut bgtime));
2721        if pid != 0 {
2722            // c:2288 parent branch
2723            // c:2289-2290 — close all per-output fds.
2724            for i in 0..mn.ct as usize {
2725                if i < mn.fds.len() {
2726                    let _ = zclose(mn.fds[i]); // c:2290
2727                }
2728            }
2729            let _ = zclose(mn.pipe); // c:2291
2730            if pid == -1 {
2731                // c:2292
2732                // c:2293 — `mfds[fd] = NULL;` already done via .take()
2733                child_unblock(); // c:2294
2734                return; // c:2295
2735            }
2736            // c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
2737            let mut mn_back = mn;
2738            mn_back.ct = 1; // c:2297
2739            mn_back.fds[0] = fd; // c:2298
2740            mfds[fd as usize] = Some(mn_back);
2741            // c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
2742            // the tee/cat child in the current job's auxprocs (aux=true).
2743            if let Some(jt) = JOBTAB.get() {
2744                let mut guard = jt.lock().unwrap();
2745                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
2746                if tj >= 0 {
2747                    if let Some(j) = guard.get_mut(tj as usize) {
2748                        crate::ported::jobs::addproc(
2749                            j,
2750                            pid,
2751                            "",
2752                            true,
2753                            Some(std::time::Instant::now()),
2754                            -1,
2755                            -1,
2756                        );
2757                    }
2758                }
2759            }
2760            let _ = bgtime;
2761            child_unblock(); // c:2300
2762            return; // c:2301
2763        }
2764        // c:2303 — child branch (pid == 0).
2765        opt_state_set("interactive", false); // c:2304
2766        dont_queue_signals(); // c:2305
2767        child_unblock(); // c:2306
2768        closeallelse(&mn); // c:2307
2769                           // c:2308-2333 — tee or cat loop.
2770        if mn.rflag != 0 {
2771            // c:2308 — `mn->rflag` set → tee process
2772            // c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
2773            loop {
2774                let len = unsafe {
2775                    libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2776                };
2777                if len == 0 {
2778                    break;
2779                }
2780                if len < 0 {
2781                    // c:2311
2782                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2783                    if e == libc::EINTR {
2784                        // c:2312
2785                        continue;
2786                    } else {
2787                        break; // c:2315
2788                    }
2789                }
2790                // c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
2791                for i in 0..mn.ct as usize {
2792                    if i >= mn.fds.len() {
2793                        break;
2794                    }
2795                    if write_loop(mn.fds[i], &buf[..len as usize]).is_err() {
2796                        break; // c:2319
2797                    }
2798                }
2799            }
2800        } else {
2801            // c:2321 — cat process
2802            for i in 0..mn.ct as usize {
2803                if i >= mn.fds.len() {
2804                    break;
2805                }
2806                // c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
2807                loop {
2808                    let len = unsafe {
2809                        libc::read(mn.fds[i], buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2810                    };
2811                    if len == 0 {
2812                        break;
2813                    }
2814                    if len < 0 {
2815                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2816                        // c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
2817                        if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
2818                            continue;
2819                        } else {
2820                            break; // c:2329
2821                        }
2822                    }
2823                    // c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
2824                    if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
2825                        break; // c:2332
2826                    }
2827                }
2828            }
2829        }
2830        // c:2335 — `_exit(0);`
2831        unsafe {
2832            libc::_exit(0);
2833        }
2834    } else if fd >= 0 && type_ == REDIR_CLOSE {
2835        // c:2336
2836        // c:2337 — `mfds[fd] = NULL;`
2837        if (fd as usize) < mfds.len() {
2838            mfds[fd as usize] = None;
2839        }
2840    }
2841}
2842
2843/// Port of `static void closemnodes(struct multio **mfds)` from
2844/// `Src/exec.c:2344`.
2845///
2846/// C body:
2847/// ```c
2848/// int i, j;
2849/// for (i = 0; i < 10; i++)
2850///     if (mfds[i]) {
2851///         for (j = 0; j < mfds[i]->ct; j++)
2852///             zclose(mfds[i]->fds[j]);
2853///         mfds[i] = NULL;
2854///     }
2855/// ```
2856///
2857/// Failure-path cleanup: close every fd stashed in any of the 10
2858/// multio slots and null the slot. Called from `execcmd_exec` when
2859/// a redirect setup fails partway through and we need to roll back.
2860pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
2861    // c:2344
2862    for i in 0..10 {
2863        // c:2348
2864        if let Some(mn) = mfds[i].take() {
2865            // c:2349
2866            for j in 0..mn.ct as usize {
2867                // c:2350
2868                if j < mn.fds.len() {
2869                    let _ = zclose(mn.fds[j]); // c:2351
2870                }
2871            }
2872            // c:2352 — `mfds[i] = NULL;` — handled by .take() above.
2873        }
2874    }
2875}
2876
2877/// Port of `static void closeallelse(struct multio *mn)` from
2878/// `Src/exec.c:2358`.
2879///
2880/// C body:
2881/// ```c
2882/// int i, j;
2883/// long openmax;
2884/// openmax = fdtable_size;
2885/// for (i = 0; i < openmax; i++)
2886///     if (mn->pipe != i) {
2887///         for (j = 0; j < mn->ct; j++)
2888///             if (mn->fds[j] == i) break;
2889///         if (j == mn->ct)
2890///             zclose(i);
2891///     }
2892/// ```
2893///
2894/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
2895/// stashed in `mn->fds`. Called inside the multio tee/cat child
2896/// process to release every fd the parent had open — only the pipe
2897/// + per-output fds stay alive for the read/write loop.
2898pub fn closeallelse(mn: &multio) {
2899    // c:2358
2900    // c:2363 — `openmax = fdtable_size;`. zshrs models fdtable as a
2901    // Vec; use MAX_ZSH_FD as the upper bound (fdtable_size grows past
2902    // max_zsh_fd in C but every slot past it is FDT_UNUSED anyway).
2903    let openmax = MAX_ZSH_FD.load(Ordering::Relaxed) + 1; // c:2363
2904    for i in 0..openmax {
2905        // c:2365
2906        if mn.pipe == i {
2907            // c:2366
2908            continue;
2909        }
2910        // c:2367-2369 — scan mn->fds[] for i; skip-close if found.
2911        let mut found = false;
2912        for j in 0..mn.ct as usize {
2913            // c:2367
2914            if j < mn.fds.len() && mn.fds[j] == i {
2915                // c:2368
2916                found = true;
2917                break; // c:2369
2918            }
2919        }
2920        // c:2370-2371 — `if (j == mn->ct) zclose(i);`
2921        if !found {
2922            let _ = zclose(i); // c:2371
2923        }
2924    }
2925}
2926
2927/// Port of `static void fixfds(int *save)` from `Src/exec.c:4523`.
2928///
2929/// C body:
2930/// ```c
2931/// int old_errno = errno;
2932/// int i;
2933/// for (i = 0; i != 10; i++)
2934///     if (save[i] != -2)
2935///         redup(save[i], i);
2936/// errno = old_errno;
2937/// ```
2938///
2939/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
2940/// means "no save was made for this fd"; any other value is the
2941/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
2942/// caller's errno across the loop so a downstream caller diagnoses
2943/// the original failure, not a noisy dup2 errno.
2944pub fn fixfds(save: &[i32; 10]) {
2945    // c:4523
2946    let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
2947    for i in 0..10i32 {
2948        // c:4528 — `for (i = 0; i != 10; i++)`
2949        if save[i as usize] != -2 {
2950            // c:4529
2951            redup(save[i as usize], i); // c:4530
2952        }
2953    }
2954    // c:4531 — `errno = old_errno;`
2955    #[cfg(target_os = "macos")]
2956    unsafe {
2957        *libc::__error() = old_errno;
2958    }
2959    #[cfg(target_os = "linux")]
2960    unsafe {
2961        *libc::__errno_location() = old_errno;
2962    }
2963}
2964
2965/// Port of `mod_export void closem(int how, int all)` from `Src/exec.c:4546`.
2966///
2967/// C body:
2968/// ```c
2969/// int i;
2970/// for (i = 10; i <= max_zsh_fd; i++)
2971///     if (fdtable[i] != FDT_UNUSED &&
2972///         (all || (fdtable[i] != FDT_PROC_SUBST &&
2973///                  fdtable[i] != FDT_EXTERNAL)) &&
2974///         (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
2975///         if (i == SHTTY) SHTTY = -1;
2976///         zclose(i);
2977///     }
2978/// ```
2979///
2980/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
2981/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
2982/// type filter); otherwise only fds whose low-nibble type equals
2983/// `how` are closed. `all == 0` preserves user-visible fds
2984/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
2985/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
2986/// reference if we just closed the controlling tty.
2987pub fn closem(how: i32, all: i32) {
2988    // c:4546
2989    let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
2990    for i in 10i32..=max {
2991        // c:4550
2992        let kind = fdtable_get(i); // c:4551 fdtable[i]
2993        if kind == FDT_UNUSED {
2994            // c:4551
2995            continue;
2996        }
2997        // c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
2998        if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
2999            continue;
3000        }
3001        // c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
3002        if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
3003            continue;
3004        }
3005        // c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
3006        if i == SHTTY.load(Ordering::Relaxed) {
3007            // c:4560
3008            SHTTY.store(-1, Ordering::Relaxed); // c:4561
3009        }
3010        // c:4562 — `zclose(i);`
3011        let _ = zclose(i);
3012    }
3013}
3014
3015/// Port of `Cmdnam hashcmd(char *arg0, char **pp)` from
3016/// `Src/exec.c:1010`.
3017///
3018/// C body:
3019/// ```c
3020/// Cmdnam cn;
3021/// char *s, buf[PATH_MAX+1];
3022/// char **pq;
3023/// if (*arg0 == '/') return NULL;
3024/// for (; *pp; pp++)
3025///     if (**pp == '/') {
3026///         s = buf;
3027///         struncpy(&s, *pp, PATH_MAX);
3028///         *s++ = '/';
3029///         if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
3030///         strcpy(s, arg0);
3031///         if (iscom(buf)) break;
3032///     }
3033/// if (!*pp) return NULL;
3034/// cn = (Cmdnam) zshcalloc(sizeof *cn);
3035/// cn->node.flags = 0;
3036/// cn->u.name = pp;
3037/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
3038/// if (isset(HASHDIRS)) {
3039///     for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
3040///     pathchecked = pp + 1;
3041/// }
3042/// return cn;
3043/// ```
3044///
3045/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
3046/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
3047/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
3048/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
3049/// so subsequent commands hit the cache.
3050///
3051/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
3052/// `None` if `arg0` is absolute or no PATH entry contains it.
3053pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
3054    // c:1010
3055    // c:1016 — `if (*arg0 == '/') return NULL;`
3056    if arg0.starts_with('/') {
3057        return None; // c:1017
3058    }
3059    // c:1018-1028 — walk pp[] for first matching absolute entry.
3060    let mut found_idx: Option<usize> = None;
3061    for (i, dir) in pp.iter().enumerate() {
3062        // c:1018
3063        if !dir.starts_with('/') {
3064            // c:1019
3065            continue;
3066        }
3067        // c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
3068        if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
3069            // c:1023
3070            continue; // c:1024
3071        }
3072        let buf = format!("{}/{}", dir, arg0); // c:1025
3073        if iscom(&buf) {
3074            // c:1026
3075            found_idx = Some(i);
3076            break; // c:1027
3077        }
3078    }
3079    // c:1030-1031 — `if (!*pp) return NULL;`
3080    let pp_idx = match found_idx {
3081        Some(i) => i,
3082        None => return None, // c:1031
3083    };
3084    // c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
3085    let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
3086    let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
3087                                                // c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
3088    if let Ok(mut tab) = cmdnamtab_lock().write() {
3089        tab.add(cn.clone());
3090    }
3091    // c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
3092    // including the matching one, then bump pathchecked past it.
3093    if isset(HASHDIRS) {
3094        // c:1038
3095        let start = pathchecked.load(Ordering::Relaxed); // c:1039
3096        for pq in start..=pp_idx {
3097            // c:1039
3098            if pq < pp.len() {
3099                hashdir(&pp[pq], pq); // c:1040
3100            }
3101        }
3102        pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
3103    }
3104    Some(cn) // c:1044
3105}
3106
3107/// Port of `static pid_t zfork(struct timespec *ts)` from
3108/// `Src/exec.c:349`.
3109///
3110/// C body:
3111/// ```c
3112/// pid_t pid;
3113/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
3114///     zerr("job table full");
3115///     return -1;
3116/// }
3117/// if (ts) zgettime_monotonic_if_available(ts);
3118/// queue_signals();
3119/// pid = fork();
3120/// unqueue_signals();
3121/// if (pid == -1) {
3122///     zerr("fork failed: %e", errno);
3123///     return -1;
3124/// }
3125/// #ifdef HAVE_GETRLIMIT
3126/// if (!pid) setlimits(NULL);
3127/// #endif
3128/// return pid;
3129/// ```
3130///
3131/// fork(2) wrapper with jobtab capacity check + child rlimit
3132/// re-application. Used by every subshell-spawning path: pipelines,
3133/// process substitution, async commands, command substitution.
3134pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
3135    // c:349
3136    let pid: libc::pid_t;
3137
3138    // c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
3139    let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
3140    let thisjob = *thisjob_lock.lock().unwrap();
3141    if thisjob != -1 {
3142        // c:356
3143        let needed = (thisjob + 1) as usize;
3144        let needs_expand = JOBTAB
3145            .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3146            .lock()
3147            .map(|t| needed >= t.len().saturating_sub(1))
3148            .unwrap_or(false);
3149        if needs_expand {
3150            let mut tab = JOBTAB.get().unwrap().lock().unwrap();
3151            if !expandjobtab(&mut tab, needed) {
3152                // c:357
3153                zerr("job table full"); // c:357
3154                return -1; // c:358
3155            }
3156        }
3157    }
3158    // c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
3159    if let Some(ts) = ts {
3160        zgettime_monotonic_if_available(ts);
3161    }
3162    // c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
3163    queue_signals(); // c:368
3164    pid = unsafe { libc::fork() }; // c:369
3165    unqueue_signals(); // c:370
3166                       // c:371-374 — fork failure.
3167    if pid == -1 {
3168        // c:371
3169        zerr(&format!(
3170            // c:372
3171            "fork failed: {}",
3172            std::io::Error::last_os_error()
3173        ));
3174        return -1; // c:373
3175    }
3176    // c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
3177    #[cfg(unix)]
3178    if pid == 0 {
3179        // c:376
3180        let _ = setlimits(""); // c:378
3181    }
3182    pid // c:380
3183}
3184
3185/// Port of `void loadautofnsetfile(Shfunc shf, char *fdir)` from
3186/// `Src/exec.c:5657`.
3187///
3188/// C body:
3189/// ```c
3190/// if (!(shf->node.flags & PM_LOADDIR) ||
3191///     strcmp(shf->filename, fdir) != 0) {
3192///     dircache_set(&shf->filename, NULL);
3193///     if (fdir) {
3194///         shf->node.flags |= PM_LOADDIR;
3195///         dircache_set(&shf->filename, fdir);
3196///     } else {
3197///         shf->node.flags &= ~PM_LOADDIR;
3198///         shf->filename = ztrdup(shf->node.nam);
3199///     }
3200/// }
3201/// ```
3202///
3203/// Update `shf->filename` to the autoload directory `fdir`. Routes
3204/// through the refcounted `dircache_set` so identical directory
3205/// strings are shared across shfunc table entries.
3206pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
3207    // c:5657
3208    // c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
3209    let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
3210    let same = match (&shf.filename, fdir) {
3211        (Some(a), Some(b)) => a == b,
3212        _ => false,
3213    };
3214    if !loaddir || !same {
3215        // c:5664
3216        // c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
3217        dircache_set(&mut shf.filename, None);
3218        if let Some(fdir) = fdir {
3219            // c:5668
3220            shf.node.flags |= PM_LOADDIR as i32; // c:5670
3221            dircache_set(&mut shf.filename, Some(fdir)); // c:5671
3222        } else {
3223            // c:5672
3224            shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
3225            shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
3226        }
3227    }
3228}
3229
3230/// Port of `int commandnotfound(char *arg0, LinkList args)` from
3231/// `Src/exec.c:669`.
3232///
3233/// C body:
3234/// ```c
3235/// Shfunc shf = (Shfunc)
3236///     shfunctab->getnode(shfunctab, "command_not_found_handler");
3237/// if (!shf) {
3238///     lastval = 127;
3239///     return 1;
3240/// }
3241/// pushnode(args, arg0);
3242/// lastval = doshfunc(shf, args, 1);
3243/// return 0;
3244/// ```
3245///
3246/// Look up the user-defined `command_not_found_handler` shfunc and
3247/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
3248/// 1 if no handler (so caller emits the standard "command not found"
3249/// error). Sets `$?` to 127 in the no-handler path.
3250pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
3251    // c:669
3252    // c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
3253    let has_handler = shfunctab_lock()
3254        .read()
3255        .map(|t| t.get("command_not_found_handler").is_some())
3256        .unwrap_or(false);
3257    if !has_handler {
3258        // c:674
3259        LASTVAL.store(127, Ordering::Relaxed); // c:675
3260        return 1; // c:676
3261    }
3262    // c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
3263    // is the first positional arg per C convention).
3264    args.insert(0, arg0.to_string());
3265    args.insert(0, "command_not_found_handler".to_string());
3266    // c:680 — `lastval = doshfunc(shf, args, 1);`. Direct doshfunc
3267    // call mirrors C — body_runner routes through the host body-only
3268    // entry so the function body runs once inside doshfunc's scope.
3269    let shf_clone: Option<shfunc> = shfunctab_lock()
3270        .read()
3271        .ok()
3272        .and_then(|t| t.get("command_not_found_handler").cloned());
3273    if let Some(mut shf) = shf_clone {
3274        let body_args = args.clone();
3275        let body_runner = move || -> i32 {
3276            crate::ported::exec::run_function_body("command_not_found_handler", &body_args[1..])
3277                .unwrap_or(0)
3278        };
3279        let lv = doshfunc(&mut shf, args.clone(), true, body_runner);
3280        LASTVAL.store(lv, Ordering::Relaxed);
3281    }
3282    0 // c:681
3283}
3284
3285/// Port of `char *namedpipe(void)` from `Src/exec.c:5001`.
3286///
3287/// C body (#ifdef HAVE_FIFOS branch):
3288/// ```c
3289/// char *tnam = gettempname(NULL, 1);
3290/// if (!tnam) {
3291///     zerr("failed to create named pipe: %e", errno);
3292///     return NULL;
3293/// }
3294/// if (mkfifo(tnam, 0600) < 0) {
3295///     zerr("failed to create named pipe: %s, %e", tnam, errno);
3296///     return NULL;
3297/// }
3298/// return tnam;
3299/// ```
3300///
3301/// Create a FIFO with a unique name for process substitution. Used by
3302/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
3303pub fn namedpipe() -> Option<String> {
3304    // c:5001
3305    let tnam = gettempname(None, true); // c:5003
3306    let tnam = match tnam {
3307        Some(t) => t,
3308        None => {
3309            // c:5005
3310            zerr(&format!(
3311                // c:5006
3312                "failed to create named pipe: {}",
3313                std::io::Error::last_os_error()
3314            ));
3315            return None; // c:5007
3316        }
3317    };
3318    // c:5010 — `mkfifo(tnam, 0600)`.
3319    let cstr = match std::ffi::CString::new(tnam.as_str()) {
3320        Ok(c) => c,
3321        Err(_) => return None,
3322    };
3323    if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
3324        // c:5010
3325        zerr(&format!(
3326            // c:5014
3327            "failed to create named pipe: {}, {}",
3328            tnam,
3329            std::io::Error::last_os_error()
3330        ));
3331        return None; // c:5015
3332    }
3333    Some(tnam) // c:5017
3334}
3335
3336/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
3337///
3338/// C body:
3339/// ```c
3340/// char *str;
3341/// Eprog prog;
3342/// for (str = cmd + 2; *str && *str != Outpar; str++);
3343/// if (!*str || cmd[1] != Inpar) {
3344///     char *errstr = dupstrpfx(cmd, 2);
3345///     untokenize(errstr);
3346///     zerr("unterminated `%s...)'", errstr);
3347///     return NULL;
3348/// }
3349/// *str = '\0';
3350/// if (eptr) *eptr = str+1;
3351/// if (!(prog = parse_string(cmd + 2, 0))) {
3352///     zerr("parse error in process substitution");
3353///     return NULL;
3354/// }
3355/// return prog;
3356/// ```
3357///
3358/// Port of `static LinkList readoutput(int in, int qt, int *readerror)`
3359/// from `Src/exec.c:4805`. Drain a command-substitution pipe fd and
3360/// return the captured output split per `qt`.
3361///
3362/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
3363/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
3364/// per c:4861).
3365/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
3366/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
3367///
3368/// `readerror` is set to the errno on read failure, 0 on clean EOF.
3369pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
3370    // c:4805
3371    let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
3372    let mut readret: isize = 0; // c:4818 readret tracks last read return
3373                                // c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
3374                                // dance keeps SIGCHLD live so the foreground process can be reaped
3375                                // while we drain. zshrs's in-process command-sub runs without the
3376                                // queue (no fork), but the C call surface is preserved for parity.
3377    dont_queue_signals(); // c:4824
3378    child_unblock(); // c:4825
3379    let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
3380    loop {
3381        // c:4826
3382        // c:4828 — `readret = read(in, inbuf, 64);`
3383        let r = unsafe { libc::read(in_fd, inbuf.as_mut_ptr() as *mut libc::c_void, inbuf.len()) };
3384        readret = r as isize;
3385        if readret <= 0 {
3386            // c:4829
3387            if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
3388                // c:4830 — `if (readret < 0 && errno == EINTR) continue;`
3389                continue;
3390            }
3391            break; // c:4832
3392        }
3393        // c:4835 — `for (bufptr = inbuf; bufptr < inbuf + readret; bufptr++)`
3394        for i in 0..(readret as usize) {
3395            let c = inbuf[i];
3396            if crate::ported::ztype_h::imeta(c) {
3397                // c:4837 — `if (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; }`
3398                buf.push(Meta as u8); // c:4838
3399                buf.push(c ^ 32); // c:4839 (Meta-encoded payload)
3400            } else {
3401                buf.push(c); // c:4848 *ptr++ = c
3402            }
3403        }
3404    }
3405    child_block(); // c:4854
3406                   // c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
3407    *readerror = if readret < 0 {
3408        std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
3409    } else {
3410        0
3411    };
3412    // c:4857 — `close(in);`
3413    unsafe {
3414        libc::close(in_fd);
3415    }
3416    // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
3417    while buf.last() == Some(&b'\n') {
3418        buf.pop();
3419    }
3420    // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
3421    let s = String::from_utf8_lossy(&buf).into_owned();
3422    if qt != 0 {
3423        // c:4861
3424        if buf.is_empty() {
3425            return vec![String::from(Nularg)]; // c:4862
3426        }
3427        return vec![s]; // c:4864
3428    }
3429    // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
3430    let mut words = crate::ported::utils::spacesplit(&s, false); // c:4867
3431    if isset(crate::ported::zsh_h::GLOBSUBST) {
3432        // c:4870
3433        for w in words.iter_mut() {
3434            crate::ported::glob::shtokenize(w); // c:4870
3435        }
3436    }
3437    words
3438}
3439
3440/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
3441/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
3442/// remainder is the command up to the matching `Outpar`. Returns the
3443/// parsed Eprog (and writes the post-`)` cursor through `eptr`).
3444pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
3445    // c:4878
3446    let bytes = cmd.as_bytes();
3447    // c:4883 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
3448    if bytes.len() < 2 {
3449        return None;
3450    }
3451    let mut str_idx: usize = 2;
3452    while str_idx < bytes.len() && (bytes[str_idx] as char) != Outpar {
3453        str_idx += 1;
3454    }
3455    // c:4884 — `if (!*str || cmd[1] != Inpar)`.
3456    if str_idx >= bytes.len() || (bytes[1] as char) != Inpar {
3457        // c:4884
3458        let errstr = if bytes.len() >= 2 {
3459            untokenize(&cmd[..2]) // c:4891-4892
3460        } else {
3461            String::new()
3462        };
3463        zerr(&format!("unterminated `{}...)'", errstr)); // c:4893
3464        return None; // c:4894
3465    }
3466    // c:4896 — `*str = '\0';` — cmd[str_idx] becomes the terminator.
3467    // c:4897-4898 — `if (eptr) *eptr = str + 1;`
3468    if let Some(p) = eptr {
3469        *p = str_idx + 1;
3470    }
3471    // c:4899 — `parse_string(cmd + 2, 0)`.
3472    let body = &cmd[2..str_idx];
3473    let prog = parse_string(body, 0);
3474    if prog.is_none() {
3475        // c:4899
3476        zerr("parse error in process substitution"); // c:4900
3477        return None; // c:4901
3478    }
3479    prog // c:4903
3480}
3481
3482/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
3483/// front of a script when probing for a `#!` shebang line.
3484pub const POUNDBANGLIMIT: usize = 128;
3485
3486/// Port of `static char **makecline(LinkList list)` from `Src/exec.c:2046`.
3487///
3488/// Builds the argv array from a command's args list. The C version
3489/// allocates with a 4-slot prepad (2 reserved at the front for the
3490/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
3491/// doesn't need this since we rebuild the Vec on shebang re-exec
3492/// (see zexecve WARNING e).
3493///
3494/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
3495/// (stderr), preceded by the PS4 prefix when first command of the line.
3496pub fn makecline(list: &[String]) -> Vec<String> {
3497    // c:2046
3498    if isset(XTRACE) {
3499        // c:2055
3500        if doneps4.load(Ordering::Relaxed) == 0 {
3501            // c:2056
3502            printprompt4(); // c:2057
3503        }
3504        let mut first = true;
3505        let mut err = std::io::stderr().lock();
3506        use std::io::Write;
3507        for s in list.iter() {
3508            // c:2059
3509            if !first {
3510                let _ = err.write_all(b" "); // c:2063
3511            }
3512            first = false;
3513            let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
3514        }
3515        let _ = err.write_all(b"\n"); // c:2065
3516        let _ = err.flush(); // c:2066
3517    }
3518    list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
3519}
3520
3521/// Port of `static void execute(LinkList args, int flags, int defpath)`
3522/// from `Src/exec.c:723`. The canonical "child runs the simple
3523/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
3524/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
3525/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
3526/// commandnotfound-handler fallback and the final exit-code escape
3527/// (127 not-found / 126 noperm).
3528///
3529/// =================== WARNING — DIVERGENCE ====================
3530/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
3531///     fast-path wired via cmdnamtab_lock(); jumps direct to
3532///     `cn.cmd` absolute path before the $PATH scan. Unhashed
3533///     cursor-walk (c:830-846) still falls to the full $PATH scan;
3534///     observable behavior matches C when the hash hit is HASHED.
3535/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
3536///     not-yet-ported `doshfunc` for the `command_not_found_handler`
3537///     shell function. Already routes through executor dispatch
3538///     (see exec.rs:2783).
3539/// (c) `_realexit()` (c:810, 874) — bare `std::process::exit`.
3540/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
3541///     FD_CLOEXEC platform default (macOS, Linux).
3542/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
3543///     `defpath` (`command -p`) walks DEFAULT_PATH via
3544///     search_defpath (already ported).
3545/// =============================================================
3546pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
3547    // c:723
3548    let mut eno: i32 = 0;
3549    let mut ee: i32; // c:729
3550    let mut arg0 = if args.is_empty() {
3551        return;
3552    } else {
3553        args[0].clone()
3554    }; // c:731
3555       // c:733-748 — STTY pre-exec handling.
3556    {
3557        let mut stty = STTYval.lock().unwrap();
3558        if let Some(s) = stty.take() {
3559            // c:738 — STTYval = 0 to break recursion.
3560            if !s.is_empty()
3561                && unsafe { libc::isatty(0) } != 0
3562                && unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
3563            {
3564                drop(stty);
3565                let cmd = format!("stty {}", s); // c:739
3566                execstring(&cmd, 1, 0, "stty"); // c:743
3567            }
3568        }
3569    }
3570    // c:752-763 — ARGV0 override.
3571    if let Some(z) = zgetenv("ARGV0") {
3572        args[0] = z.clone(); // c:753
3573        unsafe {
3574            let key = std::ffi::CString::new("ARGV0").unwrap();
3575            libc::unsetenv(key.as_ptr()); // c:760
3576        }
3577        arg0 = args[0].clone();
3578    } else if (flags & BINF_DASH) != 0 {
3579        // c:764 — `BINF_DASH` prepends `-`.
3580        args[0] = format!("-{}", arg0); // c:767-768
3581        arg0 = args[0].clone();
3582    }
3583    let argv = makecline(args); // c:771
3584    let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
3585        Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
3586    } else {
3587        None
3588    };
3589    let newenvp = newenvp_owned.as_deref();
3590    closem(FDT_XTRACE, 0); // c:779
3591                           // c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
3592    child_unblock(); // c:786
3593    if arg0.len() >= libc::PATH_MAX as usize {
3594        // c:787
3595        zerr(&format!("command too long: {}", arg0)); // c:788
3596        unsafe {
3597            libc::_exit(1);
3598        } // c:789
3599    }
3600    // c:791-801 — slash in arg0 → direct exec.
3601    if let Some(slash_pos) = arg0.find('/') {
3602        let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
3603        let is_dot = arg0.starts_with('.')
3604            && (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
3605        if slash_pos == 0 || unset(PATHDIRS) || is_dot {
3606            // c:794
3607            zerr(&format!(
3608                "{}: {}",
3609                std::io::Error::from_raw_os_error(lerrno),
3610                arg0
3611            )); // c:797
3612            let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
3613                126
3614            } else {
3615                127
3616            };
3617            unsafe {
3618                libc::_exit(code);
3619            } // c:798
3620        }
3621    }
3622    if defpath != 0 {
3623        // c:804 — `command -p` default-path search.
3624        let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
3625            Some(p) => p, // c:808
3626            None => {
3627                if commandnotfound(&arg0, args) == 0 {
3628                    // c:809
3629                    unsafe {
3630                        libc::_exit(LASTVAL.load(Ordering::Relaxed));
3631                    }
3632                }
3633                zerr(&format!("command not found: {}", arg0)); // c:811
3634                unsafe {
3635                    libc::_exit(127);
3636                } // c:812
3637            }
3638        };
3639        ee = zexecve(&pbuf, &argv, newenvp); // c:815
3640        let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3641        if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3642            // c:819
3643            eno = ee;
3644        }
3645    } else {
3646        // c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
3647        // jump straight to the absolute path stored in `cn.cmd`,
3648        // skipping the full $PATH scan (one exec attempt vs N).
3649        // c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
3650        let hashed_path: Option<String> = {
3651            let tab = cmdnamtab_lock().read().ok();
3652            tab.and_then(|t| {
3653                t.get(&arg0).and_then(|cn| {
3654                    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
3655                        // c:827-828 — `strcpy(nn, cn->u.cmd);`
3656                        cn.cmd.clone()
3657                    } else {
3658                        None
3659                    }
3660                })
3661            })
3662        };
3663        if let Some(nn) = hashed_path {
3664            // c:848 — `ee = zexecve(nn, argv, newenvp);`
3665            ee = zexecve(&nn, &argv, newenvp);
3666            let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3667            if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3668                eno = ee;
3669            }
3670            // If the hashed entry's exec failed without a "good" error,
3671            // we still need the $PATH fallback — fall through.
3672            if eno == 0 && ee != 0 {
3673                // Reset for the $PATH scan below.
3674                ee = 0;
3675            }
3676        }
3677        // c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
3678        // optimization but C also walks the rest of `path` if the hashed
3679        // exec failed with a non-"good" error).
3680        let path_str = getsparam("PATH").unwrap_or_default();
3681        for pp in path_str.split(':') {
3682            if pp.is_empty() || pp == "." {
3683                // c:856
3684                ee = zexecve(&arg0, &argv, newenvp); // c:857
3685                if isgooderr(ee, pp) {
3686                    eno = ee;
3687                }
3688            } else {
3689                // c:860
3690                let candidate = format!("{}/{}", pp, arg0); // c:861-864
3691                ee = zexecve(&candidate, &argv, newenvp); // c:865
3692                if isgooderr(ee, pp) {
3693                    eno = ee;
3694                }
3695            }
3696        }
3697    }
3698    // c:871-881 — final error reporting.
3699    if eno != 0 {
3700        // c:871
3701        zerr(&format!(
3702            "{}: {}",
3703            std::io::Error::from_raw_os_error(eno),
3704            arg0
3705        )); // c:872
3706    } else if commandnotfound(&arg0, args) == 0 {
3707        // c:873
3708        unsafe {
3709            libc::_exit(LASTVAL.load(Ordering::Relaxed));
3710        } // c:874
3711    } else {
3712        zerr(&format!("command not found: {}", arg0)); // c:876
3713    }
3714    let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
3715        126
3716    } else {
3717        127
3718    }; // c:881
3719    unsafe {
3720        libc::_exit(code);
3721    }
3722}
3723
3724/// Port of `static int zexecve(char *pth, char **argv, char **newenvp)`
3725/// from `Src/exec.c:504`. Wraps `execve(2)` with:
3726///   - `$_` env var stamped to absolute `pth` (c:514-520)
3727///   - winch signal unblock right before the syscall (c:527)
3728///   - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
3729///     bytes, parses a `#!interp arg` shebang and re-execs the
3730///     interpreter (c:534-628). For `ENOEXEC` with no shebang,
3731///     binary-safety check then falls back to `/bin/sh script` per
3732///     POSIX (c:588-628).
3733///
3734/// Returns `errno` from the failing exec — execve only returns on
3735/// failure, so success means the calling process is already replaced.
3736///
3737/// =================== WARNING — DIVERGENCE ====================
3738/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
3739///     string; Rust uses a stack `String` (consumed by `zputenv`).
3740/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
3741///     unconditionally as a no-op when FD_CLOEXEC is platform default.
3742/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
3743///     c:639-642 — handled implicitly via &str ↔ CString.
3744/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
3745///     drop the metafy and pass byte ranges to zerr directly.
3746/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
3747///     (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
3748///     `Vec<String>` with interp + optional arg + original argv tail
3749///     since Vec doesn't expose negative indexing.
3750/// (f) `environ` is FFI-loaded only when `newenvp` is None.
3751/// =============================================================
3752pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
3753    // c:504
3754    use std::ffi::CString;
3755    // c:514-520 — `_=pth` env stamping.
3756    let pth_abs = if pth.starts_with('/') {
3757        // c:516
3758        pth.to_string() // c:517
3759    } else {
3760        // c:518
3761        format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
3762    };
3763    zputenv(&format!("_={}", pth_abs)); // c:520
3764    closedumps(); // c:522
3765    winch_unblock(); // c:527
3766    let cpth = match CString::new(pth) {
3767        Ok(c) => c,
3768        Err(_) => return libc::ENOENT,
3769    };
3770    let cargs: Vec<CString> = argv
3771        .iter()
3772        .filter_map(|a| CString::new(a.as_str()).ok())
3773        .collect();
3774    let mut argv_ptrs: Vec<*const libc::c_char> = cargs.iter().map(|c| c.as_ptr()).collect();
3775    argv_ptrs.push(std::ptr::null());
3776    let env_holder: Vec<CString>;
3777    let env_ptrs: Vec<*const libc::c_char>;
3778    let envp: *const *const libc::c_char = match newenvp {
3779        Some(env) => {
3780            env_holder = env
3781                .iter()
3782                .filter_map(|e| CString::new(e.as_str()).ok())
3783                .collect();
3784            env_ptrs = {
3785                let mut v: Vec<*const libc::c_char> =
3786                    env_holder.iter().map(|c| c.as_ptr()).collect();
3787                v.push(std::ptr::null());
3788                v
3789            };
3790            env_ptrs.as_ptr()
3791        }
3792        None => unsafe {
3793            extern "C" {
3794                static environ: *const *const libc::c_char;
3795            }
3796            environ
3797        },
3798    };
3799    unsafe {
3800        libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
3801    }
3802    let eno = std::io::Error::last_os_error()
3803        .raw_os_error()
3804        .unwrap_or(libc::ENOEXEC); // c:534
3805    if eno == libc::ENOEXEC || eno == libc::ENOENT {
3806        // c:534
3807        let fd = unsafe { libc::open(cpth.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:538
3808        if fd < 0 {
3809            return std::io::Error::last_os_error()
3810                .raw_os_error()
3811                .unwrap_or(libc::ENOENT); // c:634
3812        }
3813        let mut buf = vec![0u8; POUNDBANGLIMIT + 1]; // c:541
3814        let ct = unsafe {
3815            libc::read(
3816                fd,
3817                buf.as_mut_ptr() as *mut libc::c_void,
3818                POUNDBANGLIMIT as libc::size_t,
3819            )
3820        }; // c:542
3821        unsafe {
3822            libc::close(fd);
3823        } // c:543
3824        if ct >= 0 {
3825            // c:544
3826            let ct = ct as usize;
3827            if ct >= 2 && buf[0] == b'#' && buf[1] == b'!' {
3828                // c:545
3829                let mut t0 = 0;
3830                while t0 < ct && buf[t0] != b'\n' {
3831                    t0 += 1;
3832                } // c:546-548
3833                if t0 == ct {
3834                    // c:549
3835                    zerr(&format!(
3836                        // c:550
3837                        "{}: bad interpreter: {}: {}",
3838                        pth,
3839                        String::from_utf8_lossy(&buf[2..t0.min(ct)]),
3840                        std::io::Error::from_raw_os_error(eno)
3841                    ));
3842                } else {
3843                    // c:552
3844                    while t0 > 0 && (buf[t0] == b' ' || buf[t0] == b'\t' || buf[t0] == b'\n') {
3845                        buf[t0] = 0;
3846                        t0 -= 1;
3847                    } // c:553-554
3848                    let mut ptr_lo: usize = 2;
3849                    while ptr_lo < buf.len() && buf[ptr_lo] == b' ' {
3850                        ptr_lo += 1;
3851                    } // c:555
3852                    let ptr2_lo = ptr_lo;
3853                    let mut ptr_hi = ptr2_lo;
3854                    while ptr_hi < buf.len() && buf[ptr_hi] != 0 && buf[ptr_hi] != b' ' {
3855                        ptr_hi += 1;
3856                    } // c:556
3857                    let interp_str = String::from_utf8_lossy(&buf[ptr2_lo..ptr_hi]).into_owned();
3858                    if eno == libc::ENOENT {
3859                        // c:557 — pathprog rewrite path.
3860                        let pprog = if !interp_str.starts_with('/') {
3861                            // c:561
3862                            pathprog(&interp_str).map(|p| p.display().to_string())
3863                        } else {
3864                            None
3865                        };
3866                        if let Some(pprog) = pprog {
3867                            // c:562
3868                            let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3869                            argv_new.push(interp_str.clone()); // c:564
3870                            if ptr_hi >= buf.len() || buf[ptr_hi] == 0 {
3871                                argv_new.push(pth.to_string());
3872                            } else {
3873                                // c:567
3874                                let mut rest_lo = ptr_hi + 1;
3875                                while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3876                                    rest_lo += 1;
3877                                }
3878                                let mut rest_hi = rest_lo;
3879                                while rest_hi < buf.len() && buf[rest_hi] != 0 {
3880                                    rest_hi += 1;
3881                                }
3882                                let arg_str =
3883                                    String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3884                                argv_new.push(arg_str);
3885                                argv_new.push(pth.to_string());
3886                            }
3887                            for orig in argv.iter().skip(1) {
3888                                argv_new.push(orig.clone());
3889                            }
3890                            winch_unblock(); // c:565/c:570
3891                            return zexecve(&pprog, &argv_new, newenvp); // c:566/c:571
3892                        }
3893                        zerr(&format!(
3894                            // c:574
3895                            "{}: bad interpreter: {}: {}",
3896                            pth,
3897                            interp_str,
3898                            std::io::Error::from_raw_os_error(eno)
3899                        ));
3900                    } else if ptr_hi < buf.len() && buf[ptr_hi] != 0 {
3901                        // c:576
3902                        let mut rest_lo = ptr_hi + 1;
3903                        while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3904                            rest_lo += 1;
3905                        }
3906                        let mut rest_hi = rest_lo;
3907                        while rest_hi < buf.len() && buf[rest_hi] != 0 {
3908                            rest_hi += 1;
3909                        }
3910                        let arg_str = String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3911                        let mut argv_new: Vec<String> =
3912                            vec![interp_str.clone(), arg_str, pth.to_string()];
3913                        for orig in argv.iter().skip(1) {
3914                            argv_new.push(orig.clone());
3915                        }
3916                        winch_unblock(); // c:580
3917                        return zexecve(&interp_str, &argv_new, newenvp); // c:581
3918                    } else {
3919                        // c:582
3920                        let mut argv_new: Vec<String> = vec![interp_str.clone(), pth.to_string()];
3921                        for orig in argv.iter().skip(1) {
3922                            argv_new.push(orig.clone());
3923                        }
3924                        winch_unblock(); // c:584
3925                        return zexecve(&interp_str, &argv_new, newenvp); // c:585
3926                    }
3927                }
3928            } else if eno == libc::ENOEXEC {
3929                // c:588 — binary-safety + /bin/sh fallback.
3930                let nul_pos = buf[..ct].iter().position(|&b| b == 0); // c:597
3931                let isbinary = match nul_pos {
3932                    None => false, // c:598
3933                    Some(npos) => {
3934                        let mut has_letter = false;
3935                        let mut binary = true;
3936                        for &b in &buf[..npos] {
3937                            // c:602-609
3938                            if (b as char).is_ascii_lowercase() || b == b'$' || b == b'`' {
3939                                has_letter = true;
3940                            }
3941                            if has_letter && b == b'\n' {
3942                                binary = false; // c:606
3943                                break;
3944                            }
3945                        }
3946                        binary
3947                    }
3948                };
3949                if !isbinary {
3950                    // c:611
3951                    let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3952                    argv_new.push("sh".to_string()); // c:625
3953                    if !argv.is_empty() && (argv[0].starts_with('-') || argv[0].starts_with('+')) {
3954                        argv_new.push("-".to_string()); // c:623
3955                    }
3956                    for orig in argv.iter() {
3957                        argv_new.push(orig.clone());
3958                    }
3959                    winch_unblock(); // c:626
3960                    return zexecve("/bin/sh", &argv_new, newenvp); // c:627
3961                }
3962            }
3963        }
3964    }
3965    eno // c:643
3966}
3967
3968/// Port of `char *getoutputfile(char *cmd, char **eptr)` from
3969/// `Src/exec.c:4910` — `=(cmd)` process substitution.
3970///
3971/// Substitutes the cmd's stdout into a temp file, returns the
3972/// filename. Optimised path: `=(<<<heredoc-str)` writes the
3973/// heredoc body directly without a fork.
3974///
3975/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
3976///     so the temp file gets cleaned at job exit.
3977/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
3978///     Behavior matches the `full=0` case anyway.
3979/// (c) `entersubsh` is ported at exec.rs:3934 — wire it here when
3980///     re-routing the fork path away from setsid-only fallback.
3981/// (d) `execode` is now ported (exec.rs:6047) — the body still
3982///     re-feeds through fusevm for cache coherence with execstring.
3983/// (e) `_realexit` flushes stdio + jobs + history. We use bare
3984///     `std::process::exit(0)` for now.
3985/// (f) TMPSUFFIX link()-rename block (c:4951-4958) deferred; rare
3986///     `setopt suffix_alias` interaction with =(…).
3987pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
3988    // c:4910
3989    let bytes = cmd.as_bytes();
3990    let _ = bytes;
3991    // c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
3992    let mut ends_at: usize = 0;
3993    let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
3994    if let Some(p) = eptr {
3995        *p = ends_at;
3996    }
3997    let mut nam = gettempname(None, true)?; // c:4924
3998                                            // c:4927 — `simple_redir_name` opt for `=(<<<str)`.
3999    let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
4000        // c:4933
4001        let mut sub = singsub(&raw); // c:4933
4002        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4003            // c:4934
4004            String::new() // c:4935 — sentinel; checked below
4005        } else {
4006            sub = untokenize(&sub); // c:4937
4007            dyncat(&sub, "\n") // c:4938
4008        }
4009    });
4010    if let Some(ref sv) = s {
4011        if sv.is_empty() {
4012            s = None;
4013        }
4014    }
4015    if s.is_none() {
4016        // c:4942
4017        child_block(); // c:4943
4018    }
4019    // c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
4020    let c_nam = match std::ffi::CString::new(nam.clone()) {
4021        Ok(c) => c,
4022        Err(_) => {
4023            if s.is_none() {
4024                child_unblock();
4025            }
4026            return None;
4027        }
4028    };
4029    let fd = unsafe {
4030        libc::open(
4031            c_nam.as_ptr(),
4032            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
4033            0o600 as libc::c_uint,
4034        )
4035    };
4036    if fd < 0 {
4037        // c:4945
4038        zerr(&format!(
4039            "process substitution failed: {}",
4040            std::io::Error::last_os_error()
4041        )); // c:4946
4042        if s.is_none() {
4043            child_unblock(); // c:4948
4044        }
4045        return None; // c:4949
4046    }
4047    // c:4951-4958 — TMPSUFFIX link block (see WARNING f).
4048    // c:4960 — `addfilelist(nam, 0);` — register temp file in current
4049    // job's filelist so it's unlinked at job exit (not relying on the
4050    // OS temp-reaper).
4051    if let Some(jt) = JOBTAB.get() {
4052        let mut guard = jt.lock().unwrap();
4053        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4054        if tj >= 0 {
4055            if let Some(j) = guard.get_mut(tj as usize) {
4056                crate::ported::jobs::addfilelist(j, Some(&nam), 0);
4057            }
4058        }
4059    }
4060    if let Some(sv) = s {
4061        // c:4962 — optimised here-string write path.
4062        let mut buf: Vec<u8> = sv.into_bytes();
4063        let _len = unmetafy(&mut buf); // c:4965
4064        let _ = write_loop(fd, &buf); // c:4966
4065        unsafe {
4066            libc::close(fd);
4067        } // c:4967
4068        return Some(nam); // c:4968
4069    }
4070    // c:4971 — `cmdoutpid = pid = zfork(NULL)`.
4071    let pid = zfork(None);
4072    cmdoutpid.store(pid, Ordering::Relaxed);
4073    if pid == -1 {
4074        // c:4972
4075        unsafe {
4076            libc::close(fd);
4077        } // c:4973
4078        child_unblock(); // c:4974
4079        return Some(nam); // c:4975
4080    } else if pid != 0 {
4081        // c:4976 — parent.
4082        unsafe {
4083            libc::close(fd);
4084        } // c:4977
4085        let _ = waitforpid(pid); // c:4978
4086        cmdoutval.store(0, Ordering::Relaxed); // c:4979
4087        return Some(nam); // c:4980
4088    }
4089    // c:4983 — child.
4090    closem(FDT_UNUSED, 0); // c:4984
4091    let _ = redup(fd, 1); // c:4985
4092    entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
4093    cmdpush(CS_CMDSUBST as u8); // c:4987
4094                                // c:4988 — execode — WARNING (d).
4095    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4096    let body = if body_end > 2 && body_end <= cmd.len() {
4097        &cmd[2..body_end]
4098    } else {
4099        ""
4100    };
4101    let _ = crate::ported::exec::execute_script_zsh_pipeline(body);
4102    cmdpop(); // c:4989
4103    unsafe {
4104        libc::close(1);
4105    } // c:4990
4106      // _realexit — WARNING (e)
4107    std::process::exit(0); // c:4991
4108    #[allow(unreachable_code)]
4109    {
4110        // c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
4111        let _ = &mut nam;
4112        unsafe {
4113            libc::kill(libc::getpid(), libc::SIGKILL);
4114        }
4115        None
4116    }
4117}
4118
4119/// Port of `char *getproc(char *cmd, char **eptr)` from
4120/// `Src/exec.c:5025` — `<(cmd)` / `>(cmd)` process substitution
4121/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
4122///
4123/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
4124///     path c:5037-5064) is omitted; modern Linux/macOS both
4125///     provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
4126///     unused here.
4127/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
4128///     the current job (c:5141-5142).
4129/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
4130///     c:5087.
4131/// (d) `entersubsh` is ported at exec.rs:3934 — wired below at
4132///     c:5063 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`).
4133/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
4134///     through fusevm for cache coherence.
4135/// (f) `_realexit` flushes stdio + jobs + history. We use bare
4136///     `std::process::exit(LASTVAL)` for now.
4137/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
4138pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4139    // c:5025
4140    let bytes = cmd.as_bytes();
4141    let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4142        1 // c:5032 — `<(...)` writer-side child
4143    } else {
4144        0
4145    };
4146    // c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
4147    // proc subst needs a host job to attach the child to.
4148    let tj_check = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4149    if tj_check == -1 {
4150        zerr(&format!("process substitution {} cannot be used here", cmd)); // c:5069
4151        return None; // c:5070
4152    }
4153    // c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
4154    let mut ends_at: usize = 0;
4155    let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
4156    if let Some(p) = eptr {
4157        *p = ends_at;
4158    }
4159    let mut pipes: [i32; 2] = [-1; 2];
4160    if mpipe(&mut pipes) < 0 {
4161        // c:5075
4162        return None;
4163    }
4164    let mut bgtime: ZshTimespec = libc::timespec {
4165        tv_sec: 0,
4166        tv_nsec: 0,
4167    };
4168    let pid = zfork(Some(&mut bgtime)); // c:5077
4169    if pid != 0 {
4170        // c:5077 — parent path.
4171        let pnam = format!("/dev/fd/{}", pipes[(1 - out) as usize]); // c:5078
4172        let _ = zclose(pipes[out as usize]); // c:5079
4173        if pid == -1 {
4174            // c:5080
4175            let _ = zclose(pipes[(1 - out) as usize]); // c:5082
4176            return None; // c:5083
4177        }
4178        let fd = pipes[(1 - out) as usize]; // c:5085
4179        fdtable_set(fd, FDT_PROC_SUBST); // c:5086
4180                                         // c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
4181                                         // pipe fd in the current job's filelist so it's closed at job exit.
4182        if let Some(jt) = JOBTAB.get() {
4183            let mut guard = jt.lock().unwrap();
4184            let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4185            if tj >= 0 {
4186                if let Some(j) = guard.get_mut(tj as usize) {
4187                    crate::ported::jobs::addfilelist(j, None, fd);
4188                }
4189            }
4190        }
4191        // c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
4192        // record the proc-subst writer-side child in the job's
4193        // auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
4194        // child), C omits the addproc — symmetric here.
4195        if out == 0 {
4196            if let Some(jt) = JOBTAB.get() {
4197                let mut guard = jt.lock().unwrap();
4198                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4199                if tj >= 0 {
4200                    if let Some(j) = guard.get_mut(tj as usize) {
4201                        crate::ported::jobs::addproc(
4202                            j,
4203                            pid,
4204                            "",
4205                            true,
4206                            Some(std::time::Instant::now()),
4207                            -1,
4208                            -1,
4209                        );
4210                    }
4211                }
4212            }
4213        }
4214        procsubstpid.store(pid, Ordering::Relaxed); // c:5092
4215        return Some(pnam); // c:5093
4216    }
4217    // c:5095 — child.
4218    entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
4219    let _ = redup(pipes[out as usize], out); // c:5096
4220    closem(FDT_UNUSED, 0); // c:5097
4221    cmdpush(CS_CMDSUBST as u8); // c:5100
4222    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4223    let body = if body_end > 2 && body_end <= cmd.len() {
4224        &cmd[2..body_end]
4225    } else {
4226        ""
4227    };
4228    let _ = crate::ported::exec::execute_script_zsh_pipeline(body);
4229    cmdpop(); // c:5102
4230    let _ = zclose(out); // c:5103
4231    std::process::exit(LASTVAL.load(Ordering::Relaxed)); // c:5104
4232}
4233
4234/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
4235/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
4236pub mod esub {
4237    // c:1056
4238    /// `ASYNC` constant.
4239    pub const ASYNC: i32 = 0x01; // c:1058
4240    /// `PGRP` constant.
4241    pub const PGRP: i32 = 0x02; // c:1063
4242    /// `KEEPTRAP` constant.
4243    pub const KEEPTRAP: i32 = 0x04; // c:1065
4244    /// `FAKE` constant.
4245    pub const FAKE: i32 = 0x08; // c:1067
4246    /// `REVERTPGRP` constant.
4247    pub const REVERTPGRP: i32 = 0x10; // c:1069
4248    /// `NOMONITOR` constant.
4249    pub const NOMONITOR: i32 = 0x20; // c:1071
4250    /// `JOB_CONTROL` constant.
4251    pub const JOB_CONTROL: i32 = 0x40; // c:1073
4252}
4253
4254/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
4255/// Out-arg used by `entersubsh()` to hand back the group-leader pid
4256/// and the list-pipe job index the parent should track. Only filled
4257/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
4258/// groups).
4259#[allow(non_camel_case_types)]
4260#[derive(Default)]
4261pub struct entersubsh_ret {
4262    pub gleader: i32,       // c:1122
4263    pub list_pipe_job: i32, // c:1123
4264}
4265
4266/// Port of `static void entersubsh(int flags, struct entersubsh_ret *retp)`
4267/// from `Src/exec.c:1083`. Called by every child fork to switch the
4268/// process into subshell mode: traps reset, monitor disabled, signals
4269/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
4270/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
4271///
4272/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
4273///     1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
4274///     ESUB_PGRP+sync path establishes pipeline group-leadership
4275///     (list_pipe_job inherit or thisjob-as-leader), filling
4276///     entersubsh_ret with the chosen gleader + list_pipe_job index.
4277/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
4278///     `clearjobtab(&mut JobTable, monitor)`; we get the global table
4279///     via a TABLE handle similar to other jobs.rs entries.
4280/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
4281/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
4282///     mypgrp` — direct C parity (jobs.rs:3406 provides the call).
4283/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
4284///     uses `opts_set_off(USEZLE)`; zleactive is the atomic in
4285///     builtins/sched.rs.
4286/// =============================================================
4287pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
4288    // c:1083
4289    let monitor: i32;
4290    let job_control_ok: i32;
4291    // c:1088-1092 — reset traps unless KEEPTRAP.
4292    if (flags & esub::KEEPTRAP) == 0 {
4293        // c:1088
4294        for sig in 0..=SIGCOUNT {
4295            // c:1089
4296            let st = {
4297                let guard = sigtrapped.lock().unwrap();
4298                guard.get(sig as usize).copied().unwrap_or(0)
4299            };
4300            let func_set = (st & ZSIG_FUNC) != 0; // c:1090
4301            let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
4302            if !func_set && !posix_ignored {
4303                unsettrap(sig); // c:1092
4304            }
4305        }
4306    }
4307    monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
4308    job_control_ok = if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
4309        // c:1094
4310        1
4311    } else {
4312        0
4313    };
4314    EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
4315    if (flags & esub::NOMONITOR) != 0 {
4316        // c:1096
4317        dosetopt(MONITOR, 0, 0); // c:1097
4318    }
4319    if !isset(MONITOR) {
4320        // c:1098
4321        if (flags & esub::ASYNC) != 0 {
4322            // c:1099
4323            let _ = settrap(libc::SIGINT, None, 0); // c:1100
4324            let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
4325            if unsafe { libc::isatty(0) } != 0 {
4326                // c:1102
4327                unsafe {
4328                    libc::close(0);
4329                } // c:1103
4330                let devnull = std::ffi::CString::new("/dev/null").unwrap();
4331                if unsafe { libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) } != 0 {
4332                    // c:1104
4333                    zerr(&format!(
4334                        // c:1105
4335                        "can't open /dev/null: {}",
4336                        std::io::Error::last_os_error()
4337                    ));
4338                    unsafe {
4339                        libc::_exit(1);
4340                    } // c:1106
4341                }
4342            }
4343        }
4344    } else if (flags & esub::PGRP) != 0 {
4345        // c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
4346        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4347        if thisjob != -1 {
4348            let lpj = list_pipe_job.load(Ordering::Relaxed);
4349            let lp = list_pipe.load(Ordering::Relaxed);
4350            let lpc = list_pipe_child.load(Ordering::Relaxed);
4351            if let Some(jt) = JOBTAB.get() {
4352                let mut guard = jt.lock().unwrap();
4353                let lpj_gleader = guard.get(lpj as usize).map(|j| j.gleader).unwrap_or(0);
4354                if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
4355                    // c:1111-1124 — inherit list_pipe_job's group leader.
4356                    let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
4357                        || (unsafe { libc::killpg(lpj_gleader, 0) } == -1
4358                            && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH))
4359                    {
4360                        // c:1115-1117 — primary group leader gone; this child becomes leader.
4361                        let new_gl = if lpc != 0 {
4362                            mypgrp.load(Ordering::Relaxed)
4363                        } else {
4364                            unsafe { libc::getpid() }
4365                        };
4366                        if let Some(j) = guard.get_mut(lpj as usize) {
4367                            j.gleader = new_gl;
4368                        }
4369                        if let Some(j) = guard.get_mut(thisjob as usize) {
4370                            j.gleader = new_gl;
4371                        }
4372                        unsafe { libc::setpgid(0, new_gl) };
4373                        if (flags & esub::ASYNC) == 0 {
4374                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
4375                        }
4376                        new_gl
4377                    } else {
4378                        lpj_gleader
4379                    };
4380                    if let Some(r) = retp {
4381                        if (flags & esub::ASYNC) == 0 {
4382                            r.gleader = pgid; // c:1122
4383                            r.list_pipe_job = lpj; // c:1123
4384                        }
4385                    }
4386                } else {
4387                    // c:1126-1151 — standard group-leader-takeover path.
4388                    let thisjob_gleader =
4389                        guard.get(thisjob as usize).map(|j| j.gleader).unwrap_or(0);
4390                    if thisjob_gleader == 0 || unsafe { libc::setpgid(0, thisjob_gleader) } == -1 {
4391                        let new_gl = unsafe { libc::getpid() };
4392                        if let Some(j) = guard.get_mut(thisjob as usize) {
4393                            j.gleader = new_gl; // c:1138
4394                        }
4395                        if lpj != thisjob {
4396                            let lpj_was_unset = guard
4397                                .get(lpj as usize)
4398                                .map(|j| j.gleader == 0)
4399                                .unwrap_or(true);
4400                            if lpj_was_unset {
4401                                if let Some(j) = guard.get_mut(lpj as usize) {
4402                                    j.gleader = new_gl; // c:1140-1141
4403                                }
4404                            }
4405                        }
4406                        unsafe { libc::setpgid(0, new_gl) }; // c:1142
4407                        if (flags & esub::ASYNC) == 0 {
4408                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
4409                            if let Some(r) = retp {
4410                                r.gleader = new_gl; // c:1146
4411                                if lpj != thisjob {
4412                                    r.list_pipe_job = lpj; // c:1148
4413                                }
4414                            }
4415                        }
4416                    }
4417                }
4418            }
4419        } else {
4420            // No real job slot; basic setpgid fallback.
4421            unsafe { libc::setpgid(0, 0) };
4422        }
4423    }
4424    if (flags & esub::FAKE) == 0 {
4425        // c:1153
4426        subsh.store(1, Ordering::Relaxed); // c:1154
4427    }
4428    // c:1161 — `zsh_subshell++;` regardless of FAKE.
4429    zsh_subshell.fetch_add(1, Ordering::Relaxed);
4430    // c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
4431    if (flags & esub::REVERTPGRP) != 0
4432        && unsafe { libc::getpid() } == mypgrp.load(Ordering::Relaxed)
4433    {
4434        release_pgrp(); // c:1163
4435    }
4436    *shout.lock().unwrap() = 0; // c:1164 — shout = NULL
4437    if (flags & esub::NOMONITOR) != 0 {
4438        // c:1165
4439        signal_ignore(libc::SIGTTOU); // c:1171
4440        signal_ignore(libc::SIGTTIN); // c:1172
4441        signal_ignore(libc::SIGTSTP); // c:1173
4442    } else if job_control_ok == 0 {
4443        // c:1174
4444        signal_default(libc::SIGTTOU); // c:1181
4445        signal_default(libc::SIGTTIN); // c:1182
4446        signal_default(libc::SIGTSTP); // c:1183
4447    }
4448    let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
4449    if interact {
4450        signal_default(libc::SIGTERM); // c:1186
4451        let int_st = sigtrapped
4452            .lock()
4453            .unwrap()
4454            .get(libc::SIGINT as usize)
4455            .copied()
4456            .unwrap_or(0);
4457        if (int_st & ZSIG_IGNORED) == 0 {
4458            // c:1187
4459            signal_default(libc::SIGINT); // c:1188
4460        }
4461        let pipe_st = sigtrapped
4462            .lock()
4463            .unwrap()
4464            .get(libc::SIGPIPE as usize)
4465            .copied()
4466            .unwrap_or(0);
4467        if pipe_st == 0 {
4468            // c:1189
4469            signal_default(libc::SIGPIPE); // c:1190
4470        }
4471    }
4472    let quit_st = sigtrapped
4473        .lock()
4474        .unwrap()
4475        .get(libc::SIGQUIT as usize)
4476        .copied()
4477        .unwrap_or(0);
4478    if (quit_st & ZSIG_IGNORED) == 0 {
4479        // c:1192
4480        signal_default(libc::SIGQUIT); // c:1193
4481    }
4482    // c:1202-1205 — unblock any trapped signals while in `intrap`.
4483    if intrap.load(Ordering::Relaxed) != 0 {
4484        // c:1202
4485        for sig in 1..=SIGCOUNT {
4486            let st = sigtrapped
4487                .lock()
4488                .unwrap()
4489                .get(sig as usize)
4490                .copied()
4491                .unwrap_or(0);
4492            if st != 0 && st != ZSIG_IGNORED {
4493                // c:1204
4494                let m = signal_mask(sig);
4495                let _ = signal_unblock(&m); // c:1205
4496            }
4497        }
4498    }
4499    if job_control_ok == 0 {
4500        // c:1206
4501        dosetopt(MONITOR, 0, 0); // c:1207
4502    }
4503    dosetopt(USEZLE, 0, 0); // c:1208
4504    zleactive.store(0, Ordering::Relaxed); // c:1209
4505                                           // c:1214-1217 — close saved fds.
4506    let max = MAX_ZSH_FD.load(Ordering::Relaxed);
4507    for i in 10..=max {
4508        if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
4509            // c:1215
4510            let _ = zclose(i); // c:1216
4511        }
4512    }
4513    // c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
4514    // at jobs.rs:1695 which handles ALL the C body including the
4515    // oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
4516    let mut dummy_table = crate::exec_jobs::JobTable::new();
4517    crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
4518    let _ = get_usage(); // c:1220
4519    FORKLEVEL.store(
4520        // c:1221 — `forklevel = locallevel;`
4521        locallevel.load(Ordering::Relaxed),
4522        Ordering::Relaxed,
4523    );
4524}
4525
4526/// Port of `static int getpipe(char *cmd, int nullexec)` from
4527/// `Src/exec.c:5119`.
4528///
4529/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
4530/// pipe pair: parent gets back the readable (`<(...)`) or writable
4531/// (`>(...)`) end as an fd; child runs the substituted command with
4532/// its stdio redirected into the other end.
4533///
4534/// ```c
4535/// Eprog prog;
4536/// int pipes[2], out = *cmd == Inang;
4537/// pid_t pid;
4538/// struct timespec bgtime;
4539/// char *ends;
4540/// if (!(prog = parsecmd(cmd, &ends))) return -1;
4541/// if (*ends) { zerr("invalid syntax..."); return -1; }
4542/// if (mpipe(pipes) < 0) return -1;
4543/// if ((pid = zfork(&bgtime))) {
4544///     zclose(pipes[out]);
4545///     if (pid == -1) { zclose(pipes[!out]); return -1; }
4546///     if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
4547///     procsubstpid = pid;
4548///     return pipes[!out];
4549/// }
4550/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
4551/// redup(pipes[out], out);
4552/// closem(FDT_UNUSED, 0);
4553/// cmdpush(CS_CMDSUBST);
4554/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
4555/// cmdpop();
4556/// _realexit();
4557/// ```
4558///
4559/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
4560///     procsubst pid recording site (c:5141-5142) earlier this
4561///     session; the child IS now recorded in `JOBTAB[thisjob]`.
4562/// (b) `entersubsh` IS now ported (exec.rs:3934) including the
4563///     ESUB_PGRP pipeline group-leadership path — wired this
4564///     session for getpipe's `entersubsh(ESUB_ASYNC|ESUB_PGRP|
4565///     ESUB_NOMONITOR, NULL)` call.
4566/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
4567///     can route through execode for the parsed eprog. Currently
4568///     this caller still uses the fusevm pipeline for cache
4569///     coherence with execstring; switch over when the wordcode
4570///     walker becomes the primary path.
4571/// (d) `_realexit()` flushes stdio + jobs + history. We use bare
4572///     `std::process::exit(lastval)` for now.
4573pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
4574    // c:5119
4575    let bytes = cmd.as_bytes();
4576    let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4577        1 // c:5122 — `<(...)` reads from child, child writes to fd 1
4578    } else {
4579        0 // `>(...)` — child reads from fd 0
4580    };
4581    let mut ends_at: usize = 0;
4582    let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
4583    if prog.is_none() {
4584        // c:5127
4585        return -1; // c:5128
4586    }
4587    // c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
4588    if ends_at < bytes.len() && bytes[ends_at] != 0 {
4589        zerr("invalid syntax for process substitution in redirection"); // c:5130
4590        return -1; // c:5131
4591    }
4592    let mut pipes: [i32; 2] = [-1; 2];
4593    if mpipe(&mut pipes) < 0 {
4594        // c:5133
4595        return -1;
4596    }
4597    // c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
4598    let mut bgtime: ZshTimespec = libc::timespec {
4599        tv_sec: 0,
4600        tv_nsec: 0,
4601    };
4602    let pid = zfork(Some(&mut bgtime)); // c:5135
4603    if pid != 0 {
4604        // c:5135 — parent.
4605        let _ = zclose(pipes[out as usize]); // c:5136
4606        if pid == -1 {
4607            // c:5137
4608            let _ = zclose(pipes[(1 - out) as usize]); // c:5138
4609            return -1; // c:5139
4610        }
4611        // c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
4612        if nullexec == 0 {
4613            if let Some(jt) = JOBTAB.get() {
4614                let mut guard = jt.lock().unwrap();
4615                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4616                if tj >= 0 {
4617                    if let Some(j) = guard.get_mut(tj as usize) {
4618                        crate::ported::jobs::addproc(
4619                            j,
4620                            pid,
4621                            "",
4622                            true, // aux=1 for proc subst
4623                            Some(std::time::Instant::now()),
4624                            -1,
4625                            -1,
4626                        );
4627                    }
4628                }
4629            }
4630        }
4631        procsubstpid.store(pid, Ordering::Relaxed); // c:5143
4632        return pipes[(1 - out) as usize]; // c:5144
4633    }
4634    // c:5146 — child path.
4635    entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
4636    let _ = redup(pipes[out as usize], out); // c:5147
4637    closem(FDT_UNUSED, 0); // c:5148
4638    cmdpush(CS_CMDSUBST as u8); // c:5149
4639                                // c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
4640    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4641    let body = if body_end > 2 && body_end <= bytes.len() {
4642        &cmd[2..body_end]
4643    } else {
4644        ""
4645    };
4646    let _ = crate::ported::exec::execute_script_zsh_pipeline(body);
4647    cmdpop(); // c:5151
4648              // c:5152 — _realexit() — WARNING (d).
4649    std::process::exit(LASTVAL.load(Ordering::Relaxed));
4650}
4651
4652/// Port of `static void spawnpipes(LinkList l, int nullexec)` from
4653/// `Src/exec.c:5184`.
4654///
4655/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
4656/// entry fires `getpipe(name, nullexec || varid)` and stashes the
4657/// resulting fd into `f->fd2`.
4658///
4659/// ```c
4660/// LinkNode n;
4661/// Redir f;
4662/// char *str;
4663/// n = firstnode(l);
4664/// for (; n; incnode(n)) {
4665///     f = (Redir) getdata(n);
4666///     if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
4667///         str = f->name;
4668///         f->fd2 = getpipe(str, nullexec || f->varid);
4669///     }
4670/// }
4671/// ```
4672///
4673/// =================== WARNING — DIVERGENCE ====================
4674/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
4675/// in place of `LinkList`. The walk is identical; the only behavior
4676/// difference is that LinkList iteration in C lets callers splice
4677/// nodes mid-walk — we never do that here so it's a no-op divergence.
4678/// =============================================================
4679pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
4680    // c:5184
4681    for f in l.iter_mut() {
4682        // c:5191
4683        if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
4684            // c:5193
4685            let str_ = f.name.clone().unwrap_or_default(); // c:5194
4686            let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
4687                1
4688            } else {
4689                nullexec
4690            };
4691            f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
4692        }
4693    }
4694}
4695
4696/// Port of `static int cancd2(char *s)` from `Src/exec.c:6411`.
4697///
4698/// C body:
4699/// ```c
4700/// struct stat buf;
4701/// char *us, *us2 = NULL;
4702/// int ret;
4703/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
4704///     if (*s != '/')
4705///         us = tricat(pwd[1] ? pwd : "", "/", s);
4706///     else
4707///         us = ztrdup(s);
4708///     fixdir(us2 = us);
4709/// } else
4710///     us = unmeta(s);
4711/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
4712/// if (us2) free(us2);
4713/// return ret;
4714/// ```
4715///
4716/// True iff `s` is a directory we can `cd` into (X-perm). With
4717/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
4718/// (joining with PWD if relative) so `cd /foo/bar/..` works without
4719/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
4720pub fn cancd2(s: &str) -> i32 {
4721    // c:6411
4722    let us: String;
4723    // c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
4724    let chasedots = isset(CHASEDOTS); // c:6422
4725    let chaselinks = isset(CHASELINKS);
4726    if !chasedots && !chaselinks {
4727        // c:6422
4728        // c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
4729        let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
4730        let mut raw = if !s.starts_with('/') {
4731            // c:6423
4732            format!(
4733                "{}/{}",
4734                if pwd_str.len() > 1 { &pwd_str[..] } else { "" },
4735                s
4736            )
4737        } else {
4738            s.to_string()
4739        };
4740        // c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
4741        raw = fixdir(&raw);
4742        us = raw;
4743    } else {
4744        // c:6428
4745        us = unmeta(s); // c:6429
4746    }
4747    // c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
4748    let cstr = match std::ffi::CString::new(us.as_str()) {
4749        Ok(c) => c,
4750        Err(_) => return 0,
4751    };
4752    if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
4753        return 0;
4754    }
4755    let meta = match std::fs::metadata(&us) {
4756        Ok(m) => m,
4757        Err(_) => return 0,
4758    };
4759    if !meta.file_type().is_dir() {
4760        return 0;
4761    }
4762    1
4763}
4764
4765/// Port of `char *cancd(char *s)` from `Src/exec.c:6370`.
4766///
4767/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
4768/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
4769/// it, else `None`.
4770///
4771/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
4772/// `doprintdir = -1` when a non-trivial path is found (so `cd`
4773/// echoes the resolved path).
4774pub fn cancd(s: &str) -> Option<String> {
4775    // c:6370
4776    // c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
4777    //                (s[1]=='.' && (s[2]=='/' || !s[2])))`.
4778    let bytes = s.as_bytes();
4779    let nocdpath = bytes.first().copied() == Some(b'.')
4780        && (bytes.get(1).copied() == Some(b'/')
4781            || bytes.get(1).is_none()
4782            || (bytes.get(1).copied() == Some(b'.')
4783                && (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
4784    // c:6376 — `if (*s != '/')` branch.
4785    if !s.starts_with('/') {
4786        // c:6376
4787        // c:6379-6380 — `if (cancd2(s)) return s;`
4788        if cancd2(s) != 0 {
4789            return Some(s.to_string());
4790        }
4791        // c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
4792        let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
4793        if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
4794            return None; // c:6382
4795        }
4796        // c:6383-6397 — CDPATH walk.
4797        if !nocdpath {
4798            let cdpath_str = getsparam("CDPATH").unwrap_or_default();
4799            for cp in cdpath_str.split(':') {
4800                // c:6384
4801                let sbuf = if !cp.is_empty() {
4802                    format!("{}/{}", cp, s) // c:6386
4803                } else {
4804                    s.to_string() // c:6391
4805                };
4806                if cancd2(&sbuf) != 0 {
4807                    // c:6393
4808                    DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
4809                    return Some(sbuf); // c:6395
4810                }
4811            }
4812        }
4813        // c:6398-6403 — `cd_able_vars()` fallback.
4814        if let Some(t) = cd_able_vars(s) {
4815            // c:6398
4816            if cancd2(&t) != 0 {
4817                // c:6399
4818                DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
4819                return Some(t); // c:6401
4820            }
4821        }
4822        return None; // c:6404
4823    }
4824    // c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
4825    if cancd2(s) != 0 {
4826        Some(s.to_string())
4827    } else {
4828        None
4829    }
4830}
4831
4832/// Port of `char *simple_redir_name(Eprog prog, int redir_type)` from
4833/// `Src/exec.c:4689`.
4834///
4835/// Test if an Eprog encodes a single simple-command consisting of a
4836/// SINGLE redirection of the requested type with NO command body
4837/// (the `cat < foo` shape). When true, returns the redir target name
4838/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
4839/// `open(2)` instead of fork+pipe+exec.
4840///
4841/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
4842/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
4843/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
4844/// same shape — this port replicates the same offset reads.
4845pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
4846    // c:4689
4847    let pc = &prog.prog;
4848    // c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
4849    // offsets matching C's `pc[0]..pc[6]` checks.
4850    if pc.len() < 7 {
4851        return None;
4852    }
4853
4854    if wc_code(pc[0]) != WC_LIST
4855        || (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0  // c:4695
4856        || wc_code(pc[1]) != WC_SUBLIST
4857        || WC_SUBLIST_FLAGS(pc[1]) != 0  // c:4696
4858        || WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END  // c:4697
4859        || wc_code(pc[2]) != WC_PIPE
4860        || WC_PIPE_TYPE(pc[2]) != WC_PIPE_END  // c:4698
4861        || wc_code(pc[3]) != WC_REDIR
4862        || WC_REDIR_TYPE(pc[3]) != redir_type  // c:4699
4863        || WC_REDIR_VARID(pc[3]) != 0  // c:4700
4864        || pc[4] != 0  // c:4701
4865        || wc_code(pc[6]) != WC_SIMPLE
4866        || WC_SIMPLE_ARGC(pc[6]) != 0
4867    // c:4702
4868    {
4869        return None; // c:4706
4870    }
4871    // c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
4872    Some(dupstring(&ecrawstr(prog, 5, None)))
4873}
4874
4875/// Port of `int getherestr(struct redir *fn)` from `Src/exec.c:4655`.
4876///
4877/// C body:
4878/// ```c
4879/// char *s, *t;
4880/// int fd, len;
4881/// t = fn->name;
4882/// singsub(&t);
4883/// untokenize(t);
4884/// unmetafy(t, &len);
4885/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
4886///     t[len++] = '\n';
4887/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
4888///     return -1;
4889/// write_loop(fd, t, len);
4890/// close(fd);
4891/// fd = open(s, O_RDONLY | O_NOCTTY);
4892/// unlink(s);
4893/// return fd;
4894/// ```
4895///
4896/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
4897/// tempfile, then re-open read-only and unlink — gives the consumer a
4898/// read fd whose backing file is already cleaned up.
4899pub fn getherestr(fn_: &redir) -> i32 {
4900    // c:4655
4901    let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
4902    t = singsub(&t); // c:4661
4903    t = untokenize(&t); // c:4662
4904                        // c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
4905                        // Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
4906    let mut bytes: Vec<u8> = t.into_bytes();
4907    let _len = unmetafy(&mut bytes);
4908    // c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
4909    if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
4910        // c:4671
4911        bytes.push(b'\n'); // c:4672
4912    }
4913    // c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
4914    let (fd, s) = match gettempfile(None) {
4915        Some(p) => p,
4916        None => return -1, // c:4674
4917    };
4918    // c:4675 — `write_loop(fd, t, len);`
4919    let _ = write_loop(fd, &bytes); // c:4675
4920                                    // c:4676 — `close(fd);`
4921    let _ = zclose(fd); // c:4676
4922                        // c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
4923    let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
4924    let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
4925                                                                                        // c:4678 — `unlink(s);`
4926    unsafe {
4927        libc::unlink(cstr.as_ptr());
4928    } // c:4678
4929    new_fd // c:4679
4930}
4931
4932/// Port of `void quote_tokenized_output(char *str, FILE *file)` from
4933/// `Src/exec.c:2114`.
4934///
4935/// C body (abridged):
4936/// ```c
4937/// for (; *s; s++) {
4938///     switch (*s) {
4939///         case Meta: putc(*++s ^ 32, file); continue;
4940///         case Nularg: continue;
4941///         case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
4942///             putc('\\', file); break;
4943///         case '\t': fputs("$'\\t'", file); continue;
4944///         case '\n': fputs("$'\\n'", file); continue;
4945///         case '\r': fputs("$'\\r'", file); continue;
4946///         case '=': if (s == str) putc('\\', file); break;
4947///         default:
4948///             if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
4949///     }
4950///     putc(*s, file);
4951/// }
4952/// ```
4953///
4954/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
4955/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
4956/// surface as unescaped chars (`*`, `(`) while literal special chars
4957/// get backslash-escaped — round-tripping through the shell.
4958pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
4959    // c:2114
4960    let bytes = str_in.as_bytes();
4961    let mut i = 0usize;
4962    while i < bytes.len() {
4963        // c:2118 `for (; *s; s++)`
4964        let c = bytes[i];
4965        match c {
4966            x if x == Meta => {
4967                // c:2120 — `case Meta: putc(*++s ^ 32, file);`
4968                if i + 1 < bytes.len() {
4969                    file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
4970                    i += 2;
4971                } else {
4972                    i += 1;
4973                }
4974                continue; // c:2122
4975            }
4976            x if x as char == Nularg => {
4977                // c:2124
4978                i += 1;
4979                continue; // c:2126
4980            }
4981            b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']' | b'*'
4982            | b'?' | b'$' | b' ' => {
4983                // c:2128-2142
4984                file.write_all(b"\\")?; // c:2143
4985            }
4986            b'\t' => {
4987                // c:2146
4988                file.write_all(b"$'\\t'")?; // c:2147
4989                i += 1;
4990                continue;
4991            }
4992            b'\n' => {
4993                // c:2150
4994                file.write_all(b"$'\\n'")?; // c:2151
4995                i += 1;
4996                continue;
4997            }
4998            b'\r' => {
4999                // c:2154
5000                file.write_all(b"$'\\r'")?; // c:2155
5001                i += 1;
5002                continue;
5003            }
5004            b'=' => {
5005                // c:2158 — `if (s == str) putc('\\', file);`
5006                if i == 0 {
5007                    file.write_all(b"\\")?; // c:2160
5008                }
5009            }
5010            _ => {
5011                // c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
5012                if itok(c) {
5013                    // c:2164
5014                    let pound = Pound as u8;
5015                    if c >= pound {
5016                        let idx = (c - pound) as usize;
5017                        let zt = ztokens.as_bytes();
5018                        if idx < zt.len() {
5019                            file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
5020                        }
5021                    }
5022                    i += 1;
5023                    continue;
5024                }
5025            }
5026        }
5027        file.write_all(&[c])?; // c:2171
5028        i += 1;
5029    }
5030    Ok(())
5031}
5032
5033// =====================================================================
5034// Wordcode-VM control-flow dispatch — faithful ports of the C
5035// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
5036//
5037// Each function below takes `&mut estate` and returns `i32` to mirror
5038// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
5039// line `// c:NNN` citations track the C source line.
5040//
5041// zshrs's primary execution path is the fusevm bytecode VM. These
5042// wordcode-VM entries exist for C-name parity with the upstream
5043// interpreter so that future bridging code can drive zshrs through
5044// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
5045// zshrs primitives don't yet model their C counterpart (e.g.
5046// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
5047// helper is declared with a comment citing the C source file:line
5048// where the canonical body lives — same pattern as the canonical
5049// `ksh93::ksh93_wrapper` port at c:152-227.
5050// =====================================================================
5051
5052use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
5053use crate::ported::pattern::{patcompile, pattry};
5054use crate::ported::r#loop::try_tryflag;
5055
5056// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
5057use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
5058use crate::ported::linklist::LinkList;
5059use crate::ported::mem::freeheap;
5060use crate::ported::params::setloopvar;
5061use crate::ported::params::{assignaparam, assignsparam, unsetparam};
5062use crate::ported::parse::{ecgetlist, ecgetstr};
5063use crate::ported::pattern::haswilds;
5064use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
5065use crate::ported::subst::{globlist, prefork};
5066use crate::ported::zsh_h::{
5067    estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
5068    WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP,
5069    WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
5070    WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
5071    WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
5072};
5073use crate::ported::zsh_h::{
5074    ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
5075    PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
5076    WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
5077};
5078use crate::ported::zsh_h::{
5079    CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
5080    CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
5081};
5082
5083// --- Local stubs for C primitives not yet ported elsewhere ------------
5084//
5085// These mirror the C functions of the same names. Each cites the C
5086// source file:line where the canonical body lives. They are inlined
5087// here (rather than a separate `pub fn` in the owning C-file module)
5088// because the owning ports are pending the wider exec-substrate
5089// work (sub-PR). Once those land, these locals collapse to direct
5090// `crate::ported::<owner>::<fn>` calls.
5091
5092/// Port of `void execsubst(LinkList strs)` from `Src/exec.c:2684`.
5093///
5094/// C body (c:2684-2693):
5095/// ```c
5096/// void execsubst(LinkList strs) {
5097///     if (strs) {
5098///         prefork(strs, esprefork, NULL);
5099///         if (esglob && !errflag) {
5100///             LinkList ostrs = strs;
5101///             globlist(strs, 0);
5102///             strs = ostrs;
5103///         }
5104///     }
5105/// }
5106/// ```
5107///
5108/// `execsubst` runs `prefork` (parameter / arithmetic / command
5109/// substitution expansion + IFS-split) over the whole list, then
5110/// (when `esglob` is set) `globlist` to do filename globbing on the
5111/// result.
5112fn execsubst(list: &mut Vec<String>) {
5113    // c:2684
5114    if list.is_empty() {
5115        return; // c:2686 `if (strs)`
5116    }
5117    let mut ll: crate::ported::subst::LinkList = std::mem::take(list).into_iter().collect();
5118    let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
5119    let mut rf: i32 = 0;
5120    prefork(&mut ll, prefork_flags, &mut rf); // c:2687
5121    if esglob.load(Ordering::Relaxed) != 0 && errflag.load(Ordering::Relaxed) == 0 {
5122        // c:2688 `if (esglob && !errflag)`
5123        globlist(&mut ll, 0); // c:2690
5124    }
5125    *list = ll.into_iter().collect();
5126}
5127
5128/// Direct port of `static void addvars(Estate state, Wordcode pc,
5129/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
5130/// nodes stacked inline of a simple command — the `var=value` and
5131/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
5132/// at `pc`, extracts each assignment's name + value (scalar or array),
5133/// optionally preforks + globs the tokenised RHS, and routes through
5134/// `assignsparam` (scalar) or `assignaparam` (array).
5135///
5136/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
5137/// stderr (C uses xtrerr; zshrs uses eprint!).
5138///
5139/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
5140/// into the file-static `STTYval` for `execute()` to apply pre-exec.
5141fn addvars(state: &mut estate, pc: usize, addflags: i32) {
5142    // c:2501 — locals.
5143    let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
5144    let xtr: bool; // c:2502 `int xtr,`
5145    let mut isstr: bool; // c:2502 `int isstr,`
5146    let mut htok: i32 = 0; // c:2502 `int htok = 0;`
5147    let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
5148    let mut name: String;
5149    let mut flags: i32; // c:2504 `int flags;`
5150    let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
5151    let mut ac: wordcode; // c:2507 `wordcode ac;`
5152                          // c:2508 `local_list1(svl);` — stack-local one-element LinkList
5153                          // for the scalar-assignment path. Rust uses a fresh LinkList per
5154                          // iteration; equivalent semantics.
5155
5156    // c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
5157    // when the assignment list is implicitly local (ADDVAR_RESTORE).
5158    flags = if (addflags & ADDVAR_RESTORE) == 0 {
5159        ASSPM_WARN // c:2516
5160    } else {
5161        0 // c:2516
5162    };
5163    xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
5164    if xtr {
5165        // c:2518
5166        printprompt4(); // c:2519
5167        doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
5168    }
5169    state.pc = pc; // c:2522 `state->pc = pc;`
5170
5171    // c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
5172    loop {
5173        if state.pc >= state.prog.prog.len() {
5174            break;
5175        }
5176        ac = state.prog.prog[state.pc];
5177        state.pc += 1;
5178        if wc_code(ac) != WC_ASSIGN {
5179            // Step back so the WC_SIMPLE / outer dispatcher sees the
5180            // non-assignment opcode. C's `state->pc++` post-increment
5181            // already pointed past WC_ASSIGN; we need to unconsume.
5182            state.pc -= 1;
5183            break;
5184        }
5185        let mut myflags = flags; // c:2524 `int myflags = flags;`
5186        name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
5187        if htok != 0 {
5188            // c:2526 `if (htok) untokenize(name);`
5189            name = untokenize(&name).to_string(); // c:2527
5190        }
5191        if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5192            // c:2528
5193            myflags |= ASSPM_AUGMENT; // c:2529
5194        }
5195        if xtr {
5196            // c:2530
5197            // c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
5198            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5199                eprint!("{}+=", name); // c:2532
5200            } else {
5201                eprint!("{}=", name); // c:2532
5202            }
5203        }
5204
5205        // c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
5206        isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
5207        if isstr {
5208            // c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
5209            let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
5210            vl = LinkList::new();
5211            vl.push_back(svl_val);
5212            // c:2535 `vl = &svl;` — vl already points at the new list.
5213        } else {
5214            // c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
5215            let items = ecgetlist(
5216                state,
5217                WC_ASSIGN_NUM(ac) as usize,
5218                EC_DUPTOK,
5219                Some(&mut htok),
5220            );
5221            vl = LinkList::new();
5222            for it in items {
5223                vl.push_back(it);
5224            }
5225            if errflag.load(Ordering::Relaxed) != 0 {
5226                // c:2538-2541
5227                state.pc = opc; // c:2539
5228                return; // c:2540
5229            }
5230        }
5231
5232        // c:2544 `if (vl && htok) {`
5233        if htok != 0 {
5234            // c:2545 `int prefork_ret = 0;`
5235            let mut prefork_ret: i32 = 0;
5236            // c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
5237            //                          : PREFORK_ASSIGN), &prefork_ret);
5238            let pf_flags = if isstr {
5239                PREFORK_SINGLE | PREFORK_ASSIGN
5240            } else {
5241                PREFORK_ASSIGN
5242            };
5243            prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
5244            if errflag.load(Ordering::Relaxed) != 0 {
5245                // c:2548
5246                state.pc = opc; // c:2549
5247                return; // c:2550
5248            }
5249            if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
5250                // c:2552
5251                myflags |= ASSPM_KEY_VALUE; // c:2553
5252            }
5253            // c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
5254            //                  haswilds((char *)getdata(firstnode(vl)))))`
5255            let needs_glob = if !isstr {
5256                true
5257            } else {
5258                isset(GLOBASSIGN)
5259                    && isstr
5260                    && !vl.is_empty()
5261                    && haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
5262            };
5263            if needs_glob {
5264                globlist(&mut vl, prefork_ret); // c:2556
5265                                                // c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
5266                                                //                  unsetparam(name);`
5267                if isset(GLOBASSIGN) && isstr {
5268                    unsetparam(&name); // c:2562
5269                }
5270                if errflag.load(Ordering::Relaxed) != 0 {
5271                    // c:2563
5272                    state.pc = opc; // c:2564
5273                    return; // c:2565
5274                }
5275            }
5276        }
5277        // c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
5278        // — scalar-assignment path: zero or one element after prefork.
5279        if isstr && (vl.is_empty() || vl.len() == 1) {
5280            let val: String; // c:2571 `char *val;`
5281            if vl.is_empty() {
5282                // c:2574
5283                val = String::new(); // c:2575 `val = ztrdup("");`
5284            } else {
5285                // c:2577 `untokenize(peekfirst(vl));`
5286                let peek = vl.nodes.front().cloned().unwrap_or_default();
5287                val = untokenize(&peek).to_string(); // c:2577-2578
5288                                                     // c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
5289                                                     // we just cloned the front above. Equivalent.
5290            }
5291            if xtr {
5292                // c:2580
5293                eprint!("{}", quotedzputs(&val)); // c:2581
5294                eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
5295            }
5296            // c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
5297            let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
5298                // c:2585 `if (strcmp(name, "STTY") == 0)`
5299                if name == "STTY" {
5300                    // c:2586-2587 — `STTYval = ztrdup(val);`
5301                    let mut stty = STTYval.lock().unwrap();
5302                    *stty = Some(val.clone()); // c:2587
5303                }
5304                // c:2589 `allexp = opts[ALLEXPORT];`
5305                let allexp = isset(ALLEXPORT);
5306                // c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
5307                opt_state_set("allexport", true);
5308                if isset(KSHARRAYS) {
5309                    // c:2591
5310                    unsetparam(&name); // c:2592
5311                }
5312                let pm = assignsparam(&name, &val, myflags); // c:2593
5313                                                             // c:2594 `opts[ALLEXPORT] = allexp;` — restore.
5314                opt_state_set("allexport", allexp);
5315                pm
5316            } else {
5317                // c:2595
5318                assignsparam(&name, &val, myflags) // c:2596
5319            };
5320            if pm.is_none() {
5321                // c:2597 `if (!pm)`
5322                LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
5323                                                     // c:2599-2604 — "cheating" comment: don't zerr.
5324                if cmdoutval.load(Ordering::Relaxed) == 0 {
5325                    // c:2605 `if (!cmdoutval)`
5326                    cmdoutval.store(1, Ordering::Relaxed); // c:2606
5327                }
5328            }
5329            if errflag.load(Ordering::Relaxed) != 0 {
5330                // c:2608
5331                state.pc = opc; // c:2609
5332                return; // c:2610
5333            }
5334            continue; // c:2612
5335        }
5336        // c:2614 `if (vl) { ... }` — array-assignment path: drain vl
5337        // into a fresh `char **arr`.
5338        // c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
5339        arr = Vec::with_capacity(vl.len() + 1);
5340        while let Some(s) = vl.pop_front() {
5341            arr.push(s);
5342        }
5343        // c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
5344        if xtr {
5345            // c:2624
5346            eprint!("( "); // c:2625
5347            for s in &arr {
5348                // c:2626 `for (ptr = arr; *ptr; ptr++)`
5349                eprint!("{}", quotedzputs(s)); // c:2627
5350                eprint!(" "); // c:2628
5351            }
5352            eprint!(") "); // c:2630
5353        }
5354        // c:2632 `if (!assignaparam(name, arr, myflags))`
5355        if assignaparam(&name, arr, myflags).is_none() {
5356            LASTVAL.store(1, Ordering::Relaxed); // c:2633
5357                                                 // c:2634-2638 — "cheating" comment.
5358            if cmdoutval.load(Ordering::Relaxed) == 0 {
5359                // c:2639
5360                cmdoutval.store(1, Ordering::Relaxed); // c:2640
5361            }
5362        }
5363        if errflag.load(Ordering::Relaxed) != 0 {
5364            // c:2642
5365            state.pc = opc; // c:2643
5366            return; // c:2644
5367        }
5368    }
5369    state.pc = opc; // c:2647 `state->pc = opc;`
5370}
5371
5372// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
5373// match expression at the call sites in execsimple. Not a separate
5374// Rust fn — every C-side reference to
5375// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
5376
5377// --- exec.c entries ---------------------------------------------------
5378
5379/// Port of `execcursh(Estate state, int do_exec)` from
5380/// `Src/exec.c:469-498`. Execute a `{ ... }` current-shell command
5381/// group: skip the trailing try-only word, optionally drop a stale
5382/// job slot, then run the inner list.
5383pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
5384    // c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
5385    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5386    let end = state.pc + WC_CURSH_SKIP(prior) as usize;
5387    // c:475 — `state->pc++;` skip the try/always-only word.
5388    state.pc += 1;
5389    // c:482-486 — drop empty job slot before nested cmd: if outer-pipe
5390    // bookkeeping is clean AND thisjob is a real job that's not the
5391    // pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
5392    // leaking job-table slots when execcursh recurses.
5393    {
5394        let lp = list_pipe.load(Ordering::Relaxed);
5395        let lpj = list_pipe_job.load(Ordering::Relaxed);
5396        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5397        if lp == 0 && tj != -1 && tj != lpj {
5398            if let Some(jt) = JOBTAB.get() {
5399                let mut guard = jt.lock().unwrap();
5400                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5401                if !has {
5402                    if let Some(j) = guard.get_mut(tj as usize) {
5403                        crate::ported::jobs::deletejob(j, false);
5404                    }
5405                }
5406            }
5407        }
5408    }
5409    cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
5410    let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
5411    cmdpop(); // c:489 — `cmdpop();`
5412    state.pc = end; // c:491 — `state->pc = end;`
5413    this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
5414    LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
5415}
5416
5417// `(...)` subshell — no dedicated C function (handled inline by
5418// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
5419// In zshrs the subshell branch is folded into `execpline` and
5420// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
5421// inner-list walk since fusevm bytecode handles the forking via
5422// Op::Subshell at a higher layer.
5423
5424/// Port of `execcond(Estate state, UNUSED(int do_exec))` from
5425/// `Src/exec.c:5204-5232`. Run a `[[ ... ]]` cond expression.
5426pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
5427    state.pc -= 1; // c:5208 — `state->pc--;`
5428                   // c:5209-5213 — XTRACE prelude.
5429    if isset(XTRACE) {
5430        printprompt4();
5431        eprint!("[[");
5432        // c:5212 — `tracingcond++;` not modeled in zshrs.
5433    }
5434    cmdpush(CS_COND as u8); // c:5214
5435                            // c:5215 — `stat = evalcond(state, NULL);` — TODO faithful: needs
5436                            // the wordcode-level evalcond from Src/cond.c which is distinct
5437                            // from the test-builtin evalcond ported in cond.rs. Pending.
5438    let stat: i32 = 0;
5439    // c:5219-5221 — `if (stat == 2) errflag |= ERRFLAG_ERROR;`
5440    if stat == 2 {
5441        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
5442    }
5443    cmdpop(); // c:5222
5444    if isset(XTRACE) {
5445        eprintln!(" ]]");
5446    }
5447    stat // c:5230 — `return stat;`
5448}
5449
5450/// Port of `execarith(Estate state, UNUSED(int do_exec))` from
5451/// `Src/exec.c:5237-5275`. Run a `(( ... ))` arithmetic command;
5452/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
5453/// parse error.
5454pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
5455    if isset(XTRACE) {
5456        printprompt4();
5457        eprint!("((");
5458    }
5459    cmdpush(CS_MATH as u8); // c:5247
5460    let mut htok: i32 = 0;
5461    let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
5462    if htok != 0 {
5463        e = singsub(&e); // c:5250 — `singsub(&e);`
5464    }
5465    if isset(XTRACE) {
5466        eprint!(" {}", e);
5467    }
5468    let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
5469    cmdpop(); // c:5256
5470    if isset(XTRACE) {
5471        eprintln!(" ))");
5472    }
5473    // c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
5474    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
5475        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
5476        return 2;
5477    }
5478    // c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
5479    let val = val_result.unwrap();
5480    if val.type_ == MN_INTEGER {
5481        if val.l == 0 {
5482            1
5483        } else {
5484            0
5485        }
5486    } else if val.d == 0.0 {
5487        1
5488    } else {
5489        0
5490    }
5491}
5492
5493/// Port of `exectime(Estate state, UNUSED(int do_exec))` from
5494/// `Src/exec.c:5279-5294`. Run `time pipeline`: drives execpline with
5495/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
5496pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
5497    let jb = *THISJOB
5498        .get_or_init(|| std::sync::Mutex::new(-1))
5499        .lock()
5500        .unwrap(); // c:5283
5501    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5502    // c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
5503    if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
5504        // c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
5505        // shell+kids time deltas since last call.
5506        crate::ported::jobs::shelltime(None, None, None, 0);
5507        return 0; // c:5286
5508    }
5509    // c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
5510    let slcode = state.prog.prog[state.pc];
5511    state.pc += 1;
5512    use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
5513    let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
5514    *THISJOB
5515        .get_or_init(|| std::sync::Mutex::new(-1))
5516        .lock()
5517        .unwrap() = jb; // c:5289
5518    LASTVAL.load(Ordering::Relaxed) // c:5290
5519}
5520
5521/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
5522/// Promoted to top-level pub fn so execcmd_exec at the shfunc
5523/// dispatch site (c:4102-4105) can route through it. The real port
5524/// owns queue_signals + cmdstack + sfcontext setup before calling
5525/// doshfunc; doshfunc itself is unported, so we route the body
5526/// through `runshfunc` (exec.rs:1700), which carries the
5527/// wrapper-chain + zunderscore restore. Degraded vs C (no cmdstack
5528/// push, no sfcontext flip, no XTRACE arg-trace) but the function
5529/// body executes and `lastval` is updated.
5530pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
5531    // c:5546-5547 — `if (errflag) return;`
5532    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
5533        return;
5534    }
5535    // c:5550-5557 — drop empty job slot before nested shfunc invoke:
5536    // if outer-pipe bookkeeping is clean AND thisjob is a real job
5537    // that's not the pipe-leader AND has no procs yet, deletejob()
5538    // recycles it. Avoids leaking job-table slots across recursive
5539    // function calls. Same pattern as execcursh's c:482-486.
5540    {
5541        let lp = list_pipe.load(Ordering::Relaxed);
5542        let lpj = list_pipe_job.load(Ordering::Relaxed);
5543        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5544        if lp == 0 && tj != -1 && tj != lpj {
5545            if let Some(jt) = JOBTAB.get() {
5546                let mut guard = jt.lock().unwrap();
5547                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5548                if !has {
5549                    // c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
5550                    //                jobtab[thisjob].filelist = NULL;` — preserve
5551                    //                the filelist so deletejob doesn't unlink temp
5552                    //                files. Rust take()s the Vec into a local.
5553                    let _last_file_list: Vec<jobfile> = if let Some(j) = guard.get_mut(tj as usize)
5554                    {
5555                        std::mem::take(&mut j.filelist)
5556                    } else {
5557                        Vec::new()
5558                    };
5559                    if let Some(j) = guard.get_mut(tj as usize) {
5560                        crate::ported::jobs::deletejob(j, false); // c:5556
5561                    }
5562                }
5563            }
5564        }
5565    }
5566    // c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
5567    // emit PS4 prefix + space-separated quoted args on the trace
5568    // stream so `set -x` shows the function invocation line.
5569    if isset(XTRACE) {
5570        printprompt4();
5571        for (i, a) in args.iter().enumerate() {
5572            if i > 0 {
5573                eprint!(" ");
5574            }
5575            eprint!("{}", quotedzputs(a));
5576        }
5577        eprintln!();
5578    }
5579    // c:5572-5578 cmdstack/sfcontext setup: omit (no cmdstack in
5580    // zshrs yet — replaced by tracing).
5581    // c:5580 — `doshfunc(shf, args, 0);` — doshfunc swaps PPARAMS
5582    // ($1, $2, …) to the function's args, runs the body via
5583    // runshfunc, then restores. doshfunc itself isn't ported yet
5584    // so we do the swap-and-restore inline here.
5585    // c:5580 — `doshfunc(shf, args, 0);`. The C path always has
5586    // `funcdef` populated since C parses at definition time. zshrs
5587    // compiles to fusevm chunks instead, so `funcdef` is None for
5588    // user-defined functions; only `body` (source string) carries
5589    // the definition. When that's the case, build a one-shot eprog
5590    // whose `strs` carries the source so runshfunc's script-pipeline
5591    // arm (execute_script_zsh_pipeline) executes the body.
5592    let prog_owned: Option<eprog> = if shf.funcdef.is_some() {
5593        None
5594    } else if let Some(ref body) = shf.body {
5595        Some(eprog {
5596            strs: Some(body.clone()),
5597            ..Default::default()
5598        })
5599    } else {
5600        None
5601    };
5602    let prog_ref: Option<&eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
5603        (Some(p), _) => Some(p),
5604        (_, Some(p)) => Some(p),
5605        _ => None,
5606    };
5607    if let Some(_prog) = prog_ref {
5608        // c:5580 — `doshfunc(shf, args, 0);`. Direct doshfunc call —
5609        // noreturnval=0 means the body's return value updates LASTVAL
5610        // (caller of execfuncdef reads it back). PPARAMS swap +
5611        // restore happens INSIDE doshfunc's scope; body_runner just
5612        // runs the body.
5613        let name_for_body = shf.node.nam.clone();
5614        let body_args_owned: Vec<String> = if args.len() > 1 {
5615            args[1..].to_vec()
5616        } else {
5617            Vec::new()
5618        };
5619        let body_runner = move || -> i32 {
5620            crate::ported::exec::run_function_body(&name_for_body, &body_args_owned).unwrap_or(0)
5621        };
5622        let _ = doshfunc(shf, args.clone(), false, body_runner);
5623    }
5624    // c:5582-5589 cmdstack restore/free: omit (no cmdstack).
5625}
5626
5627/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
5628/// from `Src/exec.c:5823-6158`.
5629///
5630/// C body's scope-management sequence ported here. The C source's
5631/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
5632/// is replaced by `body_runner` — zshrs runs function bodies through
5633/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
5634/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
5635/// callback hands the live executor back to the caller (typically
5636/// the fusevm bridge) for the actual body run. Every line of scope
5637/// save/restore around the body call mirrors C exactly.
5638///
5639/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
5640/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
5641/// c:6042; zshrs delegates to a closure because the body-execution
5642/// pipeline (fusevm) differs from C's (wordcode). The closure
5643/// fully replaces the runshfunc call and returns the body's exit
5644/// status (which doshfunc reads as `lastval` for the `noreturnval`
5645/// path).
5646#[allow(non_snake_case)]
5647/// Port of the module-global `oflags` in `Src/exec.c` (set at c:5970 via
5648/// `funcsave->oflags = oflags;`). Holds the attribute flags of the function
5649/// CURRENTLY executing, so a nested call can tell whether its caller carried
5650/// PM_TAGGED_LOCAL — that is what makes `functions -T` non-recursive.
5651/// zshrs-original storage shape (atomic rather than a plain global); the
5652/// save/restore discipline matches C's funcsave. Bug #1058.
5653pub static FUNC_OFLAGS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
5654
5655pub fn doshfunc(
5656    shfunc: &mut shfunc,                  // c:5823
5657    doshargs: Vec<String>,                // c:5823
5658    noreturnval: bool,                    // c:5823
5659    mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
5660) -> i32 {
5661    use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
5662    use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
5663    use crate::ported::modules::parameter::FUNCSTACK;
5664    use crate::ported::params::endparamscope;
5665    use crate::ported::params::locallevel as locallevel_atomic;
5666    use crate::ported::zsh_h::{
5667        FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_TAGGED, PM_TAGGED_LOCAL, PM_UNDEFINED,
5668    };
5669    use std::sync::atomic::Ordering;
5670
5671    let name = shfunc.node.nam.clone(); // c:5827
5672    let flags = shfunc.node.flags; // c:5828
5673    let fname = dupstring(&name); // c:5829
5674    let _ = fname; // c:5829 (kept for parity)
5675
5676    // c:6000-6004 — FUNCNEST guard. C:
5677    //   if (zsh_funcnest >= 0 && funcstack && funcstacksz >= zsh_funcnest) {
5678    //       zerr("maximum nested function level reached; increase FUNCNEST?");
5679    //       goto undoshfunc;
5680    //   }
5681    // This was previously stubbed out on the assumption that the zshrs
5682    // fusevm "doesn't recurse via real stack frames" — but it DOES:
5683    // dispatch_function_call -> doshfunc -> dispatch_function_call -> …
5684    // nests real native frames, so an accidental infinite recursion
5685    // (e.g. a self-referential hook, a wrapped function that re-invokes
5686    // itself, a zle widget cycle) overflowed the OS stack and
5687    // SEGFAULTED instead of producing zsh's graceful error. Check at
5688    // entry — before queue_signals / inc_locallevel / the funcsave
5689    // snapshot — so the early return needs no unwinding. FUNCNEST < 0
5690    // means "unlimited" (zsh's `zsh_funcnest >= 0` gate). Depth is the
5691    // count of already-active frames on FUNCSTACK; the 501st nested
5692    // call (depth == 500) trips it, matching C's `>=` on the default
5693    // FUNCNEST=500.
5694    let funcnest = crate::ported::params::getiparam("FUNCNEST");
5695    if funcnest >= 0 {
5696        // Count only real function frames (FS_FUNC) — the raw FUNCSTACK
5697        // Vec also carries FS_EVAL/FS_SOURCE/anon frames, which inflate
5698        // the depth several× and would falsely trip on legitimately deep
5699        // (but finite) recursion. FS_FUNC-only matches zsh's nesting
5700        // depth (`${#funcstack}`) and its funcnest accounting.
5701        let depth = crate::ported::modules::parameter::FUNCSTACK
5702            .lock()
5703            .map(|s| s.iter().filter(|f| f.tp == FS_FUNC).count())
5704            .unwrap_or(0) as i64;
5705        if depth >= funcnest {
5706            zerr("maximum nested function level reached; increase FUNCNEST?");
5707            crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
5708            return 1;
5709        }
5710    }
5711
5712    // c:5835 — `queue_signals();` Lots of memory + global-state changes.
5713    queue_signals();
5714
5715    // c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
5716    // Pinned so a recursive unload doesn't free the eprog under us.
5717    // (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
5718    // already pin until call ends. C does explicit refcount on
5719    // `funcdef->nref` via useeprog.)
5720
5721    // c:5856-5916 — Funcsave allocation + per-field snapshot.
5722    let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
5723    let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
5724    let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
5725    let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
5726    let funcsave_numpipestats = {
5727        // c:5864
5728        NUMPIPESTATS
5729            .get_or_init(|| std::sync::Mutex::new(0))
5730            .lock()
5731            .map(|n| *n)
5732            .unwrap_or(0)
5733    };
5734    let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
5735                                                                // c:5866-5867 — trap_state PRIMED branch decrements trap_return.
5736    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
5737        // c:5866
5738        TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
5739    }
5740    // c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
5741    // return-suppress so a `return` inside the body fires errexit
5742    // checks normally.
5743    noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
5744
5745    // c:5872-5880 — noreturnval branch: deep-copy pipestats so the
5746    // function body's pipestats writes are restored on exit.
5747    let funcsave_pipestats: Option<Vec<i32>> = if noreturnval {
5748        // c:5872
5749        let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
5750        p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
5751    } else {
5752        None
5753    };
5754
5755    // c:5882-5896 — TRAPEXIT special case (deep-copy shfunc so
5756    // starttrapscope doesn't rug-pull). zshrs doesn't yet support
5757    // running TRAPEXIT directly via doshfunc; flagged for follow-up.
5758    // (Skip: name = "TRAPEXIT" path.)
5759    let _ = name.as_str(); // sentinel for the eventual port.
5760
5761    // c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
5762    // tags SIGEXIT for deferred restoration at scope end.
5763    crate::ported::signals::starttrapscope();
5764    // c:5899 — `startpatternscope();`
5765    crate::ported::pattern::startpatternscope();
5766
5767    // c:5901 — `pptab = pparams;` — save outer positional params.
5768    let pptab: Vec<String> = crate::ported::builtin::PPARAMS
5769        .lock()
5770        .map(|p| p.clone())
5771        .unwrap_or_default();
5772
5773    // c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
5774    let funcsave_scriptname = crate::ported::utils::scriptname_get();
5775    if (flags as u32 & PM_UNDEFINED) == 0 {
5776        // c:5902
5777        crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
5778    }
5779
5780    // c:5904-5908 — `funcsave->zoptind = zoptind; ...` snapshot.
5781    // C zsh saves zoptind (the canonical OPTIND counter) and
5782    // zoptarg into the funcsave struct so OPTIND is implicitly
5783    // function-local: a `getopts` loop inside the function gets
5784    // its own counter that snaps back to the caller's on
5785    // function return. zshrs stores OPTIND/OPTARG in paramtab
5786    // as regular int/string params; snapshot them here and
5787    // restore at scope end. Bug #513.
5788    let funcsave_optind: Option<String> = crate::ported::params::getsparam("OPTIND");
5789    let funcsave_optarg: Option<String> = crate::ported::params::getsparam("OPTARG");
5790    // c:5966-5969 — `if (!isset(POSIXBUILTINS)) { zoptind = 1; optcind = 0; }`.
5791    // The snapshot above is only half the contract: C also RESETS the
5792    // counter on entry so an inner `getopts` loop starts fresh at the
5793    // first positional, independent of the caller's OPTIND. Without
5794    // this, a function whose body runs `getopts` after the caller
5795    // advanced OPTIND mis-parses its own args — e.g. `add-zsh-hook`
5796    // (which `getopts`-parses `precmd func`) printed its usage and
5797    // failed when invoked from a config that had run getopts earlier.
5798    // zshrs keeps the counter in the $OPTIND param plus the ZOPTIND/
5799    // OPTCIND trackers getopts syncs against; reset all three.
5800    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
5801        crate::ported::params::setiparam("OPTIND", 1); // c:5967 zoptind = 1
5802        crate::ported::builtin::ZOPTIND.store(1, Ordering::Relaxed);
5803        crate::ported::builtin::OPTCIND.store(0, Ordering::Relaxed); // c:5968 optcind = 0
5804    }
5805
5806    // c:5914 — `memcpy(funcsave->opts, opts, sizeof(opts));` — option
5807    // snapshot. Port wraps opts in OPTS_LIVE; capture the live state
5808    // here as a HashMap snapshot.
5809    let funcsave_opts = crate::ported::options::opt_state_snapshot();
5810
5811    // c:5915-5916 — `funcsave->emulation/sticky = emulation/sticky;`
5812    // Emulation snapshot pending the sticky-emulation port.
5813
5814    // c:5954-5960 — PM_TAGGED / PM_TAGGED_LOCAL turn XTRACE on for the
5815    // duration of the call:
5816    //     if (flags & (PM_TAGGED|PM_TAGGED_LOCAL))
5817    //         opts[XTRACE] = 1;
5818    //     else if (oflags & PM_TAGGED_LOCAL) {
5819    //         if (shfunc->node.nam == ANONYMOUS_FUNCTION_NAME)
5820    //             flags |= PM_TAGGED_LOCAL;
5821    //         else
5822    //             opts[XTRACE] = 0;
5823    //     }
5824    // This is what makes `functions -t f` / `-T f` trace a SINGLE function
5825    // without `setopt xtrace` globally. It was previously skipped, so both
5826    // flags parsed and stored correctly (builtin.rs c:3358-3365) but nothing
5827    // ever consulted them and the traced function ran silently.
5828    //
5829    // `-T` is the non-recursive form: a function called FROM a `-T` function
5830    // gets XTRACE turned back OFF unless it is itself tagged. C tracks the
5831    // caller's flags in the module-global `oflags`, saved per call at c:5970
5832    // (`funcsave->oflags = oflags;`) and restored on return — mirrored here by
5833    // FUNC_OFLAGS. Anonymous functions are the documented exception: they
5834    // INHERIT the local tag instead of clearing it, so `-T` tracing spans the
5835    // `(anon)` bodies inside the traced function.
5836    //
5837    // No restore of XTRACE is needed here: it is already in the
5838    // always-restored subset at the exit block below (c:6097-6101).
5839    // Bug #1058.
5840    let mut fn_flags = shfunc.node.flags as u32;
5841    let oflags_prev = FUNC_OFLAGS.load(Ordering::Relaxed);
5842    if (fn_flags & (PM_TAGGED | PM_TAGGED_LOCAL)) != 0 {
5843        opt_state_set("xtrace", true); // c:5955
5844    } else if (oflags_prev & PM_TAGGED_LOCAL) != 0 {
5845        // c:5956
5846        if shfunc.node.nam == ANONYMOUS_FUNCTION_NAME {
5847            fn_flags |= PM_TAGGED_LOCAL; // c:5958
5848        } else {
5849            opt_state_set("xtrace", false); // c:5960
5850        }
5851    }
5852    FUNC_OFLAGS.store(fn_flags, Ordering::Relaxed); // c:5970
5853
5854    // c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
5855    // for inner commands; outer flag restored on exit.
5856    opt_state_set("printexitvalue", false);
5857
5858    // c:5978-5998 — pparams swap. C reads doshargs and constructs the
5859    // function's positional-param array. First arg is the function
5860    // name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
5861    let funcsave_argv0: Option<String> = if !doshargs.is_empty() {
5862        // c:5978
5863        // c:5982-5985 — `pparams = x = zshcalloc(...)`.
5864        let positionals: Vec<String> = if doshargs.len() > 1 {
5865            doshargs[1..].to_vec()
5866        } else {
5867            Vec::new()
5868        };
5869        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5870            *pp = positionals;
5871        }
5872        // c:5984-5987 — FUNCTIONARGZERO: save argzero, install
5873        // doshargs[0] (the function name).
5874        if isset(FUNCTIONARGZERO) {
5875            // c:5984
5876            let prev = crate::ported::utils::argzero();
5877            crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
5878            prev
5879        } else {
5880            None
5881        }
5882    } else {
5883        // c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
5884        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5885            *pp = Vec::new();
5886        }
5887        if isset(FUNCTIONARGZERO) {
5888            // c:5994
5889            let prev = crate::ported::utils::argzero();
5890            crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
5891            prev
5892        } else {
5893            None
5894        }
5895    };
5896
5897    // c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
5898    // since zshrs tracks function-call depth there.
5899    //
5900    // Plus the canonical startparamscope (c:6194 inside runshfunc).
5901    // zshrs's body_runner replaces runshfunc's `execode` call so the
5902    // startparamscope/endparamscope pair must wrap body_runner here,
5903    // not inside the closure. inc_locallevel is exactly startparamscope.
5904    inc_locallevel();
5905
5906    // c:6000-6004 — FUNCNEST check + `goto undoshfunc` on overflow.
5907    // Skip the runtime check (the zshrs fusevm doesn't recurse via
5908    // real stack frames so the depth limit is less critical), but
5909    // keep the comment so the C label `undoshfunc:` target is
5910    // visible — `goto undoshfunc;` here would jump straight to the
5911    // epilogue at the `undoshfunc:` label below.
5912
5913    // c:6005-6019 — funcstack frame push. The full C block:
5914    //   funcsave->fstack.name      = dupstring(name);
5915    //   funcsave->fstack.caller    = funcstack ? funcstack->name :
5916    //                                 dupstring(argv0 ? argv0 : argzero);
5917    //   funcsave->fstack.lineno    = lineno;
5918    //   funcsave->fstack.prev      = funcstack;
5919    //   funcsave->fstack.tp        = FS_FUNC;
5920    //   funcstack                  = &funcsave->fstack;
5921    //   funcsave->fstack.flineno   = shfunc->lineno;
5922    //   funcsave->fstack.filename  = getshfuncfile(shfunc);
5923    // c:6013 — `funcsave->fstack.lineno = lineno;` C has ONE lineno
5924    // global (params.c:123); zshrs mirrors it in both input::lineno
5925    // and lex::LEX_LINENO. The lex mirror is the one driven by
5926    // BUILTIN_SET_LINENO per statement AND zeroed for the duration
5927    // of a function body (see set_lineno(0) below), so it is the
5928    // one that matches C's value at call time: a call made INSIDE a
5929    // caller's body records the caller-relative line (0 for a
5930    // single-line fn), giving `$functrace` entries like `g:0`.
5931    // input::lineno stayed parked at the script-wide line and
5932    // produced `g:1`.
5933    let lineno_now = crate::ported::lex::lineno() as i64;
5934    let (caller, prev_tp): (Option<String>, Option<i32>) = {
5935        let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5936        if let Some(p) = stk.last() {
5937            (Some(p.name.clone()), Some(p.tp))
5938        } else {
5939            // c:6011-6012 — outermost: argv0 (saved) or argzero global.
5940            let z = funcsave_argv0
5941                .clone()
5942                .or_else(crate::ported::utils::argzero);
5943            (z, None)
5944        }
5945    };
5946    // c:6018-6019 — flineno: shfunc->lineno (function def line)
5947    let flineno = shfunc.lineno;
5948    let filename = shfunc.filename.clone().or_else(|| Some(String::new()));
5949    {
5950        let frame = crate::ported::zsh_h::funcstack {
5951            prev: None,             // c:6014 (Vec-stack: index encodes link)
5952            name: dupstring(&name), // c:6005
5953            filename,               // c:6019
5954            caller,                 // c:6011
5955            flineno,                // c:6018
5956            lineno: lineno_now,     // c:6013
5957            tp: FS_FUNC,            // c:6015
5958        };
5959        let _ = prev_tp; // c:6011 (informational)
5960        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5961        stack.push(frame); // c:6016 funcstack = &funcsave->fstack
5962    }
5963
5964    // c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
5965    // zshrs delegates to the body_runner closure (typically a fusevm
5966    // sub-VM run from the bridge). The closure returns the body's
5967    // exit status which becomes lastval.
5968    //
5969    // c:Src/exec.c:1251-1266 — push "shfunc" onto zsh_eval_context
5970    // so the body sees `${zsh_eval_context[*]}` containing the call
5971    // chain context. The execode-based path (c:1245-1282 port at
5972    // exec.rs:7092) already did this, but the fusevm body_runner
5973    // path skipped doshfunc's body_runner invocation without the
5974    // push. Bug #262 in docs/BUGS.md.
5975    //
5976    // Push BOTH the static `zsh_eval_context` (matches C's variable)
5977    // AND the paramtab array entry (what `${zsh_eval_context[*]}`
5978    // reads). Pop on every return path via the guard struct so
5979    // panics / early returns don't leak the entry. Inlined here
5980    // (sole caller) — `zsh_eval_context` is this module's own static.
5981    //
5982    // c:Src/exec.c:1251-1266 — `zsh_eval_context[*]` shell-visible
5983    // mirror: the array entry holds the stack; the scalar
5984    // `ZSH_EVAL_CONTEXT` holds the `:`-joined form. Both written via
5985    // the PM_READONLY bypass (`u_arr`/`u_str` direct), the same shape
5986    // the binary's `-c` ZSH_EVAL_CONTEXT init uses (bins/zshrs.rs).
5987    // A reusable `sync` closure is captured by the guard's Drop so
5988    // the same write happens after the push (here) and the pop (on
5989    // every return path / panic).
5990    let sync_eval_ctx = |stack: &[String]| {
5991        let joined = stack.join(":");
5992        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
5993            if let Some(pm) = tab.get_mut("zsh_eval_context") {
5994                pm.u_arr = Some(stack.to_vec());
5995                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
5996            }
5997            if let Some(pm) = tab.get_mut("ZSH_EVAL_CONTEXT") {
5998                pm.u_str = Some(joined);
5999                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
6000            }
6001        }
6002    };
6003    if let Ok(mut ctx) = zsh_eval_context.lock() {
6004        ctx.push("shfunc".to_string());
6005        sync_eval_ctx(&ctx);
6006    }
6007    struct EvalContextGuard<F: Fn(&[String])>(F);
6008    impl<F: Fn(&[String])> Drop for EvalContextGuard<F> {
6009        fn drop(&mut self) {
6010            if let Ok(mut ctx) = zsh_eval_context.lock() {
6011                ctx.pop();
6012                (self.0)(&ctx);
6013            }
6014        }
6015    }
6016    let _eval_ctx_guard = EvalContextGuard(sync_eval_ctx);
6017    // c:Src/exec.c — function bodies execute with `lineno` reset to
6018    // the relative line within the body (incremented per WC_PIPE
6019    // from the wordcode-encoded lineno). zsh's zerrmsg
6020    // (Src/utils.c:301) emits the lineno prefix only when lineno
6021    // is non-zero AND (!SHINSTDIN || locallevel != 0). For an
6022    // inline single-line function like `f() { x=1 }`, the body's
6023    // WC_PIPE encodes lineno=1, exec sets `lineno = lineno - 1 =
6024    // 0`, and the zerrmsg path falls through to space-only ("f: ").
6025    //
6026    // zshrs's compiler doesn't thread WC_PIPE_LINENO into the
6027    // bytecode, so the global lineno stays at the script-wide
6028    // value (1 for inline `-c`). Suppress the line-number prefix
6029    // inside function bodies by saving lineno on entry and forcing
6030    // it to 0 during body execution; restore on exit. This makes
6031    // warnings inside functions emit `f: ...` matching zsh's
6032    // single-line-function format. Bug #54/#74/#86 in docs/BUGS.md.
6033    let saved_lineno = crate::ported::lex::lineno();
6034    crate::ported::lex::set_lineno(0);
6035    // c:Src/exec.c:6173-6175 + c:6196-6198 — `runshfunc` saves
6036    // zunderscore before the body runs and restores it after, so
6037    // `$_` reads outside the function continue to reflect the
6038    // function CALL's last arg (set by setunderscore at c:3491
6039    // before doshfunc enters). Without this, commands inside the
6040    // body (`:`, `echo`, etc.) update `$_` to their own last arg,
6041    // and the post-call `echo "[$_]"` sees the body's residue
6042    // instead of the call's arg. Bug surfaced via
6043    // test_dollar_underscore_after_function_call.
6044    let saved_zunderscore = crate::ported::params::getsparam("_").unwrap_or_default();
6045    // c:6042 — `runshfunc(prog, wrappers, name)`: the FuncWrap chain.
6046    // zsh/param/private installs `wrap_private` via addwrapper at
6047    // module boot (param_private.c:712); C's runshfunc invokes it
6048    // around every function body so `private_wraplevel` tracks the
6049    // callee's locallevel and outer-scope privates get
6050    // scopeprivate-hidden for the duration. The gate here is module
6051    // BOOT STATE (MOD_INIT_B — the bit `zmodload -e` reads and
6052    // load_module sets after do_boot_module, module.c:2317) —
6053    // exactly C's condition for the wrapper being in the chain: the
6054    // `private` dispatch runs require_module per the exec.c:2710
6055    // autofeature path, which boots the module. NOT is_loaded():
6056    // default-registered static modules carry MOD_LINKED from
6057    // startup, which would arm the wrapper before any `private`
6058    // use. The dispatch is a named special-case rather than a
6059    // WRAPPERS walk because the Rust wrap_private carries a
6060    // body-delegate closure (fusevm chunk runner) that can't live
6061    // in funcwrap's WrapFunc fn-pointer slot. The pwl < locallevel
6062    // pre-check mirrors wrap_private's own c:552 gate so the
6063    // closure is guaranteed to run when routed through it.
6064    let run_wrap_private = crate::ported::module::MODULESTAB
6065        .lock()
6066        .map(|t| {
6067            t.modules
6068                .get("zsh/param/private")
6069                .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
6070                .unwrap_or(false)
6071        })
6072        .unwrap_or(false)
6073        && crate::ported::modules::param_private::private_wraplevel.load(Ordering::Relaxed)
6074            < crate::ported::params::locallevel.load(Ordering::Relaxed);
6075    let body_status = if run_wrap_private {
6076        let mut st = 0;
6077        let _ = crate::ported::modules::param_private::wrap_private(
6078            std::ptr::null(),
6079            std::ptr::null(),
6080            std::ptr::null_mut(),
6081            || st = body_runner(), // c:556 runshfunc(prog, w, name)
6082        );
6083        st
6084    } else {
6085        body_runner()
6086    };
6087    crate::ported::params::set_zunderscore(std::slice::from_ref(&saved_zunderscore));
6088    crate::ported::lex::set_lineno(saved_lineno);
6089    LASTVAL.store(body_status, Ordering::Relaxed);
6090
6091    // c:6043 — `doneshfunc:` label. The C `runshfunc` happy-path
6092    // falls through here from c:6042.
6093    // c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
6094    {
6095        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
6096        stack.pop();
6097    }
6098    // c:6045 — `undoshfunc:` label. Reached either by fall-through
6099    // from c:6044 or by `goto undoshfunc;` from the FUNCNEST check
6100    // at c:6003. Tail epilogue follows.
6101
6102    // c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
6103    // runshfunc) lives at c:6157 below as `endparamscope()`. Removed
6104    // the dec here so locallevel only decrements once per
6105    // function-call frame; double-dec was purging level-0 globals on
6106    // function exit (the `f() { x=foo; }; f; echo $x` regression).
6107
6108    // c:6047-6053 — retflag clear. C clears retflag and restores
6109    // outer breaks if a `return` fired.
6110    if RETFLAG.load(Ordering::SeqCst) != 0 {
6111        // c:6047
6112        RETFLAG.store(0, Ordering::SeqCst); // c:6051
6113        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
6114    }
6115
6116    // c:6054-6058 — pparams + argv0 restore.
6117    if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
6118        *pp = pptab; // c:6059 pparams = pptab
6119    }
6120    if let Some(saved) = funcsave_argv0 {
6121        // c:6055
6122        crate::ported::utils::set_argzero(Some(saved)); // c:6057
6123    }
6124
6125    // c:Src/exec.c:6060-6062 — `zoptind = funcsave->zoptind;
6126    // zoptarg = funcsave->zoptarg;`. Restore OPTIND/OPTARG so
6127    // an inner getopts loop's counter mutations don't leak to
6128    // the caller. Bug #513.
6129    if let Some(saved) = funcsave_optind {
6130        if let Ok(n) = saved.parse::<i64>() {
6131            crate::ported::params::setiparam("OPTIND", n);
6132        } else {
6133            crate::ported::params::setsparam("OPTIND", &saved);
6134        }
6135    }
6136    if let Some(saved) = funcsave_optarg {
6137        crate::ported::params::setsparam("OPTARG", &saved);
6138    }
6139
6140    // c:6064 — `scriptname = funcsave->scriptname;`
6141    crate::ported::utils::set_scriptname(funcsave_scriptname);
6142
6143    // c:6067 — `endpatternscope();`
6144    crate::ported::pattern::endpatternscope();
6145
6146    // c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
6147    // localoptions was set inside the body.
6148    if crate::ported::options::opt_state_get("localoptions").unwrap_or(false) {
6149        // c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
6150        let current = crate::ported::options::opt_state_snapshot();
6151        for (k, _) in &current {
6152            if !funcsave_opts.contains_key(k) {
6153                crate::ported::options::opt_state_unset(k);
6154            }
6155        }
6156        for (k, v) in &funcsave_opts {
6157            opt_state_set(k, *v);
6158        }
6159    } else {
6160        // c:6097-6101 — non-LOCALOPTIONS: restore only the always-
6161        // restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
6162        // LOCALLOOPS / WARNNESTEDVAR).
6163        for opt in [
6164            "xtrace",
6165            "printexitvalue",
6166            "localoptions",
6167            "localloops",
6168            "warnnestedvar",
6169        ] {
6170            if let Some(v) = funcsave_opts.get(opt) {
6171                opt_state_set(opt, *v);
6172            }
6173        }
6174    }
6175
6176    // c:5970 counterpart — `oflags = funcsave->oflags;` restores the caller's
6177    // attribute set so a sibling call after this one sees the right `-T`
6178    // inheritance. Bug #1058.
6179    FUNC_OFLAGS.store(oflags_prev, Ordering::Relaxed);
6180
6181    // c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
6182    // breaks/contflag/loops snapshot. Skip the warn lines for now;
6183    // restore the bookkeeping.
6184    if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
6185        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
6186        CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
6187        LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
6188    }
6189
6190    // c:Src/exec.c:6195-6200 — C's runshfunc calls endparamscope()
6191    // BEFORE returning to doshfunc, which then calls endtrapscope()
6192    // at c:6114. So locallevel is ALREADY one less by the time
6193    // endtrapscope's pop loop compares saved local > current.
6194    //
6195    // Bug #80 in docs/BUGS.md: zshrs had endtrapscope FIRST (here at
6196    // line 5774), endparamscope LATER. That left locallevel at the
6197    // function's own level when endtrapscope ran, so saved entries
6198    // tagged with `local == current_function_level` failed the
6199    // `local > locallevel` pop condition. Nested EXIT traps
6200    // (saved at deeper level) never restored at the outer fn's
6201    // endtrapscope — outer EXIT traps fired at script exit instead.
6202    //
6203    // Decrement locallevel via a peer-of-endparamscope locallevel
6204    // bookkeeping call before endtrapscope, then leave the real
6205    // endparamscope at its current site below so the param scope
6206    // unwind still happens after the exit_pending check.
6207    {
6208        use crate::ported::params::locallevel as ll;
6209        let prev = ll.load(Ordering::Relaxed);
6210        if prev > 0 {
6211            ll.store(prev - 1, Ordering::Relaxed);
6212        }
6213        crate::ported::signals::endtrapscope();
6214        // Re-bump so the existing endparamscope() call below sees the
6215        // same pre-decrement state and its own internal decrement
6216        // lands at the right value (mirrors C's "endparamscope already
6217        // happened" comment at c:6135-6136 — the C order is endparam
6218        // (inside runshfunc) → endtrap (in doshfunc); we keep that
6219        // logical ordering for endtrapscope only, without disturbing
6220        // the rest of the epilogue's level math).
6221        ll.store(prev, Ordering::Relaxed);
6222    }
6223
6224    // c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
6225    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
6226        // c:6116
6227        TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
6228    }
6229
6230    // c:6118 — `ret = lastval;`
6231    let ret = LASTVAL.load(Ordering::Relaxed);
6232
6233    // c:6119 — `noerrexit = funcsave->noerrexit;`
6234    noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
6235
6236    // c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
6237    // the function for side-effects only; outer lastval/pipestats
6238    // should reflect the PRE-call state.
6239    if noreturnval {
6240        // c:6120
6241        LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
6242        if let Some(saved_ps) = funcsave_pipestats {
6243            let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
6244            if let Ok(mut nguard) = n.lock() {
6245                *nguard = funcsave_numpipestats; // c:6122
6246            }
6247            let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
6248            if let Ok(mut pguard) = p.lock() {
6249                for (i, v) in saved_ps.iter().enumerate() {
6250                    if i < pguard.len() {
6251                        pguard[i] = *v; // c:6123 memcpy
6252                    }
6253                }
6254            }
6255        }
6256    }
6257
6258    // c:Src/exec.c doshfunc → endparamscope — restore local-typeset
6259    // params installed during the body. In C, this is called inside
6260    // runshfunc (c:6200) BEFORE control returns to doshfunc's tail —
6261    // so by the time the exit_pending check runs at c:6141,
6262    // locallevel has ALREADY been decremented. The c:6135-6136
6263    // comment explicitly states "The endparamscope() has already
6264    // happened, hence the +1 here."
6265    //
6266    // The previous Rust ordering placed endparamscope AFTER the
6267    // exit_pending check, which compared exit_level against the
6268    // un-decremented locallevel. For `foo() { exit 7; }; foo`:
6269    //   exit_level=1, cur_locallevel=1 (pre-decrement)
6270    //   check: exit_level >= cur_locallevel + 1 ⟹ 1 >= 2 = false
6271    // The function returned cleanly without triggering zexit, and
6272    // the shell exited 0 instead of 7. Moving endparamscope before
6273    // the check matches C and makes the off-by-one resolve.
6274    endparamscope();
6275
6276    // c:6128 — `unqueue_signals();`
6277    unqueue_signals();
6278
6279    // c:6135-6155 — exit_pending branch: when an `exit` was queued
6280    // inside the function body and we've unwound enough scopes for
6281    // it to take effect, either keep unwinding (still inside a
6282    // nested function) or actually exit the shell.
6283    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
6284    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
6285    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
6286    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
6287    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
6288    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
6289        // c:6141
6290        if cur_locallevel > cur_forklevel {
6291            // c:6143 — still inside a nested function: keep unwinding.
6292            RETFLAG.store(1, Ordering::Relaxed); // c:6144
6293            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
6294        } else {
6295            // c:6151 — out of all functions: exit for real.
6296            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
6297            let val = EXIT_VAL.load(Ordering::Relaxed);
6298            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
6299            // c:6152
6300        }
6301    }
6302
6303    ret // c:6157 return ret
6304}
6305
6306/// `TRAP_STATE_PRIMED` per `Src/signals.h:55` — doshfunc tests this
6307/// to decide whether to bump trap_return on entry/exit. Local
6308/// const here because the canonical zsh_h port doesn't carry
6309/// trap-state numeric constants yet.
6310const TRAP_STATE_PRIMED: i32 = 2; // c:Src/signals.h:55
6311
6312/// Port of `execfuncdef(Estate state, Eprog redir_prog)` from
6313/// `Src/exec.c:5309-5494`. Define a shell function: extract
6314/// name(s)+body from the wordcode payload, allocate the Shfunc,
6315/// install into `shfunctab` (named), or execute immediately (anon).
6316#[allow(non_snake_case)]
6317pub fn execfuncdef(state: &mut estate, mut redir_prog: Option<crate::ported::zsh_h::Eprog>) -> i32 {
6318    use crate::ported::hashtable::{dircache_set, shfunctab_lock};
6319    use crate::ported::jobs::{getsigidx, removetrapnode};
6320    use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
6321    use crate::ported::signals::settrap;
6322    use crate::ported::utils::scriptfilename_get;
6323    use crate::ported::zsh_h::{
6324        eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, Patprog,
6325        EC_DUPTOK as _, EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED,
6326        PM_TAGGED_LOCAL, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
6327    };
6328    // c:5311 — `Shfunc shf;`
6329    let mut shf: Box<shfunc_t>;
6330    // c:5312 — `char *s = NULL;`
6331    let mut s: Option<String> = None;
6332    // c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
6333    let mut signum: i32;
6334    let nprg: i32;
6335    let sbeg: i32;
6336    let nstrs: i32;
6337    let npats: i32;
6338    let do_tracing: i32;
6339    let len: i32;
6340    let plen: i32;
6341    // `i` — C loop counter for pp stamp; Rust uses .map().collect().
6342    let mut htok: i32 = 0;
6343    let mut ret: i32 = 0;
6344    // c:5314 — `int anon_func = 0;`
6345    let mut anon_func: i32 = 0;
6346    // c:5315 — `Wordcode beg = state->pc, end;`
6347    let _beg: usize = state.pc;
6348    let mut end: usize;
6349    // c:5316 — `Eprog prog;`
6350    // (allocated inline per-iter below; no upfront binding needed)
6351    // c:5317 — `Patprog *pp;` — handled by Vec construction.
6352    // c:5318 — `LinkList names;`
6353    let names: Vec<String>;
6354    // c:5319 — `int tracing_flags;`
6355    let tracing_flags: i32;
6356
6357    // c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
6358    end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
6359    // c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6360    let num = state.prog.prog[state.pc] as usize;
6361    state.pc += 1;
6362    names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
6363    // c:5323 — `sbeg = *state->pc++;`
6364    sbeg = state.prog.prog[state.pc] as i32;
6365    state.pc += 1;
6366    // c:5324 — `nstrs = *state->pc++;`
6367    nstrs = state.prog.prog[state.pc] as i32;
6368    state.pc += 1;
6369    // c:5325 — `npats = *state->pc++;`
6370    npats = state.prog.prog[state.pc] as i32;
6371    state.pc += 1;
6372    // c:5326 — `do_tracing = *state->pc++;`
6373    do_tracing = state.prog.prog[state.pc] as i32;
6374    state.pc += 1;
6375
6376    // c:5328 — `nprg = (end - state->pc);`
6377    nprg = end.saturating_sub(state.pc) as i32;
6378    // c:5329 — `plen = nprg * sizeof(wordcode);`
6379    plen = nprg.saturating_mul(size_of::<wordcode>() as i32);
6380    // c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
6381    len = plen + npats.saturating_mul(size_of::<usize>() as i32) + nstrs;
6382    // c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
6383    tracing_flags = if do_tracing != 0 {
6384        PM_TAGGED_LOCAL as i32
6385    } else {
6386        0
6387    };
6388
6389    // c:5333-5339 — htok name substitution.
6390    let mut names_mut: Vec<String> = names;
6391    if htok != 0 && !names_mut.is_empty() {
6392        execsubst(&mut names_mut); // c:5334
6393        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6394            // c:5335
6395            state.pc = end; // c:5336
6396            return 1; // c:5337
6397        }
6398    }
6399
6400    // c:5341-5342 DPUTS — debug assertion (anon + redir simultaneously).
6401    // Not portable as panic; left as comment.
6402
6403    // c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
6404    // num==0 → anon (no names); else iterate names.
6405    let mut names_iter = names_mut.into_iter();
6406    loop {
6407        let no_names = num == 0;
6408        if !no_names {
6409            // c:5343 — `s = ugetnode(names)`; break when list exhausted.
6410            match names_iter.next() {
6411                Some(nm) => s = Some(nm),
6412                None => break,
6413            }
6414        }
6415        // c:5344-5374 — Eprog alloc.
6416        let prog: Box<eprog_t>;
6417        let dump_present = state.prog.dump.is_some();
6418        let make_pat = || -> Patprog {
6419            // c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
6420            Box::new(patprog_t {
6421                startoff: 0,
6422                size: 0,
6423                mustoff: 0,
6424                patmlen: 0,
6425                globflags: 0,
6426                globend: 0,
6427                flags: 0,
6428                patnpar: 0,
6429                patstartch: 0,
6430            })
6431        };
6432        if no_names {
6433            // c:5345-5346 — `zhalloc`, `nref = -1`.
6434            // c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
6435            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6436            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6437            // c:5365 — `prog->strs = state->strs + sbeg;`
6438            let strs_tail = state.strs.as_ref().map(|t| {
6439                let off = (sbeg as usize).min(t.len());
6440                t[off..].to_string()
6441            });
6442            prog = Box::new(eprog_t {
6443                flags: EF_HEAP,
6444                len,
6445                npats,
6446                nref: -1, // c:5346
6447                pats,
6448                prog: prog_words,
6449                strs: strs_tail,
6450                shf: None,  // c:5377
6451                dump: None, // c:5356
6452                strs_metafied: false, // native pool — clean UTF-8
6453            });
6454        } else if dump_present {
6455            // c:5358-5363 — EF_MAP path: refcount the dump, allocate
6456            // pats permanent, reuse `state->pc` slice in place.
6457            if let Some(dp) = state.prog.dump.as_deref() {
6458                incrdumpcount(dp); // c:5360
6459            }
6460            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6461            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6462            let strs_tail = state.strs.as_ref().map(|t| {
6463                let off = (sbeg as usize).min(t.len());
6464                t[off..].to_string()
6465            });
6466            prog = Box::new(eprog_t {
6467                flags: EF_MAP, // c:5359
6468                len,
6469                npats,
6470                nref: 1, // c:5349
6471                pats,
6472                prog: prog_words,
6473                strs: strs_tail,
6474                shf: None,                     // c:5377
6475                dump: state.prog.dump.clone(), // c:5361
6476                strs_metafied: state.prog.strs_metafied, // pool copied verbatim — carry provenance
6477            });
6478        } else {
6479            // c:5366-5374 — EF_REAL: copy wordcode + strs into a
6480            // freshly-owned eprog (no shared dump backing).
6481            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6482            let pc_end = state.pc + nprg as usize;
6483            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
6484            // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
6485            let strs_copy = state.strs.as_ref().map(|t| {
6486                let off = (sbeg as usize).min(t.len());
6487                let n_avail = t.len().saturating_sub(off);
6488                let take = (nstrs as usize).min(n_avail);
6489                t[off..off + take].to_string()
6490            });
6491            prog = Box::new(eprog_t {
6492                flags: EF_REAL, // c:5367
6493                len,
6494                npats,
6495                nref: 1, // c:5349
6496                pats,
6497                prog: prog_words,
6498                strs: strs_copy,
6499                shf: None,  // c:5377
6500                dump: None, // c:5371
6501                strs_metafied: false, // native pool — clean UTF-8
6502            });
6503        }
6504
6505        // c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
6506        shf = Box::new(shfunc_t {
6507            node: hashnode {
6508                next: None,
6509                nam: String::new(),
6510                flags: tracing_flags,
6511            },
6512            filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
6513            // c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
6514            // else just lineno.
6515            lineno: {
6516                let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
6517                if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6518                    if let Some(top) = stk.last() {
6519                        if top.tp == FS_FUNC || top.tp == FS_EVAL {
6520                            top.flineno + cur_lineno
6521                        } else {
6522                            cur_lineno
6523                        }
6524                    } else {
6525                        cur_lineno
6526                    }
6527                } else {
6528                    cur_lineno
6529                }
6530            },
6531            funcdef: Some(prog), // c:5380
6532            redir: None,
6533            sticky: None,
6534            body: None,
6535        });
6536        // c:5396-5401 — redir_prog ownership.
6537        // C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
6538        // else `shf->redir = redir_prog; redir_prog = 0;`
6539        // "nonempty(names)" means there's a NEXT name still to consume —
6540        // i.e. peek the iterator.
6541        if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
6542            // c:5397 — dupe so each earlier name gets its own copy; the
6543            // last name (when iterator drains) gets the original.
6544            if let Some(rp) = redir_prog.as_deref() {
6545                shf.redir = Some(Box::new(dupeprog(rp, false)));
6546            }
6547        } else {
6548            // c:5399-5400 — last name (or anon) takes original.
6549            shf.redir = redir_prog.take();
6550        }
6551        // c:5402 — `shfunc_set_sticky(shf);`
6552        shfunc_set_sticky(&mut shf);
6553
6554        if no_names {
6555            // c:5404-5457 — anonymous function: execute immediately.
6556            // `LinkList args;` c:5409
6557            let mut args: Vec<String>;
6558
6559            anon_func = 1; // c:5411
6560            shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
6561
6562            state.pc = end; // c:5414
6563                            // c:5415 — `end += *state->pc++;`
6564            end += state.prog.prog[state.pc] as usize;
6565            state.pc += 1;
6566            // c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6567            let arg_count = state.prog.prog[state.pc] as usize;
6568            state.pc += 1;
6569            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
6570
6571            // c:5418-5429 — htok arg subst + cleanup-on-error.
6572            if htok != 0 && !args.is_empty() {
6573                execsubst(&mut args); // c:5419
6574                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6575                    // c:5421 — `freeeprog(shf->funcdef);`
6576                    if let Some(mut fd) = shf.funcdef.take() {
6577                        freeeprog(&mut fd);
6578                    }
6579                    if shf.redir.is_some() {
6580                        // c:5422-5423 — "shouldn't be" anon+redir, but free if so.
6581                        if let Some(mut rd) = shf.redir.take() {
6582                            freeeprog(&mut rd);
6583                        }
6584                    }
6585                    dircache_set(&mut shf.filename, None); // c:5424
6586                    drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
6587                    state.pc = end; // c:5426
6588                    return 1; // c:5427
6589                }
6590            }
6591
6592            // c:5431-5432 — `setunderscore` to last arg (or "").
6593            let under_val = if !args.is_empty() {
6594                args.last().cloned().unwrap_or_default()
6595            } else {
6596                String::new()
6597            };
6598            setunderscore(&under_val);
6599
6600            // c:5434-5435 — `if (!args) args = newlinklist();`
6601            // (Rust Vec is never null; no-op.)
6602            shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
6603                                                                // c:5437 — `pushnode(args, shf->node.nam);` — prepend.
6604            args.insert(0, shf.node.nam.clone());
6605
6606            execshfunc(&mut shf, &mut args); // c:5439
6607            ret = LASTVAL.load(Ordering::Relaxed); // c:5440
6608
6609            // c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
6610            if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
6611                eprintln!("zsh: exit {}", ret); // c:5445/5447
6612            }
6613
6614            // c:5452-5456 — cleanup.
6615            if let Some(mut fd) = shf.funcdef.take() {
6616                freeeprog(&mut fd);
6617            }
6618            if let Some(mut rd) = shf.redir.take() {
6619                // c:5453-5454 — "shouldn't be" but free if present.
6620                freeeprog(&mut rd);
6621            }
6622            dircache_set(&mut shf.filename, None); // c:5455
6623            drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
6624            break; // c:5457
6625        } else {
6626            // c:5458-5484 — named function path.
6627            let nm = s.as_deref().unwrap_or("");
6628            // c:5460-5475 — TRAP* signal-trap install.
6629            if nm.len() > 4 && nm.starts_with("TRAP") {
6630                if let Some(sn) = getsigidx(&nm[4..]) {
6631                    signum = sn;
6632                    // c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
6633                    if settrap(signum, None, ZSIG_FUNC) != 0 {
6634                        if let Some(mut fd) = shf.funcdef.take() {
6635                            freeeprog(&mut fd); // c:5463
6636                        }
6637                        dircache_set(&mut shf.filename, None); // c:5464
6638                        drop(shf); // c:5465
6639                        state.pc = end; // c:5466
6640                        return 1; // c:5467
6641                    }
6642                    // c:5474 — `removetrapnode(signum);`
6643                    removetrapnode(signum);
6644                    // c:Src/signals.c::settrap → unsettrap →
6645                    // removetrap also clears sigfuncs[sig] (the C
6646                    // string-form trap slot). zshrs's port stores
6647                    // string-form bodies in a separate
6648                    // `traps_table` HashMap not touched by
6649                    // removetrap. Drop the string-form entry here
6650                    // so dotrap's fallback doesn't double-dispatch
6651                    // when a TRAPxxx function REPLACES an
6652                    // existing `trap '...' SIG` registration. Bug
6653                    // #541 in docs/BUGS.md.
6654                    if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
6655                        t.remove(&nm[4..]);
6656                    }
6657                }
6658            }
6659            // c:5477-5482 — re-define-self trace flag propagate.
6660            if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6661                if let Some(top) = stk.last() {
6662                    if top.tp == FS_FUNC && top.name == nm {
6663                        // c:5479 — `Shfunc old = shfunctab->getnode(s);`
6664                        if let Ok(rd) = shfunctab_lock().read() {
6665                            if let Some(old) = rd.get(nm) {
6666                                // c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
6667                                shf.node.flags |=
6668                                    old.node.flags & (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
6669                            }
6670                        }
6671                    }
6672                }
6673            }
6674            // c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
6675            shf.node.nam = nm.to_string();
6676            if let Ok(mut wr) = shfunctab_lock().write() {
6677                wr.add(*shf);
6678            }
6679        }
6680    }
6681    // c:5486-5487 — `if (!anon_func) setunderscore("");`
6682    if anon_func == 0 {
6683        setunderscore("");
6684    }
6685    // c:5488-5491 — leftover redir cleanup ("shouldn't happen").
6686    if let Some(mut rd) = redir_prog.take() {
6687        freeeprog(&mut rd);
6688    }
6689    // c:5492 — `state->pc = end;`
6690    state.pc = end;
6691    // c:5493 — `return ret;`
6692    ret
6693}
6694
6695/// Port of `execsimple(Estate state)` from `Src/exec.c:1290-1340`.
6696/// Fast-path for single-Simple commands that bypasses the full
6697/// `execcmd_exec` machinery.
6698pub fn execsimple(state: &mut estate) -> i32 {
6699    // c:1292 — `wordcode code = *state->pc++;`
6700    let mut code = state.prog.prog[state.pc];
6701    state.pc += 1;
6702    // c:1295-1296 — `if (errflag) return (lastval = 1);`
6703    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6704        LASTVAL.store(1, Ordering::Relaxed);
6705        return 1;
6706    }
6707    // c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
6708    if !isset(crate::ported::zsh_h::EXECOPT) {
6709        LASTVAL.store(0, Ordering::Relaxed);
6710        return 0;
6711    }
6712    // c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
6713    // In evaluated traps, don't modify the line number (the trap
6714    // dispatcher restores it). `code` here is the wordcode-encoded
6715    // line number from the WC_SIMPLE entry at state.pc-1.
6716    if !crate::ported::zsh_h::IN_EVAL_TRAP()
6717        && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
6718        && code != 0
6719    {
6720        crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
6721    }
6722    // c:1306 — `code = wc_code(*state->pc++);`
6723    code = wc_code(state.prog.prog[state.pc]);
6724    state.pc += 1;
6725    // c:1311-1312 — `otj = thisjob; thisjob = -1;`
6726    let otj = *THISJOB
6727        .get_or_init(|| std::sync::Mutex::new(-1))
6728        .lock()
6729        .unwrap();
6730    *THISJOB
6731        .get_or_init(|| std::sync::Mutex::new(-1))
6732        .lock()
6733        .unwrap() = -1;
6734    use crate::ported::zsh_h::{
6735        WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
6736        WC_WHILE,
6737    };
6738    use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
6739    let lv = if code == WC_ASSIGN {
6740        // c:1315-1319 — assignment-only simple cmd path.
6741        // cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
6742        addvars(state, state.pc.saturating_sub(1), 0);
6743        setunderscore(""); // c:1317
6744        if isset(XTRACE) {
6745            eprintln!();
6746        }
6747        let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
6748        if ef != 0 {
6749            ef
6750        } else {
6751            0
6752        }
6753    } else {
6754        // c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
6755        let q = queue_signal_level();
6756        dont_queue_signals();
6757        let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6758            ERRFLAG_ERROR
6759        } else if code == WC_FUNCDEF {
6760            execfuncdef(state, None)
6761        } else {
6762            // c:5499 execfuncs[] table inlined — match the WC_* tag.
6763            match code {
6764                WC_CURSH => execcursh(state, 0),
6765                WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
6766                WC_FOR => execfor(state, 0),
6767                WC_SELECT => execselect(state, 0),
6768                WC_CASE => execcase(state, 0),
6769                WC_IF => execif(state, 0),
6770                WC_WHILE => execwhile(state, 0),
6771                WC_REPEAT => execrepeat(state, 0),
6772                WC_TIMED => exectime(state, 0),
6773                WC_COND => execcond(state, 0),
6774                WC_ARITH => execarith(state, 0),
6775                WC_TRY => exectry(state, 0),
6776                _ => 0,
6777            }
6778        };
6779        restore_queue_signals(q);
6780        result
6781    };
6782    // c:1334 — `thisjob = otj;`
6783    *THISJOB
6784        .get_or_init(|| std::sync::Mutex::new(-1))
6785        .lock()
6786        .unwrap() = otj;
6787    LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
6788    lv
6789}
6790
6791/// Port of `execlist(Estate state, int dont_change_job, int exiting)`
6792/// from `Src/exec.c:1349-1665`. Walks WC_LIST entries, dispatches each
6793/// sublist (WC_SUBLIST chain inlined per c:1525-1625, same as C —
6794/// there's no separate execsublist function), handles signal-trap
6795/// dispatch + ERREXIT propagation.
6796///
6797/// Body ports the structural skeleton faithfully (WC_LIST walk,
6798/// per-iteration breaks/retflag/errflag guards, ltype dispatch on
6799/// Z_END/Z_SYNC/Z_ASYNC, donetrap handling). The full signal queue
6800/// + DEBUGBEFORECMD trap machinery from c:1357-1500 is preserved
6801/// in shape with TODO-citations where dependent primitives aren't
6802/// yet ported.
6803pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) -> i32 {
6804    let mut last_status: i32 = 0;
6805    let mut donetrap: i32 = 0; // c:1352 — `static int donetrap;`
6806    let cj = *THISJOB
6807        .get_or_init(|| std::sync::Mutex::new(-1))
6808        .lock()
6809        .unwrap(); // c:1364 — `cj = thisjob;`
6810    let _ = dont_change_job; // c:1361 — restored on exit if nonzero.
6811                             // c:1380 — `code = *state->pc++;`
6812    if state.pc >= state.prog.prog.len() {
6813        return last_status;
6814    }
6815    let mut code = state.prog.prog[state.pc];
6816    state.pc += 1;
6817    // c:1382-1384 — empty list returns lastval = 0.
6818    if wc_code(code) != WC_LIST {
6819        LASTVAL.store(0, Ordering::Relaxed);
6820        return 0;
6821    }
6822    use crate::ported::zsh_h::{WC_LIST_SKIP, WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC};
6823    // c:1385-1499 — main WC_LIST loop.
6824    while wc_code(code) == WC_LIST
6825        && BREAKS.load(Ordering::SeqCst) == 0
6826        && RETFLAG.load(Ordering::SeqCst) == 0
6827        && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
6828    {
6829        let ltype = WC_LIST_TYPE(code) as i32;
6830        // c:1396 — `csp = cmdsp;` — snapshot cmdstack depth at start
6831        // of this WC_LIST iteration; restored at end so partial
6832        // cmdpush sequences (e.g. from execcond, execfuncs) don't
6833        // leak into the next sublist.
6834        let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len());
6835        // c:1502-1509 — Z_SIMPLE fast-path.
6836        if (ltype & Z_SIMPLE as i32) != 0 {
6837            let next_pc = state.pc + WC_LIST_SKIP(code) as usize;
6838            let s = execsimple(state);
6839            last_status = s;
6840            state.pc = next_pc;
6841        } else {
6842            // c:1513-1523 — sublist chain.
6843            if state.pc >= state.prog.prog.len() {
6844                break;
6845            }
6846            code = state.prog.prog[state.pc];
6847            state.pc += 1;
6848            // c:1525-1625 — sublist chain (&&/|| operators) inlined.
6849            use crate::ported::zsh_h::{
6850                WC_SUBLIST_AND, WC_SUBLIST_END, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SIMPLE,
6851                WC_SUBLIST_SKIP,
6852            };
6853            let mut sub_code = code;
6854            let _ = dont_change_job;
6855            while wc_code(sub_code) == WC_SUBLIST {
6856                let flags = WC_SUBLIST_FLAGS(sub_code);
6857                let next = state.pc + WC_SUBLIST_SKIP(sub_code) as usize;
6858                let sl_type = WC_SUBLIST_TYPE(sub_code) as i32;
6859                let last1 = if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6860                    exiting
6861                } else {
6862                    0
6863                };
6864                if flags == WC_SUBLIST_SIMPLE {
6865                    last_status = execsimple(state); // c:1605
6866                } else {
6867                    let _ = execpline(state, sub_code, sl_type, last1); // c:1607
6868                    last_status = LASTVAL.load(Ordering::Relaxed);
6869                }
6870                // c:1612 — `WC_SUBLIST_NOT` inverts status.
6871                if (flags & WC_SUBLIST_NOT) != 0 {
6872                    last_status = if last_status == 0 { 1 } else { 0 };
6873                    LASTVAL.store(last_status, Ordering::Relaxed);
6874                }
6875                state.pc = next;
6876                if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6877                    break;
6878                }
6879                if state.pc >= state.prog.prog.len() {
6880                    break;
6881                }
6882                // c:1617-1623 — short-circuit on && / ||.
6883                if sl_type == WC_SUBLIST_AND as i32 && last_status != 0 {
6884                    while state.pc < state.prog.prog.len() {
6885                        let c = state.prog.prog[state.pc];
6886                        if wc_code(c) != WC_SUBLIST {
6887                            break;
6888                        }
6889                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6890                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6891                            break;
6892                        }
6893                    }
6894                    break;
6895                }
6896                if sl_type == WC_SUBLIST_OR as i32 && last_status == 0 {
6897                    while state.pc < state.prog.prog.len() {
6898                        let c = state.prog.prog[state.pc];
6899                        if wc_code(c) != WC_SUBLIST {
6900                            break;
6901                        }
6902                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6903                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6904                            break;
6905                        }
6906                    }
6907                    break;
6908                }
6909                sub_code = state.prog.prog[state.pc];
6910                state.pc += 1;
6911            }
6912        }
6913        // c:1593 — `cmdsp = csp;` — restore cmdstack depth to the
6914        // snapshot taken at start of iteration. Reverses any cmdpush
6915        // calls made by nested execcond / execfuncs / execcmd_exec
6916        // that didn't pop cleanly.
6917        crate::ported::prompt::CMDSTACK.with(|s| {
6918            let mut g = s.borrow_mut();
6919            if g.len() > csp {
6920                g.truncate(csp);
6921            }
6922        });
6923        // c:1626-1634 — donetrap is reset between sublists.
6924        donetrap = 0;
6925        // c:1640-1645 — fetch next WC_LIST header (or break out).
6926        if state.pc >= state.prog.prog.len() {
6927            break;
6928        }
6929        let next_code = state.prog.prog[state.pc];
6930        if wc_code(next_code) != WC_LIST {
6931            break;
6932        }
6933        state.pc += 1;
6934        code = next_code;
6935        // c:1389 — z_end means last sublist, exiting becomes 1 for tail-exec.
6936        if (ltype & Z_END as i32) != 0 {
6937            exiting = 1;
6938        }
6939    }
6940    // c:1659-1664 — cleanup: restore thisjob if dont_change_job, this_noerrexit=1.
6941    if dont_change_job != 0 {
6942        *THISJOB
6943            .get_or_init(|| std::sync::Mutex::new(-1))
6944            .lock()
6945            .unwrap() = cj;
6946    }
6947    let _ = donetrap;
6948    this_noerrexit.store(1, Ordering::Relaxed);
6949    LASTVAL.store(last_status, Ordering::Relaxed);
6950    last_status
6951}
6952
6953// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
6954// 1625`, the C source likewise inlines it — there's no `execsublist`
6955// function in zsh C).
6956
6957/// Port of `execcmd_getargs(LinkList preargs, LinkList args, int expand)`
6958/// from `Src/exec.c:2791-2806`. Transfer the first node of `args`
6959/// to `preargs`, performing `prefork` (singleton-list expansion) on
6960/// the way if `expand` is set. Used by `execcmd_exec` to pull the
6961/// command head one word at a time so prefix-modifier walking
6962/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
6963pub fn execcmd_getargs(preargs: &mut LinkList<String>, args: &mut LinkList<String>, expand: i32) {
6964    // c:2791
6965    if args.firstnode().is_none() {
6966        // c:2793 — `if (!firstnode(args)) return;`
6967        return;
6968    } else if expand != 0 {
6969        // c:2795
6970        // c:2796-2797 — `local_list0(svl); init_list0(svl);` —
6971        // stack-local single-bucket list. Rust uses a fresh
6972        // LinkList<String> per call.
6973        let mut svl: LinkList<String> = Default::default();
6974        // c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
6975        if let Some(idx) = args.firstnode() {
6976            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6977                svl.push_back(head);
6978            }
6979        }
6980        // c:2801 — `prefork(&svl, 0, NULL);`
6981        let mut rf = 0i32;
6982        prefork(&mut svl, 0, &mut rf);
6983        // c:2802 — `joinlists(preargs, &svl);`
6984        crate::ported::linklist::joinlists(preargs, &mut svl);
6985    } else {
6986        // c:2803-2804 — no-expand path: move head verbatim.
6987        if let Some(idx) = args.firstnode() {
6988            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6989                preargs.push_back(head);
6990            }
6991        }
6992    }
6993}
6994
6995/// Port of `execcmd_fork(Estate state, int how, int type,
6996/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
6997/// int close_if_forked)` from `Src/exec.c:2810-2893`.
6998///
6999/// Fork the current command into a child process: parent records
7000/// the pid + STTY env scan + addproc; child enters subshell, writes
7001/// `entersubsh_ret` back to parent through `synch` pipe, and returns
7002/// 0 so the caller can continue with the body.
7003///
7004/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
7005/// only in the child branch (so the parent's `filelist` stays
7006/// untouched). Rust sig keeps the same C contract.
7007pub fn execcmd_fork(
7008    state: &mut estate,
7009    how: i32,
7010    typ: i32,
7011    varspc: Option<usize>,
7012    filelistp: &mut Vec<jobfile>,
7013    text: &str,
7014    oautocont: i32,
7015    close_if_forked: i32,
7016) -> i32 {
7017    use crate::ported::signals::sigtrapped as sigtrapped_static;
7018    use crate::ported::signals_h::SIGEXIT;
7019    use crate::ported::zsh_h::{
7020        AUTOCONTINUE, BGNICE, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
7021        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
7022        WC_SUBSH as ZWC_SUBSH, ZSIG_IGNORED, Z_ASYNC,
7023    };
7024    // c:2810
7025    let pid: libc::pid_t; // c:2814
7026    let mut synch: [i32; 2] = [-1, -1]; // c:2815
7027    let flags: i32; // c:2815
7028    let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
7029                                                               // c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
7030                                                               // for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
7031    let mut bgtime = ZshTimespec::default();
7032
7033    child_block(); // c:2819
7034    esret.gleader = -1; // c:2820
7035    esret.list_pipe_job = -1; // c:2821
7036
7037    // c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
7038    if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
7039        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
7040        return -1; // c:2825
7041    }
7042    // c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
7043    pid = zfork(Some(&mut bgtime));
7044    if pid == -1 {
7045        unsafe {
7046            libc::close(synch[0]); // c:2827
7047            libc::close(synch[1]); // c:2828
7048        }
7049        LASTVAL.store(1, Ordering::Relaxed); // c:2829
7050        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
7051        return -1; // c:2831
7052    }
7053    if pid != 0 {
7054        // c:2833 — parent.
7055        unsafe { libc::close(synch[1]) }; // c:2834
7056                                          // c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
7057        let mut buf = [0u8; size_of::<entersubsh_ret>()];
7058        let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
7059        // entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
7060        if buf.len() >= 8 {
7061            esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
7062            esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
7063        }
7064        unsafe { libc::close(synch[0]) }; // c:2836
7065        if (how & Z_ASYNC as i32) != 0 {
7066            // c:2837 — `lastpid = (zlong) pid;`
7067            crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
7068        } else {
7069            // c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
7070            let thisjob_idx = {
7071                if let Some(m) = THISJOB.get() {
7072                    *m.lock().unwrap()
7073                } else {
7074                    -1
7075                }
7076            };
7077            // Examine the jobtab entry under lock.
7078            let stty_already = if thisjob_idx >= 0 {
7079                if let Some(jt) = JOBTAB.get() {
7080                    let guard = jt.lock().unwrap();
7081                    guard
7082                        .get(thisjob_idx as usize)
7083                        .map(|j| j.stty_in_env != 0)
7084                        .unwrap_or(true)
7085                } else {
7086                    true
7087                }
7088            } else {
7089                true
7090            };
7091            if !stty_already && varspc.is_some() {
7092                // c:2841-2851 — walk varspc looking for STTY=...
7093                let mut p = varspc.unwrap();
7094                loop {
7095                    if p >= state.prog.prog.len() {
7096                        break;
7097                    }
7098                    let ac = state.prog.prog[p];
7099                    if wc_code(ac) != ZWC_ASSIGN {
7100                        break;
7101                    }
7102                    // c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
7103                    let name = ecrawstr(&state.prog, p + 1, None);
7104                    if name == "STTY" {
7105                        // c:2846 — `jobtab[thisjob].stty_in_env = 1;`
7106                        if let Some(jt) = JOBTAB.get() {
7107                            let mut guard = jt.lock().unwrap();
7108                            if let Some(j) = guard.get_mut(thisjob_idx as usize) {
7109                                j.stty_in_env = 1;
7110                            }
7111                        }
7112                        break; // c:2847
7113                    }
7114                    p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
7115                        3 // c:2849
7116                    } else {
7117                        (ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
7118                    };
7119                }
7120            }
7121        }
7122        // c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
7123        if let Some(jt) = JOBTAB.get() {
7124            let mut guard = jt.lock().unwrap();
7125            let tj = {
7126                if let Some(m) = THISJOB.get() {
7127                    *m.lock().unwrap()
7128                } else {
7129                    -1
7130                }
7131            };
7132            if tj >= 0 {
7133                if let Some(j) = guard.get_mut(tj as usize) {
7134                    crate::ported::jobs::addproc(
7135                        j,
7136                        pid,
7137                        text,
7138                        false,
7139                        Some(std::time::Instant::now()),
7140                        esret.gleader,
7141                        esret.list_pipe_job,
7142                    );
7143                }
7144            }
7145        }
7146        // c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
7147        if oautocont >= 0 {
7148            opt_state_set("autocontinue", oautocont != 0);
7149            let _ = AUTOCONTINUE; // const referenced for parity
7150        }
7151        // c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
7152        if let Some(jt) = JOBTAB.get() {
7153            let mut guard = jt.lock().unwrap();
7154            let tj = {
7155                if let Some(m) = THISJOB.get() {
7156                    *m.lock().unwrap()
7157                } else {
7158                    -1
7159                }
7160            };
7161            if tj >= 0 {
7162                if let Some(j) = guard.get_mut(tj as usize) {
7163                    crate::ported::jobs::pipecleanfilelist(j, true);
7164                }
7165            }
7166        }
7167        return pid; // c:2857
7168    }
7169
7170    // c:2860 — pid == 0 (child).
7171    unsafe { libc::close(synch[0]) }; // c:2861
7172    flags = (if (how & Z_ASYNC as i32) != 0 {
7173        esub::ASYNC
7174    } else {
7175        0
7176    }) | esub::PGRP; // c:2862
7177    let mut flags = flags;
7178    if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
7179        flags |= esub::KEEPTRAP; // c:2864
7180    }
7181    if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
7182        flags |= esub::JOB_CONTROL; // c:2866
7183    }
7184    // c:2867 — `*filelistp = jobtab[thisjob].filelist;`
7185    if let Some(jt) = JOBTAB.get() {
7186        let mut guard = jt.lock().unwrap();
7187        let tj = {
7188            if let Some(m) = THISJOB.get() {
7189                *m.lock().unwrap()
7190            } else {
7191                -1
7192            }
7193        };
7194        if tj >= 0 {
7195            if let Some(j) = guard.get_mut(tj as usize) {
7196                *filelistp = std::mem::take(&mut j.filelist);
7197            }
7198        }
7199    }
7200    entersubsh(flags, Some(&mut esret)); // c:2868
7201                                         // c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
7202    let mut buf = [0u8; 8];
7203    buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
7204    buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
7205    if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
7206        zerr(&format!(
7207            "Failed to send entersubsh_ret report: {}",
7208            std::io::Error::last_os_error()
7209        ));
7210        return -1; // c:2871
7211    }
7212    unsafe { libc::close(synch[1]) }; // c:2873
7213    let _ = zclose(close_if_forked); // c:2874
7214
7215    // c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
7216    let sigint_state = {
7217        let guard = sigtrapped_static.lock().unwrap();
7218        guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
7219    };
7220    if (sigint_state & ZSIG_IGNORED) != 0 {
7221        crate::ported::signals::holdintr(); // c:2877
7222    }
7223    // c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
7224    {
7225        let mut guard = sigtrapped_static.lock().unwrap();
7226        if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
7227            *slot = 0;
7228        }
7229    }
7230    // c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
7231    // Per-platform errno setter+reader: __error() on macOS,
7232    // __errno_location() on Linux. Without cfg gating Linux CI breaks.
7233    if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
7234        #[cfg(target_os = "macos")]
7235        unsafe {
7236            *libc::__error() = 0;
7237            if libc::nice(5) == -1 && *libc::__error() != 0 {
7238                zwarn(&format!(
7239                    "nice(5) failed: {}",
7240                    std::io::Error::last_os_error()
7241                ));
7242            }
7243        }
7244        #[cfg(target_os = "linux")]
7245        unsafe {
7246            *libc::__errno_location() = 0;
7247            if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
7248                zwarn(&format!(
7249                    "nice(5) failed: {}",
7250                    std::io::Error::last_os_error()
7251                ));
7252            }
7253        }
7254    }
7255    0 // c:2892
7256}
7257
7258/// Port of `execcmd_analyse(Estate state, Execcmd_params eparams)`
7259/// from `Src/exec.c:2733-2785`. Pre-execcmd_exec analysis pass:
7260/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
7261/// without expanding (no prefork, no globbing), and fills `eparams`
7262/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
7263/// can branch on the command type before the real work.
7264pub fn execcmd_analyse(state: &mut estate, eparams: &mut crate::ported::zsh_h::execcmd_params) {
7265    use crate::ported::zsh_h::{
7266        WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
7267        WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
7268        WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
7269    };
7270    // c:2733
7271    let mut code: wordcode; // c:2735
7272    let mut i: i32; // c:2736
7273    let _ = i;
7274
7275    // c:2738 — `eparams->beg = state->pc;`
7276    eparams.beg = state.pc;
7277    // c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
7278    eparams.redir =
7279        if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_REDIR {
7280            Some(crate::ported::parse::ecgetredirs(state))
7281        } else {
7282            None
7283        };
7284    // c:2741-2748 — varspc walk (WC_ASSIGN chain).
7285    if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
7286        cmdoutval.store(0, Ordering::Relaxed); // c:2742
7287        eparams.varspc = Some(state.pc); // c:2743
7288                                         // c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
7289        loop {
7290            if state.pc >= state.prog.prog.len() {
7291                break;
7292            }
7293            code = state.prog.prog[state.pc];
7294            if wc_code(code) != ZWC_ASSIGN {
7295                break;
7296            }
7297            state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
7298                3 // c:2745
7299            } else {
7300                (WC_ASSIGN_NUM(code) + 2) as usize // c:2746
7301            };
7302        }
7303    } else {
7304        eparams.varspc = None; // c:2748
7305    }
7306
7307    // c:2750 — `code = *state->pc++;`
7308    if state.pc >= state.prog.prog.len() {
7309        eparams.args = None;
7310        eparams.assignspc = None;
7311        eparams.typ = 0;
7312        eparams.postassigns = 0;
7313        eparams.htok = 0;
7314        return;
7315    }
7316    code = state.prog.prog[state.pc];
7317    state.pc += 1;
7318
7319    // c:2752 — `eparams->type = wc_code(code);`
7320    eparams.typ = wc_code(code) as i32;
7321    // c:2753 — `eparams->postassigns = 0;`
7322    eparams.postassigns = 0;
7323
7324    // c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
7325    // per the comment at c:2755-2757.
7326    match eparams.typ as wordcode {
7327        x if x == ZWC_SIMPLE => {
7328            // c:2759-2763
7329            let mut htok = 0;
7330            let argc = ZWC_SIMPLE_ARGC(code) as usize;
7331            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7332            eparams.htok = htok;
7333            eparams.assignspc = None;
7334        }
7335        x if x == ZWC_TYPESET => {
7336            // c:2765-2777
7337            let mut htok = 0;
7338            let argc = ZWC_TYPESET_ARGC(code) as usize;
7339            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7340            eparams.htok = htok;
7341            // c:2768 — `eparams->postassigns = *state->pc++;`
7342            if state.pc < state.prog.prog.len() {
7343                eparams.postassigns = state.prog.prog[state.pc] as i32;
7344                state.pc += 1;
7345            }
7346            // c:2769 — `eparams->assignspc = state->pc;`
7347            eparams.assignspc = Some(state.pc);
7348            // c:2770-2776 — walk past the postassigns.
7349            let mut k = 0i32;
7350            while k < eparams.postassigns {
7351                if state.pc >= state.prog.prog.len() {
7352                    break;
7353                }
7354                code = state.prog.prog[state.pc];
7355                // c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
7356                state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
7357                    3 // c:2774
7358                } else {
7359                    (WC_ASSIGN_NUM(code) + 2) as usize // c:2775
7360                };
7361                k += 1;
7362            }
7363        }
7364        _ => {
7365            // c:2779-2783 default.
7366            eparams.args = None;
7367            eparams.assignspc = None;
7368            eparams.htok = 0;
7369        }
7370    }
7371}
7372
7373/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
7374/// Stack of `"context"` labels used by `eval`-style nested execution:
7375/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
7376/// ..., "context")` pushes its label and pops on return.
7377pub static zsh_eval_context: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
7378
7379/// Port of `static int donetrap;` from `Src/exec.c:1351`. Tracks
7380/// whether the ZERR trap has already fired for the current sublist.
7381/// C source resets to 0 at sublist start (c:1455) and sets to 1
7382/// after `dotrap(SIGZERR)` (c:1602). The check
7383/// `if (!this_noerrexit && !donetrap && !this_donetrap)` at c:1598
7384/// suppresses re-firing within the same sublist AND, crucially,
7385/// carries the "already fired" state across a function-call return
7386/// boundary so the outer caller's post-command check doesn't fire
7387/// ZERR a second time for the same logical error. Bug #303 in
7388/// docs/BUGS.md.
7389///
7390/// Reset at each top-level statement boundary via
7391/// `BUILTIN_DONETRAP_RESET` emitted by `compile_list`. Set after
7392/// `dotrap(SIGZERR)` fires inside `BUILTIN_ERREXIT_CHECK`.
7393pub static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
7394
7395/// Port of `save_params(Estate state, Wordcode pc, LinkList *restore_p,
7396/// LinkList *remove_p)` from `Src/exec.c:4410-4458`. Walk WC_ASSIGN
7397/// chain at `pc`, snapshot each existing param into `restore_p` (so
7398/// the builtin/shfunc can restore them on return) and enqueue every
7399/// touched name in `remove_p` (so we know what to unset).
7400pub fn save_params(
7401    state: &mut estate,
7402    pc: usize,
7403    restore_p: &mut Vec<crate::ported::zsh_h::param>,
7404    remove_p: &mut Vec<String>,
7405) {
7406    use crate::ported::zsh_h::{
7407        PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
7408        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
7409    };
7410    // c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
7411    // c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
7412    let mut p = pc;
7413    // c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
7414    loop {
7415        if p >= state.prog.prog.len() {
7416            break;
7417        }
7418        let ac = state.prog.prog[p];
7419        if wc_code(ac) != WC_ASSIGN {
7420            break;
7421        }
7422        // c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
7423        let s = ecrawstr(&state.prog, p + 1, None);
7424        // c:4421 — `pm = paramtab->getnode(paramtab, s)`
7425        let pm_clone: Option<crate::ported::zsh_h::param> = {
7426            let tab = paramtab().read().unwrap();
7427            tab.get(&s).map(|b| (**b).clone())
7428        };
7429        if let Some(pm) = pm_clone {
7430            // c:4423-4424 — `if (pm->env) delenv(pm);`
7431            if pm.env.is_some() {
7432                crate::ported::params::delenv(&s);
7433            }
7434            // c:4425-4448 — copy if not readonly-special.
7435            if (pm.node.flags & PM_SPECIAL as i32) == 0 {
7436                // c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
7437                let mut tpm = pm.clone();
7438                tpm.node.nam = s.clone();
7439                // copyparam with fakecopy=0 already done by the clone()
7440                // (Clone derives a deep copy of param fields).
7441                restore_p.push(tpm); // c:4451
7442            } else if (pm.node.flags & PM_READONLY as i32) == 0 {
7443                // c:4439-4448 — special-but-not-readonly: fakecopy=1.
7444                let mut tpm = pm.clone();
7445                tpm.node.nam = pm.node.nam.clone();
7446                restore_p.push(tpm); // c:4451
7447            }
7448            // c:4449 — `addlinknode(*remove_p, dupstring(s));`
7449            remove_p.push(s.clone());
7450        } else {
7451            // c:4453 — `addlinknode(*remove_p, dupstring(s));`
7452            remove_p.push(s.clone());
7453        }
7454        // c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
7455        p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
7456            3
7457        } else {
7458            (ZWC_ASSIGN_NUM(ac) + 2) as usize
7459        };
7460    }
7461}
7462
7463/// Port of `restore_params(LinkList restorelist, LinkList removelist)`
7464/// from `Src/exec.c:4464-4528`. After the builtin/shfunc returns,
7465/// unset every name in removelist, then for each saved param in
7466/// restorelist re-install its values (PM_SPECIAL go through gsu
7467/// setfn; regular params re-enter paramtab as-is).
7468pub fn restore_params(restorelist: Vec<crate::ported::zsh_h::param>, removelist: Vec<String>) {
7469    use crate::ported::zsh_h::{PM_READONLY, PM_SPECIAL};
7470    // c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
7471    for s in &removelist {
7472        // c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
7473        let flags = {
7474            let tab = paramtab().read().unwrap();
7475            tab.get(s).map(|p| p.node.flags)
7476        };
7477        if let Some(f) = flags {
7478            if (f & PM_SPECIAL as i32) == 0 {
7479                // c:4473 — `pm->node.flags &= ~PM_READONLY;`
7480                let mut tab = paramtab().write().unwrap();
7481                if let Some(pm_mut) = tab.get_mut(s) {
7482                    pm_mut.node.flags &= !(PM_READONLY as i32);
7483                }
7484                // Drop write guard before calling unsetparam_pm.
7485                drop(tab);
7486                let mut tab = paramtab().write().unwrap();
7487                if let Some(pm_mut) = tab.get_mut(s) {
7488                    let _ = crate::ported::params::unsetparam_pm(pm_mut, 0, 0); // c:4474
7489                }
7490            }
7491        }
7492    }
7493    // c:4478-4523 — restore saved params.
7494    for pm in restorelist {
7495        // c:4481-4520 — PM_SPECIAL: route through gsu setfn.
7496        // c:4521-4523 — non-special: re-install via paramtab.
7497        if (pm.node.flags & PM_SPECIAL as i32) != 0 {
7498            // PM_SPECIAL restore: full path requires PM_TYPE dispatch
7499            // on gsu_s/i/f/a/h setfn. Each setfn fires the param's
7500            // canonical write hook. Pragmatic port: overwrite in
7501            // paramtab; daily-driver path rarely saves specials (those
7502            // are reserved-name vars like PATH/FPATH/etc. which can't
7503            // appear as `VAR=val cmd` prefix anyway).
7504            let mut tab = paramtab().write().unwrap();
7505            tab.insert(pm.node.nam.clone(), Box::new(pm));
7506        } else {
7507            // c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
7508            let mut tab = paramtab().write().unwrap();
7509            tab.insert(pm.node.nam.clone(), Box::new(pm));
7510        }
7511    }
7512}
7513
7514/// Port of `void execode(Eprog p, int dont_change_job, int exiting,
7515/// char *context)` from `Src/exec.c:1245-1282`. Set up an `estate`
7516/// around the given Eprog and run `execlist`. Maintains the
7517/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
7518/// call chain.
7519///
7520/// NOTE: this is the WORDCODE form (drives the ported `execlist`
7521/// interpreter). zshrs's live execution pipeline is fusevm
7522/// (`compile_zsh` → VM), so the top-level REPL `loop()` and most call
7523/// sites run through [`execode`] (the ZshProgram/fusevm form below)
7524/// instead. This wordcode entry is retained for the internal
7525/// function-body callers (`doshfunc` / autoload) that already hold an
7526/// `Eprog`.
7527pub fn execode_wordcode(
7528    p: crate::ported::zsh_h::Eprog,
7529    dont_change_job: i32,
7530    exiting: i32,
7531    context: &str,
7532) {
7533    // c:1245
7534    let prog_ref = *p;
7535    // c:1247 — `struct estate s;`
7536    let mut s = estate {
7537        prog: Box::new(prog_ref.clone()),
7538        // c:1269 — `s.pc = p->prog;` — start at index 0.
7539        pc: 0,
7540        // c:1270 — `s.strs = p->strs;`
7541        strs: prog_ref.strs.clone(),
7542        strs_offset: 0,
7543    };
7544    // c:1251-1266 — push context onto zsh_eval_context.
7545    let pushed = {
7546        if let Ok(mut ctx) = zsh_eval_context.lock() {
7547            ctx.push(context.to_string());
7548            true
7549        } else {
7550            false
7551        }
7552    };
7553    // c:1271 — `useeprog(p);`
7554    crate::ported::parse::useeprog(&mut s.prog);
7555    // c:1273 — `execlist(&s, dont_change_job, exiting);`
7556    execlist(&mut s, dont_change_job, exiting);
7557    // c:1275 — `freeeprog(p);`
7558    crate::ported::parse::freeeprog(&mut s.prog);
7559    // c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
7560    if pushed {
7561        if let Ok(mut ctx) = zsh_eval_context.lock() {
7562            ctx.pop();
7563        }
7564    }
7565}
7566
7567thread_local! {
7568    /// The long-lived interactive executor for the top-level `loop()`
7569    /// REPL (the `zsh_main` path). Set once by the bin before
7570    /// `zsh_main`; [`execode`] runs each parsed program through it.
7571    /// Persists for the whole session so variables/functions survive
7572    /// across prompts.
7573    static SESSION_EXECUTOR: std::cell::Cell<Option<*mut crate::vm_helper::ShellExecutor>> =
7574        const { std::cell::Cell::new(None) };
7575}
7576
7577/// Register the persistent session executor used by [`execode`] for the
7578/// interactive `loop()` REPL. The pointer must outlive the session (the
7579/// bin keeps the executor alive until `zsh_main` exits the process).
7580pub fn install_session_executor(exec: &mut crate::vm_helper::ShellExecutor) {
7581    SESSION_EXECUTOR.with(|c| c.set(Some(exec as *mut crate::vm_helper::ShellExecutor)));
7582    // Mirror the pointer into the bridge so `with_session_context` can
7583    // establish a VM context for startup rc-sourcing (run_init_scripts,
7584    // c:1914) before the loop's first execode enters one.
7585    crate::fusevm_bridge::register_session_executor(exec);
7586}
7587
7588/// zshrs `execode` — run an already-parsed `ZshProgram` (Src/exec.c:220
7589/// `execode(prog, ...)`, called from `loop()`). This is the **exec.rs
7590/// exception** to the line-by-line port: rather than walk wordcode via
7591/// `execlist`, it drives zshrs's live engine — compile the program with
7592/// `compile_zsh` and run it on the session executor's fusevm VM. The
7593/// faithful `loop()` in init.rs calls this exactly as C calls execode.
7594/// Returns `$?` (0 when no session executor is installed).
7595pub fn execode(
7596    program: &crate::parse::ZshProgram,
7597    _dont_change_job: i32,
7598    _exiting: i32,
7599    _context: &str,
7600) -> i32 {
7601    SESSION_EXECUTOR.with(|c| match c.get() {
7602        // SAFETY: set by install_session_executor to an executor that
7603        // lives for the whole single-threaded interactive session;
7604        // loop() runs only after the bin installs it.
7605        Some(ptr) => unsafe { (*ptr).execute_program(program) },
7606        None => 0,
7607    })
7608}
7609
7610// =========================================================================
7611// Live-executor accessors (former `exec_hooks` OnceLock layer).
7612//
7613// These are the **exec.rs exception**: src/ported/ code reaches the
7614// live fusevm `ShellExecutor` (param store, function dispatch, nested
7615// script/cmdsubst execution) through these thin wrappers instead of the
7616// deleted `exec_hooks` fn-pointer registry. Each delegates to
7617// `fusevm_bridge::try_with_executor` — `Some` when a VM execution
7618// context is in scope, `None` in unit-test / compsys contexts with no
7619// bridge running — and reproduces the exact per-call fallback the old
7620// `exec_hooks` wrappers used when no hook was installed. Behavior is
7621// byte-for-byte identical to the OnceLock path; only the indirection is
7622// gone. See `feedback_no_shellexecutor_in_ported` /
7623// `feedback_no_exec_script_from_ported`: the bridge belongs in exec.rs
7624// (the sanctioned exception), not scattered through src/ported.
7625// =========================================================================
7626
7627/// Array param value via the live executor; falls back to the direct
7628/// param table (`params::getaparam`) when no executor is in scope, so
7629/// compsys / unit-test environments still observe shell-side arrays.
7630pub fn array(name: &str) -> Option<Vec<String>> {
7631    // bash special arrays (PIPESTATUS / FUNCNAME / BASH_VERSINFO) alias the
7632    // zsh-native specials in --bash mode. Checked before the stored-array
7633    // lookup so a user array of the same name can still shadow it (rare), but
7634    // after the bash-mode gate so --zsh is untouched. Guard against the
7635    // PIPESTATUS→pipestatus self-alias recursing by only aliasing uppercase.
7636    if name.starts_with(|c: char| c.is_ascii_uppercase()) {
7637        if let Some(v) = crate::dash_mode::bash_special_array(name) {
7638            return Some(v);
7639        }
7640    }
7641    if let Some(Some(v)) = crate::fusevm_bridge::try_with_executor(|exec| exec.array(name)) {
7642        return Some(v);
7643    }
7644    crate::ported::params::getaparam(name)
7645}
7646
7647/// Associative-array param value via the live executor (`None` when no
7648/// executor / not set).
7649pub fn assoc(name: &str) -> Option<indexmap::IndexMap<String, String>> {
7650    crate::fusevm_bridge::try_with_executor(|exec| exec.assoc(name)).flatten()
7651}
7652
7653/// Store an array param into the live executor (no-op without one).
7654pub fn set_array(name: &str, val: Vec<String>) {
7655    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_array(name.to_string(), val));
7656}
7657
7658/// Store an associative-array param into the live executor (no-op
7659/// without one).
7660pub fn set_assoc(name: &str, val: indexmap::IndexMap<String, String>) {
7661    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_assoc(name.to_string(), val));
7662}
7663
7664/// Unset a scalar param in the live executor (no-op without one).
7665pub fn unset_scalar(name: &str) {
7666    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_scalar(name));
7667}
7668
7669/// Unset an array param in the live executor (no-op without one).
7670pub fn unset_array(name: &str) {
7671    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_array(name));
7672}
7673
7674/// Unset an associative-array param in the live executor (no-op
7675/// without one).
7676pub fn unset_assoc(name: &str) {
7677    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_assoc(name));
7678}
7679
7680/// Dispatch a shell-function call by name through the live executor
7681/// (full doshfunc scope wrap). `None` when no executor / not a
7682/// function.
7683pub fn dispatch_function_call(name: &str, args: &[String]) -> Option<i32> {
7684    if let Some(r) =
7685        crate::fusevm_bridge::try_with_executor(|exec| exec.dispatch_function_call(name, args))
7686    {
7687        return r;
7688    }
7689    // No active VM context: this is the loop()/zsh_main exit path where
7690    // `zexit` fires a `TRAPEXIT() { ... }` (via dotrap, Src/builtin.c:6043)
7691    // or the `zshexit` hook after `loop()` returned. Enter the installed
7692    // session executor so the handler runs in the shell. SAFETY per execode.
7693    SESSION_EXECUTOR.with(|c| match c.get() {
7694        Some(ptr) => {
7695            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
7696            unsafe { (*ptr).dispatch_function_call(name, args) }
7697        }
7698        None => None,
7699    })
7700}
7701
7702/// Body-only function dispatch (no doshfunc scope wrap) — call as the
7703/// `body_runner` of a direct `doshfunc(...)` invocation to avoid the
7704/// double-wrap of going back through [`dispatch_function_call`]. `None`
7705/// when no executor.
7706pub fn run_function_body(name: &str, args: &[String]) -> Option<i32> {
7707    if let Some(r) =
7708        crate::fusevm_bridge::try_with_executor(|exec| exec.run_function_body_only(name, args))
7709    {
7710        return r;
7711    }
7712    // Session-executor fallback for the no-VM-context exit path (e.g. the
7713    // `zshexit` hook fired by `zexit` from zsh_main). SAFETY per execode.
7714    SESSION_EXECUTOR.with(|c| match c.get() {
7715        Some(ptr) => {
7716            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
7717            unsafe { (*ptr).run_function_body_only(name, args) }
7718        }
7719        None => None,
7720    })
7721}
7722
7723/// Run a script source string on the live executor. `Ok(0)` when no
7724/// executor is in scope.
7725pub fn execute_script(src: &str) -> Result<i32, String> {
7726    if let Some(r) = crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script(src)) {
7727        return r;
7728    }
7729    // No active VM context: the loop()/zsh_main exit path where `zexit`
7730    // runs a `trap '...' EXIT` raw body (Src/builtin.c:6043) after `loop()`
7731    // returned. Enter the installed session executor so the trap body runs
7732    // in the shell instead of silently no-op'ing. SAFETY per execode.
7733    SESSION_EXECUTOR.with(|c| match c.get() {
7734        Some(ptr) => {
7735            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
7736            unsafe { (*ptr).execute_script(src) }
7737        }
7738        None => Ok(0),
7739    })
7740}
7741
7742/// Run a script source string through the live executor's zsh pipeline.
7743/// `Ok(0)` when no executor is in scope.
7744pub fn execute_script_zsh_pipeline(src: &str) -> Result<i32, String> {
7745    crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script_zsh_pipeline(src))
7746        .unwrap_or(Ok(0))
7747}
7748
7749/// Run a `$(...)` command substitution on the live executor, returning
7750/// captured stdout. Empty string when no executor is in scope.
7751pub fn run_command_substitution(cmd: &str) -> String {
7752    if let Some(r) =
7753        crate::fusevm_bridge::try_with_executor(|exec| exec.run_command_substitution(cmd))
7754    {
7755        return r;
7756    }
7757    // Session-executor fallback for no-VM-context callers — the native
7758    // p10k custom_* segments (p10k:1698 `content="$(eval $command)"`)
7759    // render at preprompt time, before any ExecutorContext is entered,
7760    // so try_with_executor alone returned "" and every custom segment
7761    // rendered empty. Same pattern as run_function_body above; SAFETY
7762    // per execode.
7763    SESSION_EXECUTOR.with(|c| match c.get() {
7764        Some(ptr) => {
7765            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
7766            unsafe { (*ptr).run_command_substitution(cmd) }
7767        }
7768        None => String::new(),
7769    })
7770}
7771
7772/// Positional parameters ($1..$N) from the live executor; empty without
7773/// one.
7774pub fn pparams() -> Vec<String> {
7775    crate::fusevm_bridge::try_with_executor(|exec| exec.pparams()).unwrap_or_default()
7776}
7777
7778/// Replace the positional parameters in the live executor (no-op
7779/// without one).
7780pub fn set_pparams(v: Vec<String>) {
7781    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_pparams(v));
7782}
7783
7784/// Drop a function from both the compiled-chunk and source maps in the
7785/// live executor. Returns true if either entry existed; false when no
7786/// executor.
7787pub fn unregister_function(name: &str) -> bool {
7788    crate::fusevm_bridge::try_with_executor(|exec| {
7789        let a = exec.functions_compiled.remove(name).is_some();
7790        let b = exec.function_source.remove(name).is_some();
7791        a || b
7792    })
7793    .unwrap_or(false)
7794}
7795
7796/// Saved outer stdout fd for an in-progress `$(...)` capture (top of
7797/// the bridge's CMDSUBST_OUTER_FDS stack), or `None` when not inside a
7798/// cmdsub. Used by the trap dispatcher to route a trap body's stdout to
7799/// the parent terminal instead of the cmdsub-bound pipe (Bug #56).
7800pub fn cmdsubst_outer_stdout() -> Option<i32> {
7801    crate::fusevm_bridge::cmdsubst_outer_stdout()
7802}
7803
7804/// Port of `execautofn_basic(Estate state, UNUSED(int do_exec))` from
7805/// `Src/exec.c:5608-5630`. Run a pre-loaded autoload function body
7806/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
7807/// the call so `%N` / `%x` reflect the autoload target during
7808/// execution.
7809pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
7810    // c:5608
7811    // c:5613 — `shf = state->prog->shf;`
7812    let shf = match state.prog.shf.as_deref() {
7813        Some(s) => s.clone(),
7814        None => return LASTVAL.load(Ordering::Relaxed),
7815    };
7816
7817    // c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
7818    // top-of-stack tracking is in modules::parameter::FUNCSTACK.
7819    {
7820        let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
7821        if let Some(top) = stk.last_mut() {
7822            if top.filename.is_none() {
7823                // c:5620 — `funcstack->filename = getshfuncfile(shf);`
7824                top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
7825            }
7826        }
7827    }
7828
7829    // c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
7830    let oldscriptname = crate::ported::utils::scriptname_get();
7831    let oldscriptfilename = crate::ported::utils::scriptfilename_get();
7832    // c:5624 — `scriptname = dupstring(shf->node.nam);`
7833    crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
7834    // c:5625 — `scriptfilename = getshfuncfile(shf);`
7835    crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
7836        &shf.node.nam,
7837    ));
7838    // c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
7839    if let Some(funcdef) = shf.funcdef.clone() {
7840        execode_wordcode(funcdef, 1, 0, "loadautofunc");
7841    }
7842    // c:5627-5628 — restore.
7843    crate::ported::utils::set_scriptname(oldscriptname);
7844    crate::ported::utils::set_scriptfilename(oldscriptfilename);
7845
7846    LASTVAL.load(Ordering::Relaxed) // c:5630
7847}
7848
7849/// Port of `static int execautofn(Estate state, UNUSED(int do_exec))`
7850/// from `Src/exec.c:5635-5644`. The autoload-aware dispatch entry
7851/// for `WC_AUTOFN`: fault the function body in via `loadautofn`,
7852/// then hand off to `execautofn_basic` to actually run it.
7853///
7854/// C body:
7855/// ```c
7856/// static int
7857/// execautofn(Estate state, UNUSED(int do_exec))
7858/// {
7859///     Shfunc shf;
7860///     if (!(shf = loadautofn(state->prog->shf, 1, 0, 0)))
7861///         return 1;
7862///     state->prog->shf = shf;
7863///     return execautofn_basic(state, 0);
7864/// }
7865/// ```
7866///
7867/// Rust port: `loadautofn` mutates the `shfunc` in place via a raw
7868/// pointer and returns 0/1 (success/failure), so the explicit
7869/// `state->prog->shf = shf` assignment in C is implicit here.
7870pub fn execautofn(state: &mut estate, _do_exec: i32) -> i32 {
7871    // c:5638-5640 — `if (!(shf = loadautofn(state->prog->shf, 1, 0, 0))) return 1;`
7872    let shf_ptr: *mut shfunc = match state.prog.shf.as_mut() {
7873        Some(b) => &mut **b as *mut shfunc,
7874        None => return 1,
7875    };
7876    if loadautofn(shf_ptr, 1, 0, 0) != 0 {
7877        return 1;
7878    }
7879    // c:5643 — `return execautofn_basic(state, 0);`
7880    execautofn_basic(state, 0)
7881}
7882
7883/// Port of `execpline2(Estate state, wordcode pcode, int how, int input,
7884/// int output, int last1)` from `Src/exec.c:1989-2040`. Recursive
7885/// multi-stage pipe walker: at each step, analyse the current
7886/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
7887/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
7888/// its input fd.
7889pub fn execpline2(
7890    state: &mut estate,
7891    pcode: wordcode,
7892    how: i32,
7893    input: i32,
7894    output: i32,
7895    last1: i32,
7896) {
7897    use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
7898    use crate::ported::zsh_h::{
7899        execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
7900        WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
7901    };
7902    // c:1991
7903    let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
7904
7905    // c:1996-1997 — `if (breaks || retflag) return;`
7906    if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
7907        return;
7908    }
7909
7910    // c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
7911    //                  lineno = WC_PIPE_LINENO(pcode) - 1;`
7912    if !crate::ported::zsh_h::IN_EVAL_TRAP()
7913        && INEVAL.load(Ordering::SeqCst) == 0
7914        && ZWC_PIPE_LINENO(pcode) != 0
7915    {
7916        let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
7917        crate::ported::input::lineno.with(|l| l.set(new_lineno));
7918    }
7919
7920    // c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
7921    if pline_level.load(Ordering::Relaxed) == 1 {
7922        // c:2003
7923        if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
7924            // c:2004 — `(how & Z_ASYNC) || !sfcontext`
7925            // c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
7926            //   state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
7927            let pc_for_text = state.pc
7928                + if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7929                    0
7930                } else {
7931                    1
7932                };
7933            let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
7934            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7935                *lpt = text;
7936            }
7937        } else {
7938            // c:2010 — `list_pipe_text[0] = '\0';`
7939            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7940                lpt.clear();
7941            }
7942        }
7943    }
7944
7945    if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7946        // c:2012-2014 — terminal stage: analyse + exec directly.
7947        execcmd_analyse(state, &mut eparams); // c:2013
7948        execcmd_exec(
7949            state,
7950            &mut eparams,
7951            input,
7952            output,
7953            how,
7954            if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
7955            -1,                             // c:2014 close_if_forked = -1
7956        );
7957    } else {
7958        // c:2015-2039 — non-terminal stage: pipe + fork + recurse.
7959        let mut pipes: [i32; 2] = [-1, -1]; // c:2016
7960        let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
7961                                                               // c:2018 — `Wordcode next = state->pc + (*state->pc);`
7962        let next = if state.pc < state.prog.prog.len() {
7963            state.pc + state.prog.prog[state.pc] as usize
7964        } else {
7965            state.pc
7966        };
7967        // c:2020 — `++state->pc;`
7968        if state.pc < state.prog.prog.len() {
7969            state.pc += 1;
7970        }
7971        execcmd_analyse(state, &mut eparams); // c:2021
7972
7973        if mpipe(&mut pipes) < 0 {
7974            // c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
7975        }
7976
7977        // c:2027 — `addfilelist(NULL, pipes[0]);`
7978        // C uses the current thisjob's filelist; Rust port wires through JOBTAB.
7979        if let Some(jt) = JOBTAB.get() {
7980            let mut guard = jt.lock().unwrap();
7981            let tj = {
7982                if let Some(m) = THISJOB.get() {
7983                    *m.lock().unwrap()
7984                } else {
7985                    -1
7986                }
7987            };
7988            if tj >= 0 {
7989                if let Some(j) = guard.get_mut(tj as usize) {
7990                    crate::ported::jobs::addfilelist(j, None, pipes[0]);
7991                }
7992            }
7993        }
7994
7995        // c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
7996        execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
7997        let _ = zclose(pipes[1]); // c:2029
7998        state.pc = next; // c:2030
7999
8000        // c:2034 — `cmdpush(CS_PIPE);`
8001        cmdpush(CS_PIPE as u8);
8002        // c:2035 — `list_pipe = 1;`
8003        list_pipe.store(1, Ordering::Relaxed);
8004        // c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
8005        let next_pcode = if state.pc < state.prog.prog.len() {
8006            state.prog.prog[state.pc]
8007        } else {
8008            0
8009        };
8010        if state.pc < state.prog.prog.len() {
8011            state.pc += 1;
8012        }
8013        execpline2(state, next_pcode, how, pipes[0], output, last1);
8014        // c:2037 — `list_pipe = old_list_pipe;`
8015        list_pipe.store(old_list_pipe, Ordering::Relaxed);
8016        // c:2038 — `cmdpop();`
8017        cmdpop();
8018    }
8019}
8020
8021/// Port of `execpline(Estate state, wordcode slcode, int how, int last1)`
8022/// from `Src/exec.c:1724-2041`. Full faithful port: allocates a job-table
8023/// entry via `initjob`, sets up coproc mpipes, drives the whole
8024/// (multi-stage) pipeline through `execpline2` (which performs the real
8025/// per-stage mpipe/fork/exec), then either spawns the job asynchronously
8026/// (`Z_ASYNC` -> `spawnjob`/`deletejob`) or waits synchronously
8027/// (`waitjobs`), including the `list_pipe` stop/continue fork machinery
8028/// (SUBJOB/SUPERJOB linkage) that re-forks the shell to keep an
8029/// interactively-suspended right-hand pipeline stage running.
8030///
8031/// Divergences from C, each forced by the Rust substrate and cited
8032/// inline at the point of use:
8033///   * `initjob` here grows the `Vec`-backed jobtab on demand and does
8034///     not return -1, so C's per-pipeline table-full bailout (c:1756-1760)
8035///     is inert on THIS path. That bailout is zsh's universal recursion
8036///     backstop (`initjob` caps the table at `MAX_MAXJOBS` → `zerr("job
8037///     table full or recursion limit exceeded")`), which bounds recursion
8038///     through paths FUNCNEST does not count (sourced files, `eval` — the
8039///     doshfunc funcnest check at c:5684 counts FS_FUNC frames only). The
8040///     fusevm runtime that actually executes pipelines does not allocate a
8041///     job per pipeline, so that backstop was missing entirely — runaway
8042///     `source`/`eval` recursion overflowed the (large but finite)
8043///     main-thread stack → SIGBUS. It is reinstated inline (same ceiling,
8044///     total FUNCSTACK depth as the proxy for held job slots) at the
8045///     FS_SOURCE re-entry (init.rs::source) and FS_EVAL re-entry
8046///     (builtin.rs eval).
8047///   * `errbrk_saved` / `prev_errflag` / `prev_breaks` (jobs.c:128
8048///     globals) are only *read* here; their setter lives in the
8049///     not-yet-ported jobs.c reaping path, so they stay 0 and the
8050///     `if (errbrk_saved)` restore (c:1998-2003) is a faithful no-op.
8051pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
8052    use crate::ported::builtin::{BREAKS, LOOPS, RETFLAG};
8053    use crate::ported::init::zleentry;
8054    use crate::ported::jobs::stat as jst;
8055    use crate::ported::jobs::{
8056        addproc, clearoldjobtab, deletejob, hasprocs, initjob, makerunning, pipecleanfilelist,
8057        printjob, spawnjob, waitjobs, CURJOB, LASTVAL2, PREVJOB,
8058    };
8059    use crate::ported::modules::clone::{coprocin, coprocout};
8060    use crate::ported::signals::killjb;
8061    use crate::ported::signals_h::{
8062        queue_signal_level, queue_signals, restore_queue_signals, unqueue_signals,
8063    };
8064    use crate::ported::utils::read_loop;
8065    use crate::ported::zsh_h::{
8066        jobbing, INTERACTIVE, LONGLISTJOBS, STAT_SUBJOB_ORPHANED, WC_PIPE, WC_PIPE_END,
8067        WC_PIPE_TYPE, WC_SUBLIST_COPROC, WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, ZLE_CMD_TRASH, Z_ASYNC,
8068        Z_DISOWN, Z_TIMED,
8069    };
8070
8071    // c:1731 — `static int lastwj, lpforked;` (persist across calls).
8072    static LASTWJ: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8073    static LPFORKED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8074    // c:465 (exec.c) — `static struct timespec list_pipe_start;`. Used as
8075    // the addproc bgtime for the re-forked super-job leader (c:1841).
8076    static LIST_PIPE_START: std::sync::Mutex<Option<std::time::Instant>> =
8077        std::sync::Mutex::new(None);
8078    // c:128 (jobs.c) — `int prev_errflag, prev_breaks, errbrk_saved;`. The
8079    // setter is in the not-yet-ported reaping path, so these stay 0 here.
8080    static ERRBRK_SAVED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8081    static PREV_ERRFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8082    static PREV_BREAKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8083
8084    let jt = JOBTAB.get_or_init(|| std::sync::Mutex::new(Vec::new()));
8085    // Read/write the shared thisjob slot.
8086    let thisjob_set = |v: i32| {
8087        if let Some(m) = THISJOB.get() {
8088            *m.lock().unwrap() = v;
8089        }
8090    };
8091    let thisjob_get = || THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
8092
8093    let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:1728
8094    let mut slflags = WC_SUBLIST_FLAGS(slcode); // c:1729
8095    let code = state.prog.prog[state.pc]; // c:1730 `wordcode code = *state->pc++;`
8096    state.pc += 1;
8097
8098    // c:1733-1736 — non-pipe, non-timed sublist short-circuits with the
8099    // negated-empty status.
8100    if wc_code(code) != WC_PIPE && (how & Z_TIMED) == 0 {
8101        let r = i32::from((slflags & WC_SUBLIST_NOT) != 0);
8102        LASTVAL.store(r, Ordering::Relaxed);
8103        return r;
8104    }
8105    let mut last1 = last1;
8106    if (slflags & WC_SUBLIST_NOT) != 0 {
8107        last1 = 0; // c:1736
8108    }
8109    let mut how = how;
8110
8111    queue_signals(); // c:1744
8112
8113    let pj = thisjob_get(); // c:1746 — `pj = thisjob;`
8114    let mut ipipe: [i32; 2] = [0, 0]; // c:1747
8115    let mut opipe: [i32; 2] = [0, 0];
8116    child_block(); // c:1748
8117
8118    // c:1755 — `thisjob = newjob = initjob();` (Rust jobtab grows on
8119    // demand, so initjob never fails; the -1 bailout is unreachable).
8120    let newjob = {
8121        let mut g = jt.lock().unwrap();
8122        initjob(&mut g)
8123    };
8124    thisjob_set(newjob as i32);
8125    if (how & Z_TIMED) != 0 {
8126        // c:1760-1761
8127        let mut g = jt.lock().unwrap();
8128        g[newjob].stat |= jst::TIMED;
8129    }
8130
8131    if (slflags & WC_SUBLIST_COPROC) != 0 {
8132        // c:1763-1782
8133        how = Z_ASYNC; // c:1764
8134        if coprocin.load(Ordering::Relaxed) >= 0 {
8135            zclose(coprocin.load(Ordering::Relaxed)); // c:1766
8136            zclose(coprocout.load(Ordering::Relaxed)); // c:1767
8137        }
8138        if mpipe(&mut ipipe) < 0 {
8139            // c:1769-1771
8140            coprocin.store(-1, Ordering::Relaxed);
8141            coprocout.store(-1, Ordering::Relaxed);
8142            slflags &= !WC_SUBLIST_COPROC;
8143        } else if mpipe(&mut opipe) < 0 {
8144            // c:1772-1776
8145            unsafe {
8146                libc::close(ipipe[0]);
8147                libc::close(ipipe[1]);
8148            }
8149            coprocin.store(-1, Ordering::Relaxed);
8150            coprocout.store(-1, Ordering::Relaxed);
8151            slflags &= !WC_SUBLIST_COPROC;
8152        } else {
8153            // c:1777-1781
8154            coprocin.store(ipipe[0], Ordering::Relaxed);
8155            coprocout.store(opipe[1], Ordering::Relaxed);
8156            fdtable_set(ipipe[0], FDT_UNUSED);
8157            fdtable_set(opipe[1], FDT_UNUSED);
8158        }
8159    }
8160
8161    // c:1788-1793 — `if (!pline_level++) { ... }`.
8162    let prev_pline = pline_level.fetch_add(1, Ordering::Relaxed);
8163    if prev_pline == 0 {
8164        list_pipe_pid.store(0, Ordering::Relaxed); // c:1789
8165        nowait.store(0, Ordering::Relaxed); // c:1790
8166        simple_pline.store(
8167            i32::from(WC_PIPE_TYPE(code) == WC_PIPE_END),
8168            Ordering::Relaxed,
8169        ); // c:1791
8170        list_pipe_job.store(newjob as i32, Ordering::Relaxed); // c:1792
8171    }
8172    LASTWJ.store(0, Ordering::Relaxed); // c:1794
8173    LPFORKED.store(0, Ordering::Relaxed);
8174    execpline2(state, code, how, opipe[0], ipipe[1], last1); // c:1795
8175    pline_level.fetch_sub(1, Ordering::Relaxed); // c:1796
8176
8177    if (how & Z_ASYNC) != 0 {
8178        // c:1797-1818
8179        clearoldjobtab(); // c:1798
8180        LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1799
8181
8182        if thisjob_get() == list_pipe_job.load(Ordering::Relaxed) {
8183            list_pipe_job.store(0, Ordering::Relaxed); // c:1801-1802
8184        }
8185        {
8186            let mut g = jt.lock().unwrap();
8187            let tj = thisjob_get();
8188            if tj >= 0 {
8189                g[tj as usize].stat |= jst::NOSTTY; // c:1803
8190            }
8191        }
8192        if (slflags & WC_SUBLIST_COPROC) != 0 {
8193            zclose(ipipe[1]); // c:1805
8194            zclose(opipe[0]); // c:1806
8195        }
8196        if (how & Z_DISOWN) != 0 {
8197            // c:1808-1812
8198            let tj = thisjob_get();
8199            if tj >= 0 {
8200                let mut g = jt.lock().unwrap();
8201                pipecleanfilelist(&mut g[tj as usize], false); // c:1809
8202                deletejob(&mut g[tj as usize], true); // c:1810
8203            }
8204            thisjob_set(-1); // c:1811
8205        } else {
8206            spawnjob(); // c:1814 (locks JOBTAB internally — no guard held)
8207        }
8208        child_unblock(); // c:1815
8209        unqueue_signals(); // c:1816
8210        LASTVAL.store(0, Ordering::Relaxed); // c:1818 `return lastval = 0;`
8211        return 0;
8212    }
8213
8214    // c:1819-2033 — synchronous branch.
8215    if newjob as i32 != LASTWJ.load(Ordering::Relaxed) {
8216        // c:1820
8217        let mut jn_idx = newjob; // `Job jn = jobtab + newjob;`
8218
8219        // c:1824-1825 — a list_pipe sub-shell child exits here.
8220        if newjob as i32 == list_pipe_job.load(Ordering::Relaxed)
8221            && list_pipe_child.load(Ordering::Relaxed) != 0
8222        {
8223            unsafe { libc::_exit(0) };
8224        }
8225
8226        LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1826
8227        thisjob_set(newjob as i32);
8228
8229        // c:1828-1830 — suppress the job announcement in nested pipes.
8230        {
8231            let mut g = jt.lock().unwrap();
8232            let noprint = list_pipe.load(Ordering::Relaxed) != 0
8233                || (pline_level.load(Ordering::Relaxed) != 0
8234                    && (how & Z_TIMED) == 0
8235                    && (g[jn_idx].stat & jst::NOSTTY) == 0);
8236            if noprint {
8237                g[jn_idx].stat |= jst::NOPRINT;
8238            }
8239        }
8240
8241        if nowait.load(Ordering::Relaxed) != 0 {
8242            // c:1832-1882
8243            if pline_level.load(Ordering::Relaxed) == 0 {
8244                // c:1833-1875
8245                *CURJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap() =
8246                    newjob as i32; // c:1836
8247                // c:1838 — DPUTS(!list_pipe_pid, "invalid list_pipe_pid").
8248                // c:1840-1841 — record the re-forked leader in the super-job.
8249                {
8250                    let txt = LIST_PIPE_TEXT
8251                        .lock()
8252                        .map(|s| s.clone())
8253                        .unwrap_or_default();
8254                    let bgt = *LIST_PIPE_START.lock().unwrap();
8255                    let mut g = jt.lock().unwrap();
8256                    addproc(
8257                        &mut g[jn_idx],
8258                        list_pipe_pid.load(Ordering::Relaxed),
8259                        &txt,
8260                        false,
8261                        bgt,
8262                        -1,
8263                        -1,
8264                    );
8265                }
8266                {
8267                    let mut g = jt.lock().unwrap();
8268                    // c:1845 — `if (!jn->procs->next || lpforked == 2)`.
8269                    if g[jn_idx].procs.len() <= 1 || LPFORKED.load(Ordering::Relaxed) == 2 {
8270                        g[jn_idx].gleader = list_pipe_pid.load(Ordering::Relaxed); // c:1845
8271                        g[jn_idx].stat |= jst::SUBLEADER; // c:1847
8272                        // c:1852-1861 — adopt any orphaned subjob; we
8273                        // become its super-job.
8274                        for jobsub in 1..g.len() {
8275                            if (g[jobsub].stat & STAT_SUBJOB_ORPHANED) != 0 {
8276                                g[jn_idx].other = jobsub as i32; // c:1855
8277                                g[jn_idx].stat |= jst::SUPERJOB; // c:1856
8278                                g[jobsub].stat &= !STAT_SUBJOB_ORPHANED; // c:1857
8279                                g[jobsub].other = list_pipe_pid.load(Ordering::Relaxed); // c:1858
8280                            }
8281                        }
8282                    }
8283                    // c:1863-1869 — copy a stopped proc status from the
8284                    // subjob onto our last proc.
8285                    let other = g[jn_idx].other as usize;
8286                    let stopped = if other < g.len() {
8287                        g[other].procs.iter().find(|p| p.is_stopped()).map(|p| p.status)
8288                    } else {
8289                        None
8290                    };
8291                    if let Some(st) = stopped {
8292                        if let Some(last) = g[jn_idx].procs.last_mut() {
8293                            last.status = st;
8294                        }
8295                    }
8296                    // c:1870-1872
8297                    g[jn_idx].stat &= !(jst::DONE | jst::NOPRINT);
8298                    g[jn_idx].stat |=
8299                        jst::STOPPED | jst::CHANGED | jst::LOCKED | jst::INUSE;
8300                }
8301                // c:1875 — printjob(jn, !!isset(LONGLISTJOBS), 1).
8302                {
8303                    let g = jt.lock().unwrap();
8304                    let cur = *CURJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap();
8305                    let prev = *PREVJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap();
8306                    let s = printjob(
8307                        &g[jn_idx],
8308                        jn_idx,
8309                        i32::from(isset(LONGLISTJOBS)),
8310                        if cur >= 0 { Some(cur as usize) } else { None },
8311                        if prev >= 0 { Some(prev as usize) } else { None },
8312                    );
8313                    if !s.is_empty() {
8314                        eprintln!("{}", s);
8315                    }
8316                }
8317            } else if newjob as i32 != list_pipe_job.load(Ordering::Relaxed) {
8318                let mut g = jt.lock().unwrap();
8319                deletejob(&mut g[jn_idx], false); // c:1878
8320            } else {
8321                LASTWJ.store(-1, Ordering::Relaxed); // c:1879
8322            }
8323        }
8324
8325        ERRBRK_SAVED.store(0, Ordering::Relaxed); // c:1883
8326        // c:1884-2015 — `for (; !nowait;)` wait / continue-fork loop.
8327        loop {
8328            if nowait.load(Ordering::Relaxed) != 0 {
8329                break;
8330            }
8331            if list_pipe_child.load(Ordering::Relaxed) != 0 {
8332                // c:1886-1887
8333                let mut g = jt.lock().unwrap();
8334                g[jn_idx].stat |= jst::NOPRINT;
8335                makerunning(&mut g, jn_idx);
8336            }
8337            // c:1889-1894 — wait unless the job is LOCKED.
8338            let locked = {
8339                let g = jt.lock().unwrap();
8340                (g[jn_idx].stat & jst::LOCKED) != 0
8341            };
8342            let updated;
8343            if !locked {
8344                let tj = thisjob_get();
8345                {
8346                    let mut g = jt.lock().unwrap();
8347                    updated = hasprocs(&g, tj as usize); // c:1890
8348                    waitjobs(&mut g, tj as usize); // c:1891
8349                }
8350                child_block(); // c:1892
8351            } else {
8352                updated = false; // c:1894
8353            }
8354            // c:1895-1902 — nudge the signal queue when the LHS job is
8355            // still running but we saw no update.
8356            let lpj = list_pipe_job.load(Ordering::Relaxed);
8357            let nudge = !updated
8358                && lpj != 0
8359                && {
8360                    let g = jt.lock().unwrap();
8361                    (lpj as usize) < g.len()
8362                        && hasprocs(&g, lpj as usize)
8363                        && (g[lpj as usize].stat & jst::STOPPED) == 0
8364                };
8365            if nudge {
8366                let q = queue_signal_level();
8367                child_unblock();
8368                child_block();
8369                dont_queue_signals();
8370                restore_queue_signals(q);
8371            }
8372            // c:1903-1907 — forward a fatal signal from a done child.
8373            let jn_done = {
8374                let g = jt.lock().unwrap();
8375                (g[jn_idx].stat & jst::DONE) != 0
8376            };
8377            if list_pipe_child.load(Ordering::Relaxed) != 0
8378                && jn_done
8379                && (LASTVAL2.load(Ordering::Relaxed) & 0o200) != 0
8380            {
8381                unsafe {
8382                    libc::killpg(
8383                        mypgrp.load(Ordering::Relaxed),
8384                        LASTVAL2.load(Ordering::Relaxed) & !0o200,
8385                    );
8386                }
8387            }
8388            // c:1908-1921 — a pipeline with the shell running the RHS was
8389            // stopped; fork to let it continue.
8390            let stop_fork = list_pipe_child.load(Ordering::Relaxed) == 0
8391                && LPFORKED.load(Ordering::Relaxed) == 0
8392                && subsh.load(Ordering::Relaxed) == 0
8393                && jobbing()
8394                && (list_pipe.load(Ordering::Relaxed) != 0
8395                    || last1 != 0
8396                    || pline_level.load(Ordering::Relaxed) != 0)
8397                && {
8398                    let g = jt.lock().unwrap();
8399                    (g[jn_idx].stat & jst::STOPPED) != 0
8400                        || (lpj != 0
8401                            && pline_level.load(Ordering::Relaxed) != 0
8402                            && (lpj as usize) < g.len()
8403                            && (g[lpj as usize].stat & jst::STOPPED) != 0)
8404                };
8405            if stop_fork {
8406                let mut synch: [i32; 2] = [0, 0]; // c:1913
8407                let mut bgtime = ZshTimespec {
8408                    tv_sec: 0,
8409                    tv_nsec: 0,
8410                };
8411                let mut pid: libc::pid_t = 0;
8412                let pipe_failed = unsafe { libc::pipe(synch.as_mut_ptr()) } < 0; // c:1922
8413                if !pipe_failed {
8414                    pid = zfork(Some(&mut bgtime)); // c:1922
8415                }
8416                if pipe_failed || pid == -1 {
8417                    // c:1923-1935 — failure: can't suspend, resume the job.
8418                    if pid < 0 {
8419                        unsafe {
8420                            libc::close(synch[0]);
8421                            libc::close(synch[1]);
8422                        }
8423                    } else {
8424                        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
8425                        // c:1926
8426                    }
8427                    let _ = zleentry(ZLE_CMD_TRASH); // c:1929
8428                    eprintln!("zsh: job can't be suspended"); // c:1930
8429                    {
8430                        let mut g = jt.lock().unwrap();
8431                        makerunning(&mut g, jn_idx); // c:1932
8432                    }
8433                    killjb(jn_idx, libc::SIGCONT); // c:1933
8434                    thisjob_set(newjob as i32); // c:1934
8435                } else if pid != 0 {
8436                    // c:1936-1973 — parent: job control lives here.
8437                    let gl = {
8438                        let g = jt.lock().unwrap();
8439                        g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0)
8440                    };
8441                    LPFORKED.store(
8442                        if unsafe { libc::killpg(gl, 0) } == -1 { 2 } else { 1 },
8443                        Ordering::Relaxed,
8444                    ); // c:1951-1952
8445                    list_pipe_pid.store(pid, Ordering::Relaxed); // c:1953
8446                    *LIST_PIPE_START.lock().unwrap() = Some(std::time::Instant::now()); // c:1954
8447                    nowait.store(1, Ordering::Relaxed); // c:1955
8448                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:1956
8449                    BREAKS.store(LOOPS.load(Ordering::SeqCst), Ordering::SeqCst); // c:1957
8450                    unsafe { libc::close(synch[1]) }; // c:1958
8451                    let mut dummy = [0u8; 1];
8452                    let _ = read_loop(synch[0], &mut dummy); // c:1959
8453                    unsafe { libc::close(synch[0]) }; // c:1960
8454                    // c:1962-1970 — link super/sub jobs if we're still live.
8455                    let jn_done2 = {
8456                        let g = jt.lock().unwrap();
8457                        (g[jn_idx].stat & jst::DONE) != 0
8458                    };
8459                    if !jn_done2 {
8460                        let mut g = jt.lock().unwrap();
8461                        g[lpj as usize].other = newjob as i32; // c:1964
8462                        g[lpj as usize].stat |= jst::SUPERJOB; // c:1965
8463                        g[jn_idx].stat |= jst::SUBJOB | jst::NOPRINT; // c:1966
8464                        g[jn_idx].other = list_pipe_pid.load(Ordering::Relaxed); // c:1967
8465                        if hasprocs(&g, lpj as usize) {
8466                            g[jn_idx].gleader = g[lpj as usize].gleader; // c:1968-1969
8467                        }
8468                    }
8469                    // c:1971-1972 — stop the LHS group so the whole pipe
8470                    // suspends together.
8471                    let (do_kill, gl2) = {
8472                        let g = jt.lock().unwrap();
8473                        (
8474                            (list_pipe.load(Ordering::Relaxed) != 0 || last1 != 0)
8475                                && hasprocs(&g, lpj as usize),
8476                            g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0),
8477                        )
8478                    };
8479                    if do_kill {
8480                        unsafe { libc::killpg(gl2, libc::SIGSTOP) };
8481                    }
8482                    break; // c:1973
8483                } else {
8484                    // c:1975-2004 — child: become our own group, stop, then
8485                    // continue as the RHS sub-shell.
8486                    unsafe { libc::close(synch[0]) }; // c:1976
8487                    entersubsh(esub::ASYNC, None); // c:1977
8488                    let mypid = unsafe { libc::getpid() };
8489                    mypgrp.store(mypid, Ordering::Relaxed);
8490                    unsafe { libc::setpgid(0, mypid) }; // c:1992 setpgrp
8491                    unsafe { libc::close(synch[1]) }; // c:1993
8492                    unsafe { libc::kill(mypid, libc::SIGSTOP) }; // c:1994
8493                    list_pipe.store(0, Ordering::Relaxed); // c:1995
8494                    list_pipe_child.store(1, Ordering::Relaxed); // c:1996
8495                    dosetopt(INTERACTIVE, 0, 0); // c:1997 `opts[INTERACTIVE] = 0`
8496                    if ERRBRK_SAVED.load(Ordering::Relaxed) != 0 {
8497                        // c:1998-2003 — restore saved break/errflag state.
8498                        errflag.store(
8499                            PREV_ERRFLAG.load(Ordering::Relaxed)
8500                                | (errflag.load(Ordering::Relaxed) & ERRFLAG_INT),
8501                            Ordering::Relaxed,
8502                        );
8503                        BREAKS.store(PREV_BREAKS.load(Ordering::Relaxed), Ordering::SeqCst);
8504                    }
8505                    break;
8506                }
8507            } else if subsh.load(Ordering::Relaxed) != 0 && {
8508                let g = jt.lock().unwrap();
8509                (g[jn_idx].stat & jst::STOPPED) != 0
8510            } {
8511                // c:2008-2012
8512                if thisjob_get() == newjob as i32 {
8513                    let mut g = jt.lock().unwrap();
8514                    makerunning(&mut g, jn_idx);
8515                } else {
8516                    thisjob_set(newjob as i32);
8517                }
8518            } else {
8519                break; // c:2015
8520            }
8521        }
8522
8523        child_unblock(); // c:2017
8524        unqueue_signals(); // c:2018
8525
8526        // c:2020-2026 — a signal-killed list_pipe: drop this job and
8527        // forward the signal to the enclosing job's group.
8528        let lastval_now = LASTVAL.load(Ordering::Relaxed);
8529        let drop_and_signal = list_pipe.load(Ordering::Relaxed) != 0
8530            && (lastval_now & 0o200) != 0
8531            && pj >= 0
8532            && {
8533                let g = jt.lock().unwrap();
8534                (g[jn_idx].stat & jst::INUSE) == 0 || (g[jn_idx].stat & jst::DONE) != 0
8535            };
8536        if drop_and_signal {
8537            {
8538                let mut g = jt.lock().unwrap();
8539                deletejob(&mut g[jn_idx], false); // c:2022
8540            }
8541            jn_idx = pj as usize; // c:2023 `jn = jobtab + pj;`
8542            let gl = {
8543                let g = jt.lock().unwrap();
8544                g[jn_idx].gleader
8545            };
8546            if gl != 0 {
8547                killjb(jn_idx, lastval_now & !0o200); // c:2025
8548            }
8549        }
8550        // c:2027-2030 — final cleanup deletejob for a done/child job.
8551        let final_delete = list_pipe_child.load(Ordering::Relaxed) != 0 || {
8552            let g = jt.lock().unwrap();
8553            (g[jn_idx].stat & jst::DONE) != 0
8554                && (list_pipe.load(Ordering::Relaxed) != 0
8555                    || (pline_level.load(Ordering::Relaxed) != 0
8556                        && (g[jn_idx].stat & jst::SUBJOB) == 0))
8557        };
8558        if final_delete {
8559            let mut g = jt.lock().unwrap();
8560            deletejob(&mut g[jn_idx], false); // c:2030
8561        }
8562        thisjob_set(pj); // c:2031
8563    } else {
8564        unqueue_signals(); // c:2034
8565    }
8566
8567    // c:2035-2036 — apply `!` negation to the pipeline status.
8568    if (slflags & WC_SUBLIST_NOT) != 0
8569        && errflag.load(Ordering::Relaxed) == 0
8570        && RETFLAG.load(Ordering::SeqCst) == 0
8571    {
8572        let lv = LASTVAL.load(Ordering::Relaxed);
8573        LASTVAL.store(i32::from(lv == 0), Ordering::Relaxed);
8574    }
8575
8576    if pline_level.load(Ordering::Relaxed) == 0 {
8577        simple_pline.store(old_simple_pline, Ordering::Relaxed); // c:2039
8578    }
8579    LASTVAL.load(Ordering::Relaxed) // c:2040 `return lastval;`
8580}
8581
8582// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
8583// inlined at every call site (execsimple, execpline) as the match
8584// expression that selects the right execX function. There's no
8585// separate Rust fn for it because:
8586//   - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
8587//     occupies the canonical name (handling precommand modifiers).
8588//   - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
8589//     a table lookup at exec.c:5499 — not a separate function.
8590#[cfg(any())]
8591mod _execcmd_tail_doc_anchor {
8592    // c:2901-3700 — see inlined match in execpline + execsimple above.
8593    // c:5499 — execfuncs[] table inlined as the same match.
8594}
8595
8596// --- loop.c entries ---------------------------------------------------
8597
8598/// Port of `execfor(Estate state, int do_exec)` from `Src/loop.c:50-202`.
8599/// `for var in args; do body; done` and the C-style `for ((init;cond;adv))`
8600/// variant. WC_FOR_TYPE distinguishes PPARAM (use $@) / LIST (explicit
8601/// words) / COND (C-style).
8602pub fn execfor(state: &mut estate, do_exec: i32) -> i32 {
8603    use crate::ported::zsh_h::Z_END;
8604    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:54
8605    let iscond = WC_FOR_TYPE(code) == WC_FOR_COND; // c:55
8606    let mut last_iter = false; // c:57 — `int last = 0;`
8607    let mut val: i64 = 0; // c:59
8608    let mut vars: Vec<String> = Vec::new();
8609    let mut args: Vec<String> = Vec::new();
8610    let mut cond_expr: String = String::new();
8611    let mut advance_expr: String = String::new();
8612    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:62-63
8613    let end_pc = state.pc + WC_FOR_SKIP(code) as usize; // c:65
8614    let mut ctok = 0i32;
8615    let mut atok = 0i32;
8616    if iscond {
8617        // c:68-82 — C-style for: init expr at top, then cond/advance.
8618        let init = ecgetstr(state, EC_NODUP, None); // c:68
8619        let init_sub = singsub(&init); // c:69
8620        if isset(XTRACE) {
8621            // c:70-75
8622            let init_show = untokenize(&init_sub);
8623            printprompt4();
8624            eprintln!("{}", init_show);
8625        }
8626        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8627            let _ = wc_matheval(&init_sub); // c:77 — `matheval(str);`
8628        }
8629        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8630            // c:79-82
8631            state.pc = end_pc;
8632            simple_pline.store(old_simple_pline, Ordering::Relaxed);
8633            return 1;
8634        }
8635        cond_expr = ecgetstr(state, EC_NODUP, Some(&mut ctok)); // c:83
8636        advance_expr = ecgetstr(state, EC_NODUP, Some(&mut atok)); // c:84
8637    } else {
8638        // c:86 — `vars = ecgetlist(state, *state->pc++, EC_NODUP, NULL);`
8639        let count = state.prog.prog[state.pc] as usize;
8640        state.pc += 1;
8641        vars = ecgetlist(state, count, EC_NODUP, None);
8642        if WC_FOR_TYPE(code) == WC_FOR_LIST {
8643            // c:88-100 — explicit `for var in words`
8644            let mut htok = 0i32;
8645            let arg_count = state.prog.prog[state.pc] as usize;
8646            state.pc += 1;
8647            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
8648            if args.is_empty() {
8649                state.pc = end_pc;
8650                simple_pline.store(old_simple_pline, Ordering::Relaxed);
8651                return 0;
8652            }
8653            if htok != 0 {
8654                execsubst(&mut args); // c:96
8655                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8656                    state.pc = end_pc;
8657                    simple_pline.store(old_simple_pline, Ordering::Relaxed);
8658                    return 1;
8659                }
8660            }
8661        } else {
8662            // c:102-107 — implicit `for var` (no `in` clause) uses
8663            // the positional params $@ from PPARAMS (params.rs Mutex).
8664            args = crate::ported::builtin::PPARAMS
8665                .lock()
8666                .map(|p| p.clone())
8667                .unwrap_or_default();
8668        }
8669    }
8670    // c:111-112 — empty args ⇒ lastval = 0.
8671    if !iscond && args.is_empty() {
8672        LASTVAL.store(0, Ordering::Relaxed);
8673    }
8674    LOOPS.fetch_add(1, Ordering::SeqCst); // c:114 — `loops++;`
8675    pushheap(); // c:115
8676    cmdpush(CS_FOR as u8); // c:116
8677    let loop_pc = state.pc; // c:117
8678    let mut args_iter = args.into_iter();
8679    while !last_iter {
8680        if iscond {
8681            // c:119-138 — eval cond expression.
8682            let mut cs = cond_expr.clone();
8683            if ctok != 0 {
8684                cs = singsub(&cs);
8685            }
8686            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8687                let trimmed = cs.trim_start();
8688                if !trimmed.is_empty() {
8689                    if isset(XTRACE) {
8690                        printprompt4();
8691                        eprintln!("{}", trimmed);
8692                    }
8693                    val = wc_mathevali(trimmed).unwrap_or(0);
8694                } else {
8695                    val = 1;
8696                }
8697            }
8698            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8699                if BREAKS.load(Ordering::SeqCst) > 0 {
8700                    BREAKS.fetch_sub(1, Ordering::SeqCst);
8701                }
8702                LASTVAL.store(1, Ordering::Relaxed);
8703                break;
8704            }
8705            if val == 0 {
8706                break;
8707            }
8708        } else {
8709            // c:140-162 — for var binding from args.
8710            let mut count = 0;
8711            for name in &vars {
8712                let value = match args_iter.next() {
8713                    Some(v) => v,
8714                    None => {
8715                        if count != 0 {
8716                            last_iter = true;
8717                            String::new()
8718                        } else {
8719                            break;
8720                        }
8721                    }
8722                };
8723                if isset(XTRACE) {
8724                    printprompt4();
8725                    eprintln!("{}={}", name, value);
8726                }
8727                setloopvar(name, &value);
8728                count += 1;
8729            }
8730            if count == 0 {
8731                break;
8732            }
8733        }
8734        state.pc = loop_pc; // c:163
8735        let _do_exec_now = do_exec != 0 && !args_iter.clone().any(|_| true); // c:164 — `do_exec && args && empty(args)`
8736        let _ = execlist(state, 1, if _do_exec_now { 1 } else { 0 });
8737        // c:166-169 — breaks/continue handling.
8738        if BREAKS.load(Ordering::SeqCst) > 0 {
8739            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
8740            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
8741                break;
8742            }
8743            CONTFLAG.store(0, Ordering::SeqCst);
8744        }
8745        if RETFLAG.load(Ordering::SeqCst) != 0 {
8746            break;
8747        }
8748        // c:170-178 — C-style advance step.
8749        if iscond && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8750            let mut adv = advance_expr.clone();
8751            if atok != 0 {
8752                adv = singsub(&adv);
8753            }
8754            if isset(XTRACE) {
8755                printprompt4();
8756                eprintln!("{}", adv);
8757            }
8758            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8759                let _ = wc_matheval(&adv);
8760            }
8761        }
8762        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8763            if BREAKS.load(Ordering::SeqCst) > 0 {
8764                BREAKS.fetch_sub(1, Ordering::SeqCst);
8765            }
8766            LASTVAL.store(1, Ordering::Relaxed);
8767            break;
8768        }
8769        freeheap(); // c:184
8770    }
8771    popheap(); // c:186
8772    cmdpop(); // c:187
8773    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:188
8774    simple_pline.store(old_simple_pline, Ordering::Relaxed);
8775    state.pc = end_pc;
8776    this_noerrexit.store(1, Ordering::Relaxed);
8777    let _ = Z_END;
8778    LASTVAL.load(Ordering::Relaxed)
8779}
8780
8781/// Port of `execselect(Estate state, UNUSED(int do_exec))` from
8782/// `Src/loop.c:217-410`. `select var in words; do body; done` REPL.
8783pub fn execselect(state: &mut estate, _do_exec: i32) -> i32 {
8784    // The full select body manages a REPL prompt, terminal columns,
8785    // selectlist redraw, etc. The `selectlist` helper at loop.rs:130
8786    // already ports c:347 (menu display). Structural execselect:
8787    // c:225-410 — read vars + words like execfor, then loop on stdin
8788    // input prompting via PROMPT3, set var=word, run body.
8789    let code = state.prog.prog[state.pc.wrapping_sub(1)];
8790    let end_pc = state.pc + WC_FOR_SKIP(code) as usize;
8791    // c:228-237 — read var name + words. Skip body and use existing
8792    // bridge handler at BUILTIN_RUN_SELECT for actual REPL until full
8793    // wordcode driver lands.
8794    state.pc = end_pc;
8795    this_noerrexit.store(1, Ordering::Relaxed);
8796    LASTVAL.load(Ordering::Relaxed)
8797}
8798
8799/// Port of `execwhile(Estate state, UNUSED(int do_exec))` from
8800/// `Src/loop.c:413-498`. `while/until cond; do body; done`.
8801pub fn execwhile(state: &mut estate, _do_exec: i32) -> i32 {
8802    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:417
8803    let isuntil = WC_WHILE_TYPE(code) == WC_WHILE_UNTIL; // c:419
8804    let end_pc = state.pc + WC_WHILE_SKIP(code) as usize; // c:422
8805    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:423
8806    let mut oldval: i32 = 0; // c:424
8807    pushheap(); // c:425
8808    cmdpush(if isuntil {
8809        CS_UNTIL as u8
8810    } else {
8811        CS_WHILE as u8
8812    }); // c:426
8813    LOOPS.fetch_add(1, Ordering::SeqCst); // c:427
8814    let loop_pc = state.pc; // c:428
8815    let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:419
8816                                                                 // c:430-456 — empty-loop fast path. If loop body is two WC_ENDs,
8817                                                                 // sit in a tight signal-wait loop until ^C breaks us.
8818    if state.prog.prog.get(loop_pc) == Some(&WC_END)
8819        && state.prog.prog.get(loop_pc + 1) == Some(&WC_END)
8820    {
8821        simple_pline.store(1, Ordering::Relaxed);
8822        // c:438-439 — spin until breaks.
8823        while BREAKS.load(Ordering::SeqCst) == 0 {
8824            std::thread::yield_now();
8825        }
8826        BREAKS.fetch_sub(1, Ordering::SeqCst);
8827        simple_pline.store(old_simple_pline, Ordering::Relaxed);
8828    } else {
8829        // c:441-485 — normal loop.
8830        loop {
8831            state.pc = loop_pc; // c:442
8832            noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:443
8833            simple_pline.store(1, Ordering::Relaxed); // c:446
8834            let _ = execlist(state, 1, 0); // c:448 — exec cond.
8835            simple_pline.store(old_simple_pline, Ordering::Relaxed);
8836            noerrexit.store(olderrexit, Ordering::Relaxed); // c:451
8837            let cond_status = LASTVAL.load(Ordering::Relaxed); // c:452
8838                                                               // c:453-460 — `if (!((lastval == 0) ^ isuntil)) break;`
8839            let cond_passed = (cond_status == 0) ^ isuntil;
8840            if !cond_passed {
8841                if BREAKS.load(Ordering::SeqCst) > 0 {
8842                    BREAKS.fetch_sub(1, Ordering::SeqCst);
8843                }
8844                if RETFLAG.load(Ordering::SeqCst) == 0 {
8845                    LASTVAL.store(oldval, Ordering::Relaxed);
8846                }
8847                break;
8848            }
8849            if RETFLAG.load(Ordering::SeqCst) != 0 {
8850                // c:461
8851                if BREAKS.load(Ordering::SeqCst) > 0 {
8852                    BREAKS.fetch_sub(1, Ordering::SeqCst);
8853                }
8854                break;
8855            }
8856            simple_pline.store(1, Ordering::Relaxed); // c:468
8857            let _ = execlist(state, 1, 0); // c:470 — exec body.
8858            simple_pline.store(old_simple_pline, Ordering::Relaxed);
8859            // c:472-477 — breaks/continue handling.
8860            if BREAKS.load(Ordering::SeqCst) > 0 {
8861                let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
8862                if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
8863                    break;
8864                }
8865                CONTFLAG.store(0, Ordering::SeqCst);
8866            }
8867            // c:478-481 — errflag bail.
8868            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8869                LASTVAL.store(1, Ordering::Relaxed);
8870                break;
8871            }
8872            // c:482-483 — retflag bail.
8873            if RETFLAG.load(Ordering::SeqCst) != 0 {
8874                break;
8875            }
8876            freeheap(); // c:484
8877            oldval = LASTVAL.load(Ordering::Relaxed); // c:485
8878        }
8879    }
8880    cmdpop(); // c:489
8881    popheap(); // c:490
8882    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:491
8883    state.pc = end_pc; // c:492
8884    this_noerrexit.store(1, Ordering::Relaxed); // c:493
8885    LASTVAL.load(Ordering::Relaxed)
8886}
8887
8888/// Port of `execrepeat(Estate state, UNUSED(int do_exec))` from
8889/// `Src/loop.c:499-551`. `repeat N; do body; done`.
8890pub fn execrepeat(state: &mut estate, _do_exec: i32) -> i32 {
8891    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:503
8892    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:507
8893    let end_pc = state.pc + WC_REPEAT_SKIP(code) as usize; // c:510
8894    let mut htok = 0i32;
8895    let mut tmp = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:512
8896    if htok != 0 {
8897        tmp = singsub(&tmp); // c:514
8898        tmp = untokenize(&tmp); // c:515
8899    }
8900    let count = wc_mathevali(&tmp).unwrap_or(0); // c:517
8901    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8902        simple_pline.store(old_simple_pline, Ordering::Relaxed);
8903        return 1;
8904    }
8905    LASTVAL.store(0, Ordering::Relaxed); // c:520
8906    pushheap(); // c:521
8907    cmdpush(CS_REPEAT as u8); // c:522
8908    LOOPS.fetch_add(1, Ordering::SeqCst); // c:523
8909    let loop_pc = state.pc; // c:524
8910    let mut remaining = count;
8911    while remaining > 0 {
8912        // c:525
8913        remaining -= 1;
8914        state.pc = loop_pc;
8915        let _ = execlist(state, 1, 0); // c:527
8916        freeheap(); // c:528
8917                    // c:529-534 — breaks/continue handling.
8918        if BREAKS.load(Ordering::SeqCst) > 0 {
8919            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
8920            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
8921                break;
8922            }
8923            CONTFLAG.store(0, Ordering::SeqCst);
8924        }
8925        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8926            // c:536-538
8927            LASTVAL.store(1, Ordering::Relaxed);
8928            break;
8929        }
8930        if RETFLAG.load(Ordering::SeqCst) != 0 {
8931            // c:540
8932            break;
8933        }
8934    }
8935    cmdpop(); // c:544
8936    popheap(); // c:545
8937    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:546
8938    simple_pline.store(old_simple_pline, Ordering::Relaxed);
8939    state.pc = end_pc; // c:548
8940    this_noerrexit.store(1, Ordering::Relaxed); // c:549
8941    LASTVAL.load(Ordering::Relaxed)
8942}
8943
8944/// Port of `execif(Estate state, int do_exec)` from `Src/loop.c:553-598`.
8945/// `if cond; then body; elif ...; else ...; fi`.
8946pub fn execif(state: &mut estate, do_exec: i32) -> i32 {
8947    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:558
8948    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:559
8949    let end_pc = state.pc + WC_IF_SKIP(code0) as usize; // c:560
8950    noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:562
8951    let mut s = 0i32; // c:557 — `s = 0`
8952    let mut run = 0i32; // c:557 — `run = 0`
8953    while state.pc < end_pc {
8954        // c:563
8955        let code = state.prog.prog[state.pc];
8956        state.pc += 1;
8957        // c:565-571 — non-IF, or IF_ELSE: break out.
8958        if wc_code(code) != WC_IF || WC_IF_TYPE(code) == WC_IF_ELSE {
8959            run = if wc_code(code) == WC_IF && WC_IF_TYPE(code) == WC_IF_ELSE {
8960                2
8961            } else {
8962                1
8963            };
8964            if run == 1 {
8965                state.pc -= 1; // back up onto the body header
8966            }
8967            break;
8968        }
8969        let next_pc = state.pc + WC_IF_SKIP(code) as usize; // c:572
8970        cmdpush(if s != 0 { CS_ELIF as u8 } else { CS_IF as u8 }); // c:573
8971        let _ = execlist(state, 1, 0); // c:574
8972        cmdpop(); // c:575
8973                  // c:576-579 — selected branch: lastval == 0.
8974        if LASTVAL.load(Ordering::Relaxed) == 0 {
8975            run = 1;
8976            break;
8977        }
8978        if RETFLAG.load(Ordering::SeqCst) != 0 {
8979            // c:580
8980            break;
8981        }
8982        s = 1;
8983        state.pc = next_pc;
8984    }
8985    noerrexit.store(olderrexit, Ordering::Relaxed); // c:584
8986                                                    // c:585-591 — run selected branch.
8987    if run != 0 {
8988        cmdpush(if run == 2 {
8989            CS_ELSE as u8
8990        } else if s != 0 {
8991            CS_ELIFTHEN as u8
8992        } else {
8993            CS_IFTHEN as u8
8994        });
8995        let _ = execlist(state, 1, do_exec);
8996        cmdpop();
8997    } else if RETFLAG.load(Ordering::SeqCst) == 0
8998        && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
8999    {
9000        LASTVAL.store(0, Ordering::Relaxed); // c:592
9001    }
9002    state.pc = end_pc; // c:594
9003    this_noerrexit.store(1, Ordering::Relaxed); // c:595
9004    LASTVAL.load(Ordering::Relaxed)
9005}
9006
9007/// Port of `execcase(Estate state, int do_exec)` from `Src/loop.c:600-733`.
9008/// `case word in pat) body ;; ... esac` with `;;`/`;&`/`;|` separators.
9009pub fn execcase(state: &mut estate, do_exec: i32) -> i32 {
9010    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:603
9011    let end_pc = state.pc + WC_CASE_SKIP(code0) as usize; // c:607
9012                                                          // c:609-611 — read & expand the case-word.
9013    let raw_word = ecgetstr(state, EC_DUP, None);
9014    let word_sub = singsub(&raw_word);
9015    let word = untokenize(&word_sub);
9016    let mut anypatok = false; // c:613
9017    cmdpush(CS_CASE as u8); // c:615
9018    let mut code = 0u32;
9019    while state.pc < end_pc {
9020        // c:616
9021        code = state.prog.prog[state.pc];
9022        state.pc += 1;
9023        if wc_code(code) != WC_CASE {
9024            break;
9025        }
9026        let next_pc = state.pc + WC_CASE_SKIP(code) as usize; // c:621
9027        let nalts = state.prog.prog[state.pc] as i32; // c:622
9028        state.pc += 1;
9029        let mut patok = false;
9030        let mut nalts_remaining = nalts;
9031        while !patok && nalts_remaining > 0 {
9032            // c:629-672 — try each alternative pattern.
9033            // c:631-633 — `npat = state->pc[1]; spprog = state->prog->pats + npat;`
9034            // zshrs's pat-compile-on-demand path: extract raw pat text + try patcompile/pattry.
9035            queue_signals(); // c:636
9036            let mut htok = 0i32;
9037            let pat_raw = ecrawstr(&state.prog, state.pc, Some(&mut htok));
9038            let pat = if htok != 0 {
9039                singsub(&pat_raw)
9040            } else {
9041                pat_raw
9042            };
9043            if let Some(pprog) = patcompile(
9044                &{
9045                    let mut __pat_tok = (&pat).to_string();
9046                    crate::ported::glob::tokenize(&mut __pat_tok);
9047                    __pat_tok
9048                },
9049                PAT_STATIC,
9050                None,
9051            ) {
9052                // c:660 — `if (pprog && pattry(pprog, word)) patok = anypatok = 1;`
9053                if pattry(&pprog, &word) {
9054                    patok = true;
9055                    anypatok = true;
9056                }
9057            } else {
9058                zerr(&format!("bad pattern: {}", pat)); // c:657
9059            }
9060            state.pc += 2; // c:664 — `state->pc += 2;`
9061            nalts_remaining -= 1;
9062            unqueue_signals(); // c:666
9063        }
9064        state.pc += (2 * nalts_remaining) as usize; // c:668
9065        if patok {
9066            // c:672-684 — run selected arm body.
9067            let _ = execlist(
9068                state,
9069                1,
9070                ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
9071            );
9072            // c:675-682 — chain into ;& and ;| siblings.
9073            while RETFLAG.load(Ordering::SeqCst) == 0
9074                && wc_code(code) == WC_CASE
9075                && WC_CASE_TYPE(code) == WC_CASE_AND
9076                && state.pc < end_pc
9077            {
9078                state.pc = next_pc;
9079                code = state.prog.prog[state.pc];
9080                state.pc += 1;
9081                let inner_next = state.pc + WC_CASE_SKIP(code) as usize;
9082                let inner_nalts = state.prog.prog[state.pc] as usize;
9083                state.pc += 1 + 2 * inner_nalts;
9084                let _ = execlist(
9085                    state,
9086                    1,
9087                    ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
9088                );
9089                let _ = inner_next;
9090            }
9091            if WC_CASE_TYPE(code) != WC_CASE_TESTAND {
9092                break;
9093            }
9094        }
9095        state.pc = next_pc; // c:687
9096    }
9097    cmdpop(); // c:691
9098    state.pc = end_pc; // c:693
9099    if !anypatok {
9100        // c:695-696
9101        LASTVAL.store(0, Ordering::Relaxed);
9102    }
9103    this_noerrexit.store(1, Ordering::Relaxed); // c:697
9104    LASTVAL.load(Ordering::Relaxed)
9105}
9106
9107/// Port of `exectry(Estate state, int do_exec)` from `Src/loop.c:735-798`.
9108/// `{ try } always { finally }`: capture errflag/retflag/breaks/contflag
9109/// from the try-clause, reset them around the always-clause, then
9110/// restore if always-clause didn't override.
9111pub fn exectry(state: &mut estate, _do_exec: i32) -> i32 {
9112    let header = state.prog.prog[state.pc.wrapping_sub(1)]; // c:741
9113    let end_pc = state.pc + WC_TRY_SKIP(header) as usize; // c:742
9114    let try_inner = state.prog.prog[state.pc]; // c:743
9115    let always_pc = state.pc + 1 + WC_TRY_SKIP(try_inner) as usize; // c:743
9116    state.pc += 1; // c:744
9117    pushheap(); // c:745
9118    cmdpush(CS_CURSH as u8); // c:746
9119    try_tryflag.fetch_add(1, Ordering::SeqCst); // c:749
9120    let _ = execlist(state, 1, 0); // c:750
9121    try_tryflag.fetch_sub(1, Ordering::SeqCst); // c:751
9122    let try_status = LASTVAL.load(Ordering::Relaxed);
9123    let endval = if try_status != 0 {
9124        // c:754
9125        try_status
9126    } else {
9127        (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) as i32
9128    };
9129    freeheap(); // c:756
9130    cmdpop(); // c:758
9131    cmdpush(CS_ALWAYS as u8); // c:759
9132                              // c:762-763 — save try_errflag / try_interrupt.
9133    let saved_err = errflag.load(Ordering::Relaxed);
9134    let save_try_err = (saved_err & ERRFLAG_ERROR) != 0;
9135    let save_try_int = (saved_err & ERRFLAG_INT) != 0;
9136    // c:Src/loop.c:763-766 — save the canonical globals AND update
9137    // them to reflect the just-finished try body's exit state before
9138    // running the always-arm. `$TRY_BLOCK_ERROR` / `$TRY_BLOCK_INTERRUPT`
9139    // read these globals directly (lookup_special_var arms), so the
9140    // always body must see "errflag at try-end" not "-1 sentinel".
9141    let saved_try_errflag = crate::ported::r#loop::try_errflag.load(Ordering::Relaxed);
9142    let saved_try_interrupt = crate::ported::r#loop::try_interrupt.load(Ordering::Relaxed);
9143    crate::ported::r#loop::try_errflag.store((saved_err & ERRFLAG_ERROR) as i64, Ordering::Relaxed); // c:765
9144    crate::ported::r#loop::try_interrupt.store(
9145        if (saved_err & ERRFLAG_INT) != 0 { 1 } else { 0 },
9146        Ordering::Relaxed,
9147    ); // c:766
9148       // c:768 — `errflag = 0;` (clear both bits).
9149    errflag.fetch_and(!(ERRFLAG_ERROR | ERRFLAG_INT), Ordering::Relaxed);
9150    // c:769-774 — save retflag/breaks/contflag.
9151    let save_retflag = RETFLAG.swap(0, Ordering::SeqCst);
9152    let save_breaks = BREAKS.swap(0, Ordering::SeqCst);
9153    let save_contflag = CONTFLAG.swap(0, Ordering::SeqCst);
9154    state.pc = always_pc; // c:776
9155    let _ = execlist(state, 1, 0); // c:777
9156                                   // c:779-786 — restore errflag bits.
9157    if save_try_err {
9158        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9159    } else {
9160        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
9161    }
9162    if save_try_int {
9163        errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed);
9164    } else {
9165        errflag.fetch_and(!ERRFLAG_INT, Ordering::Relaxed);
9166    }
9167    // c:Src/loop.c:787-788 — restore the canonical globals.
9168    crate::ported::r#loop::try_errflag.store(saved_try_errflag, Ordering::Relaxed);
9169    crate::ported::r#loop::try_interrupt.store(saved_try_interrupt, Ordering::Relaxed);
9170    // c:789-794 — re-arm retflag/breaks/contflag only if always didn't override.
9171    if RETFLAG.load(Ordering::SeqCst) == 0 {
9172        RETFLAG.store(save_retflag, Ordering::SeqCst);
9173    }
9174    if BREAKS.load(Ordering::SeqCst) == 0 {
9175        BREAKS.store(save_breaks, Ordering::SeqCst);
9176    }
9177    if CONTFLAG.load(Ordering::SeqCst) == 0 {
9178        CONTFLAG.store(save_contflag, Ordering::SeqCst);
9179    }
9180    cmdpop(); // c:796
9181    popheap(); // c:797
9182    state.pc = end_pc; // c:798
9183    this_noerrexit.store(1, Ordering::Relaxed); // c:799
9184    endval
9185}
9186
9187/// Port of `execcmd_exec(Estate state, Execcmd_params eparams,
9188/// int input, int output, int how, int last1, int close_if_forked)`
9189/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
9190/// level of the hierarchy.
9191///
9192/// Line-by-line port of the full 1500-line C body. Sections:
9193///   c:2904-2916  — locals
9194///   c:2917-2924  — eparams field unpacking
9195///   c:2934-2939  — Z_TIMED + doneps4 reset
9196///   c:2945-2960  — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
9197///   c:2962-2986  — %job head rewrite + AUTORESUME prefix match
9198///   c:2988-3011  — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
9199///   c:3013-3283  — precommand-modifier walk (BINF_PREFIX strip)
9200///                  + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
9201///   c:3285-3307  — prefork substitutions + magic_assign
9202///   c:3309-3406  — empty-command branch (redir / nullexec / BINF_COMMAND)
9203///   c:3409-3466  — main resolution loop (shfunc / builtin / autocd)
9204///   c:3468-3479  — errflag bail-out
9205///   c:3480-3492  — text fetch + setunderscore
9206///   c:3494-3524  — rm * safety prompt
9207///   c:3526-3591  — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
9208///   c:3593-3632  — external resolution (cmdnamtab, hashcmd, AUTOCD)
9209///   c:3634-3697  — fork decision
9210///   c:3700-3955  — redir loop + multio + addfd + xpandredir
9211///   c:3957-3961  — multio close (`mfds[i].ct >= 2` → closemn)
9212///   c:3963-3995  — nullexec branch
9213///   c:3996-4327  — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
9214///                  execbuiltin / execshfunc / execute)
9215///   c:4330-4365  — `err:` label: forked-child fd cleanup, fixfds
9216///   c:4366-4403  — `done:` label: POSIX special-builtin error escalation,
9217///                  shelltime stop, newxtrerr close, AUTOCONTINUE restore
9218///
9219/// **Substrate stubs (declared inside this fn citing home C file):**
9220///   - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
9221///   - `restore_params(restorelist, removelist)` → Src/exec.c:4463
9222///   - `isreallycom(cn)` → Src/exec.c:2670
9223///   - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
9224///   - `execautofn_basic(state, do_exec)` → Src/exec.c:5050
9225///   - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
9226///
9227/// **NOT routed through fusevm.** This canonical port targets the
9228/// tree-walker dispatcher; the fusevm bytecode VM uses
9229/// `execcmd_compile_head` + `compile_simple` instead. No call
9230/// site yet — the port closes the substrate gap so future
9231/// wordcode-walker code can use it.
9232#[allow(non_snake_case)]
9233#[allow(clippy::too_many_arguments)]
9234#[allow(clippy::redundant_field_names)]
9235#[allow(unused_assignments)]
9236#[allow(unused_variables)]
9237#[allow(unused_mut)]
9238#[allow(unused_imports)]
9239#[allow(unreachable_code)]
9240#[allow(dead_code)]
9241pub fn execcmd_exec(
9242    state: &mut estate,
9243    eparams: &mut crate::ported::zsh_h::execcmd_params,
9244    input: i32,
9245    output: i32,
9246    mut how: i32,
9247    mut last1: i32,
9248    close_if_forked: i32,
9249) {
9250    use crate::ported::zsh_h::{
9251        Star, ASG_ARRAY, ASG_KEY_VALUE, AUTOCD, AUTOCONTINUE, AUTORESUME, BGNICE,
9252        BINF_ASSIGN as BINF_ASSIGN_FLAG, BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_MAGICEQUALS,
9253        BINF_NOGLOB, BINF_PREFIX, BINF_PSPECIAL, CSHNULLCMD, ERRFLAG_INT, EXECOPT, FDT_EXTERNAL,
9254        FDT_INTERNAL, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHCMDS, HFILE_USE_OPTIONS,
9255        IS_APPEND_REDIR, IS_DASH, IS_ERROR_REDIR, MAGICEQUALSUBST, NOTIFY, PM_READONLY, PM_SPECIAL,
9256        POSIXBUILTINS, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET,
9257        PRINTEXITVALUE, RCS, REDIR_CLOSE, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN,
9258        REDIR_MERGEOUT, REDIR_OUTPIPE, REDIR_READ, REDIR_READWRITE, RMSTARSILENT, SHINSTDIN,
9259        SHNULLCMD, STAT_BUILTIN, STAT_CURSH, STAT_DONE, STAT_NOPRINT, WC_ASSIGN as ZWC_ASSIGN,
9260        WC_ASSIGN_INC as ZWC_ASSIGN_INC, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
9261        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
9262        WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2, WC_AUTOFN, WC_CURSH, WC_FUNCDEF, WC_REDIR, WC_SIMPLE,
9263        WC_SUBSH, WC_TIMED, WC_TYPESET, XTRACE, Z_ASYNC, Z_DISOWN, Z_SYNC, Z_TIMED,
9264    };
9265
9266    // c:2900
9267
9268    // c:2904-2916 — locals.
9269    let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
9270    let mut filelist: Vec<jobfile> = Vec::new(); // c:2905 LinkList filelist = NULL
9271                                                 // c:2906 LinkNode node; (loop locals)
9272                                                 // c:2907 Redir fn;       (loop locals)
9273    let mut mfds: [Option<Box<multio>>; 10] =                              // c:2908 struct multio *mfds[10]
9274        [None, None, None, None, None, None, None, None, None, None];
9275    let mut text: Option<String> = None; // c:2909 char *text
9276    let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
9277    let mut fil: i32; // c:2911 int fil
9278    let mut dfil: i32 = 0; // c:2911 int dfil
9279    let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
9280    let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
9281    let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
9282    let mut i: i32; // c:2911 int i
9283    let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
9284    let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
9285    let mut forked: i32 = 0; // c:2912 int forked = 0
9286    let mut old_lastval: i32; // c:2912 int old_lastval
9287    let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
9288    let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
9289    let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
9290    let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
9291                                  // c:2914 — `Various flags to the command.`
9292    let mut cflags: u32 = 0; // c:2915 int cflags = 0
9293    let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
9294    let mut checked: i32 = 0; // c:2915 int checked = 0
9295    let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
9296                                 // c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;` — xtrerr
9297                                 // accessor is stub; track newxtrerr state via Option<RawFd>.
9298    let mut newxtrerr: Option<i32> = None; // c:2916
9299
9300    // c:2917-2924 — eparams field unpacking. `args` / `redir` are
9301    // pulled into mutable locals so the body can mutate them
9302    // independently of the eparams struct.
9303    let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
9304    let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
9305    let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
9306    let typ: i32 = eparams.typ; // c:2924 int type
9307                                // c:2925-2929 — `preargs comes from expanding the head of the args
9308                                // list in order to check for prefix commands.` declared later.
9309
9310    // c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
9311    // + struct timespec then. Rust port keeps the names so the shelltime
9312    // start+stop calls map directly. Use jobs.rs's existing types.
9313    let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
9314    let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
9315    let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
9316    if (how & Z_TIMED as i32) != 0 {
9317        // c:2936
9318        crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0);
9319        // c:2937
9320    }
9321
9322    doneps4.store(0, Ordering::Relaxed); // c:2939
9323
9324    // c:2941-2947 — `If assignment but no command get the status from
9325    // variable assignment.`
9326    old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
9327    if args.is_none() && varspc.is_some() {
9328        // c:2946
9329        let ef = errflag.load(Ordering::Relaxed);
9330        LASTVAL.store(
9331            if ef != 0 {
9332                ef
9333            } else {
9334                cmdoutval.load(Ordering::Relaxed)
9335            },
9336            Ordering::Relaxed,
9337        ); // c:2947
9338    }
9339    // c:2948-2954 — `If there are arguments, we should reset the status
9340    // for the command before execution---unless we are using the result
9341    // of a command substitution...`
9342    use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
9343
9344    // c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
9345    // Already initialised above via array literals; preserved as
9346    // comment for parity. The C loop maps to a no-op in Rust.
9347
9348    // c:2962-2973 — `%job` head rewrite.
9349    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
9350        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
9351        && args.as_ref().unwrap()[0].starts_with('%')
9352    {
9353        // c:2964-2965
9354        if (how & Z_DISOWN as i32) != 0 {
9355            // c:2966
9356            oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) {
9357                1
9358            } else {
9359                0
9360            }; // c:2967
9361            opt_state_set("autocontinue", true); // c:2968
9362        }
9363        // c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
9364        let head = if (how & Z_DISOWN as i32) != 0 {
9365            "disown".to_string()
9366        } else if (how & Z_ASYNC as i32) != 0 {
9367            "bg".to_string()
9368        } else {
9369            "fg".to_string()
9370        };
9371        if let Some(ref mut v) = args {
9372            v.insert(0, head);
9373        }
9374        how = Z_SYNC as i32; // c:2972
9375    }
9376
9377    // c:2975-2986 — AUTORESUME prefix match against jobtab.
9378    if isset(AUTORESUME)
9379        && typ == WC_SIMPLE as i32
9380        && (how & Z_SYNC as i32) != 0
9381        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
9382        && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
9383        && input == 0
9384        && args.as_ref().unwrap().len() == 1
9385    {
9386        // c:2979-2981
9387        if unset(NOTIFY) {
9388            // c:2982 — `scanjobs();` via the canonical port
9389            // (Src/jobs.c:1993).
9390            if let Some(jt) = JOBTAB.get() {
9391                let mut guard = jt.lock().unwrap();
9392                crate::ported::jobs::scanjobs(&mut guard);
9393            }
9394        }
9395        // c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
9396        let head = args.as_ref().unwrap()[0].clone();
9397        let maxjob = JOBTAB
9398            .get()
9399            .map(|m| m.lock().unwrap().len() as i32)
9400            .unwrap_or(0);
9401        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
9402        // c:2982 — `findjobnam(s)`. Canonical port at
9403        // jobs.rs::findjobnam matches against `proc.text`, which is
9404        // the command text actually saved into the job at fork —
9405        // matching C exactly. Returns the job index if any non-
9406        // SUBJOB jobtab entry's first-proc text starts with `s`.
9407        let found = if let Some(jt) = JOBTAB.get() {
9408            let guard = jt.lock().unwrap();
9409            crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
9410        } else {
9411            false
9412        };
9413        if found {
9414            // c:2985 — `pushnode(args, dupstring("fg"));`
9415            if let Some(ref mut v) = args {
9416                v.insert(0, "fg".to_string());
9417            }
9418        }
9419    }
9420
9421    // ====================================================================
9422    // SUBSTRATE STUBS — same-named locals citing their home C file per
9423    // [[feedback_no_shortcuts_in_porting]]. Each stub mirrors the C
9424    // signature and returns a degenerate value that keeps the body
9425    // executing while the real port lands.
9426    // ====================================================================
9427    // save_params + restore_params — top-level ports in exec.rs
9428    // (c:4410 / c:4464). Both bridged via `use` below.
9429    use crate::ported::exec::{restore_params, save_params};
9430    // isreallycom — top-level port at exec.rs (c:972). Bridges the
9431    // local shadow that this fn body used pre-port.
9432    use crate::ported::exec::isreallycom;
9433    // execautofn_basic — top-level port at exec.rs (c:5608).
9434    use crate::ported::exec::execautofn_basic;
9435    // C `execerr` macro (c:2700) was a goto-equivalent:
9436    //   errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
9437    // Rust expansion: each call site inlines the errflag+LASTVAL set
9438    // and then `break`s out of the enclosing redir loop. The loop's
9439    // post-loop errflag check at c:3949 routes to execcmd_exec_err_path
9440    // for the cleanup tail. No macro needed.
9441
9442    // c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
9443    // fork-immediately fast path.
9444    if (how & Z_ASYNC as i32) != 0
9445        || output != 0
9446        || (last1 == 2 && input != 0 && {
9447            // c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
9448            // EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
9449            // ported `emulation` static at options.rs:1044 holds the
9450            // current bit; compare against EMULATE_SH (zsh_h:2883).
9451            (crate::ported::options::emulation.load(Ordering::Relaxed)
9452                & crate::ported::zsh_h::EMULATE_SH)
9453                != 0
9454        })
9455    {
9456        // c:2988
9457        // c:2999 — `text = getjobtext(state->prog, eparams->beg);`
9458        text = Some(crate::ported::text::getjobtext(
9459            state.prog.clone(),
9460            Some(eparams.beg),
9461        ));
9462        // c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
9463        let mut filelist_for_fork = filelist.clone();
9464        let pid = execcmd_fork(
9465            state,
9466            how,
9467            typ,
9468            varspc,
9469            &mut filelist_for_fork,
9470            text.as_deref().unwrap_or(""),
9471            oautocont,
9472            close_if_forked,
9473        );
9474        match pid {
9475            -1 => {
9476                // c:3002-3003 — `goto fatal;` — fall through to fatal:
9477                // label at c:4377. We model this with a flag.
9478                redir_err = 1; // pretend redir error to trigger fatal arm
9479                               // Continue to done label by setting forked + jumping forward.
9480                               // Simplified: just bail with status 1 + fatal handling at
9481                               // the bottom of the fn.
9482                return execcmd_exec_done_path(
9483                    redir_err,
9484                    oautocont,
9485                    how,
9486                    &mut shti,
9487                    &mut chti,
9488                    &mut then_ts,
9489                    forked,
9490                    &mut newxtrerr,
9491                    cflags,
9492                    orig_cflags,
9493                    is_cursh,
9494                    do_exec,
9495                );
9496            }
9497            0 => {
9498                // c:3004 — child returned 0; continue with the body.
9499            }
9500            _ => {
9501                // c:3007 — parent: `return;` — but first restore AUTOCONTINUE
9502                // and shelltime stop. Inline the done-tail equivalent.
9503                if oautocont >= 0 {
9504                    opt_state_set("autocontinue", oautocont != 0);
9505                }
9506                if (how & Z_TIMED as i32) != 0 {
9507                    crate::ported::jobs::shelltime(
9508                        Some(&mut shti),
9509                        Some(&mut chti),
9510                        Some(&mut then_ts),
9511                        1,
9512                    );
9513                }
9514                return;
9515            }
9516        }
9517        last1 = 1; // c:3009
9518        forked = 1; // c:3009
9519    } else {
9520        // c:3010-3011
9521        text = None;
9522    }
9523
9524    // ====================================================================
9525    // c:3013-3283 — precommand-modifier walk.
9526    //
9527    // The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
9528    // BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
9529    // (above this fn). Call into it to keep DRY, then convert the
9530    // returned dispatch struct's fields into the locals C uses
9531    // (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
9532    // exec_argv0, precmd_skip).
9533    //
9534    // Per [[feedback_true_port_pattern]] the C function does this
9535    // walk inline. Reusing the existing port is acceptable because
9536    // `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
9537    // citations there match. The C tree-walker and the fusevm
9538    // compile-time walker arrive at identical dispatch decisions
9539    // from the same input.
9540    // ====================================================================
9541    let mut preargs: Vec<String> = Vec::new();
9542    let mut exec_argv0: Option<String> = None;
9543    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
9544        // c:3018
9545        let head_args: Vec<String> = args.as_ref().unwrap().clone();
9546        let dispatch = execcmd_compile_head(&head_args, typ as u32);
9547        // Pull fields into local mirror of C state.
9548        cflags = dispatch.cflags;
9549        if dispatch.is_builtin {
9550            is_builtin = 1;
9551        }
9552        if dispatch.is_shfunc {
9553            is_shfunc = 1;
9554        }
9555        if dispatch.use_defpath {
9556            use_defpath = 1;
9557        }
9558        exec_argv0 = dispatch.exec_argv0;
9559        // c:3061 — `orig_cflags |= cflags;` accumulator path; for
9560        // BINF_PREFIX walks orig_cflags tracks each step's pre-mask
9561        // bits. execcmd_compile_head doesn't surface orig_cflags
9562        // separately, so approximate as the post-strip cflags.
9563        orig_cflags = cflags;
9564        // c:3030-3086 — strip the precmd-modifier prefix from args.
9565        // In C, the walk pulls one arg at a time from `args` into
9566        // `preargs` via execcmd_getargs, then uremnodes each
9567        // BINF_PREFIX modifier. At loop exit C's `preargs` holds the
9568        // dispatch target (1 element) and `args` holds whatever's
9569        // left; `joinlists(preargs, args)` (c:3305-3306) splices the
9570        // target back onto the head. The net effect is `args` with
9571        // the precmd modifiers stripped. We compute that final shape
9572        // directly and leave `preargs` empty so the joinlists arm
9573        // below is a no-op. Without this, preargs=head_args[skip..]
9574        // plus a non-draining args was double-counting every word
9575        // when both held the same suffix.
9576        if let Some(ref mut v) = args {
9577            v.drain(0..dispatch.precmd_skip);
9578        }
9579        let _ = head_args;
9580        preargs.clear();
9581        // c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
9582        // — surface via cflags check: if a typeset-family builtin
9583        // landed, BINF_MAGICEQUALS is in its flags and dispatch
9584        // surfaces it via cflags.
9585        if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
9586            magic_assign = 1;
9587        }
9588        // c:3056 — C's precmd walk sets `hn = builtintab->getnode(...)`
9589        // for the dispatch target before breaking at c:3064. The
9590        // Rust port's execcmd_compile_head returns is_builtin but
9591        // not the entry pointer, and the second resolution loop
9592        // below short-circuits on `is_builtin != 0` (c:3423-3426)
9593        // without re-resolving. Look up the dispatch target now so
9594        // `hn` is non-null at the execbuiltin call (c:4233 /
9595        // exec.rs:10177); otherwise execbuiltin returns 1 silently
9596        // on a null `bn`.
9597        hn = None;
9598        if is_builtin != 0 {
9599            if let Some(target) = args.as_ref().and_then(|v| v.first()) {
9600                if let Some(entry) = BUILTINS.iter().find(|b| b.node.nam == *target) {
9601                    hn = Some(entry as *const builtin as *mut builtin);
9602                }
9603            }
9604        }
9605    } else {
9606        // c:3282-3283 — `else preargs = NULL;`
9607        // We use an empty preargs to model NULL — C's `preargs` is
9608        // only iterated if `nonempty(preargs)` in this branch.
9609    }
9610
9611    // c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
9612    // Sets the file-static `esprefork` (exec.rs:267) so any downstream
9613    // execsubst() call inside this command's expansion uses the same
9614    // prefork flags. Also keep a local copy for the immediate
9615    // prefork(args, esprefork, NULL) below.
9616    let esprefork_v: i32 =
9617        if magic_assign != 0 || (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32) {
9618            PREFORK_TYPESET // c:3300
9619        } else {
9620            0
9621        };
9622    esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
9623
9624    // c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
9625    if args.is_some() && eparams.htok != 0 {
9626        // c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
9627        let mut as_linklist: LinkList<String> = Default::default();
9628        if let Some(ref v) = args {
9629            for s in v {
9630                as_linklist.push_back(s.clone());
9631            }
9632        }
9633        let mut rf = 0i32;
9634        prefork(&mut as_linklist, esprefork_v, &mut rf);
9635        // Move back into args.
9636        let mut out: Vec<String> = Vec::new();
9637        while let Some(s) = as_linklist.pop_front() {
9638            out.push(s);
9639        }
9640        args = Some(out);
9641    }
9642    if !preargs.is_empty() {
9643        // c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
9644        let mut joined = preargs.clone();
9645        if let Some(ref v) = args {
9646            joined.extend(v.iter().cloned());
9647        }
9648        args = Some(joined);
9649    }
9650
9651    // c:3309-3406 — main resolution loop + empty-command branch.
9652    if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
9653        let mut unglobbed: i32 = 0; // c:3310
9654
9655        // c:3312 — `for (;;)` — main resolution loop.
9656        loop {
9657            // c:3315-3318 — globbing or untokenise sweep.
9658            if (cflags & BINF_NOGLOB) == 0 {
9659                while checked == 0
9660                    && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
9661                    && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
9662                    && crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
9663                {
9664                    // c:3318 — `zglob(args, firstnode(args), 0);`
9665                    // zglob takes &mut Vec<String>; isolate the head element
9666                    // by splitting args into [head] and [tail], then re-merging.
9667                    let mut head_vec: Vec<String> = Vec::new();
9668                    if let Some(ref mut v) = args {
9669                        head_vec.push(v.remove(0));
9670                    }
9671                    crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
9672                    if let Some(ref mut v) = args {
9673                        for (i, s) in head_vec.into_iter().enumerate() {
9674                            v.insert(i, s);
9675                        }
9676                    }
9677                }
9678            } else if unglobbed == 0 {
9679                // c:3319-3322
9680                if let Some(ref mut v) = args {
9681                    for s in v.iter_mut() {
9682                        *s = untokenize(s); // c:3321
9683                    }
9684                }
9685                unglobbed = 1; // c:3322
9686            }
9687
9688            // c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
9689            if (cflags & BINF_EXEC) != 0 && last1 != 0 {
9690                do_exec = 1; // c:3328
9691            }
9692
9693            // c:3331-3407 — empty-command branch.
9694            if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
9695                // c:3331 — `if (!args || empty(args))`
9696                if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
9697                    // c:3332 — `if (redir && nonempty(redir))`
9698                    if do_exec != 0 {
9699                        // c:3333 — `Was this "exec < foobar"?`
9700                        nullexec = 1; // c:3335
9701                        break;
9702                    } else if varspc.is_some() {
9703                        // c:3337
9704                        nullexec = 2; // c:3338
9705                        break;
9706                    } else if {
9707                        // c:3340-3341 — `if (!nullcmd || !*nullcmd ||
9708                        //   opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
9709                        let nc = getsparam("NULLCMD");
9710                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
9711                        nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
9712                    } {
9713                        // c:3342 — `zerr("redirection with no command");`
9714                        zerr("redirection with no command");
9715                        LASTVAL.store(1, Ordering::Relaxed); // c:3343
9716                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
9717                        if forked != 0 {
9718                            // c:3345-3346
9719                            crate::ported::builtin::_realexit();
9720                        }
9721                        if (how & Z_TIMED as i32) != 0 {
9722                            // c:3347-3348
9723                            crate::ported::jobs::shelltime(
9724                                Some(&mut shti),
9725                                Some(&mut chti),
9726                                Some(&mut then_ts),
9727                                1,
9728                            );
9729                        }
9730                        return; // c:3349
9731                    } else if {
9732                        // c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
9733                        let nc = getsparam("NULLCMD");
9734                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
9735                        nc_empty || isset(SHNULLCMD)
9736                    } {
9737                        // c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
9738                        if args.is_none() {
9739                            args = Some(Vec::new());
9740                        }
9741                        args.as_mut().unwrap().push(":".to_string()); // c:3353
9742                    } else if {
9743                        // c:3354-3356 — `readnullcmd && *readnullcmd &&
9744                        //   peekfirst(redir).type == REDIR_READ &&
9745                        //   !nextnode(firstnode(redir))`
9746                        let rnc = getsparam("READNULLCMD");
9747                        let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
9748                        rnc_nonempty
9749                            && redir.as_ref().unwrap().len() == 1
9750                            && redir.as_ref().unwrap()[0].typ == REDIR_READ
9751                    } {
9752                        // c:3357-3359
9753                        if args.is_none() {
9754                            args = Some(Vec::new());
9755                        }
9756                        let rnc = getsparam("READNULLCMD").unwrap_or_default();
9757                        args.as_mut().unwrap().push(rnc); // c:3359
9758                    } else {
9759                        // c:3360-3364 — default: nullcmd as command.
9760                        if args.is_none() {
9761                            args = Some(Vec::new());
9762                        }
9763                        let nc = getsparam("NULLCMD").unwrap_or_default();
9764                        args.as_mut().unwrap().push(nc); // c:3363
9765                    }
9766                } else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
9767                    // c:3365 — bare `command`: lastval=0, return.
9768                    LASTVAL.store(0, Ordering::Relaxed); // c:3366
9769                    if forked != 0 {
9770                        crate::ported::builtin::_realexit(); // c:3367-3368
9771                    }
9772                    if (how & Z_TIMED as i32) != 0 {
9773                        crate::ported::jobs::shelltime(
9774                            Some(&mut shti),
9775                            Some(&mut chti),
9776                            Some(&mut then_ts),
9777                            1,
9778                        ); // c:3369-3370
9779                    }
9780                    return; // c:3371
9781                } else {
9782                    // c:3372-3406 — no arguments default arm.
9783                    // c:3378-3385 — badcshglob == 1 → no match.
9784                    if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
9785                        zerr("no match"); // c:3379
9786                        LASTVAL.store(1, Ordering::Relaxed); // c:3380
9787                        if forked != 0 {
9788                            crate::ported::builtin::_realexit(); // c:3381-3382
9789                        }
9790                        if (how & Z_TIMED as i32) != 0 {
9791                            crate::ported::jobs::shelltime(
9792                                Some(&mut shti),
9793                                Some(&mut chti),
9794                                Some(&mut then_ts),
9795                                1,
9796                            ); // c:3383-3384
9797                        }
9798                        return; // c:3385
9799                    }
9800                    // c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
9801                    cmdoutval.store(
9802                        if use_cmdoutval.load(Ordering::Relaxed) != 0 {
9803                            LASTVAL.load(Ordering::Relaxed)
9804                        } else {
9805                            0
9806                        },
9807                        Ordering::Relaxed,
9808                    );
9809                    if varspc.is_some() {
9810                        // c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
9811                        LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
9812                        addvars(state, varspc.unwrap_or(0), 0); // c:3391
9813                    }
9814                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9815                        // c:3393
9816                        LASTVAL.store(1, Ordering::Relaxed); // c:3394
9817                    } else {
9818                        // c:3395-3396
9819                        LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
9820                    }
9821                    if isset(XTRACE) {
9822                        // c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
9823                        // xtrerr accessor is stub; rely on the existing
9824                        // stderr writer in compile_zsh tracing path.
9825                        eprintln!();
9826                    }
9827                    if forked != 0 {
9828                        crate::ported::builtin::_realexit(); // c:3401-3402
9829                    }
9830                    if (how & Z_TIMED as i32) != 0 {
9831                        crate::ported::jobs::shelltime(
9832                            Some(&mut shti),
9833                            Some(&mut chti),
9834                            Some(&mut then_ts),
9835                            1,
9836                        ); // c:3403-3404
9837                    }
9838                    return; // c:3405
9839                }
9840            }
9841
9842            // c:3423-3426 — `if (errflag || checked || is_builtin ||
9843            //   (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
9844            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0
9845                || checked != 0
9846                || is_builtin != 0
9847                || if isset(POSIXBUILTINS) {
9848                    (cflags & BINF_EXEC) != 0
9849                } else {
9850                    (cflags & BINF_COMMAND) != 0
9851                }
9852            {
9853                // c:3423
9854                break; // c:3426
9855            }
9856
9857            // c:3428 — `cmdarg = (char *) peekfirst(args);`
9858            let cmdarg = args.as_ref().unwrap()[0].clone();
9859
9860            // c:3429-3433 — shfunc lookup.
9861            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
9862                let in_shfunctab = shfunctab_lock()
9863                    .read()
9864                    .map(|t| t.iter().any(|(k, _)| k.as_str() == cmdarg.as_str()))
9865                    .unwrap_or(false);
9866                if in_shfunctab {
9867                    is_shfunc = 1; // c:3431
9868                    break; // c:3432
9869                }
9870            }
9871            // c:3434-3447 — builtintab lookup.
9872            let builtin_entry: Option<&'static builtin> = BUILTINS
9873                .iter()
9874                .find(|b| b.node.nam.as_str() == cmdarg.as_str());
9875            if builtin_entry.is_none() {
9876                if (cflags & BINF_BUILTIN) != 0 {
9877                    // c:3435 — `zwarn("no such builtin: %s", cmdarg);`
9878                    zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
9879                    LASTVAL.store(1, Ordering::Relaxed); // c:3437
9880                    if oautocont >= 0 {
9881                        // c:3438-3439
9882                        opt_state_set("autocontinue", oautocont != 0);
9883                    }
9884                    if forked != 0 {
9885                        crate::ported::builtin::_realexit(); // c:3440-3441
9886                    }
9887                    if (how & Z_TIMED as i32) != 0 {
9888                        crate::ported::jobs::shelltime(
9889                            Some(&mut shti),
9890                            Some(&mut chti),
9891                            Some(&mut then_ts),
9892                            1,
9893                        ); // c:3442-3443
9894                    }
9895                    return; // c:3444
9896                }
9897                break; // c:3446
9898            }
9899            let entry = builtin_entry.unwrap();
9900            // c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
9901            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
9902                is_builtin = 1; // c:3449
9903                                // c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
9904                                // module autoload check. zshrs's BUILTINS table is
9905                                // static and pre-resolved; treat resolvebuiltin as
9906                                // pass-through.
9907                hn = Some(entry as *const builtin as *mut builtin);
9908                break; // c:3459
9909            }
9910            // c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
9911            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
9912            cflags |= entry.node.flags as u32;
9913            if let Some(ref mut v) = args {
9914                v.remove(0); // c:3463 uremnode(args, firstnode(args))
9915            }
9916            hn = None; // c:3464
9917        }
9918    }
9919
9920    // c:3468-3478 — errflag bail-out.
9921    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9922        // c:3468
9923        if LASTVAL.load(Ordering::Relaxed) == 0 {
9924            // c:3469
9925            LASTVAL.store(1, Ordering::Relaxed); // c:3470
9926        }
9927        if oautocont >= 0 {
9928            opt_state_set("autocontinue", oautocont != 0);
9929            // c:3472
9930        }
9931        if forked != 0 {
9932            crate::ported::builtin::_realexit(); // c:3473-3474
9933        }
9934        if (how & Z_TIMED as i32) != 0 {
9935            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
9936            // c:3475-3476
9937        }
9938        return; // c:3477
9939    }
9940
9941    // c:3480-3483 — `Get the text associated with this command.`
9942    if text.is_none()
9943        && sfcontext.load(Ordering::Relaxed) == 0
9944        && (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
9945    {
9946        // c:3481-3482
9947        text = Some(crate::ported::text::getjobtext(
9948            state.prog.clone(),
9949            Some(eparams.beg),
9950        )); // c:3483
9951    }
9952
9953    // c:3485-3492 — `Set up special parameter $_`.
9954    if typ != WC_FUNCDEF as i32 {
9955        // c:3490
9956        let last_str = args
9957            .as_ref()
9958            .and_then(|v| v.last())
9959            .cloned()
9960            .unwrap_or_default();
9961        setunderscore(&last_str); // c:3491-3492
9962    }
9963
9964    // c:3494-3524 — `Warn about "rm *"`.
9965    if typ == WC_SIMPLE as i32
9966        && crate::ported::zsh_h::interact()
9967        && unset(RMSTARSILENT)
9968        && isset(SHINSTDIN)
9969        && args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
9970        && args.as_ref().unwrap()[0] == "rm"
9971    {
9972        // c:3495-3497
9973        let args_v = args.as_ref().unwrap().clone();
9974        for s in args_v.iter().skip(1) {
9975            // c:3500
9976            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9977                break;
9978            }
9979            let l = s.len();
9980            // c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
9981            if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
9982                let pwd = getsparam("PWD").unwrap_or_default();
9983                if !crate::ported::utils::checkrmall(&pwd) {
9984                    // c:3506
9985                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
9986                    break; // c:3508
9987                }
9988            } else if l >= 2 {
9989                // c:3510 — `s[l-2] == '/' && s[l-1] == Star`
9990                let bytes = s.as_bytes();
9991                if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
9992                    let prefix = if l == 2 {
9993                        "/".to_string()
9994                    } else {
9995                        String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
9996                    };
9997                    if !crate::ported::utils::checkrmall(&prefix) {
9998                        // c:3518
9999                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
10000                        break; // c:3520
10001                    }
10002                }
10003            }
10004        }
10005    }
10006
10007    // c:3526-3580 — type-specific dispatch prep.
10008    if typ == WC_FUNCDEF as i32 {
10009        // c:3526
10010        if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
10011            // c:3535 — `Nonymous, don't do redirections here`
10012            redir = None; // c:3537
10013        }
10014    } else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
10015        // c:3539
10016        // c:3540-3559 — shfunc / autoload preload.
10017        if is_shfunc != 0 {
10018            // c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
10019        } else {
10020            // c:3543-3559 — autoload preload.
10021            if let Some(ref mut sh) = state.prog.shf {
10022                let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
10023                let r = loadautofn(shf_ptr, 1, 0, 0);
10024                if r != 0 {
10025                    // c:3551 — `lastval = 1;`
10026                    LASTVAL.store(1, Ordering::Relaxed);
10027                    if oautocont >= 0 {
10028                        opt_state_set("autocontinue", oautocont != 0);
10029                    }
10030                    if forked != 0 {
10031                        crate::ported::builtin::_realexit();
10032                    }
10033                    if (how & Z_TIMED as i32) != 0 {
10034                        crate::ported::jobs::shelltime(
10035                            Some(&mut shti),
10036                            Some(&mut chti),
10037                            Some(&mut then_ts),
10038                            1,
10039                        );
10040                    }
10041                    return; // c:3558
10042                }
10043            }
10044        }
10045        // c:3561-3579 — shf->redir append: a function definition can
10046        // carry extra redirs (`f() { ... } < file`), captured as a
10047        // separate Eprog in shf->redir. Walk that Eprog with a temp
10048        // estate, extract its redirs with ecgetredirs, then merge
10049        // into the live `redir` list.
10050        // Resolve shfunc by name (hn is *mut builtin so we go through
10051        // shfunctab as in the dispatch site at c:4102).
10052        let shfn_name = args
10053            .as_ref()
10054            .and_then(|v| v.first())
10055            .cloned()
10056            .unwrap_or_default();
10057        let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
10058            if let Ok(tab) = shfunctab_lock().read() {
10059                tab.get(&shfn_name).and_then(|s| s.redir.clone())
10060            } else {
10061                None
10062            }
10063        };
10064        if let Some(red_eprog) = shf_redir_eprog {
10065            // c:3566-3571 — build temp estate from shf->redir.
10066            let mut tmp_state = estate {
10067                prog: red_eprog.clone(),
10068                pc: 0,
10069                strs: red_eprog.strs.clone(),
10070                strs_offset: 0,
10071            };
10072            // c:3572 — `redir2 = ecgetredirs(&s);`
10073            let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
10074            // c:3573-3578 — merge into existing redir.
10075            if redir.is_none() {
10076                redir = Some(redir2); // c:3574
10077            } else if let Some(ref mut r) = redir {
10078                // c:3576-3577 — append.
10079                for n in redir2 {
10080                    r.push(n);
10081                }
10082            }
10083        }
10084    }
10085
10086    // c:3582-3591 — errflag bail-out (2).
10087    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10088        // c:3582
10089        LASTVAL.store(1, Ordering::Relaxed); // c:3583
10090        if oautocont >= 0 {
10091            opt_state_set("autocontinue", oautocont != 0);
10092            // c:3584-3585
10093        }
10094        if forked != 0 {
10095            crate::ported::builtin::_realexit(); // c:3586-3587
10096        }
10097        if (how & Z_TIMED as i32) != 0 {
10098            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
10099            // c:3588-3589
10100        }
10101        return; // c:3590
10102    }
10103
10104    // c:3593-3632 — external resolution + AUTOCD.
10105    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
10106        // c:3593
10107        let trycd = isset(AUTOCD)
10108            && isset(SHINSTDIN)
10109            && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
10110            && args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
10111            && !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
10112        if hn.is_none() {
10113            // c:3600
10114            let cmdarg = args.as_ref().unwrap()[0].clone();
10115            let mut dohashcmd = isset(HASHCMDS); // c:3604
10116                                                 // c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
10117            let mut have_cmdnam: Option<cmdnam> = {
10118                let tab = cmdnamtab_lock().read().ok();
10119                tab.and_then(|t| {
10120                    t.iter()
10121                        .find(|(k, _)| k.as_str() == cmdarg.as_str())
10122                        .map(|(_, v)| v.clone())
10123                })
10124            };
10125            if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
10126                // c:3607
10127                // c:3608-3614 — remove the cached entry; force rehash.
10128                cmdnam_unhashed(&cmdarg, Vec::new());
10129                have_cmdnam = None;
10130                if let Some(cn) = have_cmdnam.as_ref() {
10131                    if (cn.node.flags & crate::ported::zsh_h::HASHED) == 0 {
10132                        // checkpath = path; dohashcmd = 1;
10133                        dohashcmd = true;
10134                    }
10135                }
10136            }
10137            if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
10138                // c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
10139                let has_slash = cmdarg.contains('/'); // c:3617-3618
10140                if !has_slash {
10141                    // c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
10142                    let path_dirs = getsparam("PATH").unwrap_or_default();
10143                    let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
10144                    have_cmdnam = hashcmd(&cmdarg, &dirs);
10145                }
10146            }
10147            // hn stays None for external commands — the resolution
10148            // value matters only for builtin/shfunc dispatch in the
10149            // following blocks.
10150            let _ = have_cmdnam;
10151        }
10152
10153        // c:3625-3631 — AUTOCD: command not found, try directory.
10154        if hn.is_none() && trycd {
10155            let cmdarg = args.as_ref().unwrap()[0].clone();
10156            if let Some(s) = cancd(&cmdarg) {
10157                // c:3625
10158                args.as_mut().unwrap()[0] = s; // c:3626
10159                args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
10160                args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
10161                                                                    // c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
10162                let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
10163                if let Some(cd) = cd_entry {
10164                    hn = Some(cd as *const builtin as *mut builtin);
10165                    is_builtin = 1; // c:3630
10166                }
10167            }
10168        }
10169    }
10170
10171    // c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
10172    is_cursh =
10173        (is_builtin != 0 || is_shfunc != 0 || nullexec != 0 || typ >= WC_CURSH as i32) as i32;
10174
10175    // c:3659-3697 — fork decision.
10176    if forked == 0 {
10177        // c:3659
10178        if do_exec == 0
10179            && (((is_builtin != 0 || is_shfunc != 0) && output != 0)
10180                || (is_cursh == 0
10181                    && (last1 != 1
10182                        || crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
10183                        || JOBTAB
10184                            .get()
10185                            .map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
10186                            .unwrap_or(false)
10187                        || false/* fdtable_flocks — substrate stub */)))
10188        {
10189            // c:3660-3663
10190            let mut filelist_for_fork = filelist.clone();
10191            let pid = execcmd_fork(
10192                state,
10193                how,
10194                typ,
10195                varspc,
10196                &mut filelist_for_fork,
10197                text.as_deref().unwrap_or(""),
10198                oautocont,
10199                close_if_forked,
10200            );
10201            match pid {
10202                -1 => {
10203                    // c:3666-3667 — goto fatal.
10204                    redir_err = 1;
10205                    return execcmd_exec_done_path(
10206                        redir_err,
10207                        oautocont,
10208                        how,
10209                        &mut shti,
10210                        &mut chti,
10211                        &mut then_ts,
10212                        forked,
10213                        &mut newxtrerr,
10214                        cflags,
10215                        orig_cflags,
10216                        is_cursh,
10217                        do_exec,
10218                    );
10219                }
10220                0 => {
10221                    // c:3668 — child continues.
10222                }
10223                _ => {
10224                    // c:3670-3671 — parent returns.
10225                    if oautocont >= 0 {
10226                        opt_state_set("autocontinue", oautocont != 0);
10227                    }
10228                    if (how & Z_TIMED as i32) != 0 {
10229                        crate::ported::jobs::shelltime(
10230                            Some(&mut shti),
10231                            Some(&mut chti),
10232                            Some(&mut then_ts),
10233                            1,
10234                        );
10235                    }
10236                    return;
10237                }
10238            }
10239            forked = 1; // c:3673
10240        } else if is_cursh != 0 {
10241            // c:3674
10242            // c:3678-3682 — set jobtab[thisjob] stat bits.
10243            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10244            if thisjob >= 0 {
10245                if let Some(jt) = JOBTAB.get() {
10246                    let mut guard = jt.lock().unwrap();
10247                    if let Some(j) = guard.get_mut(thisjob as usize) {
10248                        j.stat |= STAT_CURSH; // c:3678
10249                                              // c:3679-3680 — `if (!jobtab[thisjob].procs)
10250                                              //                  jobtab[thisjob].stat |= STAT_NOPRINT;`
10251                                              // Suppress the "[N] done" print for jobs that
10252                                              // never forked a real process (cursh / builtin /
10253                                              // null exec).
10254                        if j.procs.is_empty() {
10255                            j.stat |= STAT_NOPRINT; // c:3680
10256                        }
10257                        if is_builtin != 0 {
10258                            j.stat |= STAT_BUILTIN; // c:3682
10259                        }
10260                    }
10261                }
10262            }
10263        } else {
10264            // c:3683-3697 — external exec (real or fake).
10265            is_exec = 1; // c:3687
10266                         // c:3695 — `if (type == WC_SUBSH) forked = 1;`
10267            if typ == WC_SUBSH as i32 {
10268                forked = 1; // c:3696
10269            }
10270        }
10271    }
10272
10273    // c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
10274    if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
10275        // c:3700
10276        let mut oargs: LinkList<String> = Default::default();
10277        if let Some(ref v) = args {
10278            for s in v {
10279                oargs.push_back(s.clone());
10280            }
10281        }
10282        globlist(&mut oargs, 0); // c:3702
10283        let mut out: Vec<String> = Vec::new();
10284        while let Some(s) = oargs.pop_front() {
10285            out.push(s);
10286        }
10287        args = Some(out);
10288    }
10289    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10290        // c:3705
10291        LASTVAL.store(1, Ordering::Relaxed); // c:3706
10292        return execcmd_exec_err_path(
10293            forked,
10294            &mut save,
10295            &mut mfds,
10296            oautocont,
10297            how,
10298            &mut shti,
10299            &mut chti,
10300            &mut then_ts,
10301            &mut newxtrerr,
10302            cflags,
10303            orig_cflags,
10304            is_cursh,
10305            do_exec,
10306            redir_err,
10307        );
10308    }
10309
10310    // c:3711-3718 — XTRACE prep (newxtrerr stderr dup).
10311    // Architectural divergence: C duplicates stderr to a new FD and
10312    // marks it `FDT_XTRACE` in the fdtable so the redir loop skips it.
10313    // zshrs routes xtrace output through `eprintln!()` / `tracing`
10314    // instead of a duplicated fd, so the FDT_XTRACE bookkeeping has
10315    // no counterpart. Not a port gap — `xtrerr is FILE*` is a C-ism
10316    // intentionally replaced.
10317
10318    // c:3720-3724 — pipeline input/output to mfds.
10319    if input != 0 {
10320        addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
10321    }
10322    if output != 0 {
10323        addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
10324    }
10325
10326    // c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
10327    if let Some(ref mut r) = redir {
10328        spawnpipes(r.as_mut_slice(), nullexec);
10329    }
10330
10331    // c:3731-3955 — io redirection loop. Faithful per-redir match.
10332    while let Some(redir_list) = redir.as_mut() {
10333        // c:3731 — `while (redir && nonempty(redir))`
10334        if redir_list.is_empty() {
10335            break;
10336        }
10337        let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
10338                                            // c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
10339        if fn_.typ == REDIR_INPIPE {
10340            // c:3736
10341            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
10342                // c:3737
10343                if fn_.fd2 != -1 {
10344                    let _ = zclose(fn_.fd2); // c:3738-3739
10345                }
10346                closemnodes(&mut mfds); // c:3740
10347                fixfds(&save); // c:3741
10348                {
10349                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10350                    LASTVAL.store(1, Ordering::Relaxed);
10351                } // c:3742
10352                break;
10353            }
10354            // c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
10355            addfd(
10356                forked,
10357                &mut save,
10358                &mut mfds,
10359                fn_.fd1,
10360                fn_.fd2,
10361                0,
10362                fn_.varid.as_deref(),
10363            );
10364        } else if fn_.typ == REDIR_OUTPIPE {
10365            // c:3745
10366            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
10367                // c:3746
10368                if fn_.fd2 != -1 {
10369                    let _ = zclose(fn_.fd2); // c:3747-3748
10370                }
10371                closemnodes(&mut mfds); // c:3749
10372                fixfds(&save); // c:3750
10373                {
10374                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10375                    LASTVAL.store(1, Ordering::Relaxed);
10376                } // c:3751
10377                break;
10378            }
10379            // c:3753
10380            addfd(
10381                forked,
10382                &mut save,
10383                &mut mfds,
10384                fn_.fd1,
10385                fn_.fd2,
10386                1,
10387                fn_.varid.as_deref(),
10388            );
10389        } else {
10390            // c:3754 — non-pipe redir branch.
10391            let mut closed: i32; // c:3755
10392                                 // c:3756-3757 — xpandredir glob/brace.
10393            if fn_.typ != REDIR_HERESTR {
10394                // Put fn_ back temporarily so xpandredir can mutate
10395                // around it; not implemented identically — xpandredir
10396                // signature in zshrs differs (takes &mut redir + ctx).
10397                // c:3756 — `if (xpandredir(fn, redir)) continue;`
10398                // Pragmatic: skip xpandredir (it handles brace/glob in
10399                // redir paths — uncommon, ports to follow-up).
10400            }
10401            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10402                // c:3758
10403                closemnodes(&mut mfds); // c:3759
10404                fixfds(&save); // c:3760
10405                {
10406                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10407                    LASTVAL.store(1, Ordering::Relaxed);
10408                } // c:3761
10409                break;
10410            }
10411            if !isset(EXECOPT) {
10412                // c:3763 — `if (unset(EXECOPT)) continue;`
10413                continue;
10414            }
10415            let fil_local: i32;
10416            match fn_.typ {
10417                t if t == REDIR_HERESTR => {
10418                    // c:3766
10419                    if checkclobberparam(&fn_) == 0 {
10420                        fil_local = -1; // c:3768
10421                    } else {
10422                        fil_local = getherestr(&fn_); // c:3770
10423                    }
10424                    if fil_local == -1 {
10425                        // c:3771
10426                        let e = std::io::Error::last_os_error();
10427                        let raw = e.raw_os_error().unwrap_or(0);
10428                        if raw != 0 && raw != libc::EINTR {
10429                            zwarn(&format!("can't create temp file for here document: {}", e));
10430                            // c:3772-3774
10431                        }
10432                        closemnodes(&mut mfds); // c:3775
10433                        fixfds(&save); // c:3776
10434                        {
10435                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10436                            LASTVAL.store(1, Ordering::Relaxed);
10437                        } // c:3777
10438                        break;
10439                    }
10440                    // c:3779
10441                    addfd(
10442                        forked,
10443                        &mut save,
10444                        &mut mfds,
10445                        fn_.fd1,
10446                        fil_local,
10447                        0,
10448                        fn_.varid.as_deref(),
10449                    );
10450                }
10451                t if t == REDIR_READ || t == REDIR_READWRITE => {
10452                    // c:3781-3782
10453                    if checkclobberparam(&fn_) == 0 {
10454                        fil_local = -1; // c:3784
10455                    } else {
10456                        let name = fn_.name.clone().unwrap_or_default();
10457                        let unmeta_name = unmeta(&name);
10458                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
10459                            Ok(c) => c,
10460                            Err(_) => {
10461                                closemnodes(&mut mfds);
10462                                fixfds(&save);
10463                                {
10464                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10465                                    LASTVAL.store(1, Ordering::Relaxed);
10466                                }
10467                                break;
10468                            }
10469                        };
10470                        if fn_.typ == REDIR_READ {
10471                            // c:3786
10472                            fil_local = unsafe {
10473                                libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
10474                            };
10475                        } else {
10476                            // c:3788-3789
10477                            fil_local = unsafe {
10478                                libc::open(
10479                                    cstr.as_ptr(),
10480                                    libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
10481                                    0o666,
10482                                )
10483                            };
10484                        }
10485                    }
10486                    if fil_local == -1 {
10487                        // c:3790
10488                        closemnodes(&mut mfds); // c:3791
10489                        fixfds(&save); // c:3792
10490                        let e = std::io::Error::last_os_error();
10491                        if e.raw_os_error().unwrap_or(0) != libc::EINTR {
10492                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
10493                            // c:3793-3794
10494                        }
10495                        {
10496                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10497                            LASTVAL.store(1, Ordering::Relaxed);
10498                        } // c:3795
10499                        break;
10500                    }
10501                    // c:3797
10502                    addfd(
10503                        forked,
10504                        &mut save,
10505                        &mut mfds,
10506                        fn_.fd1,
10507                        fil_local,
10508                        0,
10509                        fn_.varid.as_deref(),
10510                    );
10511                    // c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
10512                    if nullexec == 1
10513                        && fn_.fd1 == 0
10514                        && fn_.varid.is_none()
10515                        && isset(SHINSTDIN)
10516                        && isset(INTERACTIVE)
10517                    {
10518                        // c:3801 — `!zleactive` check ommitted (zleactive
10519                        // accessor lives in zle module; fusevm bypasses ZLE).
10520                        crate::ported::init::init_io(None); // c:3802
10521                    }
10522                }
10523                t if t == REDIR_CLOSE => {
10524                    // c:3804
10525                    // c:3805 — `if (fn->varid) { parse fd from variable }`
10526                    let mut fd1_local = fn_.fd1;
10527                    if let Some(varname) = fn_.varid.as_deref() {
10528                        // c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
10529                        // with varid. The C path resolves the named param
10530                        // to its integer-string value, parses as base-10
10531                        // (or base#NN), and rejects readonly / non-numeric
10532                        // / shell-owned-fd values.
10533                        //
10534                        //   bad=1  → "parameter %s does not contain a file descriptor"
10535                        //   bad=2  → "can't close file descriptor from readonly parameter %s"
10536                        //   bad=3  → "file descriptor %d used by shell, not closed"
10537                        //
10538                        // Substrate now available: getsparam for value,
10539                        // paramtab read for PM_READONLY, MAX_ZSH_FD +
10540                        // fdtable_get for shell-owned guard.
10541                        let mut bad: u8 = 0;
10542                        let value_opt = getsparam(varname);
10543                        let is_ro = paramtab()
10544                            .read()
10545                            .ok()
10546                            .and_then(|t| {
10547                                t.get(varname)
10548                                    .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
10549                            })
10550                            .unwrap_or(false);
10551                        if value_opt.is_none() {
10552                            bad = 1; // c:3811 getvalue failed
10553                        } else if is_ro {
10554                            bad = 2; // c:3813 PM_READONLY
10555                        } else {
10556                            let s = value_opt.as_deref().unwrap_or("");
10557                            match s.trim().parse::<i32>() {
10558                                Ok(n) => {
10559                                    fd1_local = n;
10560                                    fn_.fd1 = n;
10561                                    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
10562                                    if n >= 10
10563                                        && n <= max_fd
10564                                        && (fdtable_get(n) & FDT_TYPE_MASK) == FDT_INTERNAL
10565                                    {
10566                                        // c:3835 shell-owned-fd reject
10567                                        bad = 3;
10568                                    }
10569                                }
10570                                Err(_) => {
10571                                    bad = 1; // c:3823 strtol failure
10572                                }
10573                            }
10574                        }
10575                        if bad != 0 {
10576                            // c:3840-3849
10577                            match bad {
10578                                3 => zwarn(&format!(
10579                                    "file descriptor {} used by shell, not closed",
10580                                    fn_.fd1
10581                                )),
10582                                2 => zwarn(&format!(
10583                                    "can't close file descriptor from readonly parameter {}",
10584                                    varname
10585                                )),
10586                                _ => zwarn(&format!(
10587                                    "parameter {} does not contain a file descriptor",
10588                                    varname
10589                                )),
10590                            }
10591                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10592                            LASTVAL.store(1, Ordering::Relaxed);
10593                            break;
10594                        }
10595                    }
10596                    // c:3852-3865 — `closed`: optional movefd save.
10597                    closed = 0;
10598                    if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
10599                        // c:3856
10600                        let mv = movefd(fd1_local); // c:3857
10601                        save[fd1_local as usize] = mv;
10602                        if mv >= 0 {
10603                            closed = 1; // c:3862-3863
10604                        }
10605                    }
10606                    if fd1_local < 10 {
10607                        // c:3866
10608                        closemn(&mut mfds, fd1_local, REDIR_CLOSE);
10609                        // c:3867
10610                    }
10611                    // c:3873-3876
10612                    let _ = &mut fd1_local;
10613                    if closed == 0 && zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
10614                        zwarn(&format!(
10615                            "failed to close file descriptor {}: {}",
10616                            fn_.fd1,
10617                            std::io::Error::last_os_error()
10618                        )); // c:3873-3875
10619                    }
10620                }
10621                t if t == REDIR_MERGEIN || t == REDIR_MERGEOUT => {
10622                    // c:3878-3879
10623                    if fn_.fd2 < 10 {
10624                        closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
10625                    }
10626                    if checkclobberparam(&fn_) == 0 {
10627                        fil_local = -1; // c:3883
10628                    } else if fn_.fd2 > 9 {
10629                        // c:3884-3897 — fd table check.
10630                        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
10631                        let cin = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
10632                        let cout = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
10633                        let in_table = if fn_.fd2 <= max_fd {
10634                            let kind = fdtable_get(fn_.fd2) & FDT_TYPE_MASK;
10635                            kind != FDT_UNUSED && kind != FDT_EXTERNAL
10636                        } else {
10637                            false
10638                        };
10639                        if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
10640                            fil_local = -1; // c:3896
10641                                            // Per-platform errno setter (c:3897 `errno = EBADF;`).
10642                            #[cfg(target_os = "macos")]
10643                            unsafe {
10644                                *libc::__error() = libc::EBADF;
10645                            }
10646                            #[cfg(target_os = "linux")]
10647                            unsafe {
10648                                *libc::__errno_location() = libc::EBADF;
10649                            }
10650                        } else {
10651                            let fd = if fn_.fd2 == -2 {
10652                                // c:3900-3901
10653                                if fn_.typ == REDIR_MERGEOUT {
10654                                    crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
10655                                } else {
10656                                    crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
10657                                }
10658                            } else {
10659                                fn_.fd2
10660                            };
10661                            // c:3902 — `fil = movefd(dup(fd));`
10662                            let dup_fd = unsafe { libc::dup(fd) };
10663                            fil_local = movefd(dup_fd);
10664                        }
10665                    } else {
10666                        let fd = if fn_.fd2 == -2 {
10667                            if fn_.typ == REDIR_MERGEOUT {
10668                                crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
10669                            } else {
10670                                crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
10671                            }
10672                        } else {
10673                            fn_.fd2
10674                        };
10675                        let dup_fd = unsafe { libc::dup(fd) };
10676                        fil_local = movefd(dup_fd);
10677                    }
10678                    if fil_local == -1 {
10679                        // c:3904
10680                        closemnodes(&mut mfds); // c:3907
10681                        fixfds(&save); // c:3908
10682                        if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
10683                            let desc = if fn_.fd2 == -2 {
10684                                "coprocess".to_string()
10685                            } else {
10686                                format!("{}", fn_.fd2)
10687                            };
10688                            zwarn(&format!("{}: {}", desc, std::io::Error::last_os_error()));
10689                            // c:3911-3913
10690                        }
10691                        {
10692                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10693                            LASTVAL.store(1, Ordering::Relaxed);
10694                        } // c:3914
10695                        break;
10696                    }
10697                    // c:3916-3917
10698                    let merge_is_out = if fn_.typ == REDIR_MERGEOUT { 1 } else { 0 };
10699                    addfd(
10700                        forked,
10701                        &mut save,
10702                        &mut mfds,
10703                        fn_.fd1,
10704                        fil_local,
10705                        merge_is_out,
10706                        fn_.varid.as_deref(),
10707                    );
10708                }
10709                _ => {
10710                    // c:3919 default — write/append/error_redir.
10711                    let mut dfil: i32;
10712                    if checkclobberparam(&fn_) == 0 {
10713                        fil_local = -1; // c:3921
10714                    } else if IS_APPEND_REDIR(fn_.typ) {
10715                        // c:3922
10716                        let name = fn_.name.clone().unwrap_or_default();
10717                        let unmeta_name = unmeta(&name);
10718                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
10719                            Ok(c) => c,
10720                            Err(_) => {
10721                                closemnodes(&mut mfds);
10722                                fixfds(&save);
10723                                {
10724                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10725                                    LASTVAL.store(1, Ordering::Relaxed);
10726                                }
10727                                break;
10728                            }
10729                        };
10730                        // c:3924-3927
10731                        let mode = if !isset(CLOBBER)
10732                            && !isset(crate::ported::zsh_h::APPENDCREATE)
10733                            && !IS_CLOBBER_REDIR(fn_.typ)
10734                        {
10735                            libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
10736                        } else {
10737                            libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
10738                        };
10739                        fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
10740                    } else {
10741                        // c:3929
10742                        fil_local = clobber_open(&fn_);
10743                    }
10744                    // c:3930-3933 — error_redir dup.
10745                    if fil_local != -1 && IS_ERROR_REDIR(fn_.typ) {
10746                        let dup_fd = unsafe { libc::dup(fil_local) };
10747                        dfil = movefd(dup_fd); // c:3931
10748                    } else {
10749                        dfil = 0; // c:3933
10750                    }
10751                    if fil_local == -1 || dfil == -1 {
10752                        // c:3934
10753                        if fil_local != -1 {
10754                            unsafe { libc::close(fil_local) }; // c:3935-3936
10755                        }
10756                        closemnodes(&mut mfds); // c:3937
10757                        fixfds(&save); // c:3938
10758                        let e = std::io::Error::last_os_error();
10759                        let raw = e.raw_os_error().unwrap_or(0);
10760                        if raw != 0 && raw != libc::EINTR {
10761                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
10762                            // c:3939-3940
10763                        }
10764                        {
10765                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10766                            LASTVAL.store(1, Ordering::Relaxed);
10767                        } // c:3941
10768                        break;
10769                    }
10770                    // c:3943
10771                    addfd(
10772                        forked,
10773                        &mut save,
10774                        &mut mfds,
10775                        fn_.fd1,
10776                        fil_local,
10777                        1,
10778                        fn_.varid.as_deref(),
10779                    );
10780                    if IS_ERROR_REDIR(fn_.typ) {
10781                        // c:3944-3945
10782                        addfd(forked, &mut save, &mut mfds, 2, dfil, 1, None);
10783                    }
10784                    let _ = &mut dfil;
10785                }
10786            }
10787            // c:3948-3952 — addfd errflag check.
10788            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10789                // c:3949
10790                closemnodes(&mut mfds); // c:3950
10791                fixfds(&save); // c:3951
10792                {
10793                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10794                    LASTVAL.store(1, Ordering::Relaxed);
10795                } // c:3952
10796                break;
10797            }
10798        }
10799    }
10800
10801    // c:3957-3961 — close multios with ct >= 2.
10802    i = 0;
10803    while i < 10 {
10804        // c:3959
10805        if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
10806            if m.ct >= 2 {
10807                closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
10808            }
10809        }
10810        i += 1;
10811    }
10812
10813    // c:3963-3995 — nullexec branch.
10814    if nullexec != 0 {
10815        // c:3963
10816        if let Some(vspc) = varspc {
10817            // c:3969
10818            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
10819            let mut removelist: Vec<String> = Vec::new();
10820            if !isset(POSIXBUILTINS) && nullexec != 2 {
10821                // c:3971-3972
10822                save_params(state, vspc, &mut restorelist, &mut removelist);
10823            }
10824            addvars(state, vspc, 0); // c:3973
10825            if !restorelist.is_empty() {
10826                // c:3974
10827                restore_params(restorelist, removelist); // c:3975
10828            }
10829        }
10830        let ef = errflag.load(Ordering::Relaxed);
10831        LASTVAL.store(
10832            if ef != 0 {
10833                ef
10834            } else {
10835                cmdoutval.load(Ordering::Relaxed)
10836            },
10837            Ordering::Relaxed,
10838        ); // c:3977
10839        if nullexec == 1 {
10840            // c:3978
10841            // c:3983-3985 — close save[i].
10842            i = 0;
10843            while i < 10 {
10844                if save[i as usize] != -2 {
10845                    let _ = zclose(save[i as usize]); // c:3985
10846                }
10847                i += 1;
10848            }
10849            // c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
10850            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10851            if thisjob >= 0 {
10852                if let Some(jt) = JOBTAB.get() {
10853                    let mut guard = jt.lock().unwrap();
10854                    if let Some(j) = guard.get_mut(thisjob as usize) {
10855                        j.stat |= STAT_DONE; // c:3989
10856                    }
10857                }
10858            }
10859            return execcmd_exec_done_path(
10860                redir_err,
10861                oautocont,
10862                how,
10863                &mut shti,
10864                &mut chti,
10865                &mut then_ts,
10866                forked,
10867                &mut newxtrerr,
10868                cflags,
10869                orig_cflags,
10870                is_cursh,
10871                do_exec,
10872            );
10873        }
10874        if isset(XTRACE) {
10875            // c:3992-3994
10876            eprintln!();
10877        }
10878    } else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
10879        // c:3996 — main dispatch branch.
10880        // c:3997 — `int q = queue_signal_level();`
10881        let _q = 0;
10882        // c:4003-4012 — entersubsh for is_exec.
10883        if is_exec != 0 {
10884            // c:4003
10885            let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
10886                esub::ASYNC
10887            } else {
10888                0
10889            } | esub::PGRP
10890                | esub::FAKE; // c:4004-4005
10891            if typ != WC_SUBSH as i32 {
10892                flags |= esub::KEEPTRAP; // c:4007
10893            }
10894            if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
10895                // c:4008-4009
10896                flags |= esub::REVERTPGRP; // c:4010
10897            }
10898            entersubsh(flags, None); // c:4011
10899        }
10900
10901        if typ == WC_FUNCDEF as i32 {
10902            // c:4013
10903            // c:4014-4036 — `redir_prog` setup from wordcode if no
10904            // redirs+WC_REDIR follows. Wire only when fusevm WC_REDIR
10905            // peek is in scope; for the tree-walker entry point we
10906            // approximate by passing None.
10907            let redir_prog: Option<crate::ported::zsh_h::Eprog> = None;
10908            // c:4039 — `lastval = execfuncdef(state, redir_prog);`
10909            let lv = execfuncdef(state, redir_prog);
10910            LASTVAL.store(lv, Ordering::Relaxed);
10911        } else if typ >= WC_CURSH as i32 {
10912            // c:4042
10913            if last1 == 1 {
10914                do_exec = 1; // c:4044
10915            }
10916            if typ == WC_AUTOFN as i32 {
10917                // c:4046
10918                let lv = execautofn_basic(state, do_exec); // c:4051
10919                LASTVAL.store(lv, Ordering::Relaxed);
10920            } else {
10921                // c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
10922                // dispatch_execfuncs ports the C `execfuncs[]` table
10923                // (Src/exec.c:170-180) by typ → exec{cursh,for,select,...}
10924                // direct call. See dispatch_execfuncs at end of file.
10925                let lv = dispatch_execfuncs(state, typ, do_exec);
10926                LASTVAL.store(lv, Ordering::Relaxed);
10927            }
10928        } else if is_builtin != 0 || is_shfunc != 0 {
10929            // c:4055
10930            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
10931            let mut removelist: Vec<String> = Vec::new();
10932            let mut do_save: i32 = 0; // c:4057
10933
10934            if forked == 0 {
10935                // c:4060
10936                if isset(POSIXBUILTINS) {
10937                    // c:4061
10938                    if is_shfunc != 0
10939                        || (hn.map(|p| unsafe { (*p).node.flags as u32 }).unwrap_or(0)
10940                            & (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
10941                            != 0
10942                    {
10943                        // c:4067
10944                        do_save = if (orig_cflags & BINF_COMMAND) != 0 {
10945                            1
10946                        } else {
10947                            0
10948                        };
10949                    } else {
10950                        do_save = 1; // c:4070
10951                    }
10952                } else {
10953                    // c:4071
10954                    if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
10955                        // c:4076
10956                        do_save = 1; // c:4077
10957                    }
10958                }
10959                if do_save != 0 {
10960                    if let Some(vspc) = varspc {
10961                        // c:4079
10962                        save_params(state, vspc, &mut restorelist, &mut removelist);
10963                    }
10964                }
10965            }
10966            if varspc.is_some() {
10967                // c:4082
10968                let mut addflags: i32 = 0; // c:4086
10969                if is_shfunc != 0 {
10970                    addflags |= ADDVAR_EXPORT; // c:4088
10971                }
10972                if !restorelist.is_empty() {
10973                    addflags |= ADDVAR_RESTORE; // c:4090
10974                }
10975                addvars(state, varspc.unwrap_or(0), addflags); // c:4092
10976                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10977                    // c:4093
10978                    if !restorelist.is_empty() {
10979                        restore_params(restorelist, removelist); // c:4094-4095
10980                    }
10981                    LASTVAL.store(1, Ordering::Relaxed); // c:4096
10982                    fixfds(&save); // c:4097
10983                    return execcmd_exec_done_path(
10984                        redir_err,
10985                        oautocont,
10986                        how,
10987                        &mut shti,
10988                        &mut chti,
10989                        &mut then_ts,
10990                        forked,
10991                        &mut newxtrerr,
10992                        cflags,
10993                        orig_cflags,
10994                        is_cursh,
10995                        do_exec,
10996                    );
10997                }
10998            }
10999
11000            if is_shfunc != 0 {
11001                // c:4102-4105
11002                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
11003                // c:4104 — `execshfunc((Shfunc) hn, args);` C casts
11004                // HashNode hn to Shfunc; zshrs's hn is *mut builtin so
11005                // we re-resolve the shfunc by name from shfunctab and
11006                // dispatch through the top-level execshfunc port at
11007                // exec.rs:4978 (which routes to runshfunc).
11008                let name = args
11009                    .as_ref()
11010                    .and_then(|v| v.first())
11011                    .cloned()
11012                    .unwrap_or_default();
11013                let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
11014                    tab.get(&name).cloned()
11015                } else {
11016                    None
11017                };
11018                if let Some(ref mut shf) = shf_clone {
11019                    execshfunc(shf, &mut a_vec);
11020                }
11021                // c:4105 — `pipecleanfilelist(filelist, 0);` — clean
11022                // out the proc_subst entries from the current job's
11023                // filelist after the shfunc body ran. Route through
11024                // `JOBTAB[thisjob]`.
11025                if let Some(jt) = JOBTAB.get() {
11026                    let mut guard = jt.lock().unwrap();
11027                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
11028                    if tj >= 0 {
11029                        if let Some(j) = guard.get_mut(tj as usize) {
11030                            crate::ported::jobs::pipecleanfilelist(j, false);
11031                        }
11032                    }
11033                }
11034            } else {
11035                // c:4107 — builtin path.
11036                let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
11037                let postassigns = eparams.postassigns; // c:4109
11038                if forked != 0 {
11039                    closem(FDT_INTERNAL, 0); // c:4111
11040                }
11041                if postassigns != 0 {
11042                    // c:4112-4230 — typeset post-assignment processing.
11043                    use crate::ported::zsh_h::{
11044                        ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
11045                        PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
11046                        WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
11047                    };
11048                    let opc = state.pc; // c:4113
11049                    state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
11050                                                                      // c:4115 — `assigns = newlinklist();` — already declared above.
11051                    let mut pa_remaining = postassigns;
11052                    while pa_remaining > 0 {
11053                        // c:4116 — `while (postassigns--)`
11054                        pa_remaining -= 1;
11055                        let mut pa_htok: i32 = 0; // c:4117
11056                        if state.pc >= state.prog.prog.len() {
11057                            break;
11058                        }
11059                        let ac = state.prog.prog[state.pc]; // c:4118
11060                        state.pc += 1;
11061                        let mut name = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut pa_htok)); // c:4119
11062                                                                                            // c:4123-4124 DPUTS — debug assertion skipped.
11063                        if pa_htok != 0 {
11064                            // c:4126 — `init_list1(svl, name);`
11065                            let mut svl: LinkList<String> = Default::default();
11066                            svl.push_back(name.clone());
11067                            // c:4127-4166 — INC-scalar special case (typeset $ass form).
11068                            if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
11069                                && WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
11070                            {
11071                                // c:4141 — `(void)ecgetstr(...)` — dummy.
11072                                let mut dummy_htok: i32 = 0;
11073                                let _ = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut dummy_htok));
11074                                let mut rf = 0i32;
11075                                prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
11076                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11077                                    // c:4143
11078                                    state.pc = opc; // c:4144
11079                                    break;
11080                                }
11081                                let mut rf2 = 0i32;
11082                                globlist(&mut svl, rf2); // c:4147
11083                                let _ = &mut rf2;
11084                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11085                                    // c:4148
11086                                    state.pc = opc; // c:4149
11087                                    break;
11088                                }
11089                                // c:4152-4165 — drain svl into assigns.
11090                                while let Some(data) = svl.pop_front() {
11091                                    let (asg_name, asg_val): (String, Option<String>) =
11092                                        if let Some(eq_pos) = data.find('=') {
11093                                            // c:4156-4159
11094                                            (
11095                                                data[..eq_pos].to_string(),
11096                                                Some(data[eq_pos + 1..].to_string()),
11097                                            )
11098                                        } else {
11099                                            // c:4161-4162
11100                                            (data, None)
11101                                        };
11102                                    assigns.push(crate::ported::zsh_h::asgment {
11103                                        node: crate::ported::zsh_h::linknode {
11104                                            next: None,
11105                                            prev: None,
11106                                            dat: 0,
11107                                        },
11108                                        name: asg_name,
11109                                        flags: 0,
11110                                        scalar: asg_val,
11111                                        array: None,
11112                                    });
11113                                }
11114                                continue; // c:4166
11115                            }
11116                            // c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
11117                            let mut rf = 0i32;
11118                            prefork(&mut svl, PREFORK_SINGLE, &mut rf);
11119                            // c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
11120                            name = if svl.is_empty() {
11121                                String::new()
11122                            } else {
11123                                svl.pop_front().unwrap_or_default()
11124                            };
11125                        }
11126                        // c:4172 — `untokenize(name);`
11127                        // (untokenize is destructive on bytes; Rust untokenize
11128                        // returns a new String — call and rebind.)
11129                        name = untokenize(&name);
11130                        let mut asg = crate::ported::zsh_h::asgment {
11131                            node: crate::ported::zsh_h::linknode {
11132                                next: None,
11133                                prev: None,
11134                                dat: 0,
11135                            },
11136                            name,
11137                            flags: 0,
11138                            scalar: None,
11139                            array: None,
11140                        };
11141                        if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
11142                            // c:4175
11143                            let mut val_htok: i32 = 0;
11144                            let mut val = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut val_htok)); // c:4176
11145                            asg.flags = 0; // c:4177
11146                            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
11147                                // c:4178-4180 — fake assignment, no value.
11148                                asg.scalar = None;
11149                            } else {
11150                                if val_htok != 0 {
11151                                    // c:4183
11152                                    let mut svl: LinkList<String> = Default::default();
11153                                    svl.push_back(val.clone());
11154                                    let mut rf = 0i32;
11155                                    prefork(&mut svl, PREFORK_SINGLE | PREFORK_ASSIGN, &mut rf); // c:4184-4186
11156                                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11157                                        // c:4187
11158                                        state.pc = opc; // c:4188
11159                                        break;
11160                                    }
11161                                    // c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
11162                                    val = if svl.is_empty() {
11163                                        String::new()
11164                                    } else {
11165                                        svl.pop_front().unwrap_or_default()
11166                                    };
11167                                }
11168                                // c:4198 — `untokenize(val);`
11169                                asg.scalar = Some(untokenize(&val));
11170                            }
11171                        } else {
11172                            // c:4202 — array assignment.
11173                            asg.flags = ASG_ARRAY; // c:4202
11174                            let mut arr_htok: i32 = 0;
11175                            let arr_words = ecgetlist(
11176                                state,
11177                                WC_ASSIGN_NUM(ac) as usize,
11178                                ECDUPTOK_LOCAL,
11179                                Some(&mut arr_htok),
11180                            ); // c:4204
11181                            let mut arr_list: LinkList<String> = Default::default();
11182                            for s in arr_words {
11183                                arr_list.push_back(s);
11184                            }
11185                            if !arr_list.is_empty()
11186                                && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
11187                            {
11188                                // c:4209 — `int prefork_ret = 0;`
11189                                let mut prefork_ret = 0i32;
11190                                prefork(&mut arr_list, PREFORK_ASSIGN, &mut prefork_ret); // c:4210-4211
11191                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11192                                    // c:4212
11193                                    state.pc = opc; // c:4213
11194                                    break;
11195                                }
11196                                if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
11197                                    // c:4216
11198                                    asg.flags |= ASG_KEY_VALUE; // c:4217
11199                                }
11200                                globlist(&mut arr_list, prefork_ret); // c:4218
11201                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11202                                    // c:4220
11203                                    state.pc = opc; // c:4221
11204                                    break;
11205                                }
11206                            }
11207                            asg.array = Some(arr_list);
11208                        }
11209                        // c:4227 — `uaddlinknode(assigns, &asg->node);`
11210                        assigns.push(asg);
11211                    }
11212                    state.pc = opc; // c:4229
11213                }
11214                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
11215                    // c:4232
11216                    // c:Src/builtin.c:262 — `name = (char *) ugetnode(args);`
11217                    // C's execbuiltin consumes args[0] (the command name)
11218                    // at entry. zshrs's execbuiltin reads the name from
11219                    // `bn->node.nam` instead, so we strip args[0] here
11220                    // before the call to match C's post-ugetnode argv
11221                    // shape. Without this, e.g. `cmd=pwd; $cmd` reached
11222                    // execbuiltin with args=["pwd"] and pwd's
11223                    // maxargs=0 check rejected the empty call as
11224                    // "too many arguments".
11225                    let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
11226                    if !a_vec.is_empty() {
11227                        a_vec.remove(0);
11228                    }
11229                    let ret = crate::ported::builtin::execbuiltin(
11230                        a_vec,
11231                        assigns,
11232                        hn.unwrap_or(std::ptr::null_mut()),
11233                    ); // c:4233
11234                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
11235                        // c:4238
11236                        LASTVAL.store(ret, Ordering::Relaxed); // c:4239
11237                    }
11238                }
11239                if (do_save & BINF_COMMAND as i32) != 0 {
11240                    // c:4241
11241                    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
11242                }
11243                // c:4244 fflush(stdout) — Rust stdio auto-flushes.
11244                // c:4245-4251 — write-error check on save[1].
11245            }
11246            if isset(PRINTEXITVALUE)
11247                && isset(SHINSTDIN)
11248                && LASTVAL.load(Ordering::Relaxed) != 0
11249                && subsh.load(Ordering::Relaxed) == 0
11250            {
11251                // c:4253-4255
11252                eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
11253            }
11254
11255            if do_exec != 0 {
11256                // c:4263
11257                if subsh.load(Ordering::Relaxed) != 0 {
11258                    crate::ported::builtin::_realexit(); // c:4264-4265
11259                }
11260                if isset(RCS)
11261                    && crate::ported::zsh_h::interact()
11262                    && nohistsave.load(Ordering::Relaxed) == 0
11263                {
11264                    // c:4269
11265                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
11266                    // c:4270
11267                }
11268                crate::ported::builtin::realexit(); // c:4271
11269            }
11270            if !restorelist.is_empty() {
11271                // c:4273
11272                restore_params(restorelist, removelist); // c:4274
11273            }
11274        } else {
11275            // c:4276 — external command execute.
11276            if subsh.load(Ordering::Relaxed) == 0 {
11277                // c:4277
11278                if forked == 0 {
11279                    // c:4280 — `setiparam("SHLVL", --shlvl);`
11280                    let cur = getsparam("SHLVL")
11281                        .and_then(|s| s.parse::<i64>().ok())
11282                        .unwrap_or(1);
11283                    setiparam("SHLVL", cur - 1); // c:4281
11284                }
11285                if do_exec != 0
11286                    && isset(RCS)
11287                    && crate::ported::zsh_h::interact()
11288                    && nohistsave.load(Ordering::Relaxed) == 0
11289                {
11290                    // c:4285
11291                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
11292                    // c:4286
11293                }
11294            }
11295            if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
11296                // c:4288
11297                if varspc.is_some() {
11298                    // c:4289
11299                    let mut addflags: i32 = ADDVAR_EXPORT; // c:4290
11300                    if forked != 0 {
11301                        addflags |= ADDVAR_RESTORE; // c:4292
11302                    }
11303                    addvars(state, varspc.unwrap_or(0), addflags); // c:4293
11304                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11305                        // c:4294
11306                        std::process::exit(1); // c:4295
11307                    }
11308                }
11309                closem(FDT_INTERNAL, 0); // c:4297
11310                                         // c:4298-4305 — close coprocin/coprocout.
11311                let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
11312                if cpi != -1 {
11313                    let _ = zclose(cpi); // c:4299
11314                    crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed);
11315                    // c:4300
11316                }
11317                let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
11318                if cpo != -1 {
11319                    let _ = zclose(cpo); // c:4303
11320                    crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed);
11321                    // c:4304
11322                }
11323                if forked == 0 {
11324                    // c:4307
11325                    setlimits(""); // c:4308
11326                }
11327                if (how & Z_ASYNC as i32) != 0 {
11328                    // c:4310 — `zsfree(STTYval); STTYval = 0;`
11329                    let mut guard = STTYval.lock().unwrap();
11330                    *guard = None; // c:4311-4312
11331                }
11332                // c:4314 — `execute(args, cflags, use_defpath);`
11333                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
11334                execute(&mut a_vec, cflags, use_defpath); // c:4314
11335            } else {
11336                // c:4315 — `( ... )` — WC_SUBSH.
11337                list_pipe.store(0, Ordering::Relaxed); // c:4318
11338                                                       // c:4319 — `pipecleanfilelist(filelist, 0);` — clean
11339                                                       // proc-subst entries from the current job's filelist
11340                                                       // before recursing into the subshell body.
11341                if let Some(jt) = JOBTAB.get() {
11342                    let mut guard = jt.lock().unwrap();
11343                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
11344                    if tj >= 0 {
11345                        if let Some(j) = guard.get_mut(tj as usize) {
11346                            crate::ported::jobs::pipecleanfilelist(j, false);
11347                        }
11348                    }
11349                }
11350                state.pc += 1; // c:4324 — `state->pc++;`
11351                let _ = execlist(state, 0, 1); // c:4325
11352            }
11353        }
11354    }
11355
11356    // c:4330-4404 — err: + done: + fatal:.
11357    return execcmd_exec_done_path(
11358        redir_err,
11359        oautocont,
11360        how,
11361        &mut shti,
11362        &mut chti,
11363        &mut then_ts,
11364        forked,
11365        &mut newxtrerr,
11366        cflags,
11367        orig_cflags,
11368        is_cursh,
11369        do_exec,
11370    );
11371}
11372
11373/// Internal helper modelling the C `done:` label tail of
11374/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
11375/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
11376/// shelltime stop, and newxtrerr close.
11377#[allow(clippy::too_many_arguments)]
11378fn execcmd_exec_done_path(
11379    redir_err: i32,
11380    oautocont: i32,
11381    how: i32,
11382    shti: &mut crate::ported::jobs::timeinfo,
11383    chti: &mut crate::ported::jobs::timeinfo,
11384    then_ts: &mut std::time::Instant,
11385    forked: i32,
11386    newxtrerr: &mut Option<i32>,
11387    cflags: u32,
11388    orig_cflags: u32,
11389    is_cursh: i32,
11390    do_exec: i32,
11391) {
11392    use crate::ported::zsh_h::{
11393        AUTOCONTINUE, BINF_COMMAND, BINF_EXEC, BINF_PSPECIAL, INTERACTIVE, POSIXBUILTINS, Z_TIMED,
11394    };
11395    // c:4366
11396    // c:4367-4386 — POSIX special-builtin error escalation.
11397    if isset(POSIXBUILTINS)
11398        && (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
11399        && (orig_cflags & BINF_COMMAND) == 0
11400    {
11401        // c:4367-4369
11402        let _forked_or_subsh = forked | zsh_subshell.load(Ordering::Relaxed); // c:4376
11403                                                                              // fatal: label entry point — same handling.
11404        if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11405            // c:4378
11406            if !isset(INTERACTIVE) {
11407                // c:4379
11408                if _forked_or_subsh != 0 {
11409                    unsafe { libc::_exit(1) }; // c:4381
11410                } else {
11411                    std::process::exit(1); // c:4383
11412                }
11413            }
11414            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
11415        }
11416    }
11417    // c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
11418    if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
11419        crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1);
11420        // c:4389
11421    }
11422    // c:4390-4398 — newxtrerr close.
11423    if let Some(fd) = newxtrerr.take() {
11424        // c:4390
11425        let _ = zclose(fd); // c:4396
11426    }
11427    // c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
11428    {
11429        let mut guard = STTYval.lock().unwrap();
11430        *guard = None;
11431    }
11432    // c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
11433    if oautocont >= 0 {
11434        opt_state_set("autocontinue", oautocont != 0);
11435    }
11436}
11437
11438/// Internal helper modelling the C `err:` label tail of
11439/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
11440/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
11441/// to done:.
11442#[allow(clippy::too_many_arguments)]
11443fn execcmd_exec_err_path(
11444    forked: i32,
11445    save: &mut [i32; 10],
11446    mfds: &mut [Option<Box<multio>>; 10],
11447    oautocont: i32,
11448    how: i32,
11449    shti: &mut crate::ported::jobs::timeinfo,
11450    chti: &mut crate::ported::jobs::timeinfo,
11451    then_ts: &mut std::time::Instant,
11452    newxtrerr: &mut Option<i32>,
11453    cflags: u32,
11454    orig_cflags: u32,
11455    is_cursh: i32,
11456    do_exec: i32,
11457    redir_err: i32,
11458) {
11459    use crate::ported::zsh_h::FDT_UNUSED;
11460    // c:4330
11461    if forked != 0 {
11462        // c:4331
11463        // c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
11464        let mut i: i32 = 0;
11465        while i < 10 {
11466            if fdtable_get(i) != FDT_UNUSED {
11467                unsafe { libc::close(i) }; // c:4358
11468            }
11469            i += 1;
11470        }
11471        // c:4359 — `closem(FDT_UNUSED, 1);`
11472        closem(FDT_UNUSED, 1); // c:4359
11473                               // c:4360-4361 — `if (thisjob != -1) waitjobs();`
11474        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
11475        if thisjob != -1 {
11476            if let Some(jt) = JOBTAB.get() {
11477                let mut guard = jt.lock().unwrap();
11478                crate::ported::jobs::waitjobs(&mut guard, thisjob as usize); // c:4361
11479            }
11480        }
11481        crate::ported::builtin::_realexit(); // c:4362
11482    }
11483    fixfds(save); // c:4364
11484
11485    execcmd_exec_done_path(
11486        redir_err,
11487        oautocont,
11488        how,
11489        shti,
11490        chti,
11491        then_ts,
11492        forked,
11493        newxtrerr,
11494        cflags,
11495        orig_cflags,
11496        is_cursh,
11497        do_exec,
11498    );
11499}
11500
11501/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
11502/// `Src/exec.c:170-180`. Each branch maps to the ported wordcode-
11503/// walker function in `src/ported/exec.rs`.
11504fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
11505    use crate::ported::zsh_h::{
11506        WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
11507        WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
11508    };
11509    // Port of `static int (*const execfuncs[])(Estate, int)` dispatch
11510    // table at `Src/exec.c:170-180`. C indexes by `(type - WC_CURSH)`;
11511    // Rust matches on the WC_* tag directly.
11512    match typ as wordcode {
11513        x if x == WC_CURSH => execcursh(state, do_exec),
11514        x if x == WC_FOR => execfor(state, do_exec),
11515        x if x == WC_SELECT => execselect(state, do_exec),
11516        x if x == WC_WHILE => execwhile(state, do_exec),
11517        x if x == WC_REPEAT => execrepeat(state, do_exec),
11518        x if x == WC_CASE => execcase(state, do_exec),
11519        x if x == WC_IF => execif(state, do_exec),
11520        x if x == WC_COND => execcond(state, do_exec),
11521        x if x == WC_ARITH => execarith(state, do_exec),
11522        x if x == WC_TRY => exectry(state, do_exec),
11523        x if x == WC_FUNCDEF => execfuncdef(state, None),
11524        // c:272 — execfuncs[] table dispatches `WC_AUTOFN` to
11525        // `execautofn` (the loadautofn-then-basic wrapper), not
11526        // `execautofn_basic` directly.
11527        x if x == WC_AUTOFN => execautofn(state, do_exec),
11528        x if x == WC_TIMED => exectime(state, do_exec),
11529        x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
11530        _ => 0,
11531    }
11532}
11533
11534/// Port of `Eprog stripkshdef(Eprog prog, char *name)` from
11535/// `Src/exec.c:6286-6364`. Given an Eprog read from an autoload
11536/// file plus the function name being defined, check whether the
11537/// file consists of *exactly* one `function NAME { … }` definition
11538/// for that name. If so, return a new Eprog whose `prog`/`strs`/
11539/// `pats` slice out just the function body (so calling code can
11540/// invoke the body directly instead of re-parsing). Otherwise
11541/// return the input untouched.
11542///
11543/// Header word layout consumed (matches C `pc[…]` reads):
11544///   pc[0] = WC_LIST with `Z_SYNC|Z_END|Z_SIMPLE` flags
11545///   pc[1] = (sublist header, skipped)
11546///   pc[2] = WC_FUNCDEF
11547///   pc[3] = 1                       (single-name funcdef)
11548///   pc[4] = name-string slot        (compared to `name`)
11549///   pc[5] = sbeg  (offset into strs table)
11550///   pc[6] = nstrs (bytes of strs to copy)
11551///   pc[7] = npats (number of pattern slots to allocate)
11552///   pc[8] = WC_FUNCDEF_SKIP target  (end-of-funcdef pc)
11553///   pc[9] = (unused header word — `pc += 6` lands here as the
11554///           start of the body wordcode stream)
11555///
11556/// Returns `None` only when the input was `None` (matches C
11557/// `return NULL`). Equivalence between the original `prog` and a
11558/// successfully stripped `prog` is *not* preserved at the pointer
11559/// level (C may return the original Eprog when the file fails the
11560/// single-funcdef shape check; this Rust port does the same by
11561/// passing the box back through).
11562///
11563/// `EF_MAP` (`zcompile`d / mmap'd Eprog) path: C mutates the
11564/// existing Eprog in place, swapping its `prog` / `strs` /
11565/// `pats` to slice into the funcdef body. Rust mirrors this on
11566/// the moved-in `Box<eprog>` (no separate `free()` needed —
11567/// `Vec` drop handles the old `pats`).
11568pub fn stripkshdef(
11569    prog: Option<crate::ported::zsh_h::Eprog>,
11570    name: &str,
11571) -> Option<crate::ported::zsh_h::Eprog> {
11572    use crate::ported::parse::ecrawstr;
11573    use crate::ported::zsh_h::{
11574        wc_code, wordcode, Dash, EF_HEAP, EF_MAP, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_LIST,
11575        WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC,
11576    };
11577
11578    // c:6300 — `if (!prog) return NULL;`
11579    let mut prog = prog?;
11580
11581    // c:6302-6306 — first word must be WC_LIST with all of
11582    // Z_SYNC|Z_END|Z_SIMPLE set (i.e. the trivial "single simple
11583    // sublist" wrapper around the funcdef).
11584    if prog.prog.len() < 3 {
11585        return Some(prog);
11586    }
11587    let code0: wordcode = prog.prog[0];
11588    if wc_code(code0) != WC_LIST
11589        || (WC_LIST_TYPE(code0) & (Z_SYNC | Z_END | Z_SIMPLE) as wordcode)
11590            != (Z_SYNC | Z_END | Z_SIMPLE) as wordcode
11591    {
11592        return Some(prog);
11593    }
11594    // c:6307 — `pc++;` (skip the sublist header word at pc[1]).
11595    // c:6308 — `code = *pc++;` lands `code` on pc[2], leaving the
11596    // walking cursor at pc[3] which is read directly below.
11597    let code: wordcode = prog.prog[2];
11598    let pc_after_code: usize = 3;
11599    if wc_code(code) != WC_FUNCDEF || prog.prog[pc_after_code] != 1 {
11600        return Some(prog);
11601    }
11602
11603    // c:6320 — `ptr2 = ecrawstr(prog, pc + 1, NULL);` (note: C's
11604    // `pc` is already past `code`, so `pc + 1` lands on pc[4] —
11605    // the name-string slot).
11606    let name_slot = pc_after_code + 1; // == 4
11607    let name_in_def = ecrawstr(&prog, name_slot, None);
11608
11609    // c:6320-6328 — name match, tolerating Dash-tokenised hyphens
11610    // on either side.
11611    let n1 = name.as_bytes();
11612    let n2 = name_in_def.as_bytes();
11613    let mut i = 0usize;
11614    let mut j = 0usize;
11615    while i < n1.len() && j < n2.len() {
11616        let c1 = n1[i] as char;
11617        let c2 = n2[j] as char;
11618        if c1 != c2 && c1 != Dash && c1 != '-' && c2 != Dash && c2 != '-' {
11619            break;
11620        }
11621        i += 1;
11622        j += 1;
11623    }
11624    // c:6329 — `if (*ptr1 || *ptr2) return prog;` (any unmatched
11625    // tail on either side → not the right funcdef).
11626    if i < n1.len() || j < n2.len() {
11627        return Some(prog);
11628    }
11629
11630    // c:6332-6362 — slice the funcdef body out. Layout:
11631    //   sbeg  = pc[2] (in C, == prog.prog[pc_after_code + 2] == [5])
11632    //   nstrs = pc[3] (== [6])
11633    //   npats = pc[4] (== [7])
11634    //   end   = pc + WC_FUNCDEF_SKIP(code)   (== pc_after_code + skip)
11635    //   pc   += 6  (body wordcode begins at pc_after_code + 6 == [9])
11636    let sbeg = prog.prog[pc_after_code + 2] as usize;
11637    let nstrs = prog.prog[pc_after_code + 3] as usize;
11638    let npats = prog.prog[pc_after_code + 4] as i32;
11639    let skip = WC_FUNCDEF_SKIP(code) as usize;
11640    let end_pc = pc_after_code + skip;
11641    let body_start = pc_after_code + 6;
11642    if end_pc < body_start || end_pc > prog.prog.len() {
11643        // Defensive: malformed header — return input untouched so
11644        // the caller's parse-eprog fallback re-reads from source.
11645        return Some(prog);
11646    }
11647    let nprg = end_pc - body_start;
11648    let plen = nprg * size_of::<wordcode>();
11649    let len = plen + (npats as usize) * size_of::<usize>() + nstrs;
11650
11651    // Build the new pats slice — `dummy_patprog1` slots in C; the
11652    // Rust convention (mirrors `dupeprog` at parse.rs:2716) is to
11653    // synthesize zero-initialised patprog placeholders that
11654    // pattern compile-on-first-use will overwrite.
11655    let dummy_pat = || {
11656        Box::new(crate::ported::zsh_h::patprog {
11657            startoff: 0,
11658            size: 0,
11659            mustoff: 0,
11660            patmlen: 0,
11661            globflags: 0,
11662            globend: 0,
11663            flags: 0,
11664            patnpar: 0,
11665            patstartch: 0,
11666        })
11667    };
11668    let new_pats: Vec<crate::ported::zsh_h::Patprog> =
11669        (0..npats.max(0)).map(|_| dummy_pat()).collect();
11670
11671    // c:6353 — `ret->strs = prog->strs + sbeg;` (EF_MAP) or
11672    // c:6359 — `memcpy(ret->strs, prog->strs + sbeg, nstrs);` (heap).
11673    let old_strs = prog.strs.take().unwrap_or_default();
11674    let old_bytes = old_strs.as_bytes();
11675    let new_strs = if sbeg + nstrs <= old_bytes.len() {
11676        Some(String::from_utf8_lossy(&old_bytes[sbeg..sbeg + nstrs]).into_owned())
11677    } else {
11678        Some(String::new())
11679    };
11680
11681    let new_prog: Vec<wordcode> = prog.prog[body_start..end_pc].to_vec();
11682
11683    if (prog.flags & EF_MAP) != 0 {
11684        // c:6349-6354 — in-place EF_MAP path.
11685        prog.pats = new_pats;
11686        prog.prog = new_prog;
11687        prog.strs = new_strs;
11688        prog.len = len as i32;
11689        prog.npats = npats;
11690        prog.shf = None;
11691        return Some(prog);
11692    }
11693
11694    // c:6356-6361 — heap-allocated new Eprog.
11695    let ret = Box::new(eprog {
11696        flags: EF_HEAP,
11697        len: len as i32,
11698        npats,
11699        nref: -1, // c:6363 (heap path → never refcount-freed).
11700        pats: new_pats,
11701        prog: new_prog,
11702        strs: new_strs,
11703        shf: None, // c:6363
11704        dump: None,
11705        strs_metafied: false, // native pool — clean UTF-8
11706    });
11707    Some(ret)
11708}
11709
11710#[cfg(test)]
11711mod tests {
11712    use super::*;
11713
11714    // ─── zsh-corpus pins for pure exec helpers ─────────────────────
11715
11716    /// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
11717    #[test]
11718    fn exec_corpus_isrelative_empty_is_one() {
11719        let _g = crate::test_util::global_state_lock();
11720        assert_eq!(isrelative(""), 1, "empty path is relative");
11721    }
11722
11723    /// `isrelative("foo")` = 1 (no leading slash).
11724    #[test]
11725    fn exec_corpus_isrelative_bare_name_is_one() {
11726        let _g = crate::test_util::global_state_lock();
11727        assert_eq!(isrelative("foo"), 1);
11728        assert_eq!(isrelative("bin/cmd"), 1);
11729    }
11730
11731    /// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
11732    #[test]
11733    fn exec_corpus_isrelative_absolute_clean_is_zero() {
11734        let _g = crate::test_util::global_state_lock();
11735        assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
11736        assert_eq!(isrelative("/bin/ls"), 0);
11737        assert_eq!(isrelative("/"), 0, "root is absolute");
11738    }
11739
11740    /// `isrelative("/foo/../bar")` = 1 (contains `../` component).
11741    #[test]
11742    fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
11743        let _g = crate::test_util::global_state_lock();
11744        assert_eq!(
11745            isrelative("/foo/../bar"),
11746            1,
11747            "absolute path with ../ is still 'relative' per zsh"
11748        );
11749    }
11750
11751    /// `isrelative("/foo/./bar")` = 1 (contains `./` component).
11752    #[test]
11753    fn exec_corpus_isrelative_absolute_with_dot_is_one() {
11754        let _g = crate::test_util::global_state_lock();
11755        assert_eq!(
11756            isrelative("/./x"),
11757            1,
11758            "absolute with ./ component reported relative"
11759        );
11760    }
11761
11762    /// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
11763    #[test]
11764    fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
11765        assert_eq!(is_anonymous_function_name("(anon)"), 1);
11766    }
11767
11768    /// `is_anonymous_function_name("regular_name")` = 0.
11769    #[test]
11770    fn exec_corpus_is_anonymous_function_name_rejects_normal() {
11771        assert_eq!(is_anonymous_function_name("regular_name"), 0);
11772        assert_eq!(is_anonymous_function_name(""), 0);
11773        assert_eq!(
11774            is_anonymous_function_name("anon"),
11775            0,
11776            "plain 'anon' (no parens) is NOT the sentinel"
11777        );
11778    }
11779
11780    /// `iscom("/nonexistent/never_a_path")` = false.
11781    #[test]
11782    fn exec_corpus_iscom_missing_path_false() {
11783        assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
11784    }
11785
11786    /// `iscom("/tmp")` is a directory not a regular file → false.
11787    #[test]
11788    fn exec_corpus_iscom_directory_false() {
11789        assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
11790    }
11791
11792    /// `iscom("/bin/sh")` is true on POSIX systems.
11793    #[test]
11794    fn exec_corpus_iscom_known_binary_true() {
11795        // /bin/sh exists on all POSIX systems with X perms.
11796        if std::path::Path::new("/bin/sh").exists() {
11797            assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
11798        }
11799    }
11800
11801    // ─── stripkshdef (Src/exec.c:6286) early-return paths ──────────
11802
11803    /// `stripkshdef(None, "foo")` → `None` (matches C `if (!prog)
11804    /// return NULL;` at exec.c:6300).
11805    #[test]
11806    fn exec_corpus_stripkshdef_null_input_returns_none() {
11807        assert!(stripkshdef(None, "foo").is_none());
11808    }
11809
11810    /// `stripkshdef` on an empty/degenerate Eprog returns the same
11811    /// Eprog unchanged (no funcdef-shape to strip).
11812    #[test]
11813    fn exec_corpus_stripkshdef_empty_prog_returns_input() {
11814        let prog = Box::new(eprog {
11815            prog: vec![],
11816            ..Default::default()
11817        });
11818        let out = stripkshdef(Some(prog), "foo");
11819        assert!(out.is_some(), "empty prog → returned unchanged");
11820        assert!(out.unwrap().prog.is_empty(), "no mutation");
11821    }
11822
11823    /// `stripkshdef` on a non-WC_LIST head returns the input
11824    /// untouched (early return at exec.c:6304-6306).
11825    #[test]
11826    fn exec_corpus_stripkshdef_non_list_head_returns_input() {
11827        use crate::ported::zsh_h::{wc_bld, WC_SUBLIST};
11828        let prog = Box::new(eprog {
11829            prog: vec![wc_bld(WC_SUBLIST, 0), 0, 0],
11830            ..Default::default()
11831        });
11832        let out = stripkshdef(Some(prog), "foo");
11833        assert!(out.is_some());
11834        // first word is the WC_SUBLIST sentinel we passed in,
11835        // unchanged (the function bailed before doing any slicing).
11836        let p = out.unwrap();
11837        use crate::ported::zsh_h::wc_code;
11838        assert_eq!(
11839            wc_code(p.prog[0]),
11840            WC_SUBLIST,
11841            "header word preserved verbatim"
11842        );
11843    }
11844
11845    // ═══════════════════════════════════════════════════════════════════
11846    // C-parity tests pinning Src/exec.c. Tests that capture KNOWN
11847    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
11848    // ═══════════════════════════════════════════════════════════════════
11849
11850    /// `isrelative("/abs/path")` returns 0 (false = absolute path).
11851    /// C `Src/exec.c:996-1006` — leading `/` and no `.`/`..` components.
11852    #[test]
11853    fn isrelative_absolute_path_returns_zero() {
11854        let _g = crate::test_util::global_state_lock();
11855        assert_eq!(isrelative("/usr/local/bin"), 0);
11856    }
11857
11858    /// `isrelative("foo/bar")` returns 1 (no leading slash).
11859    #[test]
11860    fn isrelative_no_leading_slash_returns_one() {
11861        let _g = crate::test_util::global_state_lock();
11862        assert_eq!(isrelative("foo/bar"), 1);
11863    }
11864
11865    /// `isrelative("/foo/./bar")` returns 1 — contains `/./` walk.
11866    /// C c:1001 — `.` with prev `/` + next `/` triggers relative flag.
11867    #[test]
11868    fn isrelative_dot_component_returns_one() {
11869        let _g = crate::test_util::global_state_lock();
11870        assert_eq!(isrelative("/foo/./bar"), 1, "/./ in path → relative");
11871    }
11872
11873    /// `isrelative("/foo/../bar")` returns 1 — contains `/..` walk.
11874    #[test]
11875    fn isrelative_dotdot_component_returns_one() {
11876        let _g = crate::test_util::global_state_lock();
11877        assert_eq!(isrelative("/foo/../bar"), 1, "/../ in path → relative");
11878    }
11879
11880    /// `isrelative("")` returns 1 — empty input has no leading `/`.
11881    /// C c:998 — `*s != '/'` includes the NUL terminator case.
11882    #[test]
11883    fn isrelative_empty_returns_one() {
11884        let _g = crate::test_util::global_state_lock();
11885        assert_eq!(isrelative(""), 1, "empty string → not absolute");
11886    }
11887
11888    /// `isrelative("/a/.b")` returns 0 — `.b` is NOT a `/./` walk
11889    /// (followed by another non-`/` char `b`).
11890    #[test]
11891    fn isrelative_dotfile_in_path_returns_zero() {
11892        let _g = crate::test_util::global_state_lock();
11893        assert_eq!(
11894            isrelative("/usr/.config/zsh"),
11895            0,
11896            "dotfile name '.config' is NOT a relative walk"
11897        );
11898    }
11899
11900    /// `is_anonymous_function_name("(anon)")` returns 1 (true).
11901    /// C `Src/exec.c` — `!strcmp(name, ANONYMOUS_FUNCTION_NAME)`.
11902    #[test]
11903    fn is_anonymous_function_name_anon_returns_one() {
11904        let _g = crate::test_util::global_state_lock();
11905        assert_eq!(is_anonymous_function_name("(anon)"), 1);
11906    }
11907
11908    /// `is_anonymous_function_name("foo")` returns 0 (false).
11909    #[test]
11910    fn is_anonymous_function_name_normal_returns_zero() {
11911        let _g = crate::test_util::global_state_lock();
11912        assert_eq!(is_anonymous_function_name("foo"), 0);
11913        assert_eq!(is_anonymous_function_name(""), 0);
11914        assert_eq!(is_anonymous_function_name("(other)"), 0);
11915    }
11916
11917    /// `isgooderr(EACCES, "/no/such/dir")` returns true when the dir
11918    /// is not actually accessible. C `Src/exec.c:isgooderr` filters
11919    /// out "unreadable / not directory" errnos so caller doesn't
11920    /// emit spurious warnings.
11921    #[test]
11922    fn isgooderr_eacces_unreadable_dir_returns_false() {
11923        let _g = crate::test_util::global_state_lock();
11924        // /no/such/dir doesn't exist → access(X_OK) fails non-zero
11925        // → !access() is 0 (false) → returns false.
11926        assert!(
11927            !isgooderr(libc::EACCES, "/no/such/dir/zshrs_test"),
11928            "unreadable dir with EACCES should NOT be 'good error'"
11929        );
11930    }
11931
11932    // ═══════════════════════════════════════════════════════════════════
11933    // Additional C-parity tests for Src/exec.c basic accessors/predicates.
11934    // ═══════════════════════════════════════════════════════════════════
11935
11936    /// c:658 — `isgooderr(ENOENT, _)` always false (regardless of dir).
11937    /// Pin: ENOENT is NEVER a "good error" because the path itself
11938    /// doesn't exist — caller should suppress the warning.
11939    #[test]
11940    fn isgooderr_enoent_always_false() {
11941        let _g = crate::test_util::global_state_lock();
11942        assert!(!isgooderr(libc::ENOENT, "/tmp"));
11943        assert!(!isgooderr(libc::ENOENT, "/no/such/dir"));
11944        assert!(!isgooderr(libc::ENOENT, ""));
11945    }
11946
11947    /// c:658 — `isgooderr(ENOTDIR, _)` always false. A path component
11948    /// being a non-dir is a structural error, not a permission issue.
11949    #[test]
11950    fn isgooderr_enotdir_always_false() {
11951        let _g = crate::test_util::global_state_lock();
11952        assert!(!isgooderr(libc::ENOTDIR, "/tmp"));
11953        assert!(!isgooderr(libc::ENOTDIR, "/"));
11954    }
11955
11956    /// c:658 — Other errnos (EPERM, EIO, ENOMEM) are "good errors"
11957    /// because they're not the suppressed three (EACCES/ENOENT/ENOTDIR).
11958    #[test]
11959    fn isgooderr_other_errno_returns_true() {
11960        let _g = crate::test_util::global_state_lock();
11961        assert!(isgooderr(libc::EPERM, "/tmp"));
11962        assert!(isgooderr(libc::EIO, "/tmp"));
11963        assert!(isgooderr(libc::ENOMEM, "/tmp"));
11964    }
11965
11966    /// c:962 — `iscom("/tmp")` returns false (directory, not S_ISREG).
11967    #[test]
11968    fn iscom_directory_returns_false() {
11969        let _g = crate::test_util::global_state_lock();
11970        assert!(!iscom("/tmp"));
11971        assert!(!iscom("/"));
11972    }
11973
11974    /// c:962 — `iscom` on non-existent path returns false (access
11975    /// X_OK fails).
11976    #[test]
11977    fn iscom_nonexistent_path_returns_false() {
11978        let _g = crate::test_util::global_state_lock();
11979        assert!(!iscom("/no/such/path/zshrs_iscom_test"));
11980        assert!(!iscom(""));
11981    }
11982
11983    /// c:962 — `iscom("/bin/sh")` returns true on every POSIX system.
11984    #[test]
11985    #[cfg(unix)]
11986    fn iscom_bin_sh_returns_true() {
11987        let _g = crate::test_util::global_state_lock();
11988        // /bin/sh is a POSIX-required executable.
11989        assert!(iscom("/bin/sh"), "/bin/sh must be executable on POSIX");
11990    }
11991
11992    /// c:5300 — anonymous function name is exactly "(anon)" — must
11993    /// not match prefixes/suffixes/case variants.
11994    #[test]
11995    fn is_anonymous_function_name_strict_match_only() {
11996        let _g = crate::test_util::global_state_lock();
11997        assert_eq!(is_anonymous_function_name("(anon"), 0, "no trailing paren");
11998        assert_eq!(is_anonymous_function_name("anon)"), 0, "no leading paren");
11999        assert_eq!(is_anonymous_function_name("(ANON)"), 0, "wrong case");
12000        assert_eq!(
12001            is_anonymous_function_name(" (anon) "),
12002            0,
12003            "leading/trailing space"
12004        );
12005        assert_eq!(is_anonymous_function_name("(anon) "), 0, "trailing space");
12006        assert_eq!(is_anonymous_function_name(" (anon)"), 0, "leading space");
12007    }
12008
12009    /// c:5289 — `ANONYMOUS_FUNCTION_NAME` constant is exactly `"(anon)"`.
12010    /// Pin so a regen that flips parens / changes case / adds prefix
12011    /// would be caught.
12012    #[test]
12013    fn anonymous_function_name_const_is_literal_anon() {
12014        let _g = crate::test_util::global_state_lock();
12015        assert_eq!(ANONYMOUS_FUNCTION_NAME, "(anon)");
12016    }
12017
12018    /// c:147-148 — `isrelative("./")` returns 1 (dot-slash prefix
12019    /// is the canonical relative-path form).
12020    #[test]
12021    fn isrelative_dot_slash_returns_one() {
12022        let _g = crate::test_util::global_state_lock();
12023        assert_eq!(isrelative("./foo"), 1);
12024        assert_eq!(isrelative("./"), 1);
12025    }
12026
12027    /// c:147-148 — `isrelative("../foo")` returns 1.
12028    #[test]
12029    fn isrelative_dotdot_slash_returns_one() {
12030        let _g = crate::test_util::global_state_lock();
12031        assert_eq!(isrelative("../foo"), 1);
12032        assert_eq!(isrelative("../"), 1);
12033    }
12034
12035    /// c:147-148 — `/.foo` (hidden file under root) is absolute.
12036    /// Pin: only `/.` (with trailing `/`) or end-of-string counts as
12037    /// a `.` component, NOT `/.foo` (which is a normal file `.foo`).
12038    #[test]
12039    fn isrelative_root_hidden_file_returns_zero() {
12040        let _g = crate::test_util::global_state_lock();
12041        assert_eq!(isrelative("/.foo"), 0, "/.foo is absolute path to dotfile");
12042        assert_eq!(isrelative("/.bashrc"), 0, "/.bashrc is absolute");
12043    }
12044
12045    /// c:147-148 — `/..bar` (file named `..bar`) is also absolute,
12046    /// since `..bar` is a regular file name, not a `..` component.
12047    #[test]
12048    fn isrelative_root_double_dot_file_returns_zero() {
12049        let _g = crate::test_util::global_state_lock();
12050        assert_eq!(isrelative("/..bar"), 0);
12051    }
12052
12053    /// c:2652 — `setunderscore("")` clears `zunderscore` and resets
12054    /// `underscoreused` to 1 (null terminator only).
12055    #[test]
12056    fn setunderscore_empty_clears_state() {
12057        let _g = crate::test_util::global_state_lock();
12058        setunderscore(""); // initialize to known empty state
12059        let zu = zunderscore.lock().unwrap();
12060        assert!(zu.is_empty(), "zunderscore must be empty after clear");
12061        drop(zu);
12062        let used = underscoreused.load(Ordering::Relaxed);
12063        assert_eq!(used, 1, "underscoreused must be 1 (NUL only) after clear");
12064    }
12065
12066    /// c:2652 — `setunderscore(str)` sets `zunderscore=str` and
12067    /// `underscoreused = str.len()+1` (string + null terminator).
12068    #[test]
12069    fn setunderscore_with_value_stores_string_and_length() {
12070        let _g = crate::test_util::global_state_lock();
12071        setunderscore("hello");
12072        let zu = zunderscore.lock().unwrap();
12073        assert_eq!(*zu, "hello");
12074        drop(zu);
12075        let used = underscoreused.load(Ordering::Relaxed);
12076        assert_eq!(used, 6, "len('hello')+1 = 6");
12077    }
12078
12079    /// c:2656 — `underscorelen` is rounded up to 32-byte boundary
12080    /// for the bump-allocator-friendly buffer growth.
12081    #[test]
12082    fn setunderscore_rounds_underscorelen_to_32() {
12083        let _g = crate::test_util::global_state_lock();
12084        setunderscore("ab"); // len 2 + 1 = 3 → ceil(32) = 32
12085        let nl = underscorelen.load(Ordering::Relaxed);
12086        assert_eq!(nl, 32, "(2+1+31) & !31 = 32");
12087    }
12088
12089    // ═══════════════════════════════════════════════════════════════════
12090    // Additional C-parity tests for Src/exec.c cancd2 +
12091    // quote_tokenized_output.
12092    // ═══════════════════════════════════════════════════════════════════
12093
12094    /// c:6411 — `cancd2("/tmp")` returns 1 (directory with X_OK exists).
12095    #[test]
12096    #[cfg(unix)]
12097    fn cancd2_existing_dir_returns_one() {
12098        let _g = crate::test_util::global_state_lock();
12099        assert_eq!(cancd2("/tmp"), 1, "/tmp is a valid cd target");
12100    }
12101
12102    /// c:6411 — `cancd2("/nonexistent")` returns 0.
12103    #[test]
12104    fn cancd2_nonexistent_returns_zero() {
12105        let _g = crate::test_util::global_state_lock();
12106        assert_eq!(cancd2("/__never_exists_zshrs_cancd2__"), 0);
12107    }
12108
12109    /// c:6411 — `cancd2` for a file (not dir) returns 0.
12110    #[test]
12111    #[cfg(unix)]
12112    fn cancd2_regular_file_returns_zero() {
12113        let _g = crate::test_util::global_state_lock();
12114        let dir = tempfile::tempdir().unwrap();
12115        let p = dir.path().join("regular_file");
12116        std::fs::write(&p, "x").unwrap();
12117        assert_eq!(
12118            cancd2(p.to_str().unwrap()),
12119            0,
12120            "regular file not a cd target"
12121        );
12122    }
12123
12124    /// c:2114 — `quote_tokenized_output` on empty string writes nothing.
12125    #[test]
12126    fn quote_tokenized_output_empty_writes_nothing() {
12127        let _g = crate::test_util::global_state_lock();
12128        let mut buf = Vec::new();
12129        quote_tokenized_output("", &mut buf).unwrap();
12130        assert!(buf.is_empty());
12131    }
12132
12133    /// c:2114 — plain ASCII passes through unchanged.
12134    #[test]
12135    fn quote_tokenized_output_plain_ascii_unchanged() {
12136        let _g = crate::test_util::global_state_lock();
12137        let mut buf = Vec::new();
12138        quote_tokenized_output("hello", &mut buf).unwrap();
12139        assert_eq!(buf, b"hello");
12140    }
12141
12142    /// c:2143 — space gets backslash-quoted.
12143    #[test]
12144    fn quote_tokenized_output_space_backslash_quoted() {
12145        let _g = crate::test_util::global_state_lock();
12146        let mut buf = Vec::new();
12147        quote_tokenized_output("a b", &mut buf).unwrap();
12148        assert_eq!(buf, b"a\\ b");
12149    }
12150
12151    /// c:2147 — tab → $'\\t'.
12152    #[test]
12153    fn quote_tokenized_output_tab_dollar_escape() {
12154        let _g = crate::test_util::global_state_lock();
12155        let mut buf = Vec::new();
12156        quote_tokenized_output("a\tb", &mut buf).unwrap();
12157        assert_eq!(buf, b"a$'\\t'b");
12158    }
12159
12160    /// c:2151 — newline → $'\\n'.
12161    #[test]
12162    fn quote_tokenized_output_newline_dollar_escape() {
12163        let _g = crate::test_util::global_state_lock();
12164        let mut buf = Vec::new();
12165        quote_tokenized_output("a\nb", &mut buf).unwrap();
12166        assert_eq!(buf, b"a$'\\n'b");
12167    }
12168
12169    /// c:2155 — CR → $'\\r'.
12170    #[test]
12171    fn quote_tokenized_output_cr_dollar_escape() {
12172        let _g = crate::test_util::global_state_lock();
12173        let mut buf = Vec::new();
12174        quote_tokenized_output("a\rb", &mut buf).unwrap();
12175        assert_eq!(buf, b"a$'\\r'b");
12176    }
12177
12178    /// c:2128 — shell metacharacters all get backslash-quoted.
12179    #[test]
12180    fn quote_tokenized_output_shell_metas_get_backslash() {
12181        let _g = crate::test_util::global_state_lock();
12182        for c in &[b'<', b'>', b'(', b')', b'|', b'#', b'$', b'*', b'?', b'~'] {
12183            let mut buf = Vec::new();
12184            let s = String::from_utf8(vec![b'a', *c, b'b']).unwrap();
12185            quote_tokenized_output(&s, &mut buf).unwrap();
12186            assert_eq!(buf, vec![b'a', b'\\', *c, b'b'], "char {:?}", *c as char);
12187        }
12188    }
12189
12190    /// c:2158 — `=` at position 0 gets quoted (path-spec).
12191    #[test]
12192    fn quote_tokenized_output_equals_at_start_quoted() {
12193        let _g = crate::test_util::global_state_lock();
12194        let mut buf = Vec::new();
12195        quote_tokenized_output("=foo", &mut buf).unwrap();
12196        assert_eq!(buf, b"\\=foo");
12197    }
12198
12199    // ═══════════════════════════════════════════════════════════════════
12200    // Additional C-parity tests for Src/exec.c
12201    // c:1287 iscom / c:1347 isrelative / c:1398 setunderscore /
12202    // c:1468 is_anonymous_function_name / c:2208 findcmd / c:3273 parsecmd
12203    // c:1264 isgooderr / c:1226 parse_string
12204    // ═══════════════════════════════════════════════════════════════════
12205
12206    /// c:1287 — `iscom("")` empty input returns false.
12207    #[test]
12208    fn iscom_empty_string_returns_false() {
12209        let _g = crate::test_util::global_state_lock();
12210        assert!(!iscom(""), "empty cmd name → not a command");
12211    }
12212
12213    /// c:1287 — `iscom` returns bool (compile-time type pin).
12214    #[test]
12215    fn iscom_returns_bool_type() {
12216        let _g = crate::test_util::global_state_lock();
12217        let _: bool = iscom("ls");
12218    }
12219
12220    /// c:1347 — `isrelative("/abs")` returns 0 (absolute path).
12221    #[test]
12222    fn isrelative_absolute_path_returns_zero_pin() {
12223        assert_eq!(isrelative("/usr/bin"), 0, "/usr/bin is absolute");
12224        assert_eq!(isrelative("/"), 0, "/ is absolute");
12225    }
12226
12227    /// c:1347 — `isrelative("rel/path")` returns 1 (relative).
12228    #[test]
12229    fn isrelative_relative_path_returns_one_pin() {
12230        assert_eq!(isrelative("foo"), 1, "foo is relative");
12231        assert_eq!(isrelative("./foo"), 1, "./foo is relative");
12232        assert_eq!(isrelative("../foo"), 1, "../foo is relative");
12233    }
12234
12235    /// c:1347 — `isrelative("")` empty returns 1 (relative by C convention).
12236    #[test]
12237    fn isrelative_empty_returns_relative() {
12238        let r = isrelative("");
12239        assert!(r == 0 || r == 1, "must be 0 or 1");
12240    }
12241
12242    /// c:1468 — `is_anonymous_function_name` returns i32 (type pin).
12243    #[test]
12244    fn is_anonymous_function_name_returns_i32_type() {
12245        let _: i32 = is_anonymous_function_name("(anon)");
12246    }
12247
12248    /// c:1468 — `is_anonymous_function_name("")` empty returns 0.
12249    #[test]
12250    fn is_anonymous_function_name_empty_returns_zero() {
12251        assert_eq!(
12252            is_anonymous_function_name(""),
12253            0,
12254            "empty name is not anonymous"
12255        );
12256    }
12257
12258    /// c:1468 — `is_anonymous_function_name` is deterministic.
12259    #[test]
12260    fn is_anonymous_function_name_is_deterministic() {
12261        for s in ["", "name", "(anon)", "(anon: foo)"] {
12262            let first = is_anonymous_function_name(s);
12263            for _ in 0..3 {
12264                assert_eq!(
12265                    is_anonymous_function_name(s),
12266                    first,
12267                    "is_anonymous_function_name({:?}) must be deterministic",
12268                    s
12269                );
12270            }
12271        }
12272    }
12273
12274    /// c:1226 — `parse_string("")` empty returns Option<eprog> (type pin).
12275    #[test]
12276    fn parse_string_returns_option_eprog_type() {
12277        let _g = crate::test_util::global_state_lock();
12278        let _: Option<eprog> = parse_string("", 0);
12279    }
12280
12281    /// c:1398 — `setunderscore("")` empty string is safe.
12282    #[test]
12283    fn setunderscore_empty_no_panic() {
12284        let _g = crate::test_util::global_state_lock();
12285        setunderscore("");
12286    }
12287
12288    /// c:1264 — `isgooderr` returns bool (compile-time type pin).
12289    #[test]
12290    fn isgooderr_returns_bool_type() {
12291        let _: bool = isgooderr(0, "/tmp");
12292    }
12293
12294    // ═══════════════════════════════════════════════════════════════════
12295    // Additional C-parity tests for Src/exec.c
12296    // c:3325 makecline / c:4603 cancd / c:4674 simple_redir_name /
12297    // c:1287 iscom / c:1314 isreallycom / c:3076 commandnotfound
12298    // ═══════════════════════════════════════════════════════════════════
12299
12300    /// c:3325 — `makecline` returns Vec<String> (compile-time type pin).
12301    #[test]
12302    fn makecline_returns_vec_string_type() {
12303        let _g = crate::test_util::global_state_lock();
12304        let _: Vec<String> = makecline(&[]);
12305    }
12306
12307    /// c:3325 — `makecline([])` empty returns empty Vec.
12308    #[test]
12309    fn makecline_empty_input_returns_empty() {
12310        let _g = crate::test_util::global_state_lock();
12311        let r = makecline(&[]);
12312        assert!(r.is_empty(), "empty input → empty output");
12313    }
12314
12315    /// c:3325 — `makecline` preserves input order.
12316    #[test]
12317    fn makecline_preserves_input_order() {
12318        let _g = crate::test_util::global_state_lock();
12319        let input = vec!["one".to_string(), "two".to_string(), "three".to_string()];
12320        let out = makecline(&input);
12321        assert_eq!(out, input, "makecline must preserve order");
12322    }
12323
12324    /// c:3325 — `makecline` clones (output is independent of input).
12325    #[test]
12326    fn makecline_returns_independent_copy() {
12327        let _g = crate::test_util::global_state_lock();
12328        let input = vec!["a".to_string(), "b".to_string()];
12329        let out = makecline(&input);
12330        assert_eq!(out.len(), input.len(), "lengths match");
12331        // Output can be mutated without affecting input.
12332        let mut out_mut = out;
12333        out_mut.push("c".to_string());
12334        assert_eq!(input.len(), 2, "input unchanged");
12335    }
12336
12337    /// c:4603 — `cancd("")` empty path returns None.
12338    /// ZSHRS BUG: empty path returns Some(...) instead of None. C path
12339    /// at Src/exec.c:6376 enters relative-path branch which calls cancd2("")
12340    /// — that should return 0 (not a valid dir), causing the fn to fall
12341    /// through CDPATH and cd_able_vars, both of which should miss for
12342    /// the empty string. Likely cd_able_vars("") or CDPATH-with-empty-element
12343    /// is silently matching $HOME or "." here.
12344    /// C-faithful behavior: `cancd("")` enters the `!starts_with('/')`
12345    /// branch (c:6376), calls `cancd2("")` which appends to PWD →
12346    /// "PWD/" → fixdir → PWD itself → access+stat succeed → returns
12347    /// `Some(pwd)`. Verified against `/bin/zsh -fc 'cd ""; echo $?'`
12348    /// → `0` (success). The previous test expectation (None) was
12349    /// based on a misread of the C source — pin actual behavior.
12350    #[test]
12351    fn cancd_empty_returns_none() {
12352        let _g = crate::test_util::global_state_lock();
12353        // cancd("") returns Some — empty path resolves through PWD per
12354        // the cancd2 path; matches C zsh's `cd ""` exit-0 behavior.
12355        // Pin PWD to a known-existing dir so a prior test that left
12356        // PWD set to a non-directory doesn't masquerade as the bug.
12357        let saved_pwd = crate::ported::params::getsparam("PWD");
12358        crate::ported::params::setsparam("PWD", "/");
12359        let r = cancd("");
12360        if let Some(p) = saved_pwd {
12361            crate::ported::params::setsparam("PWD", &p);
12362        } else {
12363            crate::ported::params::unsetparam("PWD");
12364        }
12365        assert!(
12366            r.is_some(),
12367            "empty path → Some(pwd) per cancd2-via-PWD path"
12368        );
12369    }
12370
12371    /// c:4603 — `cancd("/")` root dir returns Some (always exists).
12372    #[test]
12373    fn cancd_root_returns_some() {
12374        let _g = crate::test_util::global_state_lock();
12375        let r = cancd("/");
12376        assert_eq!(r.as_deref(), Some("/"), "root dir cancd → Some(/)");
12377    }
12378
12379    /// c:4603 — `cancd` returns Option<String> (compile-time type pin).
12380    #[test]
12381    fn cancd_returns_option_string_type() {
12382        let _g = crate::test_util::global_state_lock();
12383        let _: Option<String> = cancd("/");
12384    }
12385
12386    /// c:4603 — `cancd("/__nonexistent__")` returns None.
12387    #[test]
12388    fn cancd_nonexistent_returns_none() {
12389        let _g = crate::test_util::global_state_lock();
12390        assert!(
12391            cancd("/__nonexistent_zshrs_dir_xyz__").is_none(),
12392            "nonexistent dir → None"
12393        );
12394    }
12395
12396    /// c:4603 — `cancd("/tmp")` exists → Some.
12397    #[test]
12398    fn cancd_tmp_returns_some() {
12399        let _g = crate::test_util::global_state_lock();
12400        let r = cancd("/tmp");
12401        assert!(r.is_some(), "/tmp exists → Some");
12402    }
12403
12404    /// c:4603 — `cancd` is deterministic for stable paths.
12405    #[test]
12406    fn cancd_is_deterministic_for_stable_paths() {
12407        let _g = crate::test_util::global_state_lock();
12408        for p in ["/", "/tmp", "/__never__"] {
12409            let first = cancd(p).is_some();
12410            for _ in 0..3 {
12411                assert_eq!(
12412                    cancd(p).is_some(),
12413                    first,
12414                    "cancd({:?}) must be deterministic",
12415                    p
12416                );
12417            }
12418        }
12419    }
12420
12421    /// c:1287 — `iscom` is deterministic for stable paths.
12422    #[test]
12423    fn iscom_is_deterministic_for_stable_paths() {
12424        let _g = crate::test_util::global_state_lock();
12425        for p in ["/tmp", "/__never__", "/bin/sh"] {
12426            let first = iscom(p);
12427            for _ in 0..3 {
12428                assert_eq!(iscom(p), first, "iscom({:?}) must be deterministic", p);
12429            }
12430        }
12431    }
12432
12433    /// c:3076 — `commandnotfound("", ...)` empty cmd returns i32.
12434    #[test]
12435    fn commandnotfound_returns_i32_type() {
12436        let _g = crate::test_util::global_state_lock();
12437        let mut args = Vec::new();
12438        let _: i32 = commandnotfound("", &mut args);
12439    }
12440}
12441
12442#[cfg(test)]
12443// Relocated from the deleted src/ported/exec_hooks.rs. These pin the
12444// no-executor fallback behavior + return types of the live-executor
12445// accessor wrappers (array/assoc/dispatch_function_call/...), which
12446// now live in this module (exec.rs) instead of the exec_hooks OnceLock
12447// layer. Behavior is identical; only the indirection was removed.
12448mod exec_accessor_tests {
12449    use super::*;
12450    use indexmap::IndexMap;
12451
12452    // ─── zsh-corpus pins: default (no-hook) fallback behavior ─────
12453
12454    /// `dispatch_function_call` returns None when no hook installed.
12455    /// Tests may run in a fresh process where no fusevm bridge wired
12456    /// the dispatch yet; pin: no-panic, None-return.
12457    #[test]
12458    fn exec_hooks_corpus_dispatch_returns_none_when_not_installed() {
12459        let _g = crate::test_util::global_state_lock();
12460        // We can't unset OnceLock once set, but if test runs first
12461        // in this process it should be None. The defensive pin is:
12462        // either None or Some — never panic.
12463        let _ = dispatch_function_call("__never_a_real_function_zshrs__", &["a".into()]);
12464        // No panic = pass.
12465    }
12466
12467    /// `execute_script` returns `Ok(0)` when no hook installed.
12468    #[test]
12469    fn exec_hooks_corpus_execute_script_returns_ok_zero_when_not_installed() {
12470        let _g = crate::test_util::global_state_lock();
12471        let r = execute_script("nothing real");
12472        match r {
12473            Ok(_) | Err(_) => {} // either is acceptable post-install
12474        }
12475    }
12476
12477    /// `run_command_substitution` returns "" by default.
12478    #[test]
12479    fn exec_hooks_corpus_run_command_substitution_default_empty_or_real() {
12480        let _g = crate::test_util::global_state_lock();
12481        // Returns "" if no hook, or real output if hook installed.
12482        let _ = run_command_substitution("echo zshrs_hook_test");
12483        // No panic = pass; we can't pin exact result because hook
12484        // state depends on previous tests in same process.
12485    }
12486
12487    /// `array` falls back to params::getaparam when no hook.
12488    /// Set a real array via params, then look up through hook entry.
12489    #[test]
12490    fn exec_hooks_corpus_array_falls_back_to_getaparam() {
12491        let _g = crate::test_util::global_state_lock();
12492        crate::ported::params::unsetparam("EH_FB");
12493        crate::ported::params::setaparam("EH_FB", vec!["x".into(), "y".into(), "z".into()]);
12494        let got = array("EH_FB");
12495        assert_eq!(
12496            got.as_deref(),
12497            Some(&["x".to_string(), "y".to_string(), "z".to_string()][..]),
12498            "array() hook falls back to params::getaparam",
12499        );
12500        crate::ported::params::unsetparam("EH_FB");
12501    }
12502
12503    /// `pparams()` returns empty Vec when no hook installed.
12504    #[test]
12505    fn exec_hooks_corpus_pparams_returns_empty_when_not_installed() {
12506        let _g = crate::test_util::global_state_lock();
12507        let p = pparams();
12508        // Either empty (no hook) or whatever the installed hook returns.
12509        let _ = p; // no panic = pass
12510    }
12511
12512    /// `unregister_function` returns false by default.
12513    #[test]
12514    fn exec_hooks_corpus_unregister_function_default_false() {
12515        let _g = crate::test_util::global_state_lock();
12516        let r = unregister_function("__never_registered_xyz__");
12517        // If hook installed, hook decides; if not, returns false.
12518        // Pin: doesn't panic and returns a bool.
12519        let _ = r;
12520    }
12521
12522    /// `set_pparams` doesn't panic when called.
12523    #[test]
12524    fn exec_hooks_corpus_set_pparams_does_not_panic() {
12525        let _g = crate::test_util::global_state_lock();
12526        set_pparams(vec!["a".into(), "b".into()]);
12527        // No panic = pass.
12528    }
12529
12530    // ═══════════════════════════════════════════════════════════════════
12531    // Additional default-path (no-hook-installed) parity tests.
12532    // exec_hooks fallback semantics must remain stable: every accessor
12533    // returns a safe default when no fusevm executor has wired its hook.
12534    // ═══════════════════════════════════════════════════════════════════
12535
12536    /// `assoc()` returns None when no hook installed (no params
12537    /// fallback — assoc has no equivalent of getaparam fallback).
12538    #[test]
12539    fn exec_hooks_assoc_returns_none_when_no_hook() {
12540        let _g = crate::test_util::global_state_lock();
12541        // If a prior test installed a hook, the result is hook-defined;
12542        // we only pin no-panic + valid Option<...>.
12543        let _ = assoc("__never_real_assoc_zshrs__");
12544    }
12545
12546    /// `set_array` is a no-op when no hook installed (silently
12547    /// drops the write rather than panicking — fusevm-less env safe).
12548    #[test]
12549    fn exec_hooks_set_array_no_hook_does_not_panic() {
12550        let _g = crate::test_util::global_state_lock();
12551        set_array("__never_real_array_zshrs__", vec!["x".into(), "y".into()]);
12552    }
12553
12554    /// `set_assoc` is a no-op when no hook installed.
12555    #[test]
12556    fn exec_hooks_set_assoc_no_hook_does_not_panic() {
12557        let _g = crate::test_util::global_state_lock();
12558        let mut m = IndexMap::new();
12559        m.insert("k".to_string(), "v".to_string());
12560        set_assoc("__never_real_assoc_zshrs__", m);
12561    }
12562
12563    /// `unset_scalar`, `unset_array`, `unset_assoc` are all no-ops
12564    /// when no hook installed.
12565    #[test]
12566    fn exec_hooks_unset_variants_no_hook_dont_panic() {
12567        let _g = crate::test_util::global_state_lock();
12568        unset_scalar("__never_real_scalar_zshrs__");
12569        unset_array("__never_real_array_zshrs__");
12570        unset_assoc("__never_real_assoc_zshrs__");
12571    }
12572
12573    /// `run_function_body` returns None when no hook installed.
12574    #[test]
12575    fn exec_hooks_run_function_body_returns_none_when_no_hook() {
12576        let _g = crate::test_util::global_state_lock();
12577        let _ = run_function_body("__never_a_real_fn_zshrs__", &["a".into()]);
12578        // No panic = pass; result is Option-typed.
12579    }
12580
12581    /// `execute_script_zsh_pipeline` returns Ok(0) when no hook installed.
12582    #[test]
12583    fn exec_hooks_execute_script_zsh_pipeline_default_ok_zero() {
12584        let _g = crate::test_util::global_state_lock();
12585        let r = execute_script_zsh_pipeline("no hook");
12586        // If hook installed, result is hook-defined; if not, Ok(0).
12587        let _ = r;
12588    }
12589
12590    /// `array()` returns None when name doesn't exist in params either
12591    /// (no fallback hits).
12592    #[test]
12593    fn exec_hooks_array_returns_none_for_missing_name() {
12594        let _g = crate::test_util::global_state_lock();
12595        crate::ported::params::unsetparam("__no_such_array_zshrs__");
12596        let got = array("__no_such_array_zshrs__");
12597        // Either None (no hook + no param) or hook-returned value.
12598        let _ = got;
12599    }
12600
12601    /// `array()` empty name doesn't panic (some callers pass "" for
12602    /// special parameter probes).
12603    #[test]
12604    fn exec_hooks_array_empty_name_does_not_panic() {
12605        let _g = crate::test_util::global_state_lock();
12606        let _ = array("");
12607    }
12608
12609    /// Idempotent: calling array() twice with same name yields same
12610    /// result (no observable side effect on the fallback path).
12611    #[test]
12612    fn exec_hooks_array_is_idempotent() {
12613        let _g = crate::test_util::global_state_lock();
12614        crate::ported::params::unsetparam("EH_IDEMPOTENT");
12615        crate::ported::params::setaparam("EH_IDEMPOTENT", vec!["a".into(), "b".into()]);
12616        let first = array("EH_IDEMPOTENT");
12617        let second = array("EH_IDEMPOTENT");
12618        assert_eq!(first, second);
12619        crate::ported::params::unsetparam("EH_IDEMPOTENT");
12620    }
12621
12622    /// `pparams()` returns an empty vec (not None) when no hook —
12623    /// callers can iterate without an Option-check.
12624    #[test]
12625    fn exec_hooks_pparams_returns_vec_not_none() {
12626        let _g = crate::test_util::global_state_lock();
12627        // Type assertion: result is Vec<String>, not Option<Vec<String>>.
12628        let p: Vec<String> = pparams();
12629        let _ = p; // either [] or hook-installed value
12630    }
12631
12632    /// Round-trip via params fallback: set then read returns same value.
12633    #[test]
12634    fn exec_hooks_array_set_get_roundtrip_via_params() {
12635        let _g = crate::test_util::global_state_lock();
12636        crate::ported::params::unsetparam("EH_RT");
12637        let vals = vec!["one".to_string(), "two".to_string(), "three".to_string()];
12638        crate::ported::params::setaparam("EH_RT", vals.clone());
12639        let got = array("EH_RT").expect("set then get should hit params fallback");
12640        assert_eq!(got, vals);
12641        crate::ported::params::unsetparam("EH_RT");
12642    }
12643
12644    /// `unregister_function` is consistently a bool — pin the return
12645    /// type so accidental refactor to () would fail the type check.
12646    #[test]
12647    fn exec_hooks_unregister_function_returns_bool() {
12648        let _g = crate::test_util::global_state_lock();
12649        let r: bool = unregister_function("__never_xyz_zshrs__");
12650        let _ = r;
12651    }
12652
12653    // ═══════════════════════════════════════════════════════════════════
12654    // Additional contract-pin tests for exec_hooks default behavior.
12655    // ═══════════════════════════════════════════════════════════════════
12656
12657    /// `run_command_substitution` returns String (never None / never Option).
12658    #[test]
12659    fn exec_hooks_run_command_substitution_returns_string_type() {
12660        let _g = crate::test_util::global_state_lock();
12661        let _: String = run_command_substitution("anything");
12662    }
12663
12664    /// `execute_script` returns Result<i32, String>. Pin signature.
12665    #[test]
12666    fn exec_hooks_execute_script_returns_result_type() {
12667        let _g = crate::test_util::global_state_lock();
12668        let _: Result<i32, String> = execute_script("anything");
12669    }
12670
12671    /// `execute_script_zsh_pipeline` returns Result<i32, String>.
12672    #[test]
12673    fn exec_hooks_execute_script_zsh_pipeline_returns_result_type() {
12674        let _g = crate::test_util::global_state_lock();
12675        let _: Result<i32, String> = execute_script_zsh_pipeline("anything");
12676    }
12677
12678    /// `dispatch_function_call` returns Option<i32>.
12679    #[test]
12680    fn exec_hooks_dispatch_function_call_returns_option_i32() {
12681        let _g = crate::test_util::global_state_lock();
12682        let _: Option<i32> = dispatch_function_call("__never_real__", &[]);
12683    }
12684
12685    /// `run_function_body` returns Option<i32>.
12686    #[test]
12687    fn exec_hooks_run_function_body_returns_option_i32() {
12688        let _g = crate::test_util::global_state_lock();
12689        let _: Option<i32> = run_function_body("__never_real__", &[]);
12690    }
12691
12692    /// `array` returns Option<Vec<String>>.
12693    #[test]
12694    fn exec_hooks_array_returns_option_vec_string() {
12695        let _g = crate::test_util::global_state_lock();
12696        let _: Option<Vec<String>> = array("anything");
12697    }
12698
12699    /// `assoc` returns Option<IndexMap<String, String>>.
12700    #[test]
12701    fn exec_hooks_assoc_returns_option_indexmap() {
12702        let _g = crate::test_util::global_state_lock();
12703        let _: Option<IndexMap<String, String>> = assoc("anything");
12704    }
12705
12706    /// Empty-string args to `dispatch_function_call` doesn't panic.
12707    #[test]
12708    fn exec_hooks_dispatch_empty_args_no_panic() {
12709        let _g = crate::test_util::global_state_lock();
12710        let _ = dispatch_function_call("", &[]);
12711        let _ = dispatch_function_call("name", &[]);
12712    }
12713
12714    /// Empty-string args to `run_function_body` doesn't panic.
12715    #[test]
12716    fn exec_hooks_run_function_body_empty_args_no_panic() {
12717        let _g = crate::test_util::global_state_lock();
12718        let _ = run_function_body("", &[]);
12719        let _ = run_function_body("name", &[]);
12720    }
12721
12722    /// Empty-string name to `execute_script` doesn't panic and returns
12723    /// Result.
12724    #[test]
12725    fn exec_hooks_execute_script_empty_src_no_panic() {
12726        let _g = crate::test_util::global_state_lock();
12727        let _ = execute_script("");
12728        let _ = execute_script_zsh_pipeline("");
12729    }
12730
12731    /// Empty-string cmd to `run_command_substitution` doesn't panic.
12732    #[test]
12733    fn exec_hooks_run_command_substitution_empty_no_panic() {
12734        let _g = crate::test_util::global_state_lock();
12735        let _: String = run_command_substitution("");
12736    }
12737
12738    /// `unregister_function("")` doesn't panic.
12739    #[test]
12740    fn exec_hooks_unregister_function_empty_name_no_panic() {
12741        let _g = crate::test_util::global_state_lock();
12742        let _: bool = unregister_function("");
12743    }
12744
12745    /// `unset_*` with empty name does not panic.
12746    #[test]
12747    fn exec_hooks_unset_with_empty_name_no_panic() {
12748        let _g = crate::test_util::global_state_lock();
12749        unset_scalar("");
12750        unset_array("");
12751        unset_assoc("");
12752    }
12753
12754    // ═══════════════════════════════════════════════════════════════════
12755    // Additional contract-pin tests for exec_hooks fallback semantics
12756    // c:213 pparams / c:217 set_pparams / c:223 unregister_function
12757    // ═══════════════════════════════════════════════════════════════════
12758
12759    /// `pparams()` is deterministic for repeated calls without state changes.
12760    #[test]
12761    fn pparams_deterministic_without_changes() {
12762        let _g = crate::test_util::global_state_lock();
12763        let first = pparams();
12764        for _ in 0..3 {
12765            assert_eq!(
12766                pparams(),
12767                first,
12768                "pparams() must be deterministic across reads"
12769            );
12770        }
12771    }
12772
12773    /// `pparams()` returns Vec<String> (not Option) — pin type contract.
12774    #[test]
12775    fn pparams_returns_vec_string_no_option() {
12776        let _g = crate::test_util::global_state_lock();
12777        let _: Vec<String> = pparams();
12778    }
12779
12780    /// `array(name)` with name containing null-byte doesn't panic.
12781    #[test]
12782    fn array_with_special_chars_in_name_no_panic() {
12783        let _g = crate::test_util::global_state_lock();
12784        let _ = array("name with spaces");
12785        let _ = array("name/with/slashes");
12786        let _ = array("$dollarsigns");
12787    }
12788
12789    /// `assoc(name)` empty name no panic.
12790    #[test]
12791    fn assoc_empty_name_no_panic() {
12792        let _g = crate::test_util::global_state_lock();
12793        let _ = assoc("");
12794    }
12795
12796    /// `set_array(empty, ...)` empty name no panic.
12797    #[test]
12798    fn set_array_empty_name_no_panic() {
12799        let _g = crate::test_util::global_state_lock();
12800        set_array("", vec![]);
12801    }
12802
12803    /// `set_assoc(empty, ...)` empty name no panic.
12804    #[test]
12805    fn set_assoc_empty_name_no_panic() {
12806        let _g = crate::test_util::global_state_lock();
12807        let m = indexmap::IndexMap::new();
12808        set_assoc("", m);
12809    }
12810
12811    /// `set_pparams(empty)` is safe.
12812    #[test]
12813    fn set_pparams_empty_vec_no_panic() {
12814        let _g = crate::test_util::global_state_lock();
12815        set_pparams(vec![]);
12816    }
12817
12818    /// Repeated `pparams()` doesn't allocate growing state.
12819    #[test]
12820    fn pparams_repeated_doesnt_grow_state() {
12821        let _g = crate::test_util::global_state_lock();
12822        let first_len = pparams().len();
12823        for _ in 0..10 {
12824            let n = pparams().len();
12825            assert_eq!(n, first_len, "len must not grow across reads");
12826        }
12827    }
12828
12829    /// `unregister_function` is deterministic for nonexistent name.
12830    #[test]
12831    fn unregister_function_unknown_deterministic() {
12832        let _g = crate::test_util::global_state_lock();
12833        let first = unregister_function("__never_real_xyz__");
12834        for _ in 0..3 {
12835            assert_eq!(unregister_function("__never_real_xyz__"), first);
12836        }
12837    }
12838
12839    /// `array(name)` repeated reads of nonexistent name are deterministic.
12840    #[test]
12841    fn array_unknown_is_deterministic() {
12842        let _g = crate::test_util::global_state_lock();
12843        let first = array("__never_real_array_xyz__").is_none();
12844        for _ in 0..3 {
12845            assert_eq!(array("__never_real_array_xyz__").is_none(), first);
12846        }
12847    }
12848
12849    /// `assoc(name)` repeated reads of nonexistent name are deterministic.
12850    #[test]
12851    fn assoc_unknown_is_deterministic() {
12852        let _g = crate::test_util::global_state_lock();
12853        let first = assoc("__never_real_assoc_xyz__").is_none();
12854        for _ in 0..3 {
12855            assert_eq!(assoc("__never_real_assoc_xyz__").is_none(), first);
12856        }
12857    }
12858
12859    // ═══════════════════════════════════════════════════════════════════
12860    // Additional C-parity tests for src/ported/exec_hooks.rs
12861    // c:134 array / c:147 assoc / c:181 dispatch_function_call /
12862    // c:188 run_function_body / c:192 execute_script /
12863    // c:206 run_command_substitution / c:213 pparams / c:223 unregister_function
12864    // ═══════════════════════════════════════════════════════════════════
12865
12866    /// `array(name)` returns Option<Vec<String>> (compile-time pin).
12867    #[test]
12868    fn array_returns_option_vec_string_type() {
12869        let _g = crate::test_util::global_state_lock();
12870        let _: Option<Vec<String>> = array("any");
12871    }
12872
12873    /// `assoc(name)` returns Option<IndexMap<String,String>> (compile-time pin).
12874    #[test]
12875    fn assoc_returns_option_indexmap_type() {
12876        let _g = crate::test_util::global_state_lock();
12877        let _: Option<indexmap::IndexMap<String, String>> = assoc("any");
12878    }
12879
12880    /// `dispatch_function_call` returns Option<i32> (compile-time pin).
12881    #[test]
12882    fn dispatch_function_call_returns_option_i32_type() {
12883        let _g = crate::test_util::global_state_lock();
12884        let _: Option<i32> = dispatch_function_call("__never__", &[]);
12885    }
12886
12887    /// `run_function_body` returns Option<i32> (compile-time pin).
12888    #[test]
12889    fn run_function_body_returns_option_i32_type() {
12890        let _g = crate::test_util::global_state_lock();
12891        let _: Option<i32> = run_function_body("__never__", &[]);
12892    }
12893
12894    /// `execute_script` returns Result<i32, String> (compile-time pin).
12895    #[test]
12896    fn execute_script_returns_result_type() {
12897        let _g = crate::test_util::global_state_lock();
12898        let _: Result<i32, String> = execute_script("");
12899    }
12900
12901    /// `execute_script_zsh_pipeline` returns Result<i32, String> (compile-time pin).
12902    #[test]
12903    fn execute_script_zsh_pipeline_returns_result_type() {
12904        let _g = crate::test_util::global_state_lock();
12905        let _: Result<i32, String> = execute_script_zsh_pipeline("");
12906    }
12907
12908    /// `run_command_substitution` returns String (compile-time pin).
12909    #[test]
12910    fn run_command_substitution_returns_string_type() {
12911        let _g = crate::test_util::global_state_lock();
12912        let _: String = run_command_substitution("");
12913    }
12914
12915    /// `pparams` returns Vec<String> (compile-time pin).
12916    #[test]
12917    fn pparams_returns_vec_string_type() {
12918        let _g = crate::test_util::global_state_lock();
12919        let _: Vec<String> = pparams();
12920    }
12921
12922    /// `unregister_function` returns bool (compile-time pin).
12923    #[test]
12924    fn unregister_function_returns_bool_type() {
12925        let _g = crate::test_util::global_state_lock();
12926        let _: bool = unregister_function("__never__");
12927    }
12928
12929    /// `unset_scalar`/`unset_array`/`unset_assoc` for nonexistent name safe.
12930    #[test]
12931    fn unset_variants_nonexistent_no_panic() {
12932        let _g = crate::test_util::global_state_lock();
12933        unset_scalar("__never_unset_scalar__");
12934        unset_array("__never_unset_array__");
12935        unset_assoc("__never_unset_assoc__");
12936    }
12937
12938    /// `set_pparams` with no hook installed is a silent no-op
12939    /// (c:217-220 — `if let Some(f) = PPARAMS_SET.get() { f(v); }`).
12940    /// Pin the no-hook contract so a refactor that panics on missing
12941    /// hook gets caught.
12942    #[test]
12943    fn set_pparams_without_hook_is_silent_noop() {
12944        let _g = crate::test_util::global_state_lock();
12945        // No hook installed in test context — must not panic.
12946        set_pparams(vec!["a".into(), "b".into(), "c".into()]);
12947        set_pparams(vec![]);
12948    }
12949
12950    // ═══════════════════════════════════════════════════════════════════
12951    // Additional contract pins for exec_hooks.rs
12952    // No-hook-installed contract: every accessor must be safe + deterministic
12953    // ═══════════════════════════════════════════════════════════════════
12954
12955    /// `array("")` empty name returns deterministic value (no panic).
12956    #[test]
12957    fn array_empty_name_no_panic_deterministic() {
12958        let _g = crate::test_util::global_state_lock();
12959        let a = array("");
12960        let b = array("");
12961        assert_eq!(a, b, "array(\"\") must be deterministic");
12962    }
12963
12964    /// `set_array` then `array` without hook should not panic.
12965    #[test]
12966    fn set_array_then_get_no_hook_safe() {
12967        let _g = crate::test_util::global_state_lock();
12968        set_array("__test_hook_arr__", vec!["a".into(), "b".into()]);
12969        let _ = array("__test_hook_arr__");
12970    }
12971
12972    /// `set_assoc` then `assoc` without hook should not panic.
12973    #[test]
12974    fn set_assoc_then_get_no_hook_safe() {
12975        let _g = crate::test_util::global_state_lock();
12976        let mut m = IndexMap::new();
12977        m.insert("k".to_string(), "v".to_string());
12978        set_assoc("__test_hook_assoc__", m);
12979        let _ = assoc("__test_hook_assoc__");
12980    }
12981
12982    /// `run_function_body` with no hook returns `Option<i32>` (type pin, alt).
12983    #[test]
12984    fn run_function_body_returns_option_i32_type_alt() {
12985        let _g = crate::test_util::global_state_lock();
12986        let _: Option<i32> = run_function_body("foo", &[]);
12987    }
12988
12989    /// `run_function_body` is deterministic for the same input when no hook.
12990    #[test]
12991    fn run_function_body_no_hook_deterministic() {
12992        let _g = crate::test_util::global_state_lock();
12993        let a = run_function_body("__never__", &[]);
12994        let b = run_function_body("__never__", &[]);
12995        assert_eq!(a, b);
12996    }
12997
12998    /// `dispatch_function_call` is deterministic across calls.
12999    #[test]
13000    fn dispatch_function_call_no_hook_deterministic() {
13001        let _g = crate::test_util::global_state_lock();
13002        let a = dispatch_function_call("__never__", &[]);
13003        let b = dispatch_function_call("__never__", &[]);
13004        assert_eq!(a, b);
13005    }
13006
13007    /// `pparams` returns `Vec<String>` (compile-time pin, alt).
13008    #[test]
13009    fn pparams_returns_vec_string_type_alt() {
13010        let _g = crate::test_util::global_state_lock();
13011        let _: Vec<String> = pparams();
13012    }
13013
13014    /// `pparams` is deterministic across repeated reads when no hook installed
13015    /// (this is observably true only if no other test has installed it; pin
13016    /// the no-mutation invariant).
13017    #[test]
13018    fn pparams_repeated_reads_are_observable_type() {
13019        let _g = crate::test_util::global_state_lock();
13020        // Just type pin — value depends on whether another test installed a
13021        // hook between these calls (PPARAMS_GET is OnceLock).
13022        let _a = pparams();
13023        let _b = pparams();
13024    }
13025
13026    /// `run_command_substitution` returns `String` type pin (alt).
13027    #[test]
13028    fn run_command_substitution_returns_string_type_alt() {
13029        let _g = crate::test_util::global_state_lock();
13030        let _: String = run_command_substitution("echo x");
13031    }
13032
13033    /// `run_command_substitution` empty command doesn't panic.
13034    #[test]
13035    fn run_command_substitution_empty_cmd_no_panic() {
13036        let _g = crate::test_util::global_state_lock();
13037        let _ = run_command_substitution("");
13038    }
13039
13040    /// `execute_script` returns `Result<i32, String>` type pin.
13041    #[test]
13042    fn execute_script_returns_result_i32_string_type() {
13043        let _g = crate::test_util::global_state_lock();
13044        let _: Result<i32, String> = execute_script("foo");
13045    }
13046
13047    /// `unregister_function("")` empty name returns bool safely.
13048    #[test]
13049    fn unregister_function_empty_name_returns_bool() {
13050        let _g = crate::test_util::global_state_lock();
13051        let _: bool = unregister_function("");
13052    }
13053}