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_hooks::*` 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, multio, redir,
80    shfunc, unset, wc_code, Emulation_options, Inang, Inpar, Meta, Nularg, Outpar, Pound,
81    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.
205pub static retflag: std::sync::atomic::AtomicI32 = // c:165 (Src/exec.c)
206    std::sync::atomic::AtomicI32::new(0);
207
208/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
209/// recent `$(cmd)` command-substitution child. Used by exit-status
210/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
211/// the pid `waitpid`-d for it.
212pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
213    std::sync::atomic::AtomicI32::new(0);
214
215/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
216/// Pid of the most recent process-substitution child (`<(cmd)` /
217/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
218/// jobs aren't wait-collected by the parent until the fd is closed.
219pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
220    std::sync::atomic::AtomicI32::new(0);
221
222/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
223/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
224/// runs alongside a substitution.
225pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
226    std::sync::atomic::AtomicI32::new(0);
227
228/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
229/// `lastval` is updated from `cmdoutval` after the command
230/// (i.e. the command had substitutions whose exit status matters).
231pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
232    std::sync::atomic::AtomicI32::new(0);
233
234/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
235/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
236/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
237/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
238/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
239/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
240pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
241    std::sync::atomic::AtomicI32::new(0);
242
243/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
244/// currently-executing pipeline is the long-running pipe-into-loop
245/// shape (`cat foo | while read a; do ... done`) — drives the
246/// super/sub-job tracking documented in the famous `Allen Edeln…`
247/// comment block above this declaration in C.
248pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
249    std::sync::atomic::AtomicI32::new(0);
250
251/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
252/// dispatch of a "simple" pipeline (single-stage / no shell-construct
253/// tail) so the `list_pipe` machinery short-circuits.
254pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
255    std::sync::atomic::AtomicI32::new(0);
256
257/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
258/// PID of the sub-shell created to host the loop-after-pipe pattern;
259/// passed up the recursive `execlist` stack so the cat-job's super-
260/// job entry can record it.
261pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
262    std::sync::atomic::AtomicI32::new(0);
263
264/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
265/// `execpline` doesn't wait for the pipeline; used during the
266/// list_pipe sub-shell fork bookkeeping.
267pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
268    std::sync::atomic::AtomicI32::new(0);
269
270/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
271/// pipeline depth (counts nested pipelines within the current
272/// `execlist` call chain).
273pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
274    std::sync::atomic::AtomicI32::new(0);
275
276/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
277/// Set in the child after the list_pipe fork so the child knows to
278/// continue executing the loop body (vs the parent which records
279/// the pid + returns).
280pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
281    std::sync::atomic::AtomicI32::new(0);
282
283/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
284/// table index of the pipeline's first-stage job (the `cat` in
285/// `cat foo | while ...`).
286pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
287    std::sync::atomic::AtomicI32::new(0);
288
289/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
290/// `printprompt4` has emitted the `$PS4` prefix for the current
291/// xtrace command — prevents double-printing when an inner sub-eval
292/// also wants to xtrace.
293pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
294    std::sync::atomic::AtomicI32::new(0);
295
296/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
297///
298/// File-static "execsubst parameters" — callers (execcmd_exec at
299/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
300/// uses them as the `flags` arg to prefork() and the gate on
301/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
302/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
303/// when the dispatched builtin has `BINF_NOGLOB`.
304pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
305    std::sync::atomic::AtomicI32::new(0);
306pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
307    std::sync::atomic::AtomicI32::new(1);
308
309/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
310/// of the linked exec-context save stack — `execsave` pushes a frame
311/// before signal-handler / trap dispatch; `execrestore` pops it
312/// afterwards so the interrupted command resumes with its state intact.
313pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
314    std::sync::Mutex::new(None);
315
316/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
317/// `stty` argument string captured by `addvars` when the command's
318/// inline env contains `STTY=...`. Applied by `execute` before fork
319/// + exec so the spawned program sees its tty configured. Reset to
320/// `None` after consumption to avoid infinite recursion.
321pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
322    std::sync::Mutex::new(None);
323
324/// Convert a here-document into a here-string. Line-by-line port of
325/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
326/// input stream via `hgetc()` until the terminator line is matched,
327/// returning the collected body as a string. `strp` is in/out: on
328/// entry the raw terminator (possibly with token markers + leading
329/// tabs); on return the munged terminator (after `quotesubst` +
330/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
331///
332/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
333/// Rust's `String` auto-grows so the OOM branch is effectively
334/// unreachable, but the return type stays `Option<String>` to mirror
335/// the C signature which can return NULL.
336///
337/// Port of `gethere(char **strp, int typ)` from `Src/exec.c:4573`.
338pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
339    // c:4573 (Src/exec.c)
340    let mut buf: String; // c:4575 char *buf
341    let mut bsiz: usize; // c:4576 int bsiz
342    let mut qt: i32 = 0; // c:4576 int qt = 0
343    let mut strip: i32 = 0; // c:4576 int strip = 0
344                            // c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
345                            // `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
346                            // C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
347                            // `s` (the loop iterator for the inull-scan) stays local to its
348                            // for-loop. `c` mirrors the C `char c`.
349    let mut t: usize; // c:4577 char *t
350    let mut c: Option<char>; // c:4577 char c
351    let mut str: String = strp.clone(); // c:4578 char *str = *strp
352
353    // c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
354    for s in str.bytes() {
355        if inull(s) {
356            // c:4581
357            qt = 1; // c:4582
358            break; // c:4583
359        }
360    }
361    str = quotesubst(&str); // c:4585
362    str = untokenize(&str); // c:4586
363    if typ == REDIR_HEREDOCDASH {
364        // c:4587
365        strip = 1; // c:4588
366                   // c:4589-4590 — while (*str == '\t') str++;
367        while str.starts_with('\t') {
368            str.remove(0);
369        }
370    }
371    *strp = str.clone(); // c:4592 *strp = str
372
373    // c:4593 — bptr = buf = zalloc(bsiz = 256);
374    bsiz = 256;
375    buf = String::with_capacity(bsiz);
376    let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
377
378    // c:4594 — for (;;)
379    loop {
380        t = buf.len(); // c:4595 t = bptr
381
382        // c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
383        loop {
384            c = hgetc();
385            if !(c == Some('\t') && strip != 0) {
386                break;
387            }
388        }
389
390        // c:4599 — for (;;) — inner body-read loop
391        loop {
392            // c:4600-4613 — buffer-growth realloc dance. Rust's
393            // String auto-grows; nothing to do.
394            // c:4614 — if (lexstop || c == '\n') break;
395            if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
396                break;
397            }
398            // c:4616 — if (!qt && c == '\\')
399            if qt == 0 && c == Some('\\') {
400                buf.push('\\'); // c:4617 *bptr++ = c
401                c = hgetc(); // c:4618
402                if c == Some('\n') {
403                    // c:4619
404                    buf.pop(); // c:4620 bptr--
405                    c = hgetc(); // c:4621
406                    continue; // c:4622
407                }
408            }
409            if let Some(ch) = c {
410                // c:4625 *bptr++ = c
411                buf.push(ch);
412            }
413            c = hgetc(); // c:4626
414        }
415        // c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
416
417        // c:4629-4630 — if (!strcmp(t, str)) break;
418        if &buf[t..] == str.as_str() {
419            break;
420        }
421        // c:4631-4634 — if (lexstop) { t = bptr; break; }
422        if LEX_LEXSTOP.with(|f| f.get()) {
423            t = buf.len();
424            break;
425        }
426        // c:4635 — *bptr++ = '\n';
427        buf.push('\n');
428    }
429    // c:4637 — *t = '\0';
430    buf.truncate(t);
431
432    // c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
433    // The C dance frees the realloc'd block and re-allocates via the
434    // string-heap allocator. Rust drops the old String when reassigned.
435    buf = dupstring(&buf);
436
437    if qt == 0 {
438        // c:4641
439        // c:4642 — int ef = errflag;
440        let ef = errflag.load(Ordering::Relaxed);
441        // c:4644 — parsestr(&buf);
442        if let Ok(parsed) = parsestr(&buf) {
443            buf = parsed;
444        }
445        // c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
446        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
447            let cur = errflag.load(Ordering::Relaxed);
448            errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
449        }
450    }
451    Some(buf) // c:4651 return buf
452}
453
454/// Port of `LinkList getoutput(char *cmd, int qt)` from
455/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
456/// active executor, then routes the captured stdout through
457/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
458///
459/// C return shape: `LinkList` of `char*`. Rust port returns
460/// `Vec<String>` (same shape, owned).
461///
462/// `qt` matches C exactly:
463///   - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
464///     entire output as a single-element vec. C c:4858-4862: if
465///     output empty, returns a single Nularg sentinel so callers
466///     see "empty value" rather than "no value".
467///   - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
468///     `spacesplit(buf, allownull=false)` per c:4865-4871.
469///
470/// Uses `with_executor` (panics on missing VM context), not
471/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
472/// `execpline` directly — there's no "no shell" code path. The
473/// silent-no-op pattern (return empty string when no executor) would
474/// mask catastrophic state corruption as "command produced no output",
475/// which is the failure mode the `subst.rs:496` warning block flags.
476/* $(...) */
477// c:4709
478/// `getoutput` — see implementation.
479pub fn getoutput(cmd: &str, qt: i32) -> Vec<String> {
480    // c:4713
481    // c:4715 — `Eprog prog;`
482    let prog: Option<eprog>;
483    // c:4716 — `int pipes[2];`  (collapsed: in-process executor; no fork)
484    // c:4717 — `pid_t pid;`     (collapsed)
485    let mut s: String; // c:4718
486                       // c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
487                       //                !sourcelevel && unset(INTERACTIVECOMMENTS));
488                       //                prog = parse_string(cmd, 0); nocomments = onc;`
489    let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
490    let new_nc = crate::ported::zsh_h::interact()
491        && crate::ported::init::sourcelevel.load(Ordering::Relaxed) == 0
492        && !isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
493    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
494    prog = parse_string(cmd, 0);
495    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
496
497    if prog.is_none() {
498        // c:4725
499        return Vec::new(); // c:4726 return NULL
500    }
501    let prog = prog.unwrap();
502
503    if !isset(crate::ported::zsh_h::EXECOPT) {
504        // c:4728
505        return Vec::new(); // c:4729 newlinklist()
506    }
507
508    // c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
509    if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
510        /* $(< word) */
511        // c:4732
512        s = red_name;
513        s = singsub(&s); // c:4737
514        if errflag.load(Ordering::Relaxed) != 0 {
515            return Vec::new(); // c:4739
516        }
517        let s = untokenize(&s); // c:4740
518        let path_meta = unmeta(&s); // c:4741 unmeta(s)
519        let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
520            Ok(c) => c,
521            Err(_) => return Vec::new(),
522        };
523        let stream = unsafe {
524            libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
525        };
526        if stream == -1 {
527            // c:4742 — `zwarn("%e: %s", errno, s);`
528            let errno = std::io::Error::last_os_error();
529            zerr(&format!("{}: {}", errno, s));
530            LASTVAL.store(1, Ordering::Relaxed);
531            cmdoutval.store(1, Ordering::Relaxed);
532            return Vec::new(); // c:4744
533        }
534        // c:4746 — `retval = readoutput(stream, qt, &readerror);`
535        let mut readerror: i32 = 0;
536        let retval = readoutput(stream, qt, &mut readerror); // c:4746
537        if readerror != 0 {
538            // c:4747
539            zerr(&format!(
540                "error when reading {}: {}", // c:4748
541                s,
542                std::io::Error::from_raw_os_error(readerror)
543            ));
544            LASTVAL.store(1, Ordering::Relaxed);
545            cmdoutval.store(1, Ordering::Relaxed);
546        }
547        return retval; // c:4751
548    }
549
550    // c:4753-4790 — Full fork path: mpipe + zfork + parent
551    // readoutput / waitforpid / child execode + _realexit. fusevm runs
552    // command substitution in-process, so the fork shape collapses to a
553    // synchronous executor call. C control points preserved as cites:
554    //   c:4753 mpipe       — handled by ShellExecutor pipe wiring
555    //   c:4758 child_block — no-op (no fork)
556    //   c:4760 zfork       — replaced by in-process exec
557    //   c:4768-4776 parent — equivalent to executor return
558    //   c:4778-4789 child  — entersubsh+execode+_realexit collapse
559    cmdoutval.store(0, Ordering::Relaxed); // c:4759
560    let buf = crate::ported::exec_hooks::run_command_substitution(cmd);
561    LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed); // c:4775
562
563    // c:4772 retval = readoutput — post-walk (c:4855-4871 tail) inlined.
564    let buf = buf.trim_end_matches('\n');
565    if qt != 0 {
566        if buf.is_empty() {
567            vec![String::from(Nularg)] // c:4859-4861
568        } else {
569            vec![buf.to_string()] // c:4863
570        }
571    } else {
572        crate::ported::utils::spacesplit(buf, false) // c:4865
573    }
574}
575
576/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
577/// int ignore_loaddir)` from `Src/exec.c:5050`. Walks `$fpath` for a
578/// file named `shf->node.nam`, reads it, installs the text body on
579/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
580///
581/// C body (abridged):
582///   1. `name = shf->node.nam`
583///   2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
584///   3. If !test_only && file found: parse → store eprog on
585///      `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
586///   4. Returns shf on success, NULL on failure.
587///
588/// Rust port: returns 0 = success, 1 = failure (matches the
589/// existing call-site convention in `bin_functions -c`). Stores
590/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
591/// `hashtable.rs:362`); the parser pass that converts text →
592/// Eprog runs lazily at first call site.
593/// Port of `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)` from `Src/exec.c:5682`.
594pub fn loadautofn(
595    shf: *mut shfunc, // c:5682 (Src/exec.c)
596    _ks: i32,
597    autol: i32,
598    _ignore_loaddir: i32,
599) -> i32 {
600    if shf.is_null() {
601        return 1;
602    }
603    // c:5054 — `name = shf->node.nam`.
604    let name = unsafe { (*shf).node.nam.clone() };
605    // c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
606    let mut dir_path: Option<String> = None;
607    let path = match getfpfunc(&name, &mut dir_path, None, 0) {
608        Some(p) => p,
609        None => {
610            // c:Src/exec.c:5713-5719 — file not found path. C:
611            //   `if (prog == &dummy_eprog) {
612            //        locallevel--;
613            //        zwarn("%s: function definition file not found",
614            //              shf->node.nam);
615            //        locallevel++;
616            //        popheap();
617            //        return NULL;
618            //    }`
619            // C's getfpfunc returns &dummy_eprog as the "not found"
620            // sentinel when test_only==0; loadautofn detects it and
621            // emits the diagnostic before returning NULL. Rust's
622            // getfpfunc returns Option::None for the same condition,
623            // so we emit the same diagnostic here. The locallevel
624            // dance is preserved as a comment because the Rust
625            // port's zwarn doesn't reference locallevel in the
626            // format string itself (the dance in C is only to keep
627            // the prefix line counter consistent with the function-
628            // body context). Bug #107 in docs/BUGS.md.
629            crate::ported::utils::zwarn(&format!(
630                "{}: function definition file not found",
631                name
632            ));
633            return 1; // c:5719 NULL
634        }
635    };
636    let _ = autol;
637    // Previously the Rust port treated this parameter as
638    // "test_only" and early-returned when set, so the `+X`
639    // call from `eval_autoload` (`loadautofn(shf, mode, 1, d)`)
640    // never actually loaded the file. C's parameter is `autol`
641    // (autoload mode), NOT a test-only flag — the C body
642    // unconditionally loads/parses regardless of autol. autol=1
643    // controls the EF_RUN / map-flag dance for the wordcode prog
644    // (c:5725-5749), but the loaded-body / PM_UNDEFINED-clear
645    // path runs in all cases. Removing the early-return so
646    // `autoload -U +X funcname` actually loads the body and
647    // `type funcname` reports `function from /path/file` instead
648    // of `autoload shell function`. Bug #160 in docs/BUGS.md.
649    // c:5100-5140 — read the file. C uses zopen + read + parse_string +
650    // execsave; Rust port stores raw text on the ShFunc and defers
651    // parse-to-Eprog until the first call.
652    let body = match std::fs::read_to_string(&path) {
653        Ok(t) => t,
654        Err(_) => return 1,
655    };
656    // c:Src/exec.c:5735/5757 — `loadautofnsetfile(shf, fdir)`. The
657    // helper stamps PM_LOADDIR alongside the filename when fdir is
658    // present, so `whence -v NAME` later concatenates the directory
659    // with `/NAME` (PM_LOADDIR branch at hashtable.rs:1350). zshrs's
660    // prior `shf->filename = dir_path` assignment skipped the flag
661    // → `type colors` printed `from /path/to/functions` instead of
662    // `from /path/to/functions/colors`. Mirror C exactly.
663    unsafe {
664        loadautofnsetfile(&mut *shf, dir_path.as_deref().or(Some(&path)));
665    }
666    // c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
667    unsafe {
668        (*shf).node.flags &= !(PM_UNDEFINED as i32);
669    }
670    // Sync the body string into the Rust-side ShFunc table so the
671    // lazy-parse path can find it later.
672    if let Ok(mut tab) = shfunctab_lock().write() {
673        if let Some(existing) = tab.get_mut(&name) {
674            existing.body = Some(body);
675            existing.filename = dir_path;
676        } else {
677            tab.add(shfunc {
678                node: hashnode {
679                    next: None,
680                    nam: name.clone(),
681                    flags: 0,
682                },
683                filename: dir_path,
684                lineno: 0,
685                funcdef: None,
686                redir: None,
687                sticky: None,
688                body: Some(body),
689            });
690        }
691    }
692    0
693}
694
695/// Port of `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)` from Src/exec.c:5260. Walks `$fpath` (or the
696/// supplied `spec_path` slice) for a file named `name` and writes the
697/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
698/// Returns `Some(file_contents_path)` on success, `None` when not found.
699pub fn getfpfunc(
700    name: &str,
701    dir_path_out: &mut Option<String>, // c:5260 (Src/exec.c)
702    spec_path: Option<&[String]>,
703    _all_loaded: i32,
704) -> Option<String> {
705    // C reads $fpath via `getaparam("fpath")` (the param-table array form
706    // tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
707    // misses any in-script modification like `fpath=(/some/dir $fpath)`
708    // because that mutates the internal param table, not the inherited
709    // process env. Fall back to env only when the param table is empty
710    // (cold start before any param-table init).
711    let dirs: Vec<String> = match spec_path {
712        Some(s) => s.to_vec(),
713        None => crate::ported::params::getaparam("fpath")
714            .filter(|v| !v.is_empty())
715            .or_else(|| getsparam("FPATH").map(|v| v.split(':').map(String::from).collect()))
716            .or_else(|| {
717                std::env::var("FPATH")
718                    .ok()
719                    .map(|v| v.split(':').map(String::from).collect())
720            })
721            .unwrap_or_default(),
722    };
723    for dir in &dirs {
724        if dir.is_empty() {
725            continue;
726        }
727        let path = format!("{}/{}", dir, name);
728        if std::path::Path::new(&path).exists() {
729            *dir_path_out = Some(dir.clone());
730            return Some(path);
731        }
732    }
733    None
734}
735
736/// Port of `resolvebuiltin(const char *cmdarg, HashNode hn)` from
737/// `Src/exec.c:2703`. Ensures that an autoload-stub builtin has its
738/// module loaded before the caller invokes its `handlerfunc`. If the
739/// stub has no handler, `ensurefeature` is asked to load the module
740/// and re-lookup the builtin node. C body (abridged):
741/// ```c
742/// if (!((Builtin) hn)->handlerfunc) {
743///     char *modname = dupstring(((Builtin) hn)->optstr);
744///     (void)ensurefeature(modname, "b:", ...);
745///     hn = builtintab->getnode(builtintab, cmdarg);
746///     if (!hn) { lastval=1; zerr(...); return NULL; }
747/// }
748/// return hn;
749/// ```
750///
751/// WARNING: zshrs's builtin table is the static `BUILTINS` array in
752/// `src/ported/builtin.rs`. Module autoload routes through
753/// `module::ensurefeature(MODULESTAB, modname, "b:", Some(cmdarg))`;
754/// after the module loads the handler should be wired into BUILTINS.
755pub fn resolvebuiltin<'a>(
756    cmdarg: &str, // c:2703 (Src/exec.c)
757    hn: &'a builtin,
758) -> Option<&'a builtin> {
759    // c:2705 — `if (!((Builtin) hn)->handlerfunc)`.
760    if hn.handlerfunc.is_none() {
761        // c:2706 — `modname = dupstring(((Builtin)hn)->optstr)`.
762        let modname = hn.optstr.clone().unwrap_or_default();
763        // c:2712 — `ensurefeature(modname, "b:", cmdarg)`.
764        let _ = {
765            let mut t = crate::ported::module::MODULESTAB.lock().unwrap();
766            crate::ported::module::ensurefeature(&mut t, &modname, "b:", Some(cmdarg))
767        };
768        // c:2715-2716 — re-lookup the now-(hopefully)-resolved builtin.
769        if let Some(re) = BUILTINS.iter().find(|b| b.node.nam == cmdarg) {
770            if re.handlerfunc.is_some() {
771                return Some(re); // c:2723
772            }
773        }
774        // c:2717-2721 — `lastval = 1; zerr(...)` + return NULL.
775        zerr(&format!(
776            "autoloading module {} failed to define builtin: {}",
777            modname, cmdarg
778        ));
779        return None; // c:2720
780    }
781    Some(hn) // c:2723
782}
783
784/// Dispatch decision returned by `execcmd_compile_head` — the
785/// fusevm-bytecode-time head resolver that mirrors the local-variable
786/// state the C `execcmd_exec` function carries through `c:2913-2916`
787/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
788/// precmd-modifier strip count. The fusevm bytecode compiler reads
789/// this to emit the correct dispatch opcode in
790/// `src/extensions/compile_zsh.rs::compile_simple`.
791///
792/// Not a C struct — invented to bridge the divergence between the
793/// C wordcode-walker (which mutates locals + falls through to
794/// invocation) and zshrs's split parse → compile → VM pipeline.
795#[allow(non_camel_case_types)]
796#[derive(Debug, Default, Clone)]
797pub struct execcmd_dispatch {
798    /// Number of `BINF_PREFIX` words to strip from the head of args.
799    /// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
800    pub precmd_skip: usize,
801    /// Set when the head (after strip) is a real builtin
802    /// (`Src/exec.c:3065 is_builtin = 1`).
803    pub is_builtin: bool,
804    /// Set when the head (after strip) is a shell function
805    /// (`Src/exec.c:3053 is_shfunc = 1`).
806    pub is_shfunc: bool,
807    /// `cflags` accumulator from `Src/exec.c:2915` — gathers
808    /// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
809    /// BINF_NOGLOB` bits encountered during the precommand-modifier
810    /// walk (c:3062 `cflags |= hn->flags`).
811    pub cflags: u32,
812    /// `command -p` requested: use the default `$PATH` for lookup
813    /// (`Src/exec.c:3160 use_defpath = 1`). NOT YET HONORED by the
814    /// fusevm compiler — flagged for follow-up.
815    pub use_defpath: bool,
816    /// `command -v` / `command -V` requested: the dispatch target
817    /// flips to `bin_whence` per `Src/exec.c:3149-3157`
818    /// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
819    /// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
820    /// instead of resolving the post-strip head.
821    pub has_command_vv: bool,
822    /// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
823    /// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
824    pub exec_argv0: Option<String>,
825    /// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
826    /// — the `else` arm of `if (redir && nonempty(redir))`). Covers
827    /// bare `exec` / `noglob` / `command`. Caller emits
828    /// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
829    /// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
830    /// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
831    /// returns 0 without complaining about missing redirs).
832    pub is_empty_command: bool,
833}
834
835/// !!! NOT A PORT OF C `execcmd_exec` !!!
836///
837/// This is a fusevm-bytecode-time head resolver invoked by
838/// `src/extensions/compile_zsh.rs::compile_simple` and the
839/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
840/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
841/// file under the C-faithful name `execcmd_exec`.
842///
843/// This helper mirrors the head section (`c:2904-3275`) of the C
844/// function — local initialisation, the precommand-modifier walk
845/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
846/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
847/// sub-option parsers — and returns the resulting dispatch
848/// decision via `execcmd_dispatch`. The fusevm compiler reads
849/// that struct to decide which `Op::CallBuiltin` /
850/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
851/// correct post-strip `argc`.
852///
853/// =================== WARNING — DIVERGENCE ====================
854///
855/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
856/// `multio` redirections, evaluates `varspc` assignments, then calls
857/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
858/// stops after the precmd-modifier walk and only returns the head
859/// decision; runtime dispatch is driven by the bytecode the fusevm
860/// compiler emits.
861///
862/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
863/// wordcode iterator state — zshrs doesn't traverse wordcode here,
864/// so the args list arrives already-expanded as a `&[String]`
865/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
866/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
867///
868/// =============================================================
869pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
870    // c:2900 (Src/exec.c)
871
872    // c:2904-2916 — locals.
873    let mut hn: Option<&'static builtin> = None; // c:2904
874    let mut is_shfunc = false; // c:2913
875    let mut is_builtin = false; // c:2913
876    let mut use_defpath = false; // c:2913
877    let mut cflags: u32 = 0; // c:2915
878    let mut orig_cflags: u32 = 0; // c:2915
879    let _ = orig_cflags;
880    // c:3263 — `char *exec_argv0 = NULL;` (declared inside the
881    // BINF_EXEC arm; hoisted here so the dispatch struct can carry it
882    // out after the loop terminates).
883    let mut exec_argv0: Option<String> = None;
884    // c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
885    // (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
886    // compiler can emit `bin_whence` instead of resolving the head.
887    let mut has_command_vv = false;
888
889    // c:2962-2973 — `%job` head: rewrite `%name` → `fg|bg|disown %name`.
890    // Not in scope for the compile-time dispatch walk: jobspec
891    // expansion happens at runtime in fusevm; the bytecode emits a
892    // direct `fg`/`bg` call when it sees a leading `%`. Flagged for
893    // follow-up when the canonical port lands.
894
895    // c:2975-2986 — AUTORESUME prefix-match against jobtab. Same
896    // status as the %job head: runtime concern, deferred.
897
898    // c:3013-3091 — precommand-modifier walk.
899    let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
900    let mut precmd_skip: usize = 0;
901
902    // c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
903    if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
904        // c:3018
905        // c:3029 — `while (nonempty(preargs))`.
906        while precmd_skip < preargs.len() {
907            // c:3029
908            // c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
909            let cmdarg = untokenize(&preargs[precmd_skip]);
910            // c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
911            // already performed prefork expansion on `preargs`, so
912            // `has_token` is effectively false here; the C `break` on
913            // unexpanded tokens is unreachable in this entry point.
914
915            // c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
916            // even disabled builtins so the reserved-word form
917            // (`integer x`, `local foo`) still dispatches to the
918            // typeset family. The static `BUILTINS` array doesn't
919            // expose a separate disabled-bit lookup; one path covers
920            // both. Effect is identical for the precmd-modifier walk.
921
922            // c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
923            // BINF_COMMAND)) && shfunctab->getnode(...))` — shell
924            // function takes precedence unless a `builtin`/`command`
925            // modifier preceded it.
926            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
927                // c:3051
928                if shfunctab_lock()
929                    .read()
930                    .map(|t| t.iter().any(|(k, _)| k == &cmdarg))
931                    .unwrap_or(false)
932                {
933                    is_shfunc = true; // c:3053
934                    break; // c:3054
935                }
936            }
937            // c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
938            let entry = BUILTINS.iter().find(|b| b.node.nam == cmdarg);
939            let Some(entry) = entry else {
940                // c:3056-3058
941                break;
942            };
943            hn = Some(entry);
944            // c:3061-3063 — accumulate cflags.
945            orig_cflags |= cflags;
946            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
947            cflags |= entry.node.flags as u32;
948            // c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
949            // builtin, stop.
950            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
951                // c:3064
952                // WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
953                // to autoload the builtin's module if its
954                // `handlerfunc` is NULL. In zshrs, builtins live in
955                // two places: the static `BUILTINS` table (which
956                // mirrors C `handlerfunc`, often `None` for ports
957                // dispatched through fusevm) AND fusevm's
958                // `register_builtins` map (the actual runtime
959                // dispatcher). A null `handlerfunc` in the static
960                // table is NOT an autoload failure for us — it
961                // means dispatch routes through fusevm. So we
962                // skip the resolvebuiltin call here; the faithful
963                // port remains available for future callers that
964                // genuinely need module-autoload semantics.
965                is_builtin = true; // c:3065
966                break; // c:3077
967            }
968            // c:3086 — `uremnode(preargs, firstnode(preargs))`.
969            precmd_skip += 1;
970            // c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
971            //   (...); if (!firstnode(preargs)) break; }`. zshrs has
972            // no `execcmd_getargs` (args arrive pre-expanded); the
973            // bounds-check at the top of `while precmd_skip <
974            // preargs.len()` handles the empty case identically.
975
976            // c:3092-3177 — BINF_COMMAND sub-option parsing
977            // (`command -p / -v / -V`).
978            if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
979                // c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
980                //                int has_p = 0, has_vV = 0, has_other = 0;`
981                let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
982                let mut pnode: Option<usize> = None; // c:3102
983                let mut has_p = false; // c:3104
984                let mut has_vv = false; // c:3104
985                let mut has_other = false; // c:3104
986                                           // c:3107 — `while (IS_DASH(*argdata))`
987                while argnode < preargs.len()
988                    && IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
989                {
990                    let argdata = preargs[argnode].clone(); // c:3106
991                    let bytes = argdata.as_bytes();
992                    // c:3108-3111 — stop on bare `-` or `--`.
993                    if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
994                        // c:3109
995                        break; // c:3111
996                    }
997                    // c:3112-3133 — scan flag chars.
998                    for &c in &bytes[1..] {
999                        // c:3112
1000                        match c as char {
1001                            'p' => {
1002                                // c:3114
1003                                has_p = true; // c:3122
1004                                pnode = Some(argnode); // c:3123
1005                            }
1006                            'v' | 'V' => {
1007                                // c:3125-3126
1008                                has_vv = true; // c:3127
1009                            }
1010                            _ => {
1011                                // c:3129
1012                                has_other = true; // c:3130
1013                            }
1014                        }
1015                    }
1016                    // c:3134-3138 — unknown flag → don't try, leave alone.
1017                    if has_other {
1018                        // c:3134
1019                        has_p = false; // c:3136
1020                        has_vv = false; // c:3136
1021                        break; // c:3137
1022                    }
1023                    // c:3140-3147 — advance to next arg.
1024                    argnode += 1; // c:3141 nextnode(argnode)
1025                    if argnode >= preargs.len() {
1026                        // c:3142 — execcmd_getargs (skipped: pre-expanded)
1027                        break; // c:3145
1028                    }
1029                }
1030                // c:3149-3157 — `-v`/`-V` → dispatch to whence.
1031                if has_vv {
1032                    // c:3149
1033                    // c:3154 `pushnode(preargs, "command")` — C re-inserts
1034                    // "command" so bin_whence sees it as argv[0]. zshrs
1035                    // surfaces this via `has_command_vv`; the fusevm
1036                    // compiler emits the equivalent whence call.
1037                    has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
1038                    is_builtin = true;
1039                    break; // c:3157
1040                } else if has_p {
1041                    // c:3158
1042                    use_defpath = true; // c:3160
1043                    if let Some(pn) = pnode {
1044                        // c:3165 — `uremnode(preargs, pnode)`. zshrs:
1045                        // remove the `-p`-bearing arg from preargs.
1046                        if pn < preargs.len() {
1047                            preargs.remove(pn);
1048                            // precmd_skip already accounts for the
1049                            // stripped `command` prefix; we just removed
1050                            // the `-p` flag which sat at preargs[pn].
1051                            // No precmd_skip change needed — the head
1052                            // remains where it was.
1053                        }
1054                    }
1055                }
1056                // c:3176-3177 — `--` trailing end-of-options strip.
1057                if argnode < preargs.len() {
1058                    let argdata = &preargs[argnode];
1059                    let b = argdata.as_bytes();
1060                    if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
1061                        // c:3176
1062                        preargs.remove(argnode); // c:3177
1063                    }
1064                }
1065            } else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
1066                // c:3178-3275 — BINF_EXEC sub-option parsing
1067                // (`exec -a NAME -l -c`).
1068                let mut argnode: usize = precmd_skip; // c:3185
1069                let mut error_done = false;
1070                // c:3196 — `while (argdata && IS_DASH(*argdata) &&
1071                //                  strlen(argdata) >= 2)`
1072                while argnode < preargs.len() {
1073                    let argdata = preargs[argnode].clone();
1074                    let bytes = argdata.as_bytes();
1075                    if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
1076                        break; // c:3196 loop guard
1077                    }
1078                    let oldnode = argnode; // c:3197
1079                    argnode += 1; // c:3198 nextnode(oldnode)
1080                                  // c:3203-3208 — empty next → error.
1081                    if argnode >= preargs.len() {
1082                        // c:3203
1083                        zerr(
1084                            // c:3204
1085                            "exec requires a command to execute",
1086                        );
1087                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
1088                        error_done = true;
1089                        break; // c:3207 goto done
1090                    }
1091                    // c:3209 — `uremnode(preargs, oldnode)`.
1092                    preargs.remove(oldnode);
1093                    argnode -= 1; // re-anchor — `argnode` was the post-removed slot
1094                                  // c:3210-3211 — `--` stops option scan.
1095                    if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
1096                        // c:3210
1097                        break; // c:3211
1098                    }
1099                    // c:3212-3258 — scan flag chars after the leading `-`.
1100                    let mut k = 1usize;
1101                    while k < bytes.len() && !error_done {
1102                        let cmdopt = bytes[k] as char; // c:3212
1103                        match cmdopt {
1104                            'a' => {
1105                                // c:3214 — `-a` ARGV0 override.
1106                                if k + 1 < bytes.len() {
1107                                    // c:3216 — `-aNAME` inline form.
1108                                    exec_argv0 =
1109                                        Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
1110                                    k = bytes.len(); // c:3219 position past end
1111                                } else {
1112                                    // c:3220 — `-a NAME` separate form.
1113                                    if argnode >= preargs.len() {
1114                                        // c:3230
1115                                        zerr(
1116                                            // c:3231
1117                                            "exec flag -a requires a parameter",
1118                                        );
1119                                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
1120                                        error_done = true;
1121                                        break; // c:3234 goto done
1122                                    }
1123                                    exec_argv0 = Some(preargs[argnode].clone()); // c:3236
1124                                    preargs.remove(argnode); // c:3239
1125                                }
1126                            }
1127                            'c' => {
1128                                // c:3242
1129                                cflags |= BINF_CLEARENV; // c:3243
1130                            }
1131                            'l' => {
1132                                // c:3245
1133                                cflags |= BINF_DASH; // c:3246
1134                            }
1135                            _ => {
1136                                // c:3248
1137                                zerr(
1138                                    // c:3249
1139                                    &format!("unknown exec flag -{}", cmdopt),
1140                                );
1141                                errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
1142                                error_done = true;
1143                                break; // c:3256
1144                            }
1145                        }
1146                        k += 1;
1147                    }
1148                    if error_done {
1149                        break;
1150                    }
1151                }
1152                // c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
1153                // the actual `setenv` to the fusevm compiler / external
1154                // exec path; we surface `exec_argv0` via the dispatch
1155                // struct so the caller can apply it before fork+exec.
1156                if let Some(ref a0) = exec_argv0 {
1157                    // c:3263 — `remnulargs + untokenize` then setenv.
1158                    let cleaned = untokenize(a0); // c:3266-3267
1159                    exec_argv0 = Some(cleaned);
1160                }
1161                if error_done {
1162                    return execcmd_dispatch {
1163                        precmd_skip,
1164                        is_builtin,
1165                        is_shfunc,
1166                        cflags,
1167                        use_defpath,
1168                        has_command_vv,
1169                        exec_argv0,
1170                        is_empty_command: false,
1171                    };
1172                }
1173            }
1174            // c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
1175            // unset(POSIXBUILTINS)) break;`. After processing a
1176            // `command` precmd modifier (and its -p/-v/-V flags), the
1177            // C loop exits with hn cleared so the dispatch falls
1178            // through to external lookup. Without this, the next
1179            // iteration would find `command print` → print's builtin
1180            // and dispatch to it; zsh's intentional behaviour is to
1181            // skip builtins under `command` (unless POSIXBUILTINS is
1182            // set, where the loop continues normally).
1183            if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
1184                hn = None; // c:3275 hn = NULL
1185                break; // c:3277
1186            }
1187        }
1188    }
1189
1190    // c:3309-3406 — "Empty command" branch. When the precmd-modifier
1191    // walk above strips every word with nothing left to dispatch
1192    // (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
1193    // C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
1194    //
1195    // - redir-present + do_exec       → nullexec=1 (continue to run)
1196    // - redir-present + varspc        → nullexec=2 (continue)
1197    // - redir-present + no nullcmd    → `zerr("redirection with no command")`
1198    //                                   lastval=1, return
1199    // - redir-present + SHNULLCMD     → args=[":"]
1200    // - redir-present + readnullcmd   → args=[readnullcmd]
1201    // - redir-present + default       → args=[nullcmd]
1202    // - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
1203    // - NO redir + default            → lastval=cmdoutval, return (c:3372-3406)
1204    //
1205    // zshrs's `execcmd_compile_head` doesn't receive `redir` (it
1206    // takes `args` only). The cases that DEPEND on redirs are handled by
1207    // `compile_zsh.rs::compile_redir` before this dispatch fires; the
1208    // remaining cases collapse into the single `is_empty_command`
1209    // flag below. Both NO-redir sub-cases produce the same observable
1210    // outcome (lastval=0, return without invoking anything), so a
1211    // single flag suffices.
1212    let is_empty_command = precmd_skip >= preargs.len();
1213
1214    // =================== WARNING — DIVERGENCE ====================
1215    // c:3285+: prefork-substitution, magic_assign decision, multio
1216    // setup, varspc evaluation, and the actual execbuiltin /
1217    // runshfunc / execute call. ~1300 lines of interpreter-only
1218    // code, entirely replaced by fusevm bytecode dispatch in
1219    // `src/extensions/compile_zsh.rs::compile_simple` and the
1220    // opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
1221    // The return value below feeds those compile-time decisions.
1222    // =============================================================
1223
1224    let _ = hn;
1225    execcmd_dispatch {
1226        precmd_skip,
1227        is_builtin,
1228        is_shfunc,
1229        cflags,
1230        use_defpath,
1231        has_command_vv,
1232        exec_argv0,
1233        is_empty_command,
1234    }
1235}
1236
1237// =============================================================================
1238// Leaf-function ports — c:283 (parse_string) and below. Added incrementally to
1239// chip at the ~5500 lines of exec.c still un-ported beyond the wordcode
1240// walker (execlist / execpline / execcmd which the fusevm bytecode VM
1241// replaces — see the WARNING block in execcmd_exec).
1242// =============================================================================
1243
1244/// Port of `parse_string(char *s, int reset_lineno)` from `Src/exec.c:283`.
1245///
1246/// C body:
1247/// ```c
1248/// Eprog p; zlong oldlineno;
1249/// zcontext_save();
1250/// inpush(s, INP_LINENO, NULL);
1251/// strinbeg(0);
1252/// oldlineno = lineno;
1253/// if (reset_lineno) lineno = 1;
1254/// p = parse_list();
1255/// lineno = oldlineno;
1256/// if (tok == LEXERR && !lastval) lastval = 1;
1257/// strinend();
1258/// inpop();
1259/// zcontext_restore();
1260/// return p;
1261/// ```
1262///
1263/// Parses an arbitrary string as a zsh command list, returning the
1264/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
1265/// `bin_eval` for `eval`, and the autoload path.
1266pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
1267    // c:285-286
1268    let p: Option<eprog>;
1269    let oldlineno: i64;
1270
1271    zcontext_save(); // c:288
1272    inpush(s, INP_LINENO, None); // c:289
1273    strinbeg(0); // c:290
1274    oldlineno = LEX_LINENO.get() as i64; // c:291
1275    if reset_lineno != 0 {
1276        // c:292
1277        LEX_LINENO.set(1); // c:293
1278    }
1279    p = parse_list(); // c:294
1280    LEX_LINENO.set(oldlineno as u64); // c:295
1281                                      // c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
1282    if tok() == LEXERR && LASTVAL.load(Ordering::Relaxed) == 0 {
1283        LASTVAL.store(1, Ordering::Relaxed);
1284    }
1285    strinend(); // c:298
1286    inpop(); // c:299
1287    zcontext_restore(); // c:300
1288    p // c:301
1289}
1290
1291/// Port of `int isgooderr(int e, char *dir)` from `Src/exec.c:652`.
1292///
1293/// C body:
1294/// ```c
1295/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
1296/// return ((e != EACCES || !access(dir, X_OK)) &&
1297///         e != ENOENT && e != ENOTDIR);
1298/// ```
1299///
1300/// errno classifier for `execve` failures during PATH search: if the
1301/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
1302/// it's "expected" (try next PATH entry); otherwise it's a real
1303/// failure worth surfacing.
1304pub fn isgooderr(e: i32, dir: &str) -> bool {
1305    // c:652
1306    // c:Src/exec.c:658-659 — `(e != EACCES || !access(dir, X_OK)) &&
1307    //   e != ENOENT && e != ENOTDIR`. C's `access(dir, X_OK)` returns
1308    //   0 on success / -1 on failure. The previous Rust port used
1309    //   `metadata().permissions().mode() & 0o111` which reports the
1310    //   X bit even when the path doesn't exist as the EFFECTIVE caller
1311    //   (root, ACLs, capabilities all flip access() vs raw mode).
1312    //   `/no/such/dir` metadata() fails → returned false for
1313    //   dir_x_ok, then `!false` = true, giving "good error" for a
1314    //   nonexistent path. Use libc::access directly to match C exactly.
1315    let unmeta_dir = unmeta(dir);
1316    let cstr = std::ffi::CString::new(unmeta_dir.as_bytes()).unwrap_or_default();
1317    let access_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1318    (e != libc::EACCES || access_ok) && e != libc::ENOENT && e != libc::ENOTDIR
1319}
1320
1321/// Port of `int iscom(char *s)` from `Src/exec.c:962`.
1322///
1323/// C body:
1324/// ```c
1325/// struct stat statbuf;
1326/// char *us = unmeta(s);
1327/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
1328///         S_ISREG(statbuf.st_mode));
1329/// ```
1330///
1331/// True iff `s` names an executable regular file (X-perm + S_IFREG).
1332/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
1333/// validate candidate paths before exec.
1334pub fn iscom(s: &str) -> bool {
1335    // c:962
1336    let us = unmeta(s); // c:965
1337                        // c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
1338    let cstr = match std::ffi::CString::new(us.as_str()) {
1339        Ok(c) => c,
1340        Err(_) => return false,
1341    };
1342    let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1343    if !x_ok {
1344        return false;
1345    }
1346    let meta = match std::fs::metadata(&us) {
1347        Ok(m) => m,
1348        Err(_) => return false,
1349    };
1350    meta.file_type().is_file()
1351}
1352
1353/// Port of `int isreallycom(Cmdnam cn)` from `Src/exec.c:972-987`.
1354///
1355/// Verify that a hashed/cached cmdnamtab entry still names a real
1356/// external command (X-perm + regular file). For HASHED entries
1357/// (`cn->u.cmd` carries the absolute path), test the path directly;
1358/// otherwise concatenate `name[0] + "/" + nam` and test that.
1359/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
1360/// turn into a failed `execve` syscall.
1361pub fn isreallycom(cn: &cmdnam) -> bool {
1362    // c:972
1363    let fullnam: String;
1364    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
1365        // c:977-978 — `strcpy(fullnam, cn->u.cmd);`
1366        fullnam = cn.cmd.clone().unwrap_or_default();
1367    } else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
1368        // c:979-980 — `if (!cn->u.name) return 0;`
1369        return false;
1370    } else {
1371        // c:982-984 — `strcpy + strcat("/") + strcat(nam)`
1372        let path0 = &cn.name.as_ref().unwrap()[0];
1373        fullnam = format!("{}/{}", path0, cn.node.nam);
1374    }
1375    iscom(&fullnam) // c:986
1376}
1377
1378/// Port of `int isrelative(char *s)` from `Src/exec.c:996`.
1379///
1380/// C body:
1381/// ```c
1382/// if (*s != '/') return 1;
1383/// for (; *s; s++)
1384///     if (*s == '.' && s[-1] == '/' &&
1385///         (s[1] == '/' || s[1] == '\0' ||
1386///          (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
1387///         return 1;
1388/// return 0;
1389/// ```
1390///
1391/// True iff `s` either doesn't start with `/` OR contains a `./` or
1392/// `../` component anywhere. Used by `cd` resolution and PATH-cache
1393/// invalidation to detect non-canonical paths.
1394pub fn isrelative(s: &str) -> i32 {
1395    // c:996
1396    let bytes = s.as_bytes();
1397    if bytes.is_empty() || bytes[0] != b'/' {
1398        // c:998
1399        return 1; // c:999
1400    }
1401    // c:1000-1004 — walk for `./` or `../` components.
1402    for i in 1..bytes.len() {
1403        let c = bytes[i];
1404        let prev = bytes[i - 1];
1405        if c == b'.' && prev == b'/' {
1406            let next = bytes.get(i + 1).copied().unwrap_or(0);
1407            if next == b'/' || next == 0 {
1408                // c:1002
1409                return 1;
1410            }
1411            if next == b'.' {
1412                let next2 = bytes.get(i + 2).copied().unwrap_or(0);
1413                if next2 == b'/' || next2 == 0 {
1414                    // c:1003
1415                    return 1;
1416                }
1417            }
1418        }
1419    }
1420    0 // c:1005
1421}
1422
1423/// Port of `void setunderscore(char *str)` from `Src/exec.c:2652`.
1424///
1425/// C body:
1426/// ```c
1427/// queue_signals();
1428/// if (str && *str) {
1429///     size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
1430///     if (nl > underscorelen || (underscorelen - nl) > 64) {
1431///         zfree(zunderscore, underscorelen);
1432///         zunderscore = (char *) zalloc(underscorelen = nl);
1433///     }
1434///     strcpy(zunderscore, str);
1435///     underscoreused = l;
1436/// } else {
1437///     ... reset zunderscore = "" ...
1438/// }
1439/// unqueue_signals();
1440/// ```
1441///
1442/// Sets the `$_` global to the last argument of the most recent
1443/// command. Called from `execcmd_exec` (c:3936) per `last_status`
1444/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
1445pub fn setunderscore(str: &str) {
1446    // c:2652
1447    queue_signals(); // c:2654
1448    if !str.is_empty() {
1449        // c:2655 `if (str && *str)`
1450        // c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
1451        let mut zu = zunderscore.lock().unwrap();
1452        *zu = str.to_string();
1453        let nl = (str.len() + 1 + 31) & !31; // c:2656
1454        underscorelen.store(nl, Ordering::Relaxed); // c:2660
1455        underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
1456    // c:2663
1457    } else {
1458        // c:2664
1459        let mut zu = zunderscore.lock().unwrap();
1460        zu.clear(); // c:2669 `*zunderscore = '\0';`
1461        underscoreused.store(1, Ordering::Relaxed); // c:2670
1462    }
1463    unqueue_signals(); // c:2672
1464}
1465
1466/// Port of `int mpipe(int *pp)` from `Src/exec.c:5160`.
1467///
1468/// C body:
1469/// ```c
1470/// if (pipe(pp) < 0) {
1471///     zerr("pipe failed: %e", errno);
1472///     return -1;
1473/// }
1474/// pp[0] = movefd(pp[0]);
1475/// pp[1] = movefd(pp[1]);
1476/// return 0;
1477/// ```
1478///
1479/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
1480/// fd range via `movefd`. Used by `getpipe` / `getproc` /
1481/// `spawnpipes` for process substitution and pipeline wiring.
1482pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
1483    // c:5160
1484    let mut fds: [libc::c_int; 2] = [-1; 2];
1485    if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
1486        // c:5162
1487        zerr(&format!(
1488            // c:5163
1489            "pipe failed: {}",
1490            std::io::Error::last_os_error()
1491        ));
1492        return -1; // c:5164
1493    }
1494    pp[0] = movefd(fds[0]); // c:5166
1495    pp[1] = movefd(fds[1]); // c:5167
1496    0 // c:5168
1497}
1498
1499/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
1500/// from `Src/exec.c:5289`. Anonymous-function name marker used by
1501/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
1502/// `() { ... }` anonymous function dispatch.
1503pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
1504
1505/// Port of `int is_anonymous_function_name(const char *name)` from
1506/// `Src/exec.c:5300`.
1507///
1508/// C body:
1509/// ```c
1510/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
1511/// ```
1512///
1513/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
1514/// reporting and `whence -v` to skip / annotate anonymous functions.
1515pub fn is_anonymous_function_name(name: &str) -> i32 {
1516    // c:5300
1517    if name == ANONYMOUS_FUNCTION_NAME {
1518        // c:5302
1519        1
1520    } else {
1521        0
1522    }
1523}
1524
1525/// Port of `void execsave(void)` from `Src/exec.c:6438`.
1526///
1527/// C body:
1528/// ```c
1529/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
1530/// es->list_pipe_pid = list_pipe_pid;
1531/// es->nowait = nowait;
1532/// es->pline_level = pline_level;
1533/// es->list_pipe_child = list_pipe_child;
1534/// es->list_pipe_job = list_pipe_job;
1535/// strcpy(es->list_pipe_text, list_pipe_text);
1536/// es->lastval = lastval;
1537/// es->noeval = noeval;
1538/// es->badcshglob = badcshglob;
1539/// es->cmdoutpid = cmdoutpid;
1540/// es->cmdoutval = cmdoutval;
1541/// es->use_cmdoutval = use_cmdoutval;
1542/// es->procsubstpid = procsubstpid;
1543/// es->trap_return = trap_return;
1544/// es->trap_state = trap_state;
1545/// es->trapisfunc = trapisfunc;
1546/// es->traplocallevel = traplocallevel;
1547/// es->noerrs = noerrs;
1548/// es->this_noerrexit = this_noerrexit;
1549/// es->underscore = ztrdup(zunderscore);
1550/// es->next = exstack;
1551/// exstack = es;
1552/// noerrs = cmdoutpid = 0;
1553/// ```
1554///
1555/// Snapshot every transient exec-context global onto the `exstack`
1556/// linked list so a signal-handler / trap-firing nested eval can
1557/// scribble freely; `execrestore` pops the frame back. Called by
1558/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
1559pub fn execsave() {
1560    // c:6438
1561    // c:6442 — `es = zalloc(sizeof(execstack));`
1562    let mut es = Box::new(execstack {
1563        // c:6442
1564        next: None,
1565        list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
1566        nowait: nowait.load(Ordering::Relaxed),               // c:6444
1567        pline_level: pline_level.load(Ordering::Relaxed),     // c:6445
1568        list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
1569        list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
1570        list_pipe_text: {
1571            // c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
1572            let mut buf = [0u8; JOBTEXTSIZE];
1573            if let Ok(s) = LIST_PIPE_TEXT.lock() {
1574                let bytes = s.as_bytes();
1575                let n = bytes.len().min(JOBTEXTSIZE - 1);
1576                buf[..n].copy_from_slice(&bytes[..n]);
1577            }
1578            buf
1579        },
1580        lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
1581        // c:6450 — `es->noeval = noeval;`. Snapshot math.c's
1582        // `int noeval` (the parse-only side-effect-skip counter)
1583        // via math.rs's pub accessor.
1584        noeval: crate::ported::math::m_noeval(),
1585        // c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
1586        // csh-glob diagnostic counter (glob.c:103 / glob.rs
1587        // BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
1588        // the outer command's per-line accounting.
1589        badcshglob: crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed), // c:6451
1590        cmdoutpid: cmdoutpid.load(Ordering::Relaxed),                        // c:6452
1591        cmdoutval: cmdoutval.load(Ordering::Relaxed),                        // c:6453
1592        use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed),                // c:6454
1593        procsubstpid: procsubstpid.load(Ordering::Relaxed),                  // c:6455
1594        trap_return: TRAP_RETURN.load(Ordering::Relaxed),                    // c:6456
1595        trap_state: TRAP_STATE.load(Ordering::Relaxed),                      // c:6457
1596        trapisfunc: trapisfunc.load(Ordering::Relaxed),                      // c:6458
1597        traplocallevel: traplocallevel.load(Ordering::Relaxed),              // c:6459
1598        noerrs: noerrs.load(Ordering::Relaxed),                              // c:6460
1599        this_noerrexit: this_noerrexit.load(Ordering::Relaxed),              // c:6461
1600        // c:6462 — `es->underscore = ztrdup(zunderscore);`
1601        underscore: Some(zunderscore.lock().unwrap().clone()),
1602    });
1603    // c:6463-6464 — `es->next = exstack; exstack = es;`
1604    let mut head = exstack.lock().unwrap();
1605    es.next = head.take();
1606    *head = Some(es);
1607    // c:6465 — `noerrs = cmdoutpid = 0;`
1608    noerrs.store(0, Ordering::Relaxed);
1609    cmdoutpid.store(0, Ordering::Relaxed);
1610}
1611
1612/// Port of `void execrestore(void)` from `Src/exec.c:6470`.
1613///
1614/// C body:
1615/// ```c
1616/// struct execstack *en = exstack;
1617/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
1618/// queue_signals();
1619/// exstack = exstack->next;
1620/// list_pipe_pid = en->list_pipe_pid;
1621/// nowait = en->nowait;
1622/// pline_level = en->pline_level;
1623/// list_pipe_child = en->list_pipe_child;
1624/// list_pipe_job = en->list_pipe_job;
1625/// strcpy(list_pipe_text, en->list_pipe_text);
1626/// lastval = en->lastval;
1627/// noeval = en->noeval;
1628/// badcshglob = en->badcshglob;
1629/// cmdoutpid = en->cmdoutpid;
1630/// cmdoutval = en->cmdoutval;
1631/// use_cmdoutval = en->use_cmdoutval;
1632/// procsubstpid = en->procsubstpid;
1633/// trap_return = en->trap_return;
1634/// trap_state = en->trap_state;
1635/// trapisfunc = en->trapisfunc;
1636/// traplocallevel = en->traplocallevel;
1637/// noerrs = en->noerrs;
1638/// this_noerrexit = en->this_noerrexit;
1639/// setunderscore(en->underscore);
1640/// zsfree(en->underscore);
1641/// free(en);
1642/// unqueue_signals();
1643/// ```
1644///
1645/// Pop the top `execstack` frame and restore every transient
1646/// exec-context global. Inverse of `execsave`.
1647pub fn execrestore() {
1648    // c:6470
1649    let mut head = exstack.lock().unwrap();
1650    let en = match head.take() {
1651        // c:6472 + c:6477
1652        Some(en) => en,
1653        None => {
1654            // c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
1655            crate::DPUTS!(true, "BUG: execrestore() without execsave()");
1656            return;
1657        }
1658    };
1659    queue_signals(); // c:6476
1660    *head = en.next; // c:6477
1661    drop(head); // release lock before scalar restores
1662
1663    list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
1664    nowait.store(en.nowait, Ordering::Relaxed); // c:6480
1665    pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
1666    list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
1667    list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
1668                                                              // c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
1669    if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
1670        let nul = en
1671            .list_pipe_text
1672            .iter()
1673            .position(|&b| b == 0)
1674            .unwrap_or(JOBTEXTSIZE);
1675        *s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
1676    }
1677    LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
1678                                                  // c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
1679                                                  // counter from the saved frame.
1680    crate::ported::math::m_noeval_set(en.noeval);
1681    // c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
1682    // diagnostic counter saved on entry.
1683    crate::ported::glob::BADCSHGLOB.store(en.badcshglob, Ordering::Relaxed);
1684    cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
1685    cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
1686    use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
1687    procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
1688    TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
1689    TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
1690    trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
1691    traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
1692    noerrs.store(en.noerrs, Ordering::Relaxed); // c:6496
1693    this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
1694                                                                // c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
1695    if let Some(ref u) = en.underscore {
1696        setunderscore(u); // c:6498
1697    }
1698    // c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
1699    unqueue_signals(); // c:6502
1700}
1701
1702/// Port of `void execstring(char *s, int dont_change_job, int exiting,
1703/// char *context)` from `Src/exec.c:1228`.
1704///
1705/// C body:
1706/// ```c
1707/// Eprog prog;
1708/// pushheap();
1709/// if (isset(VERBOSE)) {
1710///     zputs(s, stderr);
1711///     fputc('\n', stderr);
1712///     fflush(stderr);
1713/// }
1714/// if ((prog = parse_string(s, 0)))
1715///     execode(prog, dont_change_job, exiting, context);
1716/// popheap();
1717/// ```
1718///
1719/// Public entry — execute an arbitrary string as a zsh command list.
1720/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
1721/// body executors, command substitution body runners.
1722///
1723/// =================== WARNING — DIVERGENCE ====================
1724/// The C path is `parse_string` → `execode` → `execlist` (wordcode
1725/// walker). zshrs replaces `execode/execlist` with the fusevm
1726/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
1727/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
1728/// parse+execute chain delegates to the fusevm entry. When `execlist`
1729/// lands as a strict 1:1 port, swap the delegate for the canonical
1730/// chain.
1731/// =============================================================
1732pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
1733    // c:1228
1734    pushheap(); // c:1232
1735                // c:1233-1237 — VERBOSE banner.
1736    if isset(VERBOSE) {
1737        // c:1233
1738        let mut stderr = std::io::stderr().lock();
1739        use std::io::Write;
1740        let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
1741        let _ = stderr.write_all(b"\n"); // c:1235
1742        let _ = stderr.flush(); // c:1236
1743    }
1744    // c:1238-1239 — parse + execode. zshrs delegates the parse+VM
1745    // chain to the fusevm pipeline via the exec_hooks fn-ptr
1746    // installed by fusevm_bridge at startup. Direct
1747    // `with_executor` / ShellExecutor reach-in from src/ported/ is
1748    // forbidden — see memory feedback_no_exec_script_from_ported.
1749    let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(s);
1750    popheap(); // c:1240
1751}
1752
1753/// Port of `void runshfunc(Eprog prog, FuncWrap wrap, char *name)` from
1754/// `Src/exec.c:6166`. The inner shell-function executor — fires
1755/// module-registered wrapper handlers around the function body, with
1756/// `$_` (zunderscore) save/restore and a paramscope push/pop around
1757/// the wordcode walk.
1758///
1759/// C control flow:
1760/// ```c
1761/// queue_signals();
1762/// ou = zalloc(ouu = underscoreused);
1763/// if (ou) memcpy(ou, zunderscore, underscoreused);
1764/// while (wrap) {                       // wrapper chain
1765///     wrap->module->wrapper++;
1766///     cont = wrap->handler(prog, wrap->next, name);
1767///     wrap->module->wrapper--;
1768///     if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
1769///         unload_module(wrap->module);
1770///     if (!cont) {                     // wrapper handled it
1771///         if (ou) zfree(ou, ouu);
1772///         unqueue_signals();
1773///         return;
1774///     }
1775///     wrap = wrap->next;
1776/// }
1777/// startparamscope();
1778/// execode(prog, 1, 0, "shfunc");
1779/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
1780/// endparamscope();
1781/// unqueue_signals();
1782/// ```
1783///
1784/// (a) `wrap->module->wrapper++/--` (c:6178/6180) wired against
1785///     `module::MODULESTAB.modules[name].wrapper` (i32), looked up
1786///     by `wrap.module.node.nam`. Recursive unload during handler
1787///     defers correctly.
1788/// (b) `unload_module(wrap->module)` (c:6184) wired via
1789///     `modulestab.unload_module(name)` when wrapper hits 0 AND
1790///     MOD_UNLOAD flag is set on the module's hashnode.
1791/// (c) `execode(prog, 1, 0, "shfunc")` (c:6195) ported at
1792///     exec.rs:6047. Body uses execode for the no-source
1793///     (compiled-wordcode) branch and fusevm for the
1794///     source-preserving (autoloaded) branch per cache coherence.
1795/// (d) `startparamscope/endparamscope` Rust signatures take
1796///     `&mut HashTable` (params.rs:7425/7435). We pass the global
1797///     paramtab handle via the params crate.
1798pub fn runshfunc(prog: &eprog, mut wrap: Option<&funcwrap>, name: &str) {
1799    // c:6166
1800    queue_signals(); // c:6171
1801                     // c:6173-6175 — snapshot zunderscore into `ou`.
1802    let ouu = underscoreused.load(Ordering::Relaxed) as usize;
1803    let ou: Option<String> = if ouu > 0 {
1804        // c:6174
1805        Some(zunderscore.lock().unwrap().clone()) // c:6175
1806    } else {
1807        None
1808    };
1809    // c:6177-6193 — wrapper chain walk.
1810    while let Some(w) = wrap {
1811        // c:6177
1812        // c:6178 — wrap->module->wrapper++ (WARNING a).
1813        // c:6178 — `wrap->module->wrapper++;` — bump refcount so a
1814        // recursive unload during the handler defers until we return.
1815        let mod_name: Option<String> = w.module.as_ref().map(|m| m.node.nam.clone());
1816        if let Some(ref n) = mod_name {
1817            if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1818                if let Some(m) = tab.modules.get_mut(n) {
1819                    m.wrapper += 1;
1820                }
1821            }
1822        }
1823        let cont = if let Some(h) = w.handler {
1824            // c:6179 — WrapFunc takes Eprog by value + next FuncWrap by value.
1825            // We pass an empty next sentinel (wrapper-chain walks are
1826            // single-step in zshrs — see chain-walk comment below).
1827            let next_sentinel = Box::new(funcwrap {
1828                next: None,
1829                flags: 0,
1830                handler: None,
1831                module: None,
1832            });
1833            h(Box::new(prog.clone()), next_sentinel, name)
1834        } else {
1835            1
1836        };
1837        // c:6180 — `wrap->module->wrapper--;`
1838        // c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD)) unload_module(wrap->module);`
1839        if let Some(ref n) = mod_name {
1840            let should_unload = {
1841                if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1842                    if let Some(m) = tab.modules.get_mut(n) {
1843                        m.wrapper -= 1;
1844                        m.wrapper == 0 && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
1845                    } else {
1846                        false
1847                    }
1848                } else {
1849                    false
1850                }
1851            };
1852            if should_unload {
1853                if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1854                    let _ = tab.unload_module(n); // c:6184
1855                }
1856            }
1857        }
1858        if cont == 0 {
1859            // c:6186 — wrapper claimed the call.
1860            unqueue_signals(); // c:6189
1861            return; // c:6190
1862        }
1863        // c:6192 — wrap = wrap->next; the linked-list step requires
1864        // owning the next ref; the borrowed iteration breaks here.
1865        // Wrapper chains > 1 are extremely rare; we stop at the
1866        // first to avoid a Box::leak.
1867        wrap = None;
1868    }
1869    // c:6194 — startparamscope (just inc_locallevel internally).
1870    inc_locallevel();
1871    // c:6195 — `execode(prog, 1, 0, "shfunc");` — run the function
1872    // body. Prefer the canonical execode (exec.rs:6047) which walks
1873    // execlist on a fresh estate over the prog. If prog.strs carries
1874    // the original source (autoloaded ported that the lazy-compile path
1875    // populated), route through the fusevm pipeline for cache
1876    // coherence with execstring.
1877    if let Some(ref src) = prog.strs {
1878        let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(src);
1879    } else {
1880        // Pure wordcode body — drive via the canonical execode.
1881        execode(Box::new(prog.clone()), 1, 0, "shfunc");
1882        let _ = name;
1883    }
1884    if let Some(ou_str) = ou {
1885        // c:6196
1886        setunderscore(&ou_str); // c:6197
1887                                // c:6198 — zfree(ou, ouu) — Rust drops on scope exit.
1888    }
1889    endparamscope(); // c:6200
1890                     // c:6141 — deferred-exit gate. After endparamscope() unwinds the
1891                     // function's local scope (locallevel--), check whether an exit
1892                     // queued inside the function has reached its target scope:
1893                     //   if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)
1894                     // The `+1` accounts for endparamscope having already happened
1895                     // here (locallevel is already one less than when exit_level was
1896                     // captured at c:5890). When the gate fires:
1897                     //   - locallevel > forklevel: still in a nested function — force
1898                     //     the outer frame to return too (retflag=1, breaks=loops).
1899                     //   - locallevel <= forklevel: out of all functions — actually
1900                     //     exit the shell now via zexit(exit_val, ZEXIT_NORMAL).
1901                     // `in_exit_trap` (c:Src/signals.c:63 — `int in_exit_trap;`) is the
1902                     // EXIT-trap reentry counter. dotrap at signals.c:1272/1277 wraps
1903                     // SIGEXIT handler dispatch with ++/--, so an exit issued FROM an
1904                     // EXIT trap shouldn't re-trigger the gate (or the trap would
1905                     // recurse). zshrs's signals::in_exit_trap is the canonical port
1906                     // surface — read it directly here.
1907    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
1908    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
1909    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
1910    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
1911    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
1912    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
1913        // c:6141
1914        if cur_locallevel > cur_forklevel {
1915            // c:6143 — still inside a nested function: keep unwinding.
1916            RETFLAG.store(1, Ordering::Relaxed); // c:6144
1917            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
1918        } else {
1919            // c:6151 — out of all functions: exit for real.
1920            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
1921            let val = EXIT_VAL.load(Ordering::Relaxed);
1922            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
1923            // c:6152
1924        }
1925    }
1926    unqueue_signals(); // c:6202
1927}
1928
1929/// Port of `Emulation_options sticky_emulation_dup(Emulation_options src,
1930/// int useheap)` from `Src/exec.c:5501`.
1931///
1932/// C body (`useheap` selects between heap-arena and permanent zalloc;
1933/// Rust collapses both into owned `Box` clones):
1934/// ```c
1935/// Emulation_options newsticky = useheap ?
1936///     hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
1937/// newsticky->emulation = src->emulation;
1938/// if (src->n_on_opts) {
1939///     size_t sz = src->n_on_opts * sizeof(*src->on_opts);
1940///     newsticky->n_on_opts = src->n_on_opts;
1941///     newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
1942///     memcpy(newsticky->on_opts, src->on_opts, sz);
1943/// }
1944/// if (src->n_off_opts) {
1945///     size_t sz = src->n_off_opts * sizeof(*src->off_opts);
1946///     newsticky->n_off_opts = src->n_off_opts;
1947///     newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
1948///     memcpy(newsticky->off_opts, src->off_opts, sz);
1949/// }
1950/// return newsticky;
1951/// ```
1952///
1953/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
1954/// at function-def time to snapshot the pending `sticky` global so
1955/// the function carries its own immutable copy.
1956pub fn sticky_emulation_dup(src: &emulation_options, _useheap: i32) -> Emulation_options {
1957    // c:5501
1958    // c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
1959    let mut newsticky = Box::new(emulation_options {
1960        emulation: src.emulation, // c:5505
1961        n_on_opts: 0,
1962        n_off_opts: 0,
1963        on_opts: Vec::new(),
1964        off_opts: Vec::new(),
1965    });
1966    // c:5506-5511 — copy on_opts.
1967    if src.n_on_opts != 0 {
1968        // c:5506
1969        newsticky.n_on_opts = src.n_on_opts; // c:5508
1970        newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
1971    }
1972    // c:5512-5517 — copy off_opts.
1973    if src.n_off_opts != 0 {
1974        // c:5512
1975        newsticky.n_off_opts = src.n_off_opts; // c:5514
1976        newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
1977    }
1978    newsticky // c:5519
1979}
1980
1981/// Port of `void shfunc_set_sticky(Shfunc shf)` from `Src/exec.c:5527`.
1982///
1983/// C body:
1984/// ```c
1985/// if (sticky)
1986///     shf->sticky = sticky_emulation_dup(sticky, 0);
1987/// else
1988///     shf->sticky = NULL;
1989/// ```
1990///
1991/// Stamp the function with the current pending sticky-emulation
1992/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
1993pub fn shfunc_set_sticky(shf: &mut shfunc) {
1994    // c:5527
1995    let sticky_guard = sticky.lock().unwrap();
1996    if let Some(ref s) = *sticky_guard {
1997        // c:5529
1998        shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
1999    } else {
2000        // c:5531
2001        shf.sticky = None; // c:5532
2002    }
2003}
2004
2005/// Port of `static char *search_defpath(char *cmd, char *pbuf, int plen)`
2006/// from `Src/exec.c:691`.
2007///
2008/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
2009/// Used by `command -p` to bypass the user's `$PATH` and search the
2010/// system default (`/bin:/usr/bin:...`).
2011pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
2012    // c:691
2013    // c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
2014    for ps in DEFAULT_PATH.split(':') {
2015        // c:695
2016        // c:697 — `if (*ps == '/')`.
2017        if !ps.starts_with('/') {
2018            continue;
2019        }
2020        // c:700-707 — PATH_MAX bounds check on `<dir>` segment.
2021        if ps.len() >= plen {
2022            // c:700 / c:704
2023            continue; // c:701 / c:705
2024        }
2025        // c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
2026        let full_len = ps.len() + 1 + cmd.len();
2027        if full_len >= plen {
2028            // c:709
2029            continue; // c:710
2030        }
2031        let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
2032                                             // c:712 — `if (iscom(pbuf)) return pbuf;`
2033        if iscom(&buf) {
2034            // c:712
2035            return Some(buf); // c:713
2036        }
2037    }
2038    None // c:716
2039}
2040
2041/// Port of `static int checkclobberparam(struct redir *f)` from
2042/// `Src/exec.c:2178`.
2043///
2044/// C body:
2045/// ```c
2046/// struct value vbuf; Value v;
2047/// char *s = f->varid; int fd;
2048/// if (!s) return 1;
2049/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
2050/// if (v->pm->node.flags & PM_READONLY) {
2051///     zwarn("can't allocate file descriptor to readonly parameter %s",
2052///           f->varid);
2053///     errno = 0;
2054///     return 0;
2055/// }
2056/// /* We can't clobber the value in the parameter if it's
2057///  * already an opened file descriptor */
2058/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
2059///     (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
2060///     fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
2061///     zwarn("can't clobber parameter %s containing file descriptor %d",
2062///          f->varid, fd);
2063///     errno = 0;
2064///     return 0;
2065/// }
2066/// return 1;
2067/// ```
2068///
2069/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
2070/// name) is writable and (under NOCLOBBER) doesn't currently hold an
2071/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
2072/// already emitted).
2073///
2074/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
2075/// NOCLOBBER is set and the param's value is the fd-number of an
2076/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
2077/// the existing fd doesn't get clobbered by the upcoming open(2).
2078pub fn checkclobberparam(f: &redir) -> i32 {
2079    // c:2178
2080    // c:2182 — `char *s = f->varid;`
2081    let s = match &f.varid {
2082        Some(v) => v.clone(),
2083        None => return 1, // c:2185-2186 — `if (!s) return 1;`
2084    };
2085    // c:2186 — `if (!(v = getvalue(&vbuf, &s, 0))) return 1;`
2086    let mut vbuf = crate::ported::zsh_h::value {
2087        pm: None,
2088        arr: Vec::new(),
2089        scanflags: 0,
2090        valflags: 0,
2091        start: 0,
2092        end: 0,
2093    };
2094    let mut cursor: &str = s.as_str();
2095    let v_opt = crate::ported::params::getvalue(Some(&mut vbuf), &mut cursor, 0);
2096    if v_opt.is_none() {
2097        return 1; // c:2187
2098    }
2099    // c:2188-2197 — readonly refusal via v->pm->node.flags.
2100    let readonly = vbuf
2101        .pm
2102        .as_ref()
2103        .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
2104        .unwrap_or(false);
2105    if readonly {
2106        // c:2191
2107        zwarn(&format!(
2108            // c:2192
2109            "can't allocate file descriptor to readonly parameter {}",
2110            s
2111        ));
2112        // c:2195 — `errno = 0;` not flagged as a system error.
2113        return 0; // c:2196
2114    }
2115    // c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
2116    // AND the param holds a valid fd that's already in our fdtable as
2117    // FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
2118    // open so we don't clobber it.
2119    if !isset(CLOBBER) {
2120        // c:2201 — `getstrvalue(v)` — read the param's string form.
2121        let val_str = crate::ported::params::getstrvalue(Some(&mut vbuf));
2122        if let Ok(fd) = val_str.trim().parse::<i32>() {
2123            // c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
2124            let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2125            if fd >= 0 && fd <= max_fd {
2126                let kind = fdtable_get(fd);
2127                if kind == FDT_EXTERNAL {
2128                    zwarn(&format!("{}: file descriptor {} already open", s, fd)); // c:2206-2210
2129                    return 0; // c:2211
2130                }
2131            }
2132        }
2133    }
2134    1 // c:2214
2135}
2136
2137/// Port of `static int clobber_open(struct redir *f)` from
2138/// `Src/exec.c:2221`.
2139///
2140/// C body:
2141/// ```c
2142/// struct stat buf;
2143/// int fd, oerrno;
2144/// char *ufname = unmeta(f->name);
2145/// /* If clobbering, just open. */
2146/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
2147///     return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
2148/// /* If not clobbering, attempt to create file exclusively. */
2149/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
2150///     return fd;
2151/// /* If that fails, we are still allowed to open non-regular files. */
2152/// oerrno = errno;
2153/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
2154///     if (!fstat(fd, &buf)) {
2155///         if (!S_ISREG(buf.st_mode)) return fd;
2156///         /* CLOBBER_EMPTY allows re-use of empty regular files. */
2157///         if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
2158///     }
2159///     close(fd);
2160/// }
2161/// errno = oerrno;
2162/// return -1;
2163/// ```
2164///
2165/// Open the redir target for write with the NOCLOBBER rules:
2166/// - CLOBBER set or `>|` form → just open with O_TRUNC
2167/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
2168///   files (FIFOs, devices, sockets) OR empty regular files under
2169///   CLOBBEREMPTY.
2170pub fn clobber_open(f: &redir) -> i32 {
2171    // c:2221
2172    let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
2173    let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
2174        Ok(c) => c,
2175        Err(_) => return -1,
2176    };
2177    // c:2228-2230 — clobber path: just open + truncate.
2178    if isset(CLOBBER) || IS_CLOBBER_REDIR(f.typ) {
2179        // c:2228
2180        // c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
2181        let fd = unsafe {
2182            libc::open(
2183                ufname.as_ptr(),
2184                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
2185                0o666 as libc::c_uint,
2186            )
2187        };
2188        return fd; // c:2230
2189    }
2190    // c:2233-2235 — try O_EXCL create first.
2191    let fd = unsafe {
2192        // c:2233
2193        libc::open(
2194            ufname.as_ptr(),
2195            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
2196            0o666 as libc::c_uint,
2197        )
2198    };
2199    if fd >= 0 {
2200        return fd; // c:2235
2201    }
2202    // c:2240 — `oerrno = errno;` — save for restoration on the recover path.
2203    let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2204    // c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
2205    // OR (CLOBBEREMPTY && size == 0).
2206    let fd = unsafe {
2207        // c:2241
2208        libc::open(
2209            ufname.as_ptr(),
2210            libc::O_WRONLY | libc::O_NOCTTY,
2211            0o666 as libc::c_uint,
2212        )
2213    };
2214    if fd != -1 {
2215        let mut buf: libc::stat = unsafe { std::mem::zeroed() };
2216        if unsafe { libc::fstat(fd, &mut buf) } == 0 {
2217            // c:2242
2218            // c:2243-2244 — non-regular file: accept.
2219            if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
2220                // c:2243
2221                return fd; // c:2244
2222            }
2223            // c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
2224            if isset(CLOBBEREMPTY) && buf.st_size == 0 {
2225                // c:2256
2226                return fd; // c:2257
2227            }
2228        }
2229        unsafe {
2230            libc::close(fd);
2231        } // c:2259
2232    }
2233    // c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
2234    // "file exists" not the noisier "couldn't reopen" trailing errno.
2235    // Per-platform errno setter: __error() on macOS, __errno_location()
2236    // on Linux. Without cfg gating the build breaks on Linux (CI).
2237    #[cfg(target_os = "macos")]
2238    unsafe {
2239        *libc::__error() = oerrno;
2240    }
2241    #[cfg(target_os = "linux")]
2242    unsafe {
2243        *libc::__errno_location() = oerrno;
2244    }
2245    -1 // c:2263
2246}
2247
2248/// Port of `char *findcmd(char *arg0, int docopy, int default_path)`
2249/// from `Src/exec.c:897`. Walk `$PATH` (or DEFAULT_PATH under
2250/// `default_path=1`) for `arg0`, returning the matching path on
2251/// success. `_docopy` is the C source's "duplicate the result"
2252/// flag — Rust ownership covers it without an explicit copy step.
2253/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
2254/// `command -p`).
2255pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
2256    // c:897
2257    // c:903-908 — if (default_path) → search_defpath; return.
2258    if default_path != 0 {
2259        return search_defpath(arg0, libc::PATH_MAX as usize);
2260    }
2261    // c:912-913 — strlen(arg0) > PATH_MAX → NULL.
2262    if arg0.len() > libc::PATH_MAX as usize {
2263        return None;
2264    }
2265    // c:Src/exec.c:914-920 — `/`-bearing arg path resolution.
2266    //   if ((s = strchr(arg0, '/'))) {
2267    //       RET_IF_COM(arg0);   // ← unconditional accept on iscom hit
2268    //       if (arg0 == s || unset(PATHDIRS) ||
2269    //           !strncmp(arg0, "./", 2) ||
2270    //           !strncmp(arg0, "../", 3))
2271    //           return NULL;
2272    //   }
2273    // The Rust port had the iscom check gated on `starts_with('/')`,
2274    // so `type ./target/debug/zshrs` returned None even when the
2275    // file was executable. Bug #496 family.
2276    if arg0.contains('/') {
2277        if iscom(arg0) {
2278            return Some(arg0.to_string()); // c:915 RET_IF_COM
2279        }
2280        // c:916-919 — absolute OR PATHDIRS-off OR `./` / `../` →
2281        // give up here (no $PATH walk for these). Relative without
2282        // those prefixes falls through to the $PATH scan below for
2283        // the PATHDIRS=set case.
2284        if arg0.starts_with('/')
2285            || !isset(PATHDIRS)
2286            || arg0.starts_with("./")
2287            || arg0.starts_with("../")
2288        {
2289            return None;
2290        }
2291        // else fall through to PATH walk.
2292    }
2293    // c:943-951 — walk `path[]` (the shell `$path` array). Read $PATH
2294    // from paramtab so shell-private edits via `path=(...)` take
2295    // effect (not OS env only).
2296    let path = getsparam("PATH")?;
2297    for dir in path.split(':') {
2298        if dir.is_empty() {
2299            continue;
2300        }
2301        let candidate = format!("{}/{}", dir, arg0);
2302        if iscom(&candidate) {
2303            return Some(candidate);
2304        }
2305    }
2306    None // c:952
2307}
2308
2309/// Port of `static void addfd(int forked, int *save, struct multio **mfds,
2310///                             int fd1, int fd2, int rflag, char *varid)`
2311/// from `Src/exec.c:2397`.
2312///
2313/// C body (~100 lines, three branches):
2314/// ```c
2315/// if (varid) {
2316///     /* {varid}>file form — move fd above 10 and bind $varid to it */
2317/// } else if (!mfds[fd1] || unset(MULTIOS)) {
2318///     /* new multio OR MULTIOS off — first redir on this fd */
2319/// } else {
2320///     /* additional redir on a fd that's already a multio (split or extend) */
2321/// }
2322/// ```
2323///
2324/// Register `fd2` (already-open) as a redirection target for `fd1`.
2325/// Three branches: `varid` writes the moved fd to `$varid` and bumps
2326/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
2327/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
2328/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
2329/// via `mpipe`, or appends another fd to an already-split stream
2330/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
2331///
2332/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
2333/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
2334/// longer a hard cap (still 8 for the initial allocation, grown
2335/// on demand thereafter).
2336///
2337/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
2338/// stores the int value but doesn't expose a bitwise-OR setter; we
2339/// re-read + OR + re-store as two atomic-feeling steps.
2340pub fn addfd(
2341    forked: i32,
2342    save: &mut [i32; 10],
2343    mfds: &mut [Option<Box<multio>>; 10],
2344    fd1: i32,
2345    fd2: i32,
2346    rflag: i32,
2347    varid: Option<&str>,
2348) {
2349    // c:2397
2350    let mut pipes: [i32; 2] = [-1; 2]; // c:2400
2351
2352    // c:2402-2417 — `if (varid)` branch — {varid}>file shape.
2353    if let Some(vid) = varid {
2354        // c:2402
2355        let fd_moved = movefd(fd2); // c:2404
2356        if fd_moved == -1 {
2357            // c:2405
2358            zerr(&format!(
2359                // c:2406
2360                "cannot move fd {}: {}",
2361                fd2,
2362                std::io::Error::last_os_error()
2363            ));
2364            return; // c:2407
2365        }
2366        // c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
2367        fdtable_set(fd_moved, FDT_EXTERNAL);
2368        // c:2410 — `setiparam(varid, (zlong)fd1);`
2369        setiparam(vid, fd_moved as i64);
2370        // c:2415-2416 — `if (errflag) zclose(fd1);`
2371        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
2372            // c:2415
2373            let _ = zclose(fd_moved); // c:2416
2374        }
2375        return;
2376    }
2377    // c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
2378    let fd1u = fd1 as usize;
2379    if fd1u >= mfds.len() {
2380        return;
2381    }
2382    if mfds[fd1u].is_none() || unset(MULTIOS) {
2383        // c:2418
2384        if mfds[fd1u].is_none() {
2385            // c:2419 — `starting a new multio`
2386            // c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
2387            mfds[fd1u] = Some(Box::new(multio {
2388                ct: 0,
2389                rflag: 0,
2390                pipe: -1,
2391                // c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
2392                // grow on demand via push() below. Pre-fill MULTIOUNIT
2393                // slots with -1 so existing indexed writes (fds[0], fds[1])
2394                // still work without explicit resize().
2395                fds: vec![-1; MULTIOUNIT],
2396            }));
2397            // c:2421 — `if (!forked && save[fd1] == -2)`
2398            if forked == 0 && save[fd1u] == -2 {
2399                if fd1 == fd2 {
2400                    // c:2422
2401                    save[fd1u] = -1; // c:2423
2402                } else {
2403                    // c:2424
2404                    let fd_n = movefd(fd1); // c:2425
2405                    if fd_n < 0 {
2406                        // c:2430
2407                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2408                        if e != libc::EBADF {
2409                            // c:2431
2410                            zerr(&format!(
2411                                // c:2432
2412                                "cannot duplicate fd {}: {}",
2413                                fd1,
2414                                std::io::Error::from_raw_os_error(e)
2415                            ));
2416                            mfds[fd1u] = None; // c:2433
2417                            closemnodes(mfds); // c:2434
2418                            return; // c:2435
2419                        }
2420                    } else {
2421                        // c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
2422                        crate::DPUTS!(
2423                            fdtable_get(fd_n) != FDT_INTERNAL,
2424                            "Saved file descriptor not marked as internal"
2425                        );
2426                        // c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
2427                        let cur = fdtable_get(fd_n);
2428                        fdtable_set(fd_n, cur | FDT_SAVED_MASK);
2429                    }
2430                    save[fd1u] = fd_n; // c:2442
2431                }
2432            }
2433        }
2434        // c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
2435        // handled above; this is the non-varid branch.)
2436        let _ = redup(fd2, fd1);
2437        // c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
2438        if let Some(mn) = mfds[fd1u].as_mut() {
2439            mn.ct = 1; // c:2448
2440            mn.fds[0] = fd1; // c:2449
2441            mn.rflag = rflag; // c:2450
2442        }
2443    } else {
2444        // c:2451 — extend existing multio.
2445        // c:2452-2456 — rflag mismatch check.
2446        let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
2447        if cur_rflag != rflag {
2448            // c:2452
2449            zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
2450            closemnodes(mfds); // c:2454
2451            return; // c:2455
2452        }
2453        let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
2454        if cur_ct == 1 {
2455            // c:2457 — split the stream.
2456            // c:2458 — `int fdN = movefd(fd1);`
2457            let fd_n = movefd(fd1);
2458            if fd_n < 0 {
2459                // c:2459
2460                zerr(&format!(
2461                    // c:2460
2462                    "multio failed for fd {}: {}",
2463                    fd1,
2464                    std::io::Error::last_os_error()
2465                ));
2466                closemnodes(mfds); // c:2461
2467                return; // c:2462
2468            }
2469            if let Some(mn) = mfds[fd1u].as_mut() {
2470                mn.fds[0] = fd_n; // c:2464
2471            }
2472            // c:2465 — `fdN = movefd(fd2);`
2473            let fd_n2 = movefd(fd2);
2474            if fd_n2 < 0 {
2475                // c:2466
2476                zerr(&format!(
2477                    // c:2467
2478                    "multio failed for fd {}: {}",
2479                    fd2,
2480                    std::io::Error::last_os_error()
2481                ));
2482                closemnodes(mfds); // c:2468
2483                return; // c:2469
2484            }
2485            if let Some(mn) = mfds[fd1u].as_mut() {
2486                mn.fds[1] = fd_n2; // c:2471
2487            }
2488            // c:2472 — `mpipe(pipes)`
2489            if mpipe(&mut pipes) < 0 {
2490                // c:2472
2491                zerr(&format!(
2492                    // c:2473
2493                    "multio failed for fd {}: {}",
2494                    fd2,
2495                    std::io::Error::last_os_error()
2496                ));
2497                closemnodes(mfds); // c:2474
2498                return; // c:2475
2499            }
2500            // c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
2501            if let Some(mn) = mfds[fd1u].as_mut() {
2502                mn.pipe = pipes[(1 - rflag) as usize];
2503            }
2504            // c:2478 — `redup(pipes[rflag], fd1);`
2505            let _ = redup(pipes[rflag as usize], fd1);
2506            // c:2479 — `mfds[fd1]->ct = 2;`
2507            if let Some(mn) = mfds[fd1u].as_mut() {
2508                mn.ct = 2;
2509            }
2510        } else {
2511            // c:2480 — extend already-split stream.
2512            // c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
2513            //                              sizeof + ct*sizeof(int));`
2514            // Rust's `Vec<i32>` grows on demand; ensure capacity for the
2515            // new slot before the indexed write below.
2516            if let Some(mn) = mfds[fd1u].as_mut() {
2517                while mn.fds.len() <= cur_ct as usize {
2518                    mn.fds.push(-1);
2519                }
2520            }
2521            // c:2487 — `if ((fdN = movefd(fd2)) < 0)`
2522            let fd_n = movefd(fd2);
2523            if fd_n < 0 {
2524                zerr(&format!(
2525                    // c:2488
2526                    "multio failed for fd {}: {}",
2527                    fd2,
2528                    std::io::Error::last_os_error()
2529                ));
2530                closemnodes(mfds); // c:2489
2531                return; // c:2490
2532            }
2533            // c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
2534            if let Some(mn) = mfds[fd1u].as_mut() {
2535                let slot = mn.ct as usize;
2536                if slot < mn.fds.len() {
2537                    mn.fds[slot] = fd_n;
2538                    mn.ct += 1;
2539                }
2540            }
2541        }
2542    }
2543}
2544
2545/// Port of `static void closemn(struct multio **mfds, int fd, int type)`
2546/// from `Src/exec.c:2273`.
2547///
2548/// C body (abridged — the meat is the fork-into-tee-or-cat child):
2549/// ```c
2550/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
2551///     struct multio *mn = mfds[fd];
2552///     char buf[TCBUFSIZE]; int len, i;
2553///     pid_t pid; struct timespec bgtime;
2554///     child_block();
2555///     if ((pid = zfork(&bgtime))) {
2556///         for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
2557///         zclose(mn->pipe);
2558///         if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
2559///         mn->ct = 1; mn->fds[0] = fd;
2560///         addproc(pid, NULL, 1, &bgtime, -1, -1);
2561///         child_unblock(); return;
2562///     }
2563///     /* pid == 0 (child) */
2564///     opts[INTERACTIVE] = 0;
2565///     dont_queue_signals();
2566///     child_unblock();
2567///     closeallelse(mn);
2568///     if (mn->rflag) {
2569///         /* tee process: read mn->pipe, write each mn->fds[i] */
2570///     } else {
2571///         /* cat process: read each mn->fds[i], write mn->pipe */
2572///     }
2573///     _exit(0);
2574/// } else if (fd >= 0 && type == REDIR_CLOSE)
2575///     mfds[fd] = NULL;
2576/// ```
2577///
2578/// Success-path close of a multio. For ct>=2 (multiple-output
2579/// redirection), forks a tee/cat child that proxies bytes between
2580/// the original fd and the per-output fds. Single-output multios
2581/// (ct=1) skip the fork entirely and just clear the slot.
2582///
2583/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
2584/// tee/cat child in the current job's auxprocs.
2585pub fn closemn(mfds: &mut [Option<Box<multio>>; 10], fd: i32, type_: i32) {
2586    // c:2273
2587    // c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
2588    let needs_tee = fd >= 0
2589        && (fd as usize) < mfds.len()
2590        && mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
2591    if needs_tee {
2592        // c:2275
2593        // Take the multio out of the slot so we can move pieces into
2594        // the child without aliasing the slot.
2595        let mn = mfds[fd as usize].take().unwrap();
2596        let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
2597                                   // c:2287 — `child_block();` block SIGCHLD before fork race.
2598        child_block();
2599        // c:2288 — `pid = zfork(&bgtime);`
2600        let mut bgtime = ZshTimespec {
2601            tv_sec: 0,
2602            tv_nsec: 0,
2603        };
2604        let pid = zfork(Some(&mut bgtime));
2605        if pid != 0 {
2606            // c:2288 parent branch
2607            // c:2289-2290 — close all per-output fds.
2608            for i in 0..mn.ct as usize {
2609                if i < mn.fds.len() {
2610                    let _ = zclose(mn.fds[i]); // c:2290
2611                }
2612            }
2613            let _ = zclose(mn.pipe); // c:2291
2614            if pid == -1 {
2615                // c:2292
2616                // c:2293 — `mfds[fd] = NULL;` already done via .take()
2617                child_unblock(); // c:2294
2618                return; // c:2295
2619            }
2620            // c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
2621            let mut mn_back = mn;
2622            mn_back.ct = 1; // c:2297
2623            mn_back.fds[0] = fd; // c:2298
2624            mfds[fd as usize] = Some(mn_back);
2625            // c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
2626            // the tee/cat child in the current job's auxprocs (aux=true).
2627            if let Some(jt) = JOBTAB.get() {
2628                let mut guard = jt.lock().unwrap();
2629                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
2630                if tj >= 0 {
2631                    if let Some(j) = guard.get_mut(tj as usize) {
2632                        crate::ported::jobs::addproc(
2633                            j,
2634                            pid,
2635                            "",
2636                            true,
2637                            Some(std::time::Instant::now()),
2638                            -1,
2639                            -1,
2640                        );
2641                    }
2642                }
2643            }
2644            let _ = bgtime;
2645            child_unblock(); // c:2300
2646            return; // c:2301
2647        }
2648        // c:2303 — child branch (pid == 0).
2649        opt_state_set("interactive", false); // c:2304
2650        dont_queue_signals(); // c:2305
2651        child_unblock(); // c:2306
2652        closeallelse(&mn); // c:2307
2653                           // c:2308-2333 — tee or cat loop.
2654        if mn.rflag != 0 {
2655            // c:2308 — `mn->rflag` set → tee process
2656            // c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
2657            loop {
2658                let len = unsafe {
2659                    libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2660                };
2661                if len == 0 {
2662                    break;
2663                }
2664                if len < 0 {
2665                    // c:2311
2666                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2667                    if e == libc::EINTR {
2668                        // c:2312
2669                        continue;
2670                    } else {
2671                        break; // c:2315
2672                    }
2673                }
2674                // c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
2675                for i in 0..mn.ct as usize {
2676                    if i >= mn.fds.len() {
2677                        break;
2678                    }
2679                    if write_loop(mn.fds[i], &buf[..len as usize]).is_err() {
2680                        break; // c:2319
2681                    }
2682                }
2683            }
2684        } else {
2685            // c:2321 — cat process
2686            for i in 0..mn.ct as usize {
2687                if i >= mn.fds.len() {
2688                    break;
2689                }
2690                // c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
2691                loop {
2692                    let len = unsafe {
2693                        libc::read(mn.fds[i], buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2694                    };
2695                    if len == 0 {
2696                        break;
2697                    }
2698                    if len < 0 {
2699                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2700                        // c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
2701                        if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
2702                            continue;
2703                        } else {
2704                            break; // c:2329
2705                        }
2706                    }
2707                    // c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
2708                    if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
2709                        break; // c:2332
2710                    }
2711                }
2712            }
2713        }
2714        // c:2335 — `_exit(0);`
2715        unsafe {
2716            libc::_exit(0);
2717        }
2718    } else if fd >= 0 && type_ == REDIR_CLOSE {
2719        // c:2336
2720        // c:2337 — `mfds[fd] = NULL;`
2721        if (fd as usize) < mfds.len() {
2722            mfds[fd as usize] = None;
2723        }
2724    }
2725}
2726
2727/// Port of `static void closemnodes(struct multio **mfds)` from
2728/// `Src/exec.c:2344`.
2729///
2730/// C body:
2731/// ```c
2732/// int i, j;
2733/// for (i = 0; i < 10; i++)
2734///     if (mfds[i]) {
2735///         for (j = 0; j < mfds[i]->ct; j++)
2736///             zclose(mfds[i]->fds[j]);
2737///         mfds[i] = NULL;
2738///     }
2739/// ```
2740///
2741/// Failure-path cleanup: close every fd stashed in any of the 10
2742/// multio slots and null the slot. Called from `execcmd_exec` when
2743/// a redirect setup fails partway through and we need to roll back.
2744pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
2745    // c:2344
2746    for i in 0..10 {
2747        // c:2348
2748        if let Some(mn) = mfds[i].take() {
2749            // c:2349
2750            for j in 0..mn.ct as usize {
2751                // c:2350
2752                if j < mn.fds.len() {
2753                    let _ = zclose(mn.fds[j]); // c:2351
2754                }
2755            }
2756            // c:2352 — `mfds[i] = NULL;` — handled by .take() above.
2757        }
2758    }
2759}
2760
2761/// Port of `static void closeallelse(struct multio *mn)` from
2762/// `Src/exec.c:2358`.
2763///
2764/// C body:
2765/// ```c
2766/// int i, j;
2767/// long openmax;
2768/// openmax = fdtable_size;
2769/// for (i = 0; i < openmax; i++)
2770///     if (mn->pipe != i) {
2771///         for (j = 0; j < mn->ct; j++)
2772///             if (mn->fds[j] == i) break;
2773///         if (j == mn->ct)
2774///             zclose(i);
2775///     }
2776/// ```
2777///
2778/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
2779/// stashed in `mn->fds`. Called inside the multio tee/cat child
2780/// process to release every fd the parent had open — only the pipe
2781/// + per-output fds stay alive for the read/write loop.
2782pub fn closeallelse(mn: &multio) {
2783    // c:2358
2784    // c:2363 — `openmax = fdtable_size;`. zshrs models fdtable as a
2785    // Vec; use MAX_ZSH_FD as the upper bound (fdtable_size grows past
2786    // max_zsh_fd in C but every slot past it is FDT_UNUSED anyway).
2787    let openmax = MAX_ZSH_FD.load(Ordering::Relaxed) + 1; // c:2363
2788    for i in 0..openmax {
2789        // c:2365
2790        if mn.pipe == i {
2791            // c:2366
2792            continue;
2793        }
2794        // c:2367-2369 — scan mn->fds[] for i; skip-close if found.
2795        let mut found = false;
2796        for j in 0..mn.ct as usize {
2797            // c:2367
2798            if j < mn.fds.len() && mn.fds[j] == i {
2799                // c:2368
2800                found = true;
2801                break; // c:2369
2802            }
2803        }
2804        // c:2370-2371 — `if (j == mn->ct) zclose(i);`
2805        if !found {
2806            let _ = zclose(i); // c:2371
2807        }
2808    }
2809}
2810
2811/// Port of `static void fixfds(int *save)` from `Src/exec.c:4523`.
2812///
2813/// C body:
2814/// ```c
2815/// int old_errno = errno;
2816/// int i;
2817/// for (i = 0; i != 10; i++)
2818///     if (save[i] != -2)
2819///         redup(save[i], i);
2820/// errno = old_errno;
2821/// ```
2822///
2823/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
2824/// means "no save was made for this fd"; any other value is the
2825/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
2826/// caller's errno across the loop so a downstream caller diagnoses
2827/// the original failure, not a noisy dup2 errno.
2828pub fn fixfds(save: &[i32; 10]) {
2829    // c:4523
2830    let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
2831    for i in 0..10i32 {
2832        // c:4528 — `for (i = 0; i != 10; i++)`
2833        if save[i as usize] != -2 {
2834            // c:4529
2835            redup(save[i as usize], i); // c:4530
2836        }
2837    }
2838    // c:4531 — `errno = old_errno;`
2839    #[cfg(target_os = "macos")]
2840    unsafe {
2841        *libc::__error() = old_errno;
2842    }
2843    #[cfg(target_os = "linux")]
2844    unsafe {
2845        *libc::__errno_location() = old_errno;
2846    }
2847}
2848
2849/// Port of `mod_export void closem(int how, int all)` from `Src/exec.c:4546`.
2850///
2851/// C body:
2852/// ```c
2853/// int i;
2854/// for (i = 10; i <= max_zsh_fd; i++)
2855///     if (fdtable[i] != FDT_UNUSED &&
2856///         (all || (fdtable[i] != FDT_PROC_SUBST &&
2857///                  fdtable[i] != FDT_EXTERNAL)) &&
2858///         (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
2859///         if (i == SHTTY) SHTTY = -1;
2860///         zclose(i);
2861///     }
2862/// ```
2863///
2864/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
2865/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
2866/// type filter); otherwise only fds whose low-nibble type equals
2867/// `how` are closed. `all == 0` preserves user-visible fds
2868/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
2869/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
2870/// reference if we just closed the controlling tty.
2871pub fn closem(how: i32, all: i32) {
2872    // c:4546
2873    let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
2874    for i in 10i32..=max {
2875        // c:4550
2876        let kind = fdtable_get(i); // c:4551 fdtable[i]
2877        if kind == FDT_UNUSED {
2878            // c:4551
2879            continue;
2880        }
2881        // c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
2882        if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
2883            continue;
2884        }
2885        // c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
2886        if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
2887            continue;
2888        }
2889        // c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
2890        if i == SHTTY.load(Ordering::Relaxed) {
2891            // c:4560
2892            SHTTY.store(-1, Ordering::Relaxed); // c:4561
2893        }
2894        // c:4562 — `zclose(i);`
2895        let _ = zclose(i);
2896    }
2897}
2898
2899/// Port of `Cmdnam hashcmd(char *arg0, char **pp)` from
2900/// `Src/exec.c:1010`.
2901///
2902/// C body:
2903/// ```c
2904/// Cmdnam cn;
2905/// char *s, buf[PATH_MAX+1];
2906/// char **pq;
2907/// if (*arg0 == '/') return NULL;
2908/// for (; *pp; pp++)
2909///     if (**pp == '/') {
2910///         s = buf;
2911///         struncpy(&s, *pp, PATH_MAX);
2912///         *s++ = '/';
2913///         if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
2914///         strcpy(s, arg0);
2915///         if (iscom(buf)) break;
2916///     }
2917/// if (!*pp) return NULL;
2918/// cn = (Cmdnam) zshcalloc(sizeof *cn);
2919/// cn->node.flags = 0;
2920/// cn->u.name = pp;
2921/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
2922/// if (isset(HASHDIRS)) {
2923///     for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
2924///     pathchecked = pp + 1;
2925/// }
2926/// return cn;
2927/// ```
2928///
2929/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
2930/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
2931/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
2932/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
2933/// so subsequent commands hit the cache.
2934///
2935/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
2936/// `None` if `arg0` is absolute or no PATH entry contains it.
2937pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
2938    // c:1010
2939    // c:1016 — `if (*arg0 == '/') return NULL;`
2940    if arg0.starts_with('/') {
2941        return None; // c:1017
2942    }
2943    // c:1018-1028 — walk pp[] for first matching absolute entry.
2944    let mut found_idx: Option<usize> = None;
2945    for (i, dir) in pp.iter().enumerate() {
2946        // c:1018
2947        if !dir.starts_with('/') {
2948            // c:1019
2949            continue;
2950        }
2951        // c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
2952        if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
2953            // c:1023
2954            continue; // c:1024
2955        }
2956        let buf = format!("{}/{}", dir, arg0); // c:1025
2957        if iscom(&buf) {
2958            // c:1026
2959            found_idx = Some(i);
2960            break; // c:1027
2961        }
2962    }
2963    // c:1030-1031 — `if (!*pp) return NULL;`
2964    let pp_idx = match found_idx {
2965        Some(i) => i,
2966        None => return None, // c:1031
2967    };
2968    // c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
2969    let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
2970    let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
2971                                                // c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
2972    if let Ok(mut tab) = cmdnamtab_lock().write() {
2973        tab.add(cn.clone());
2974    }
2975    // c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
2976    // including the matching one, then bump pathchecked past it.
2977    if isset(HASHDIRS) {
2978        // c:1038
2979        let start = pathchecked.load(Ordering::Relaxed); // c:1039
2980        for pq in start..=pp_idx {
2981            // c:1039
2982            if pq < pp.len() {
2983                hashdir(&pp[pq], pq); // c:1040
2984            }
2985        }
2986        pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
2987    }
2988    Some(cn) // c:1044
2989}
2990
2991/// Port of `static pid_t zfork(struct timespec *ts)` from
2992/// `Src/exec.c:349`.
2993///
2994/// C body:
2995/// ```c
2996/// pid_t pid;
2997/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
2998///     zerr("job table full");
2999///     return -1;
3000/// }
3001/// if (ts) zgettime_monotonic_if_available(ts);
3002/// queue_signals();
3003/// pid = fork();
3004/// unqueue_signals();
3005/// if (pid == -1) {
3006///     zerr("fork failed: %e", errno);
3007///     return -1;
3008/// }
3009/// #ifdef HAVE_GETRLIMIT
3010/// if (!pid) setlimits(NULL);
3011/// #endif
3012/// return pid;
3013/// ```
3014///
3015/// fork(2) wrapper with jobtab capacity check + child rlimit
3016/// re-application. Used by every subshell-spawning path: pipelines,
3017/// process substitution, async commands, command substitution.
3018pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
3019    // c:349
3020    let pid: libc::pid_t;
3021
3022    // c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
3023    let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
3024    let thisjob = *thisjob_lock.lock().unwrap();
3025    if thisjob != -1 {
3026        // c:356
3027        let needed = (thisjob + 1) as usize;
3028        let needs_expand = JOBTAB
3029            .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3030            .lock()
3031            .map(|t| needed >= t.len().saturating_sub(1))
3032            .unwrap_or(false);
3033        if needs_expand {
3034            let mut tab = JOBTAB.get().unwrap().lock().unwrap();
3035            if !expandjobtab(&mut tab, needed) {
3036                // c:357
3037                zerr("job table full"); // c:357
3038                return -1; // c:358
3039            }
3040        }
3041    }
3042    // c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
3043    if let Some(ts) = ts {
3044        zgettime_monotonic_if_available(ts);
3045    }
3046    // c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
3047    queue_signals(); // c:368
3048    pid = unsafe { libc::fork() }; // c:369
3049    unqueue_signals(); // c:370
3050                       // c:371-374 — fork failure.
3051    if pid == -1 {
3052        // c:371
3053        zerr(&format!(
3054            // c:372
3055            "fork failed: {}",
3056            std::io::Error::last_os_error()
3057        ));
3058        return -1; // c:373
3059    }
3060    // c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
3061    #[cfg(unix)]
3062    if pid == 0 {
3063        // c:376
3064        let _ = setlimits(""); // c:378
3065    }
3066    pid // c:380
3067}
3068
3069/// Port of `void loadautofnsetfile(Shfunc shf, char *fdir)` from
3070/// `Src/exec.c:5657`.
3071///
3072/// C body:
3073/// ```c
3074/// if (!(shf->node.flags & PM_LOADDIR) ||
3075///     strcmp(shf->filename, fdir) != 0) {
3076///     dircache_set(&shf->filename, NULL);
3077///     if (fdir) {
3078///         shf->node.flags |= PM_LOADDIR;
3079///         dircache_set(&shf->filename, fdir);
3080///     } else {
3081///         shf->node.flags &= ~PM_LOADDIR;
3082///         shf->filename = ztrdup(shf->node.nam);
3083///     }
3084/// }
3085/// ```
3086///
3087/// Update `shf->filename` to the autoload directory `fdir`. Routes
3088/// through the refcounted `dircache_set` so identical directory
3089/// strings are shared across shfunc table entries.
3090pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
3091    // c:5657
3092    // c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
3093    let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
3094    let same = match (&shf.filename, fdir) {
3095        (Some(a), Some(b)) => a == b,
3096        _ => false,
3097    };
3098    if !loaddir || !same {
3099        // c:5664
3100        // c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
3101        dircache_set(&mut shf.filename, None);
3102        if let Some(fdir) = fdir {
3103            // c:5668
3104            shf.node.flags |= PM_LOADDIR as i32; // c:5670
3105            dircache_set(&mut shf.filename, Some(fdir)); // c:5671
3106        } else {
3107            // c:5672
3108            shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
3109            shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
3110        }
3111    }
3112}
3113
3114/// Port of `int commandnotfound(char *arg0, LinkList args)` from
3115/// `Src/exec.c:669`.
3116///
3117/// C body:
3118/// ```c
3119/// Shfunc shf = (Shfunc)
3120///     shfunctab->getnode(shfunctab, "command_not_found_handler");
3121/// if (!shf) {
3122///     lastval = 127;
3123///     return 1;
3124/// }
3125/// pushnode(args, arg0);
3126/// lastval = doshfunc(shf, args, 1);
3127/// return 0;
3128/// ```
3129///
3130/// Look up the user-defined `command_not_found_handler` shfunc and
3131/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
3132/// 1 if no handler (so caller emits the standard "command not found"
3133/// error). Sets `$?` to 127 in the no-handler path.
3134pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
3135    // c:669
3136    // c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
3137    let has_handler = shfunctab_lock()
3138        .read()
3139        .map(|t| t.get("command_not_found_handler").is_some())
3140        .unwrap_or(false);
3141    if !has_handler {
3142        // c:674
3143        LASTVAL.store(127, Ordering::Relaxed); // c:675
3144        return 1; // c:676
3145    }
3146    // c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
3147    // is the first positional arg per C convention).
3148    args.insert(0, arg0.to_string());
3149    args.insert(0, "command_not_found_handler".to_string());
3150    // c:680 — `lastval = doshfunc(shf, args, 1);`. Direct doshfunc
3151    // call mirrors C — body_runner routes through the host body-only
3152    // entry so the function body runs once inside doshfunc's scope.
3153    let shf_clone: Option<shfunc> = shfunctab_lock()
3154        .read()
3155        .ok()
3156        .and_then(|t| t.get("command_not_found_handler").cloned());
3157    if let Some(mut shf) = shf_clone {
3158        let body_args = args.clone();
3159        let body_runner = move || -> i32 {
3160            crate::ported::exec_hooks::run_function_body(
3161                "command_not_found_handler",
3162                &body_args[1..],
3163            )
3164            .unwrap_or(0)
3165        };
3166        let lv = doshfunc(&mut shf, args.clone(), true, body_runner);
3167        LASTVAL.store(lv, Ordering::Relaxed);
3168    }
3169    0 // c:681
3170}
3171
3172/// Port of `char *namedpipe(void)` from `Src/exec.c:5001`.
3173///
3174/// C body (#ifdef HAVE_FIFOS branch):
3175/// ```c
3176/// char *tnam = gettempname(NULL, 1);
3177/// if (!tnam) {
3178///     zerr("failed to create named pipe: %e", errno);
3179///     return NULL;
3180/// }
3181/// if (mkfifo(tnam, 0600) < 0) {
3182///     zerr("failed to create named pipe: %s, %e", tnam, errno);
3183///     return NULL;
3184/// }
3185/// return tnam;
3186/// ```
3187///
3188/// Create a FIFO with a unique name for process substitution. Used by
3189/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
3190pub fn namedpipe() -> Option<String> {
3191    // c:5001
3192    let tnam = gettempname(None, true); // c:5003
3193    let tnam = match tnam {
3194        Some(t) => t,
3195        None => {
3196            // c:5005
3197            zerr(&format!(
3198                // c:5006
3199                "failed to create named pipe: {}",
3200                std::io::Error::last_os_error()
3201            ));
3202            return None; // c:5007
3203        }
3204    };
3205    // c:5010 — `mkfifo(tnam, 0600)`.
3206    let cstr = match std::ffi::CString::new(tnam.as_str()) {
3207        Ok(c) => c,
3208        Err(_) => return None,
3209    };
3210    if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
3211        // c:5010
3212        zerr(&format!(
3213            // c:5014
3214            "failed to create named pipe: {}, {}",
3215            tnam,
3216            std::io::Error::last_os_error()
3217        ));
3218        return None; // c:5015
3219    }
3220    Some(tnam) // c:5017
3221}
3222
3223/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
3224///
3225/// C body:
3226/// ```c
3227/// char *str;
3228/// Eprog prog;
3229/// for (str = cmd + 2; *str && *str != Outpar; str++);
3230/// if (!*str || cmd[1] != Inpar) {
3231///     char *errstr = dupstrpfx(cmd, 2);
3232///     untokenize(errstr);
3233///     zerr("unterminated `%s...)'", errstr);
3234///     return NULL;
3235/// }
3236/// *str = '\0';
3237/// if (eptr) *eptr = str+1;
3238/// if (!(prog = parse_string(cmd + 2, 0))) {
3239///     zerr("parse error in process substitution");
3240///     return NULL;
3241/// }
3242/// return prog;
3243/// ```
3244///
3245/// Port of `static LinkList readoutput(int in, int qt, int *readerror)`
3246/// from `Src/exec.c:4805`. Drain a command-substitution pipe fd and
3247/// return the captured output split per `qt`.
3248///
3249/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
3250/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
3251/// per c:4861).
3252/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
3253/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
3254///
3255/// `readerror` is set to the errno on read failure, 0 on clean EOF.
3256pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
3257    // c:4805
3258    let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
3259    let mut readret: isize = 0; // c:4818 readret tracks last read return
3260                                // c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
3261                                // dance keeps SIGCHLD live so the foreground process can be reaped
3262                                // while we drain. zshrs's in-process command-sub runs without the
3263                                // queue (no fork), but the C call surface is preserved for parity.
3264    dont_queue_signals(); // c:4824
3265    child_unblock(); // c:4825
3266    let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
3267    loop {
3268        // c:4826
3269        // c:4828 — `readret = read(in, inbuf, 64);`
3270        let r = unsafe { libc::read(in_fd, inbuf.as_mut_ptr() as *mut libc::c_void, inbuf.len()) };
3271        readret = r as isize;
3272        if readret <= 0 {
3273            // c:4829
3274            if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
3275                // c:4830 — `if (readret < 0 && errno == EINTR) continue;`
3276                continue;
3277            }
3278            break; // c:4832
3279        }
3280        // c:4835 — `for (bufptr = inbuf; bufptr < inbuf + readret; bufptr++)`
3281        for i in 0..(readret as usize) {
3282            let c = inbuf[i];
3283            if crate::ported::ztype_h::imeta(c) {
3284                // c:4837 — `if (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; }`
3285                buf.push(Meta as u8); // c:4838
3286                buf.push(c ^ 32); // c:4839 (Meta-encoded payload)
3287            } else {
3288                buf.push(c); // c:4848 *ptr++ = c
3289            }
3290        }
3291    }
3292    child_block(); // c:4854
3293                   // c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
3294    *readerror = if readret < 0 {
3295        std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
3296    } else {
3297        0
3298    };
3299    // c:4857 — `close(in);`
3300    unsafe {
3301        libc::close(in_fd);
3302    }
3303    // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
3304    while buf.last() == Some(&b'\n') {
3305        buf.pop();
3306    }
3307    // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
3308    let s = String::from_utf8_lossy(&buf).into_owned();
3309    if qt != 0 {
3310        // c:4861
3311        if buf.is_empty() {
3312            return vec![String::from(Nularg)]; // c:4862
3313        }
3314        return vec![s]; // c:4864
3315    }
3316    // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
3317    let mut words = crate::ported::utils::spacesplit(&s, false); // c:4867
3318    if isset(crate::ported::zsh_h::GLOBSUBST) {
3319        // c:4870
3320        for w in words.iter_mut() {
3321            crate::ported::glob::shtokenize(w); // c:4870
3322        }
3323    }
3324    words
3325}
3326
3327/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
3328/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
3329/// remainder is the command up to the matching `Outpar`. Returns the
3330/// parsed Eprog (and writes the post-`)` cursor through `eptr`).
3331pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
3332    // c:4878
3333    let bytes = cmd.as_bytes();
3334    // c:4883 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
3335    if bytes.len() < 2 {
3336        return None;
3337    }
3338    let mut str_idx: usize = 2;
3339    while str_idx < bytes.len() && (bytes[str_idx] as char) != Outpar {
3340        str_idx += 1;
3341    }
3342    // c:4884 — `if (!*str || cmd[1] != Inpar)`.
3343    if str_idx >= bytes.len() || (bytes[1] as char) != Inpar {
3344        // c:4884
3345        let errstr = if bytes.len() >= 2 {
3346            untokenize(&cmd[..2]) // c:4891-4892
3347        } else {
3348            String::new()
3349        };
3350        zerr(&format!("unterminated `{}...)'", errstr)); // c:4893
3351        return None; // c:4894
3352    }
3353    // c:4896 — `*str = '\0';` — cmd[str_idx] becomes the terminator.
3354    // c:4897-4898 — `if (eptr) *eptr = str + 1;`
3355    if let Some(p) = eptr {
3356        *p = str_idx + 1;
3357    }
3358    // c:4899 — `parse_string(cmd + 2, 0)`.
3359    let body = &cmd[2..str_idx];
3360    let prog = parse_string(body, 0);
3361    if prog.is_none() {
3362        // c:4899
3363        zerr("parse error in process substitution"); // c:4900
3364        return None; // c:4901
3365    }
3366    prog // c:4903
3367}
3368
3369/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
3370/// front of a script when probing for a `#!` shebang line.
3371pub const POUNDBANGLIMIT: usize = 128;
3372
3373/// Port of `static char **makecline(LinkList list)` from `Src/exec.c:2046`.
3374///
3375/// Builds the argv array from a command's args list. The C version
3376/// allocates with a 4-slot prepad (2 reserved at the front for the
3377/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
3378/// doesn't need this since we rebuild the Vec on shebang re-exec
3379/// (see zexecve WARNING e).
3380///
3381/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
3382/// (stderr), preceded by the PS4 prefix when first command of the line.
3383pub fn makecline(list: &[String]) -> Vec<String> {
3384    // c:2046
3385    if isset(XTRACE) {
3386        // c:2055
3387        if doneps4.load(Ordering::Relaxed) == 0 {
3388            // c:2056
3389            printprompt4(); // c:2057
3390        }
3391        let mut first = true;
3392        let mut err = std::io::stderr().lock();
3393        use std::io::Write;
3394        for s in list.iter() {
3395            // c:2059
3396            if !first {
3397                let _ = err.write_all(b" "); // c:2063
3398            }
3399            first = false;
3400            let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
3401        }
3402        let _ = err.write_all(b"\n"); // c:2065
3403        let _ = err.flush(); // c:2066
3404    }
3405    list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
3406}
3407
3408/// Port of `static void execute(LinkList args, int flags, int defpath)`
3409/// from `Src/exec.c:723`. The canonical "child runs the simple
3410/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
3411/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
3412/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
3413/// commandnotfound-handler fallback and the final exit-code escape
3414/// (127 not-found / 126 noperm).
3415///
3416/// =================== WARNING — DIVERGENCE ====================
3417/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
3418///     fast-path wired via cmdnamtab_lock(); jumps direct to
3419///     `cn.cmd` absolute path before the $PATH scan. Unhashed
3420///     cursor-walk (c:830-846) still falls to the full $PATH scan;
3421///     observable behavior matches C when the hash hit is HASHED.
3422/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
3423///     not-yet-ported `doshfunc` for the `command_not_found_handler`
3424///     shell function. Already routes through executor dispatch
3425///     (see exec.rs:2783).
3426/// (c) `_realexit()` (c:810, 874) — bare `std::process::exit`.
3427/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
3428///     FD_CLOEXEC platform default (macOS, Linux).
3429/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
3430///     `defpath` (`command -p`) walks DEFAULT_PATH via
3431///     search_defpath (already ported).
3432/// =============================================================
3433pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
3434    // c:723
3435    let mut eno: i32 = 0;
3436    let mut ee: i32; // c:729
3437    let mut arg0 = if args.is_empty() {
3438        return;
3439    } else {
3440        args[0].clone()
3441    }; // c:731
3442       // c:733-748 — STTY pre-exec handling.
3443    {
3444        let mut stty = STTYval.lock().unwrap();
3445        if let Some(s) = stty.take() {
3446            // c:738 — STTYval = 0 to break recursion.
3447            if !s.is_empty()
3448                && unsafe { libc::isatty(0) } != 0
3449                && unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
3450            {
3451                drop(stty);
3452                let cmd = format!("stty {}", s); // c:739
3453                execstring(&cmd, 1, 0, "stty"); // c:743
3454            }
3455        }
3456    }
3457    // c:752-763 — ARGV0 override.
3458    if let Some(z) = zgetenv("ARGV0") {
3459        args[0] = z.clone(); // c:753
3460        unsafe {
3461            let key = std::ffi::CString::new("ARGV0").unwrap();
3462            libc::unsetenv(key.as_ptr()); // c:760
3463        }
3464        arg0 = args[0].clone();
3465    } else if (flags & BINF_DASH) != 0 {
3466        // c:764 — `BINF_DASH` prepends `-`.
3467        args[0] = format!("-{}", arg0); // c:767-768
3468        arg0 = args[0].clone();
3469    }
3470    let argv = makecline(args); // c:771
3471    let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
3472        Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
3473    } else {
3474        None
3475    };
3476    let newenvp = newenvp_owned.as_deref();
3477    closem(FDT_XTRACE, 0); // c:779
3478                           // c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
3479    child_unblock(); // c:786
3480    if arg0.len() >= libc::PATH_MAX as usize {
3481        // c:787
3482        zerr(&format!("command too long: {}", arg0)); // c:788
3483        unsafe {
3484            libc::_exit(1);
3485        } // c:789
3486    }
3487    // c:791-801 — slash in arg0 → direct exec.
3488    if let Some(slash_pos) = arg0.find('/') {
3489        let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
3490        let is_dot = arg0.starts_with('.')
3491            && (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
3492        if slash_pos == 0 || unset(PATHDIRS) || is_dot {
3493            // c:794
3494            zerr(&format!(
3495                "{}: {}",
3496                std::io::Error::from_raw_os_error(lerrno),
3497                arg0
3498            )); // c:797
3499            let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
3500                126
3501            } else {
3502                127
3503            };
3504            unsafe {
3505                libc::_exit(code);
3506            } // c:798
3507        }
3508    }
3509    if defpath != 0 {
3510        // c:804 — `command -p` default-path search.
3511        let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
3512            Some(p) => p, // c:808
3513            None => {
3514                if commandnotfound(&arg0, args) == 0 {
3515                    // c:809
3516                    unsafe {
3517                        libc::_exit(LASTVAL.load(Ordering::Relaxed));
3518                    }
3519                }
3520                zerr(&format!("command not found: {}", arg0)); // c:811
3521                unsafe {
3522                    libc::_exit(127);
3523                } // c:812
3524            }
3525        };
3526        ee = zexecve(&pbuf, &argv, newenvp); // c:815
3527        let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3528        if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3529            // c:819
3530            eno = ee;
3531        }
3532    } else {
3533        // c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
3534        // jump straight to the absolute path stored in `cn.cmd`,
3535        // skipping the full $PATH scan (one exec attempt vs N).
3536        // c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
3537        let hashed_path: Option<String> = {
3538            let tab = cmdnamtab_lock().read().ok();
3539            tab.and_then(|t| {
3540                t.get(&arg0).and_then(|cn| {
3541                    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
3542                        // c:827-828 — `strcpy(nn, cn->u.cmd);`
3543                        cn.cmd.clone()
3544                    } else {
3545                        None
3546                    }
3547                })
3548            })
3549        };
3550        if let Some(nn) = hashed_path {
3551            // c:848 — `ee = zexecve(nn, argv, newenvp);`
3552            ee = zexecve(&nn, &argv, newenvp);
3553            let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3554            if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3555                eno = ee;
3556            }
3557            // If the hashed entry's exec failed without a "good" error,
3558            // we still need the $PATH fallback — fall through.
3559            if eno == 0 && ee != 0 {
3560                // Reset for the $PATH scan below.
3561                ee = 0;
3562            }
3563        }
3564        // c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
3565        // optimization but C also walks the rest of `path` if the hashed
3566        // exec failed with a non-"good" error).
3567        let path_str = getsparam("PATH").unwrap_or_default();
3568        for pp in path_str.split(':') {
3569            if pp.is_empty() || pp == "." {
3570                // c:856
3571                ee = zexecve(&arg0, &argv, newenvp); // c:857
3572                if isgooderr(ee, pp) {
3573                    eno = ee;
3574                }
3575            } else {
3576                // c:860
3577                let candidate = format!("{}/{}", pp, arg0); // c:861-864
3578                ee = zexecve(&candidate, &argv, newenvp); // c:865
3579                if isgooderr(ee, pp) {
3580                    eno = ee;
3581                }
3582            }
3583        }
3584    }
3585    // c:871-881 — final error reporting.
3586    if eno != 0 {
3587        // c:871
3588        zerr(&format!(
3589            "{}: {}",
3590            std::io::Error::from_raw_os_error(eno),
3591            arg0
3592        )); // c:872
3593    } else if commandnotfound(&arg0, args) == 0 {
3594        // c:873
3595        unsafe {
3596            libc::_exit(LASTVAL.load(Ordering::Relaxed));
3597        } // c:874
3598    } else {
3599        zerr(&format!("command not found: {}", arg0)); // c:876
3600    }
3601    let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
3602        126
3603    } else {
3604        127
3605    }; // c:881
3606    unsafe {
3607        libc::_exit(code);
3608    }
3609}
3610
3611/// Port of `static int zexecve(char *pth, char **argv, char **newenvp)`
3612/// from `Src/exec.c:504`. Wraps `execve(2)` with:
3613///   - `$_` env var stamped to absolute `pth` (c:514-520)
3614///   - winch signal unblock right before the syscall (c:527)
3615///   - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
3616///     bytes, parses a `#!interp arg` shebang and re-execs the
3617///     interpreter (c:534-628). For `ENOEXEC` with no shebang,
3618///     binary-safety check then falls back to `/bin/sh script` per
3619///     POSIX (c:588-628).
3620///
3621/// Returns `errno` from the failing exec — execve only returns on
3622/// failure, so success means the calling process is already replaced.
3623///
3624/// =================== WARNING — DIVERGENCE ====================
3625/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
3626///     string; Rust uses a stack `String` (consumed by `zputenv`).
3627/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
3628///     unconditionally as a no-op when FD_CLOEXEC is platform default.
3629/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
3630///     c:639-642 — handled implicitly via &str ↔ CString.
3631/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
3632///     drop the metafy and pass byte ranges to zerr directly.
3633/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
3634///     (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
3635///     `Vec<String>` with interp + optional arg + original argv tail
3636///     since Vec doesn't expose negative indexing.
3637/// (f) `environ` is FFI-loaded only when `newenvp` is None.
3638/// =============================================================
3639pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
3640    // c:504
3641    use std::ffi::CString;
3642    // c:514-520 — `_=pth` env stamping.
3643    let pth_abs = if pth.starts_with('/') {
3644        // c:516
3645        pth.to_string() // c:517
3646    } else {
3647        // c:518
3648        format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
3649    };
3650    zputenv(&format!("_={}", pth_abs)); // c:520
3651    closedumps(); // c:522
3652    winch_unblock(); // c:527
3653    let cpth = match CString::new(pth) {
3654        Ok(c) => c,
3655        Err(_) => return libc::ENOENT,
3656    };
3657    let cargs: Vec<CString> = argv
3658        .iter()
3659        .filter_map(|a| CString::new(a.as_str()).ok())
3660        .collect();
3661    let mut argv_ptrs: Vec<*const libc::c_char> = cargs.iter().map(|c| c.as_ptr()).collect();
3662    argv_ptrs.push(std::ptr::null());
3663    let env_holder: Vec<CString>;
3664    let env_ptrs: Vec<*const libc::c_char>;
3665    let envp: *const *const libc::c_char = match newenvp {
3666        Some(env) => {
3667            env_holder = env
3668                .iter()
3669                .filter_map(|e| CString::new(e.as_str()).ok())
3670                .collect();
3671            env_ptrs = {
3672                let mut v: Vec<*const libc::c_char> =
3673                    env_holder.iter().map(|c| c.as_ptr()).collect();
3674                v.push(std::ptr::null());
3675                v
3676            };
3677            env_ptrs.as_ptr()
3678        }
3679        None => unsafe {
3680            extern "C" {
3681                static environ: *const *const libc::c_char;
3682            }
3683            environ
3684        },
3685    };
3686    unsafe {
3687        libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
3688    }
3689    let eno = std::io::Error::last_os_error()
3690        .raw_os_error()
3691        .unwrap_or(libc::ENOEXEC); // c:534
3692    if eno == libc::ENOEXEC || eno == libc::ENOENT {
3693        // c:534
3694        let fd = unsafe { libc::open(cpth.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:538
3695        if fd < 0 {
3696            return std::io::Error::last_os_error()
3697                .raw_os_error()
3698                .unwrap_or(libc::ENOENT); // c:634
3699        }
3700        let mut buf = vec![0u8; POUNDBANGLIMIT + 1]; // c:541
3701        let ct = unsafe {
3702            libc::read(
3703                fd,
3704                buf.as_mut_ptr() as *mut libc::c_void,
3705                POUNDBANGLIMIT as libc::size_t,
3706            )
3707        }; // c:542
3708        unsafe {
3709            libc::close(fd);
3710        } // c:543
3711        if ct >= 0 {
3712            // c:544
3713            let ct = ct as usize;
3714            if ct >= 2 && buf[0] == b'#' && buf[1] == b'!' {
3715                // c:545
3716                let mut t0 = 0;
3717                while t0 < ct && buf[t0] != b'\n' {
3718                    t0 += 1;
3719                } // c:546-548
3720                if t0 == ct {
3721                    // c:549
3722                    zerr(&format!(
3723                        // c:550
3724                        "{}: bad interpreter: {}: {}",
3725                        pth,
3726                        String::from_utf8_lossy(&buf[2..t0.min(ct)]),
3727                        std::io::Error::from_raw_os_error(eno)
3728                    ));
3729                } else {
3730                    // c:552
3731                    while t0 > 0 && (buf[t0] == b' ' || buf[t0] == b'\t' || buf[t0] == b'\n') {
3732                        buf[t0] = 0;
3733                        t0 -= 1;
3734                    } // c:553-554
3735                    let mut ptr_lo: usize = 2;
3736                    while ptr_lo < buf.len() && buf[ptr_lo] == b' ' {
3737                        ptr_lo += 1;
3738                    } // c:555
3739                    let ptr2_lo = ptr_lo;
3740                    let mut ptr_hi = ptr2_lo;
3741                    while ptr_hi < buf.len() && buf[ptr_hi] != 0 && buf[ptr_hi] != b' ' {
3742                        ptr_hi += 1;
3743                    } // c:556
3744                    let interp_str = String::from_utf8_lossy(&buf[ptr2_lo..ptr_hi]).into_owned();
3745                    if eno == libc::ENOENT {
3746                        // c:557 — pathprog rewrite path.
3747                        let pprog = if !interp_str.starts_with('/') {
3748                            // c:561
3749                            pathprog(&interp_str).map(|p| p.display().to_string())
3750                        } else {
3751                            None
3752                        };
3753                        if let Some(pprog) = pprog {
3754                            // c:562
3755                            let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3756                            argv_new.push(interp_str.clone()); // c:564
3757                            if ptr_hi >= buf.len() || buf[ptr_hi] == 0 {
3758                                argv_new.push(pth.to_string());
3759                            } else {
3760                                // c:567
3761                                let mut rest_lo = ptr_hi + 1;
3762                                while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3763                                    rest_lo += 1;
3764                                }
3765                                let mut rest_hi = rest_lo;
3766                                while rest_hi < buf.len() && buf[rest_hi] != 0 {
3767                                    rest_hi += 1;
3768                                }
3769                                let arg_str =
3770                                    String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3771                                argv_new.push(arg_str);
3772                                argv_new.push(pth.to_string());
3773                            }
3774                            for orig in argv.iter().skip(1) {
3775                                argv_new.push(orig.clone());
3776                            }
3777                            winch_unblock(); // c:565/c:570
3778                            return zexecve(&pprog, &argv_new, newenvp); // c:566/c:571
3779                        }
3780                        zerr(&format!(
3781                            // c:574
3782                            "{}: bad interpreter: {}: {}",
3783                            pth,
3784                            interp_str,
3785                            std::io::Error::from_raw_os_error(eno)
3786                        ));
3787                    } else if ptr_hi < buf.len() && buf[ptr_hi] != 0 {
3788                        // c:576
3789                        let mut rest_lo = ptr_hi + 1;
3790                        while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3791                            rest_lo += 1;
3792                        }
3793                        let mut rest_hi = rest_lo;
3794                        while rest_hi < buf.len() && buf[rest_hi] != 0 {
3795                            rest_hi += 1;
3796                        }
3797                        let arg_str = String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3798                        let mut argv_new: Vec<String> =
3799                            vec![interp_str.clone(), arg_str, pth.to_string()];
3800                        for orig in argv.iter().skip(1) {
3801                            argv_new.push(orig.clone());
3802                        }
3803                        winch_unblock(); // c:580
3804                        return zexecve(&interp_str, &argv_new, newenvp); // c:581
3805                    } else {
3806                        // c:582
3807                        let mut argv_new: Vec<String> = vec![interp_str.clone(), pth.to_string()];
3808                        for orig in argv.iter().skip(1) {
3809                            argv_new.push(orig.clone());
3810                        }
3811                        winch_unblock(); // c:584
3812                        return zexecve(&interp_str, &argv_new, newenvp); // c:585
3813                    }
3814                }
3815            } else if eno == libc::ENOEXEC {
3816                // c:588 — binary-safety + /bin/sh fallback.
3817                let nul_pos = buf[..ct].iter().position(|&b| b == 0); // c:597
3818                let isbinary = match nul_pos {
3819                    None => false, // c:598
3820                    Some(npos) => {
3821                        let mut has_letter = false;
3822                        let mut binary = true;
3823                        for &b in &buf[..npos] {
3824                            // c:602-609
3825                            if (b as char).is_ascii_lowercase() || b == b'$' || b == b'`' {
3826                                has_letter = true;
3827                            }
3828                            if has_letter && b == b'\n' {
3829                                binary = false; // c:606
3830                                break;
3831                            }
3832                        }
3833                        binary
3834                    }
3835                };
3836                if !isbinary {
3837                    // c:611
3838                    let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3839                    argv_new.push("sh".to_string()); // c:625
3840                    if !argv.is_empty() && (argv[0].starts_with('-') || argv[0].starts_with('+')) {
3841                        argv_new.push("-".to_string()); // c:623
3842                    }
3843                    for orig in argv.iter() {
3844                        argv_new.push(orig.clone());
3845                    }
3846                    winch_unblock(); // c:626
3847                    return zexecve("/bin/sh", &argv_new, newenvp); // c:627
3848                }
3849            }
3850        }
3851    }
3852    eno // c:643
3853}
3854
3855/// Port of `char *getoutputfile(char *cmd, char **eptr)` from
3856/// `Src/exec.c:4910` — `=(cmd)` process substitution.
3857///
3858/// Substitutes the cmd's stdout into a temp file, returns the
3859/// filename. Optimised path: `=(<<<heredoc-str)` writes the
3860/// heredoc body directly without a fork.
3861///
3862/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
3863///     so the temp file gets cleaned at job exit.
3864/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
3865///     Behavior matches the `full=0` case anyway.
3866/// (c) `entersubsh` is ported at exec.rs:3934 — wire it here when
3867///     re-routing the fork path away from setsid-only fallback.
3868/// (d) `execode` is now ported (exec.rs:6047) — the body still
3869///     re-feeds through fusevm for cache coherence with execstring.
3870/// (e) `_realexit` flushes stdio + jobs + history. We use bare
3871///     `std::process::exit(0)` for now.
3872/// (f) TMPSUFFIX link()-rename block (c:4951-4958) deferred; rare
3873///     `setopt suffix_alias` interaction with =(…).
3874pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
3875    // c:4910
3876    let bytes = cmd.as_bytes();
3877    let _ = bytes;
3878    // c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
3879    let mut ends_at: usize = 0;
3880    let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
3881    if let Some(p) = eptr {
3882        *p = ends_at;
3883    }
3884    let mut nam = gettempname(None, true)?; // c:4924
3885                                            // c:4927 — `simple_redir_name` opt for `=(<<<str)`.
3886    let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
3887        // c:4933
3888        let mut sub = singsub(&raw); // c:4933
3889        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
3890            // c:4934
3891            String::new() // c:4935 — sentinel; checked below
3892        } else {
3893            sub = untokenize(&sub); // c:4937
3894            dyncat(&sub, "\n") // c:4938
3895        }
3896    });
3897    if let Some(ref sv) = s {
3898        if sv.is_empty() {
3899            s = None;
3900        }
3901    }
3902    if s.is_none() {
3903        // c:4942
3904        child_block(); // c:4943
3905    }
3906    // c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
3907    let c_nam = match std::ffi::CString::new(nam.clone()) {
3908        Ok(c) => c,
3909        Err(_) => {
3910            if s.is_none() {
3911                child_unblock();
3912            }
3913            return None;
3914        }
3915    };
3916    let fd = unsafe {
3917        libc::open(
3918            c_nam.as_ptr(),
3919            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
3920            0o600 as libc::c_uint,
3921        )
3922    };
3923    if fd < 0 {
3924        // c:4945
3925        zerr(&format!(
3926            "process substitution failed: {}",
3927            std::io::Error::last_os_error()
3928        )); // c:4946
3929        if s.is_none() {
3930            child_unblock(); // c:4948
3931        }
3932        return None; // c:4949
3933    }
3934    // c:4951-4958 — TMPSUFFIX link block (see WARNING f).
3935    // c:4960 — `addfilelist(nam, 0);` — register temp file in current
3936    // job's filelist so it's unlinked at job exit (not relying on the
3937    // OS temp-reaper).
3938    if let Some(jt) = JOBTAB.get() {
3939        let mut guard = jt.lock().unwrap();
3940        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
3941        if tj >= 0 {
3942            if let Some(j) = guard.get_mut(tj as usize) {
3943                crate::ported::jobs::addfilelist(j, Some(&nam), 0);
3944            }
3945        }
3946    }
3947    if let Some(sv) = s {
3948        // c:4962 — optimised here-string write path.
3949        let mut buf: Vec<u8> = sv.into_bytes();
3950        let _len = unmetafy(&mut buf); // c:4965
3951        let _ = write_loop(fd, &buf); // c:4966
3952        unsafe {
3953            libc::close(fd);
3954        } // c:4967
3955        return Some(nam); // c:4968
3956    }
3957    // c:4971 — `cmdoutpid = pid = zfork(NULL)`.
3958    let pid = zfork(None);
3959    cmdoutpid.store(pid, Ordering::Relaxed);
3960    if pid == -1 {
3961        // c:4972
3962        unsafe {
3963            libc::close(fd);
3964        } // c:4973
3965        child_unblock(); // c:4974
3966        return Some(nam); // c:4975
3967    } else if pid != 0 {
3968        // c:4976 — parent.
3969        unsafe {
3970            libc::close(fd);
3971        } // c:4977
3972        let _ = waitforpid(pid); // c:4978
3973        cmdoutval.store(0, Ordering::Relaxed); // c:4979
3974        return Some(nam); // c:4980
3975    }
3976    // c:4983 — child.
3977    closem(FDT_UNUSED, 0); // c:4984
3978    let _ = redup(fd, 1); // c:4985
3979    entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
3980    cmdpush(CS_CMDSUBST as u8); // c:4987
3981                                // c:4988 — execode — WARNING (d).
3982    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
3983    let body = if body_end > 2 && body_end <= cmd.len() {
3984        &cmd[2..body_end]
3985    } else {
3986        ""
3987    };
3988    let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
3989    cmdpop(); // c:4989
3990    unsafe {
3991        libc::close(1);
3992    } // c:4990
3993      // _realexit — WARNING (e)
3994    std::process::exit(0); // c:4991
3995    #[allow(unreachable_code)]
3996    {
3997        // c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
3998        let _ = &mut nam;
3999        unsafe {
4000            libc::kill(libc::getpid(), libc::SIGKILL);
4001        }
4002        None
4003    }
4004}
4005
4006/// Port of `char *getproc(char *cmd, char **eptr)` from
4007/// `Src/exec.c:5025` — `<(cmd)` / `>(cmd)` process substitution
4008/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
4009///
4010/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
4011///     path c:5037-5064) is omitted; modern Linux/macOS both
4012///     provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
4013///     unused here.
4014/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
4015///     the current job (c:5141-5142).
4016/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
4017///     c:5087.
4018/// (d) `entersubsh` is ported at exec.rs:3934 — wired below at
4019///     c:5063 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`).
4020/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
4021///     through fusevm for cache coherence.
4022/// (f) `_realexit` flushes stdio + jobs + history. We use bare
4023///     `std::process::exit(LASTVAL)` for now.
4024/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
4025pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4026    // c:5025
4027    let bytes = cmd.as_bytes();
4028    let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4029        1 // c:5032 — `<(...)` writer-side child
4030    } else {
4031        0
4032    };
4033    // c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
4034    // proc subst needs a host job to attach the child to.
4035    let tj_check = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4036    if tj_check == -1 {
4037        zerr(&format!("process substitution {} cannot be used here", cmd)); // c:5069
4038        return None; // c:5070
4039    }
4040    // c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
4041    let mut ends_at: usize = 0;
4042    let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
4043    if let Some(p) = eptr {
4044        *p = ends_at;
4045    }
4046    let mut pipes: [i32; 2] = [-1; 2];
4047    if mpipe(&mut pipes) < 0 {
4048        // c:5075
4049        return None;
4050    }
4051    let mut bgtime: ZshTimespec = libc::timespec {
4052        tv_sec: 0,
4053        tv_nsec: 0,
4054    };
4055    let pid = zfork(Some(&mut bgtime)); // c:5077
4056    if pid != 0 {
4057        // c:5077 — parent path.
4058        let pnam = format!("/dev/fd/{}", pipes[(1 - out) as usize]); // c:5078
4059        let _ = zclose(pipes[out as usize]); // c:5079
4060        if pid == -1 {
4061            // c:5080
4062            let _ = zclose(pipes[(1 - out) as usize]); // c:5082
4063            return None; // c:5083
4064        }
4065        let fd = pipes[(1 - out) as usize]; // c:5085
4066        fdtable_set(fd, FDT_PROC_SUBST); // c:5086
4067                                         // c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
4068                                         // pipe fd in the current job's filelist so it's closed at job exit.
4069        if let Some(jt) = JOBTAB.get() {
4070            let mut guard = jt.lock().unwrap();
4071            let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4072            if tj >= 0 {
4073                if let Some(j) = guard.get_mut(tj as usize) {
4074                    crate::ported::jobs::addfilelist(j, None, fd);
4075                }
4076            }
4077        }
4078        // c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
4079        // record the proc-subst writer-side child in the job's
4080        // auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
4081        // child), C omits the addproc — symmetric here.
4082        if out == 0 {
4083            if let Some(jt) = JOBTAB.get() {
4084                let mut guard = jt.lock().unwrap();
4085                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4086                if tj >= 0 {
4087                    if let Some(j) = guard.get_mut(tj as usize) {
4088                        crate::ported::jobs::addproc(
4089                            j,
4090                            pid,
4091                            "",
4092                            true,
4093                            Some(std::time::Instant::now()),
4094                            -1,
4095                            -1,
4096                        );
4097                    }
4098                }
4099            }
4100        }
4101        procsubstpid.store(pid, Ordering::Relaxed); // c:5092
4102        return Some(pnam); // c:5093
4103    }
4104    // c:5095 — child.
4105    entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
4106    let _ = redup(pipes[out as usize], out); // c:5096
4107    closem(FDT_UNUSED, 0); // c:5097
4108    cmdpush(CS_CMDSUBST as u8); // c:5100
4109    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4110    let body = if body_end > 2 && body_end <= cmd.len() {
4111        &cmd[2..body_end]
4112    } else {
4113        ""
4114    };
4115    let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
4116    cmdpop(); // c:5102
4117    let _ = zclose(out); // c:5103
4118    std::process::exit(LASTVAL.load(Ordering::Relaxed)); // c:5104
4119}
4120
4121/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
4122/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
4123pub mod esub {
4124    // c:1056
4125    /// `ASYNC` constant.
4126    pub const ASYNC: i32 = 0x01; // c:1058
4127    /// `PGRP` constant.
4128    pub const PGRP: i32 = 0x02; // c:1063
4129    /// `KEEPTRAP` constant.
4130    pub const KEEPTRAP: i32 = 0x04; // c:1065
4131    /// `FAKE` constant.
4132    pub const FAKE: i32 = 0x08; // c:1067
4133    /// `REVERTPGRP` constant.
4134    pub const REVERTPGRP: i32 = 0x10; // c:1069
4135    /// `NOMONITOR` constant.
4136    pub const NOMONITOR: i32 = 0x20; // c:1071
4137    /// `JOB_CONTROL` constant.
4138    pub const JOB_CONTROL: i32 = 0x40; // c:1073
4139}
4140
4141/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
4142/// Out-arg used by `entersubsh()` to hand back the group-leader pid
4143/// and the list-pipe job index the parent should track. Only filled
4144/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
4145/// groups).
4146#[allow(non_camel_case_types)]
4147#[derive(Default)]
4148pub struct entersubsh_ret {
4149    pub gleader: i32,       // c:1122
4150    pub list_pipe_job: i32, // c:1123
4151}
4152
4153/// Port of `static void entersubsh(int flags, struct entersubsh_ret *retp)`
4154/// from `Src/exec.c:1083`. Called by every child fork to switch the
4155/// process into subshell mode: traps reset, monitor disabled, signals
4156/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
4157/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
4158///
4159/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
4160///     1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
4161///     ESUB_PGRP+sync path establishes pipeline group-leadership
4162///     (list_pipe_job inherit or thisjob-as-leader), filling
4163///     entersubsh_ret with the chosen gleader + list_pipe_job index.
4164/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
4165///     `clearjobtab(&mut JobTable, monitor)`; we get the global table
4166///     via a TABLE handle similar to other jobs.rs entries.
4167/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
4168/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
4169///     mypgrp` — direct C parity (jobs.rs:3406 provides the call).
4170/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
4171///     uses `opts_set_off(USEZLE)`; zleactive is the atomic in
4172///     builtins/sched.rs.
4173/// =============================================================
4174pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
4175    // c:1083
4176    let monitor: i32;
4177    let job_control_ok: i32;
4178    // c:1088-1092 — reset traps unless KEEPTRAP.
4179    if (flags & esub::KEEPTRAP) == 0 {
4180        // c:1088
4181        for sig in 0..=SIGCOUNT {
4182            // c:1089
4183            let st = {
4184                let guard = sigtrapped.lock().unwrap();
4185                guard.get(sig as usize).copied().unwrap_or(0)
4186            };
4187            let func_set = (st & ZSIG_FUNC) != 0; // c:1090
4188            let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
4189            if !func_set && !posix_ignored {
4190                unsettrap(sig); // c:1092
4191            }
4192        }
4193    }
4194    monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
4195    job_control_ok = if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
4196        // c:1094
4197        1
4198    } else {
4199        0
4200    };
4201    EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
4202    if (flags & esub::NOMONITOR) != 0 {
4203        // c:1096
4204        dosetopt(MONITOR, 0, 0); // c:1097
4205    }
4206    if !isset(MONITOR) {
4207        // c:1098
4208        if (flags & esub::ASYNC) != 0 {
4209            // c:1099
4210            let _ = settrap(libc::SIGINT, None, 0); // c:1100
4211            let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
4212            if unsafe { libc::isatty(0) } != 0 {
4213                // c:1102
4214                unsafe {
4215                    libc::close(0);
4216                } // c:1103
4217                let devnull = std::ffi::CString::new("/dev/null").unwrap();
4218                if unsafe { libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) } != 0 {
4219                    // c:1104
4220                    zerr(&format!(
4221                        // c:1105
4222                        "can't open /dev/null: {}",
4223                        std::io::Error::last_os_error()
4224                    ));
4225                    unsafe {
4226                        libc::_exit(1);
4227                    } // c:1106
4228                }
4229            }
4230        }
4231    } else if (flags & esub::PGRP) != 0 {
4232        // c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
4233        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4234        if thisjob != -1 {
4235            let lpj = list_pipe_job.load(Ordering::Relaxed);
4236            let lp = list_pipe.load(Ordering::Relaxed);
4237            let lpc = list_pipe_child.load(Ordering::Relaxed);
4238            if let Some(jt) = JOBTAB.get() {
4239                let mut guard = jt.lock().unwrap();
4240                let lpj_gleader = guard.get(lpj as usize).map(|j| j.gleader).unwrap_or(0);
4241                if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
4242                    // c:1111-1124 — inherit list_pipe_job's group leader.
4243                    let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
4244                        || (unsafe { libc::killpg(lpj_gleader, 0) } == -1
4245                            && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH))
4246                    {
4247                        // c:1115-1117 — primary group leader gone; this child becomes leader.
4248                        let new_gl = if lpc != 0 {
4249                            mypgrp.load(Ordering::Relaxed)
4250                        } else {
4251                            unsafe { libc::getpid() }
4252                        };
4253                        if let Some(j) = guard.get_mut(lpj as usize) {
4254                            j.gleader = new_gl;
4255                        }
4256                        if let Some(j) = guard.get_mut(thisjob as usize) {
4257                            j.gleader = new_gl;
4258                        }
4259                        unsafe { libc::setpgid(0, new_gl) };
4260                        if (flags & esub::ASYNC) == 0 {
4261                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
4262                        }
4263                        new_gl
4264                    } else {
4265                        lpj_gleader
4266                    };
4267                    if let Some(r) = retp {
4268                        if (flags & esub::ASYNC) == 0 {
4269                            r.gleader = pgid; // c:1122
4270                            r.list_pipe_job = lpj; // c:1123
4271                        }
4272                    }
4273                } else {
4274                    // c:1126-1151 — standard group-leader-takeover path.
4275                    let thisjob_gleader =
4276                        guard.get(thisjob as usize).map(|j| j.gleader).unwrap_or(0);
4277                    if thisjob_gleader == 0 || unsafe { libc::setpgid(0, thisjob_gleader) } == -1 {
4278                        let new_gl = unsafe { libc::getpid() };
4279                        if let Some(j) = guard.get_mut(thisjob as usize) {
4280                            j.gleader = new_gl; // c:1138
4281                        }
4282                        if lpj != thisjob {
4283                            let lpj_was_unset = guard
4284                                .get(lpj as usize)
4285                                .map(|j| j.gleader == 0)
4286                                .unwrap_or(true);
4287                            if lpj_was_unset {
4288                                if let Some(j) = guard.get_mut(lpj as usize) {
4289                                    j.gleader = new_gl; // c:1140-1141
4290                                }
4291                            }
4292                        }
4293                        unsafe { libc::setpgid(0, new_gl) }; // c:1142
4294                        if (flags & esub::ASYNC) == 0 {
4295                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
4296                            if let Some(r) = retp {
4297                                r.gleader = new_gl; // c:1146
4298                                if lpj != thisjob {
4299                                    r.list_pipe_job = lpj; // c:1148
4300                                }
4301                            }
4302                        }
4303                    }
4304                }
4305            }
4306        } else {
4307            // No real job slot; basic setpgid fallback.
4308            unsafe { libc::setpgid(0, 0) };
4309        }
4310    }
4311    if (flags & esub::FAKE) == 0 {
4312        // c:1153
4313        subsh.store(1, Ordering::Relaxed); // c:1154
4314    }
4315    // c:1161 — `zsh_subshell++;` regardless of FAKE.
4316    zsh_subshell.fetch_add(1, Ordering::Relaxed);
4317    // c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
4318    if (flags & esub::REVERTPGRP) != 0
4319        && unsafe { libc::getpid() } == mypgrp.load(Ordering::Relaxed)
4320    {
4321        release_pgrp(); // c:1163
4322    }
4323    *shout.lock().unwrap() = 0; // c:1164 — shout = NULL
4324    if (flags & esub::NOMONITOR) != 0 {
4325        // c:1165
4326        signal_ignore(libc::SIGTTOU); // c:1171
4327        signal_ignore(libc::SIGTTIN); // c:1172
4328        signal_ignore(libc::SIGTSTP); // c:1173
4329    } else if job_control_ok == 0 {
4330        // c:1174
4331        signal_default(libc::SIGTTOU); // c:1181
4332        signal_default(libc::SIGTTIN); // c:1182
4333        signal_default(libc::SIGTSTP); // c:1183
4334    }
4335    let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
4336    if interact {
4337        signal_default(libc::SIGTERM); // c:1186
4338        let int_st = sigtrapped
4339            .lock()
4340            .unwrap()
4341            .get(libc::SIGINT as usize)
4342            .copied()
4343            .unwrap_or(0);
4344        if (int_st & ZSIG_IGNORED) == 0 {
4345            // c:1187
4346            signal_default(libc::SIGINT); // c:1188
4347        }
4348        let pipe_st = sigtrapped
4349            .lock()
4350            .unwrap()
4351            .get(libc::SIGPIPE as usize)
4352            .copied()
4353            .unwrap_or(0);
4354        if pipe_st == 0 {
4355            // c:1189
4356            signal_default(libc::SIGPIPE); // c:1190
4357        }
4358    }
4359    let quit_st = sigtrapped
4360        .lock()
4361        .unwrap()
4362        .get(libc::SIGQUIT as usize)
4363        .copied()
4364        .unwrap_or(0);
4365    if (quit_st & ZSIG_IGNORED) == 0 {
4366        // c:1192
4367        signal_default(libc::SIGQUIT); // c:1193
4368    }
4369    // c:1202-1205 — unblock any trapped signals while in `intrap`.
4370    if intrap.load(Ordering::Relaxed) != 0 {
4371        // c:1202
4372        for sig in 1..=SIGCOUNT {
4373            let st = sigtrapped
4374                .lock()
4375                .unwrap()
4376                .get(sig as usize)
4377                .copied()
4378                .unwrap_or(0);
4379            if st != 0 && st != ZSIG_IGNORED {
4380                // c:1204
4381                let m = signal_mask(sig);
4382                let _ = signal_unblock(&m); // c:1205
4383            }
4384        }
4385    }
4386    if job_control_ok == 0 {
4387        // c:1206
4388        dosetopt(MONITOR, 0, 0); // c:1207
4389    }
4390    dosetopt(USEZLE, 0, 0); // c:1208
4391    zleactive.store(0, Ordering::Relaxed); // c:1209
4392                                           // c:1214-1217 — close saved fds.
4393    let max = MAX_ZSH_FD.load(Ordering::Relaxed);
4394    for i in 10..=max {
4395        if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
4396            // c:1215
4397            let _ = zclose(i); // c:1216
4398        }
4399    }
4400    // c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
4401    // at jobs.rs:1695 which handles ALL the C body including the
4402    // oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
4403    let mut dummy_table = crate::exec_jobs::JobTable::new();
4404    crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
4405    let _ = get_usage(); // c:1220
4406    FORKLEVEL.store(
4407        // c:1221 — `forklevel = locallevel;`
4408        locallevel.load(Ordering::Relaxed),
4409        Ordering::Relaxed,
4410    );
4411}
4412
4413/// Port of `static int getpipe(char *cmd, int nullexec)` from
4414/// `Src/exec.c:5119`.
4415///
4416/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
4417/// pipe pair: parent gets back the readable (`<(...)`) or writable
4418/// (`>(...)`) end as an fd; child runs the substituted command with
4419/// its stdio redirected into the other end.
4420///
4421/// ```c
4422/// Eprog prog;
4423/// int pipes[2], out = *cmd == Inang;
4424/// pid_t pid;
4425/// struct timespec bgtime;
4426/// char *ends;
4427/// if (!(prog = parsecmd(cmd, &ends))) return -1;
4428/// if (*ends) { zerr("invalid syntax..."); return -1; }
4429/// if (mpipe(pipes) < 0) return -1;
4430/// if ((pid = zfork(&bgtime))) {
4431///     zclose(pipes[out]);
4432///     if (pid == -1) { zclose(pipes[!out]); return -1; }
4433///     if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
4434///     procsubstpid = pid;
4435///     return pipes[!out];
4436/// }
4437/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
4438/// redup(pipes[out], out);
4439/// closem(FDT_UNUSED, 0);
4440/// cmdpush(CS_CMDSUBST);
4441/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
4442/// cmdpop();
4443/// _realexit();
4444/// ```
4445///
4446/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
4447///     procsubst pid recording site (c:5141-5142) earlier this
4448///     session; the child IS now recorded in `JOBTAB[thisjob]`.
4449/// (b) `entersubsh` IS now ported (exec.rs:3934) including the
4450///     ESUB_PGRP pipeline group-leadership path — wired this
4451///     session for getpipe's `entersubsh(ESUB_ASYNC|ESUB_PGRP|
4452///     ESUB_NOMONITOR, NULL)` call.
4453/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
4454///     can route through execode for the parsed eprog. Currently
4455///     this caller still uses the fusevm pipeline for cache
4456///     coherence with execstring; switch over when the wordcode
4457///     walker becomes the primary path.
4458/// (d) `_realexit()` flushes stdio + jobs + history. We use bare
4459///     `std::process::exit(lastval)` for now.
4460pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
4461    // c:5119
4462    let bytes = cmd.as_bytes();
4463    let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4464        1 // c:5122 — `<(...)` reads from child, child writes to fd 1
4465    } else {
4466        0 // `>(...)` — child reads from fd 0
4467    };
4468    let mut ends_at: usize = 0;
4469    let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
4470    if prog.is_none() {
4471        // c:5127
4472        return -1; // c:5128
4473    }
4474    // c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
4475    if ends_at < bytes.len() && bytes[ends_at] != 0 {
4476        zerr("invalid syntax for process substitution in redirection"); // c:5130
4477        return -1; // c:5131
4478    }
4479    let mut pipes: [i32; 2] = [-1; 2];
4480    if mpipe(&mut pipes) < 0 {
4481        // c:5133
4482        return -1;
4483    }
4484    // c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
4485    let mut bgtime: ZshTimespec = libc::timespec {
4486        tv_sec: 0,
4487        tv_nsec: 0,
4488    };
4489    let pid = zfork(Some(&mut bgtime)); // c:5135
4490    if pid != 0 {
4491        // c:5135 — parent.
4492        let _ = zclose(pipes[out as usize]); // c:5136
4493        if pid == -1 {
4494            // c:5137
4495            let _ = zclose(pipes[(1 - out) as usize]); // c:5138
4496            return -1; // c:5139
4497        }
4498        // c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
4499        if nullexec == 0 {
4500            if let Some(jt) = JOBTAB.get() {
4501                let mut guard = jt.lock().unwrap();
4502                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4503                if tj >= 0 {
4504                    if let Some(j) = guard.get_mut(tj as usize) {
4505                        crate::ported::jobs::addproc(
4506                            j,
4507                            pid,
4508                            "",
4509                            true, // aux=1 for proc subst
4510                            Some(std::time::Instant::now()),
4511                            -1,
4512                            -1,
4513                        );
4514                    }
4515                }
4516            }
4517        }
4518        procsubstpid.store(pid, Ordering::Relaxed); // c:5143
4519        return pipes[(1 - out) as usize]; // c:5144
4520    }
4521    // c:5146 — child path.
4522    entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
4523    let _ = redup(pipes[out as usize], out); // c:5147
4524    closem(FDT_UNUSED, 0); // c:5148
4525    cmdpush(CS_CMDSUBST as u8); // c:5149
4526                                // c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
4527    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4528    let body = if body_end > 2 && body_end <= bytes.len() {
4529        &cmd[2..body_end]
4530    } else {
4531        ""
4532    };
4533    let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
4534    cmdpop(); // c:5151
4535              // c:5152 — _realexit() — WARNING (d).
4536    std::process::exit(LASTVAL.load(Ordering::Relaxed));
4537}
4538
4539/// Port of `static void spawnpipes(LinkList l, int nullexec)` from
4540/// `Src/exec.c:5184`.
4541///
4542/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
4543/// entry fires `getpipe(name, nullexec || varid)` and stashes the
4544/// resulting fd into `f->fd2`.
4545///
4546/// ```c
4547/// LinkNode n;
4548/// Redir f;
4549/// char *str;
4550/// n = firstnode(l);
4551/// for (; n; incnode(n)) {
4552///     f = (Redir) getdata(n);
4553///     if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
4554///         str = f->name;
4555///         f->fd2 = getpipe(str, nullexec || f->varid);
4556///     }
4557/// }
4558/// ```
4559///
4560/// =================== WARNING — DIVERGENCE ====================
4561/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
4562/// in place of `LinkList`. The walk is identical; the only behavior
4563/// difference is that LinkList iteration in C lets callers splice
4564/// nodes mid-walk — we never do that here so it's a no-op divergence.
4565/// =============================================================
4566pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
4567    // c:5184
4568    for f in l.iter_mut() {
4569        // c:5191
4570        if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
4571            // c:5193
4572            let str_ = f.name.clone().unwrap_or_default(); // c:5194
4573            let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
4574                1
4575            } else {
4576                nullexec
4577            };
4578            f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
4579        }
4580    }
4581}
4582
4583/// Port of `static int cancd2(char *s)` from `Src/exec.c:6411`.
4584///
4585/// C body:
4586/// ```c
4587/// struct stat buf;
4588/// char *us, *us2 = NULL;
4589/// int ret;
4590/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
4591///     if (*s != '/')
4592///         us = tricat(pwd[1] ? pwd : "", "/", s);
4593///     else
4594///         us = ztrdup(s);
4595///     fixdir(us2 = us);
4596/// } else
4597///     us = unmeta(s);
4598/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
4599/// if (us2) free(us2);
4600/// return ret;
4601/// ```
4602///
4603/// True iff `s` is a directory we can `cd` into (X-perm). With
4604/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
4605/// (joining with PWD if relative) so `cd /foo/bar/..` works without
4606/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
4607pub fn cancd2(s: &str) -> i32 {
4608    // c:6411
4609    let us: String;
4610    // c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
4611    let chasedots = isset(CHASEDOTS); // c:6422
4612    let chaselinks = isset(CHASELINKS);
4613    if !chasedots && !chaselinks {
4614        // c:6422
4615        // c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
4616        let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
4617        let mut raw = if !s.starts_with('/') {
4618            // c:6423
4619            format!(
4620                "{}/{}",
4621                if pwd_str.len() > 1 { &pwd_str[..] } else { "" },
4622                s
4623            )
4624        } else {
4625            s.to_string()
4626        };
4627        // c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
4628        raw = fixdir(&raw);
4629        us = raw;
4630    } else {
4631        // c:6428
4632        us = unmeta(s); // c:6429
4633    }
4634    // c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
4635    let cstr = match std::ffi::CString::new(us.as_str()) {
4636        Ok(c) => c,
4637        Err(_) => return 0,
4638    };
4639    if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
4640        return 0;
4641    }
4642    let meta = match std::fs::metadata(&us) {
4643        Ok(m) => m,
4644        Err(_) => return 0,
4645    };
4646    if !meta.file_type().is_dir() {
4647        return 0;
4648    }
4649    1
4650}
4651
4652/// Port of `char *cancd(char *s)` from `Src/exec.c:6370`.
4653///
4654/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
4655/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
4656/// it, else `None`.
4657///
4658/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
4659/// `doprintdir = -1` when a non-trivial path is found (so `cd`
4660/// echoes the resolved path).
4661pub fn cancd(s: &str) -> Option<String> {
4662    // c:6370
4663    // c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
4664    //                (s[1]=='.' && (s[2]=='/' || !s[2])))`.
4665    let bytes = s.as_bytes();
4666    let nocdpath = bytes.first().copied() == Some(b'.')
4667        && (bytes.get(1).copied() == Some(b'/')
4668            || bytes.get(1).is_none()
4669            || (bytes.get(1).copied() == Some(b'.')
4670                && (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
4671    // c:6376 — `if (*s != '/')` branch.
4672    if !s.starts_with('/') {
4673        // c:6376
4674        // c:6379-6380 — `if (cancd2(s)) return s;`
4675        if cancd2(s) != 0 {
4676            return Some(s.to_string());
4677        }
4678        // c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
4679        let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
4680        if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
4681            return None; // c:6382
4682        }
4683        // c:6383-6397 — CDPATH walk.
4684        if !nocdpath {
4685            let cdpath_str = getsparam("CDPATH").unwrap_or_default();
4686            for cp in cdpath_str.split(':') {
4687                // c:6384
4688                let sbuf = if !cp.is_empty() {
4689                    format!("{}/{}", cp, s) // c:6386
4690                } else {
4691                    s.to_string() // c:6391
4692                };
4693                if cancd2(&sbuf) != 0 {
4694                    // c:6393
4695                    DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
4696                    return Some(sbuf); // c:6395
4697                }
4698            }
4699        }
4700        // c:6398-6403 — `cd_able_vars()` fallback.
4701        if let Some(t) = cd_able_vars(s) {
4702            // c:6398
4703            if cancd2(&t) != 0 {
4704                // c:6399
4705                DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
4706                return Some(t); // c:6401
4707            }
4708        }
4709        return None; // c:6404
4710    }
4711    // c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
4712    if cancd2(s) != 0 {
4713        Some(s.to_string())
4714    } else {
4715        None
4716    }
4717}
4718
4719/// Port of `char *simple_redir_name(Eprog prog, int redir_type)` from
4720/// `Src/exec.c:4689`.
4721///
4722/// Test if an Eprog encodes a single simple-command consisting of a
4723/// SINGLE redirection of the requested type with NO command body
4724/// (the `cat < foo` shape). When true, returns the redir target name
4725/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
4726/// `open(2)` instead of fork+pipe+exec.
4727///
4728/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
4729/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
4730/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
4731/// same shape — this port replicates the same offset reads.
4732pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
4733    // c:4689
4734    let pc = &prog.prog;
4735    // c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
4736    // offsets matching C's `pc[0]..pc[6]` checks.
4737    if pc.len() < 7 {
4738        return None;
4739    }
4740
4741    if wc_code(pc[0]) != WC_LIST
4742        || (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0  // c:4695
4743        || wc_code(pc[1]) != WC_SUBLIST
4744        || WC_SUBLIST_FLAGS(pc[1]) != 0  // c:4696
4745        || WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END  // c:4697
4746        || wc_code(pc[2]) != WC_PIPE
4747        || WC_PIPE_TYPE(pc[2]) != WC_PIPE_END  // c:4698
4748        || wc_code(pc[3]) != WC_REDIR
4749        || WC_REDIR_TYPE(pc[3]) != redir_type  // c:4699
4750        || WC_REDIR_VARID(pc[3]) != 0  // c:4700
4751        || pc[4] != 0  // c:4701
4752        || wc_code(pc[6]) != WC_SIMPLE
4753        || WC_SIMPLE_ARGC(pc[6]) != 0
4754    // c:4702
4755    {
4756        return None; // c:4706
4757    }
4758    // c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
4759    Some(dupstring(&ecrawstr(prog, 5, None)))
4760}
4761
4762/// Port of `int getherestr(struct redir *fn)` from `Src/exec.c:4655`.
4763///
4764/// C body:
4765/// ```c
4766/// char *s, *t;
4767/// int fd, len;
4768/// t = fn->name;
4769/// singsub(&t);
4770/// untokenize(t);
4771/// unmetafy(t, &len);
4772/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
4773///     t[len++] = '\n';
4774/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
4775///     return -1;
4776/// write_loop(fd, t, len);
4777/// close(fd);
4778/// fd = open(s, O_RDONLY | O_NOCTTY);
4779/// unlink(s);
4780/// return fd;
4781/// ```
4782///
4783/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
4784/// tempfile, then re-open read-only and unlink — gives the consumer a
4785/// read fd whose backing file is already cleaned up.
4786pub fn getherestr(fn_: &redir) -> i32 {
4787    // c:4655
4788    let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
4789    t = singsub(&t); // c:4661
4790    t = untokenize(&t); // c:4662
4791                        // c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
4792                        // Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
4793    let mut bytes: Vec<u8> = t.into_bytes();
4794    let _len = unmetafy(&mut bytes);
4795    // c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
4796    if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
4797        // c:4671
4798        bytes.push(b'\n'); // c:4672
4799    }
4800    // c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
4801    let (fd, s) = match gettempfile(None) {
4802        Some(p) => p,
4803        None => return -1, // c:4674
4804    };
4805    // c:4675 — `write_loop(fd, t, len);`
4806    let _ = write_loop(fd, &bytes); // c:4675
4807                                    // c:4676 — `close(fd);`
4808    let _ = zclose(fd); // c:4676
4809                        // c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
4810    let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
4811    let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
4812                                                                                        // c:4678 — `unlink(s);`
4813    unsafe {
4814        libc::unlink(cstr.as_ptr());
4815    } // c:4678
4816    new_fd // c:4679
4817}
4818
4819/// Port of `void quote_tokenized_output(char *str, FILE *file)` from
4820/// `Src/exec.c:2114`.
4821///
4822/// C body (abridged):
4823/// ```c
4824/// for (; *s; s++) {
4825///     switch (*s) {
4826///         case Meta: putc(*++s ^ 32, file); continue;
4827///         case Nularg: continue;
4828///         case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
4829///             putc('\\', file); break;
4830///         case '\t': fputs("$'\\t'", file); continue;
4831///         case '\n': fputs("$'\\n'", file); continue;
4832///         case '\r': fputs("$'\\r'", file); continue;
4833///         case '=': if (s == str) putc('\\', file); break;
4834///         default:
4835///             if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
4836///     }
4837///     putc(*s, file);
4838/// }
4839/// ```
4840///
4841/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
4842/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
4843/// surface as unescaped chars (`*`, `(`) while literal special chars
4844/// get backslash-escaped — round-tripping through the shell.
4845pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
4846    // c:2114
4847    let bytes = str_in.as_bytes();
4848    let mut i = 0usize;
4849    while i < bytes.len() {
4850        // c:2118 `for (; *s; s++)`
4851        let c = bytes[i];
4852        match c {
4853            x if x == Meta => {
4854                // c:2120 — `case Meta: putc(*++s ^ 32, file);`
4855                if i + 1 < bytes.len() {
4856                    file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
4857                    i += 2;
4858                } else {
4859                    i += 1;
4860                }
4861                continue; // c:2122
4862            }
4863            x if x as char == Nularg => {
4864                // c:2124
4865                i += 1;
4866                continue; // c:2126
4867            }
4868            b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']' | b'*'
4869            | b'?' | b'$' | b' ' => {
4870                // c:2128-2142
4871                file.write_all(b"\\")?; // c:2143
4872            }
4873            b'\t' => {
4874                // c:2146
4875                file.write_all(b"$'\\t'")?; // c:2147
4876                i += 1;
4877                continue;
4878            }
4879            b'\n' => {
4880                // c:2150
4881                file.write_all(b"$'\\n'")?; // c:2151
4882                i += 1;
4883                continue;
4884            }
4885            b'\r' => {
4886                // c:2154
4887                file.write_all(b"$'\\r'")?; // c:2155
4888                i += 1;
4889                continue;
4890            }
4891            b'=' => {
4892                // c:2158 — `if (s == str) putc('\\', file);`
4893                if i == 0 {
4894                    file.write_all(b"\\")?; // c:2160
4895                }
4896            }
4897            _ => {
4898                // c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
4899                if itok(c) {
4900                    // c:2164
4901                    let pound = Pound as u8;
4902                    if c >= pound {
4903                        let idx = (c - pound) as usize;
4904                        let zt = ztokens.as_bytes();
4905                        if idx < zt.len() {
4906                            file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
4907                        }
4908                    }
4909                    i += 1;
4910                    continue;
4911                }
4912            }
4913        }
4914        file.write_all(&[c])?; // c:2171
4915        i += 1;
4916    }
4917    Ok(())
4918}
4919
4920// =====================================================================
4921// Wordcode-VM control-flow dispatch — faithful ports of the C
4922// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
4923//
4924// Each function below takes `&mut estate` and returns `i32` to mirror
4925// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
4926// line `// c:NNN` citations track the C source line.
4927//
4928// zshrs's primary execution path is the fusevm bytecode VM. These
4929// wordcode-VM entries exist for C-name parity with the upstream
4930// interpreter so that future bridging code can drive zshrs through
4931// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
4932// zshrs primitives don't yet model their C counterpart (e.g.
4933// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
4934// helper is declared with a comment citing the C source file:line
4935// where the canonical body lives — same pattern as the canonical
4936// `ksh93::ksh93_wrapper` port at c:152-227.
4937// =====================================================================
4938
4939use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
4940use crate::ported::pattern::{patcompile, pattry};
4941use crate::ported::r#loop::try_tryflag;
4942
4943// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
4944use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
4945use crate::ported::linklist::LinkList;
4946use crate::ported::mem::freeheap;
4947use crate::ported::params::setloopvar;
4948use crate::ported::params::{assignaparam, assignsparam, unsetparam};
4949use crate::ported::parse::{ecgetlist, ecgetstr};
4950use crate::ported::pattern::haswilds;
4951use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
4952use crate::ported::subst::{globlist, prefork};
4953use crate::ported::zsh_h::{
4954    estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
4955    WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP,
4956    WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
4957    WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
4958    WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
4959};
4960use crate::ported::zsh_h::{
4961    ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
4962    PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
4963    WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
4964};
4965use crate::ported::zsh_h::{
4966    CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
4967    CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
4968};
4969
4970// --- Local stubs for C primitives not yet ported elsewhere ------------
4971//
4972// These mirror the C functions of the same names. Each cites the C
4973// source file:line where the canonical body lives. They are inlined
4974// here (rather than a separate `pub fn` in the owning C-file module)
4975// because the owning ports are pending the wider exec-substrate
4976// work (sub-PR). Once those land, these locals collapse to direct
4977// `crate::ported::<owner>::<fn>` calls.
4978
4979/// Port of `void execsubst(LinkList strs)` from `Src/exec.c:2684`.
4980///
4981/// C body (c:2684-2693):
4982/// ```c
4983/// void execsubst(LinkList strs) {
4984///     if (strs) {
4985///         prefork(strs, esprefork, NULL);
4986///         if (esglob && !errflag) {
4987///             LinkList ostrs = strs;
4988///             globlist(strs, 0);
4989///             strs = ostrs;
4990///         }
4991///     }
4992/// }
4993/// ```
4994///
4995/// `execsubst` runs `prefork` (parameter / arithmetic / command
4996/// substitution expansion + IFS-split) over the whole list, then
4997/// (when `esglob` is set) `globlist` to do filename globbing on the
4998/// result.
4999fn execsubst(list: &mut Vec<String>) {
5000    // c:2684
5001    if list.is_empty() {
5002        return; // c:2686 `if (strs)`
5003    }
5004    let mut ll: crate::ported::subst::LinkList = std::mem::take(list).into_iter().collect();
5005    let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
5006    let mut rf: i32 = 0;
5007    prefork(&mut ll, prefork_flags, &mut rf); // c:2687
5008    if esglob.load(Ordering::Relaxed) != 0 && errflag.load(Ordering::Relaxed) == 0 {
5009        // c:2688 `if (esglob && !errflag)`
5010        globlist(&mut ll, 0); // c:2690
5011    }
5012    *list = ll.into_iter().collect();
5013}
5014
5015/// Direct port of `static void addvars(Estate state, Wordcode pc,
5016/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
5017/// nodes stacked inline of a simple command — the `var=value` and
5018/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
5019/// at `pc`, extracts each assignment's name + value (scalar or array),
5020/// optionally preforks + globs the tokenised RHS, and routes through
5021/// `assignsparam` (scalar) or `assignaparam` (array).
5022///
5023/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
5024/// stderr (C uses xtrerr; zshrs uses eprint!).
5025///
5026/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
5027/// into the file-static `STTYval` for `execute()` to apply pre-exec.
5028fn addvars(state: &mut estate, pc: usize, addflags: i32) {
5029    // c:2501 — locals.
5030    let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
5031    let xtr: bool; // c:2502 `int xtr,`
5032    let mut isstr: bool; // c:2502 `int isstr,`
5033    let mut htok: i32 = 0; // c:2502 `int htok = 0;`
5034    let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
5035    let mut name: String;
5036    let mut flags: i32; // c:2504 `int flags;`
5037    let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
5038    let mut ac: wordcode; // c:2507 `wordcode ac;`
5039                          // c:2508 `local_list1(svl);` — stack-local one-element LinkList
5040                          // for the scalar-assignment path. Rust uses a fresh LinkList per
5041                          // iteration; equivalent semantics.
5042
5043    // c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
5044    // when the assignment list is implicitly local (ADDVAR_RESTORE).
5045    flags = if (addflags & ADDVAR_RESTORE) == 0 {
5046        ASSPM_WARN // c:2516
5047    } else {
5048        0 // c:2516
5049    };
5050    xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
5051    if xtr {
5052        // c:2518
5053        printprompt4(); // c:2519
5054        doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
5055    }
5056    state.pc = pc; // c:2522 `state->pc = pc;`
5057
5058    // c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
5059    loop {
5060        if state.pc >= state.prog.prog.len() {
5061            break;
5062        }
5063        ac = state.prog.prog[state.pc];
5064        state.pc += 1;
5065        if wc_code(ac) != WC_ASSIGN {
5066            // Step back so the WC_SIMPLE / outer dispatcher sees the
5067            // non-assignment opcode. C's `state->pc++` post-increment
5068            // already pointed past WC_ASSIGN; we need to unconsume.
5069            state.pc -= 1;
5070            break;
5071        }
5072        let mut myflags = flags; // c:2524 `int myflags = flags;`
5073        name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
5074        if htok != 0 {
5075            // c:2526 `if (htok) untokenize(name);`
5076            name = untokenize(&name).to_string(); // c:2527
5077        }
5078        if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5079            // c:2528
5080            myflags |= ASSPM_AUGMENT; // c:2529
5081        }
5082        if xtr {
5083            // c:2530
5084            // c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
5085            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5086                eprint!("{}+=", name); // c:2532
5087            } else {
5088                eprint!("{}=", name); // c:2532
5089            }
5090        }
5091
5092        // c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
5093        isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
5094        if isstr {
5095            // c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
5096            let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
5097            vl = LinkList::new();
5098            vl.push_back(svl_val);
5099            // c:2535 `vl = &svl;` — vl already points at the new list.
5100        } else {
5101            // c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
5102            let items = ecgetlist(
5103                state,
5104                WC_ASSIGN_NUM(ac) as usize,
5105                EC_DUPTOK,
5106                Some(&mut htok),
5107            );
5108            vl = LinkList::new();
5109            for it in items {
5110                vl.push_back(it);
5111            }
5112            if errflag.load(Ordering::Relaxed) != 0 {
5113                // c:2538-2541
5114                state.pc = opc; // c:2539
5115                return; // c:2540
5116            }
5117        }
5118
5119        // c:2544 `if (vl && htok) {`
5120        if htok != 0 {
5121            // c:2545 `int prefork_ret = 0;`
5122            let mut prefork_ret: i32 = 0;
5123            // c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
5124            //                          : PREFORK_ASSIGN), &prefork_ret);
5125            let pf_flags = if isstr {
5126                PREFORK_SINGLE | PREFORK_ASSIGN
5127            } else {
5128                PREFORK_ASSIGN
5129            };
5130            prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
5131            if errflag.load(Ordering::Relaxed) != 0 {
5132                // c:2548
5133                state.pc = opc; // c:2549
5134                return; // c:2550
5135            }
5136            if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
5137                // c:2552
5138                myflags |= ASSPM_KEY_VALUE; // c:2553
5139            }
5140            // c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
5141            //                  haswilds((char *)getdata(firstnode(vl)))))`
5142            let needs_glob = if !isstr {
5143                true
5144            } else {
5145                isset(GLOBASSIGN)
5146                    && isstr
5147                    && !vl.is_empty()
5148                    && haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
5149            };
5150            if needs_glob {
5151                globlist(&mut vl, prefork_ret); // c:2556
5152                                                // c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
5153                                                //                  unsetparam(name);`
5154                if isset(GLOBASSIGN) && isstr {
5155                    unsetparam(&name); // c:2562
5156                }
5157                if errflag.load(Ordering::Relaxed) != 0 {
5158                    // c:2563
5159                    state.pc = opc; // c:2564
5160                    return; // c:2565
5161                }
5162            }
5163        }
5164        // c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
5165        // — scalar-assignment path: zero or one element after prefork.
5166        if isstr && (vl.is_empty() || vl.len() == 1) {
5167            let val: String; // c:2571 `char *val;`
5168            if vl.is_empty() {
5169                // c:2574
5170                val = String::new(); // c:2575 `val = ztrdup("");`
5171            } else {
5172                // c:2577 `untokenize(peekfirst(vl));`
5173                let peek = vl.nodes.front().cloned().unwrap_or_default();
5174                val = untokenize(&peek).to_string(); // c:2577-2578
5175                                                     // c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
5176                                                     // we just cloned the front above. Equivalent.
5177            }
5178            if xtr {
5179                // c:2580
5180                eprint!("{}", quotedzputs(&val)); // c:2581
5181                eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
5182            }
5183            // c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
5184            let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
5185                // c:2585 `if (strcmp(name, "STTY") == 0)`
5186                if name == "STTY" {
5187                    // c:2586-2587 — `STTYval = ztrdup(val);`
5188                    let mut stty = STTYval.lock().unwrap();
5189                    *stty = Some(val.clone()); // c:2587
5190                }
5191                // c:2589 `allexp = opts[ALLEXPORT];`
5192                let allexp = isset(ALLEXPORT);
5193                // c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
5194                opt_state_set("allexport", true);
5195                if isset(KSHARRAYS) {
5196                    // c:2591
5197                    unsetparam(&name); // c:2592
5198                }
5199                let pm = assignsparam(&name, &val, myflags); // c:2593
5200                                                             // c:2594 `opts[ALLEXPORT] = allexp;` — restore.
5201                opt_state_set("allexport", allexp);
5202                pm
5203            } else {
5204                // c:2595
5205                assignsparam(&name, &val, myflags) // c:2596
5206            };
5207            if pm.is_none() {
5208                // c:2597 `if (!pm)`
5209                LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
5210                                                     // c:2599-2604 — "cheating" comment: don't zerr.
5211                if cmdoutval.load(Ordering::Relaxed) == 0 {
5212                    // c:2605 `if (!cmdoutval)`
5213                    cmdoutval.store(1, Ordering::Relaxed); // c:2606
5214                }
5215            }
5216            if errflag.load(Ordering::Relaxed) != 0 {
5217                // c:2608
5218                state.pc = opc; // c:2609
5219                return; // c:2610
5220            }
5221            continue; // c:2612
5222        }
5223        // c:2614 `if (vl) { ... }` — array-assignment path: drain vl
5224        // into a fresh `char **arr`.
5225        // c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
5226        arr = Vec::with_capacity(vl.len() + 1);
5227        while let Some(s) = vl.pop_front() {
5228            arr.push(s);
5229        }
5230        // c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
5231        if xtr {
5232            // c:2624
5233            eprint!("( "); // c:2625
5234            for s in &arr {
5235                // c:2626 `for (ptr = arr; *ptr; ptr++)`
5236                eprint!("{}", quotedzputs(s)); // c:2627
5237                eprint!(" "); // c:2628
5238            }
5239            eprint!(") "); // c:2630
5240        }
5241        // c:2632 `if (!assignaparam(name, arr, myflags))`
5242        if assignaparam(&name, arr, myflags).is_none() {
5243            LASTVAL.store(1, Ordering::Relaxed); // c:2633
5244                                                 // c:2634-2638 — "cheating" comment.
5245            if cmdoutval.load(Ordering::Relaxed) == 0 {
5246                // c:2639
5247                cmdoutval.store(1, Ordering::Relaxed); // c:2640
5248            }
5249        }
5250        if errflag.load(Ordering::Relaxed) != 0 {
5251            // c:2642
5252            state.pc = opc; // c:2643
5253            return; // c:2644
5254        }
5255    }
5256    state.pc = opc; // c:2647 `state->pc = opc;`
5257}
5258
5259// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
5260// match expression at the call sites in execsimple. Not a separate
5261// Rust fn — every C-side reference to
5262// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
5263
5264// --- exec.c entries ---------------------------------------------------
5265
5266/// Port of `execcursh(Estate state, int do_exec)` from
5267/// `Src/exec.c:469-498`. Execute a `{ ... }` current-shell command
5268/// group: skip the trailing try-only word, optionally drop a stale
5269/// job slot, then run the inner list.
5270pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
5271    // c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
5272    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5273    let end = state.pc + WC_CURSH_SKIP(prior) as usize;
5274    // c:475 — `state->pc++;` skip the try/always-only word.
5275    state.pc += 1;
5276    // c:482-486 — drop empty job slot before nested cmd: if outer-pipe
5277    // bookkeeping is clean AND thisjob is a real job that's not the
5278    // pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
5279    // leaking job-table slots when execcursh recurses.
5280    {
5281        let lp = list_pipe.load(Ordering::Relaxed);
5282        let lpj = list_pipe_job.load(Ordering::Relaxed);
5283        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5284        if lp == 0 && tj != -1 && tj != lpj {
5285            if let Some(jt) = JOBTAB.get() {
5286                let mut guard = jt.lock().unwrap();
5287                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5288                if !has {
5289                    if let Some(j) = guard.get_mut(tj as usize) {
5290                        crate::ported::jobs::deletejob(j, false);
5291                    }
5292                }
5293            }
5294        }
5295    }
5296    cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
5297    let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
5298    cmdpop(); // c:489 — `cmdpop();`
5299    state.pc = end; // c:491 — `state->pc = end;`
5300    this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
5301    LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
5302}
5303
5304// `(...)` subshell — no dedicated C function (handled inline by
5305// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
5306// In zshrs the subshell branch is folded into `execpline` and
5307// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
5308// inner-list walk since fusevm bytecode handles the forking via
5309// Op::Subshell at a higher layer.
5310
5311/// Port of `execcond(Estate state, UNUSED(int do_exec))` from
5312/// `Src/exec.c:5204-5232`. Run a `[[ ... ]]` cond expression.
5313pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
5314    state.pc -= 1; // c:5208 — `state->pc--;`
5315                   // c:5209-5213 — XTRACE prelude.
5316    if isset(XTRACE) {
5317        printprompt4();
5318        eprint!("[[");
5319        // c:5212 — `tracingcond++;` not modeled in zshrs.
5320    }
5321    cmdpush(CS_COND as u8); // c:5214
5322                            // c:5215 — `stat = evalcond(state, NULL);` — TODO faithful: needs
5323                            // the wordcode-level evalcond from Src/cond.c which is distinct
5324                            // from the test-builtin evalcond ported in cond.rs. Pending.
5325    let stat: i32 = 0;
5326    // c:5219-5221 — `if (stat == 2) errflag |= ERRFLAG_ERROR;`
5327    if stat == 2 {
5328        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
5329    }
5330    cmdpop(); // c:5222
5331    if isset(XTRACE) {
5332        eprintln!(" ]]");
5333    }
5334    stat // c:5230 — `return stat;`
5335}
5336
5337/// Port of `execarith(Estate state, UNUSED(int do_exec))` from
5338/// `Src/exec.c:5237-5275`. Run a `(( ... ))` arithmetic command;
5339/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
5340/// parse error.
5341pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
5342    if isset(XTRACE) {
5343        printprompt4();
5344        eprint!("((");
5345    }
5346    cmdpush(CS_MATH as u8); // c:5247
5347    let mut htok: i32 = 0;
5348    let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
5349    if htok != 0 {
5350        e = singsub(&e); // c:5250 — `singsub(&e);`
5351    }
5352    if isset(XTRACE) {
5353        eprint!(" {}", e);
5354    }
5355    let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
5356    cmdpop(); // c:5256
5357    if isset(XTRACE) {
5358        eprintln!(" ))");
5359    }
5360    // c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
5361    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
5362        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
5363        return 2;
5364    }
5365    // c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
5366    let val = val_result.unwrap();
5367    if val.type_ == MN_INTEGER {
5368        if val.l == 0 {
5369            1
5370        } else {
5371            0
5372        }
5373    } else if val.d == 0.0 {
5374        1
5375    } else {
5376        0
5377    }
5378}
5379
5380/// Port of `exectime(Estate state, UNUSED(int do_exec))` from
5381/// `Src/exec.c:5279-5294`. Run `time pipeline`: drives execpline with
5382/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
5383pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
5384    let jb = *THISJOB
5385        .get_or_init(|| std::sync::Mutex::new(-1))
5386        .lock()
5387        .unwrap(); // c:5283
5388    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5389    // c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
5390    if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
5391        // c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
5392        // shell+kids time deltas since last call.
5393        crate::ported::jobs::shelltime(None, None, None, 0);
5394        return 0; // c:5286
5395    }
5396    // c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
5397    let slcode = state.prog.prog[state.pc];
5398    state.pc += 1;
5399    use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
5400    let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
5401    *THISJOB
5402        .get_or_init(|| std::sync::Mutex::new(-1))
5403        .lock()
5404        .unwrap() = jb; // c:5289
5405    LASTVAL.load(Ordering::Relaxed) // c:5290
5406}
5407
5408/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
5409/// Promoted to top-level pub fn so execcmd_exec at the shfunc
5410/// dispatch site (c:4102-4105) can route through it. The real port
5411/// owns queue_signals + cmdstack + sfcontext setup before calling
5412/// doshfunc; doshfunc itself is unported, so we route the body
5413/// through `runshfunc` (exec.rs:1700), which carries the
5414/// wrapper-chain + zunderscore restore. Degraded vs C (no cmdstack
5415/// push, no sfcontext flip, no XTRACE arg-trace) but the function
5416/// body executes and `lastval` is updated.
5417pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
5418    // c:5546-5547 — `if (errflag) return;`
5419    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
5420        return;
5421    }
5422    // c:5550-5557 — drop empty job slot before nested shfunc invoke:
5423    // if outer-pipe bookkeeping is clean AND thisjob is a real job
5424    // that's not the pipe-leader AND has no procs yet, deletejob()
5425    // recycles it. Avoids leaking job-table slots across recursive
5426    // function calls. Same pattern as execcursh's c:482-486.
5427    {
5428        let lp = list_pipe.load(Ordering::Relaxed);
5429        let lpj = list_pipe_job.load(Ordering::Relaxed);
5430        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5431        if lp == 0 && tj != -1 && tj != lpj {
5432            if let Some(jt) = JOBTAB.get() {
5433                let mut guard = jt.lock().unwrap();
5434                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5435                if !has {
5436                    // c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
5437                    //                jobtab[thisjob].filelist = NULL;` — preserve
5438                    //                the filelist so deletejob doesn't unlink temp
5439                    //                files. Rust take()s the Vec into a local.
5440                    let _last_file_list: Vec<String> = if let Some(j) = guard.get_mut(tj as usize) {
5441                        std::mem::take(&mut j.filelist)
5442                    } else {
5443                        Vec::new()
5444                    };
5445                    if let Some(j) = guard.get_mut(tj as usize) {
5446                        crate::ported::jobs::deletejob(j, false); // c:5556
5447                    }
5448                }
5449            }
5450        }
5451    }
5452    // c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
5453    // emit PS4 prefix + space-separated quoted args on the trace
5454    // stream so `set -x` shows the function invocation line.
5455    if isset(XTRACE) {
5456        printprompt4();
5457        for (i, a) in args.iter().enumerate() {
5458            if i > 0 {
5459                eprint!(" ");
5460            }
5461            eprint!("{}", quotedzputs(a));
5462        }
5463        eprintln!();
5464    }
5465    // c:5572-5578 cmdstack/sfcontext setup: omit (no cmdstack in
5466    // zshrs yet — replaced by tracing).
5467    // c:5580 — `doshfunc(shf, args, 0);` — doshfunc swaps PPARAMS
5468    // ($1, $2, …) to the function's args, runs the body via
5469    // runshfunc, then restores. doshfunc itself isn't ported yet
5470    // so we do the swap-and-restore inline here.
5471    // c:5580 — `doshfunc(shf, args, 0);`. The C path always has
5472    // `funcdef` populated since C parses at definition time. zshrs
5473    // compiles to fusevm chunks instead, so `funcdef` is None for
5474    // user-defined functions; only `body` (source string) carries
5475    // the definition. When that's the case, build a one-shot eprog
5476    // whose `strs` carries the source so runshfunc's script-pipeline
5477    // arm (execute_script_zsh_pipeline) executes the body.
5478    let prog_owned: Option<eprog> = if shf.funcdef.is_some() {
5479        None
5480    } else if let Some(ref body) = shf.body {
5481        Some(eprog {
5482            strs: Some(body.clone()),
5483            ..Default::default()
5484        })
5485    } else {
5486        None
5487    };
5488    let prog_ref: Option<&eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
5489        (Some(p), _) => Some(p),
5490        (_, Some(p)) => Some(p),
5491        _ => None,
5492    };
5493    if let Some(_prog) = prog_ref {
5494        // c:5580 — `doshfunc(shf, args, 0);`. Direct doshfunc call —
5495        // noreturnval=0 means the body's return value updates LASTVAL
5496        // (caller of execfuncdef reads it back). PPARAMS swap +
5497        // restore happens INSIDE doshfunc's scope; body_runner just
5498        // runs the body.
5499        let name_for_body = shf.node.nam.clone();
5500        let body_args_owned: Vec<String> = if args.len() > 1 {
5501            args[1..].to_vec()
5502        } else {
5503            Vec::new()
5504        };
5505        let body_runner = move || -> i32 {
5506            crate::ported::exec_hooks::run_function_body(&name_for_body, &body_args_owned)
5507                .unwrap_or(0)
5508        };
5509        let _ = doshfunc(shf, args.clone(), false, body_runner);
5510    }
5511    // c:5582-5589 cmdstack restore/free: omit (no cmdstack).
5512}
5513
5514/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
5515/// from `Src/exec.c:5823-6158`.
5516///
5517/// C body's scope-management sequence ported here. The C source's
5518/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
5519/// is replaced by `body_runner` — zshrs runs function bodies through
5520/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
5521/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
5522/// callback hands the live executor back to the caller (typically
5523/// the fusevm bridge) for the actual body run. Every line of scope
5524/// save/restore around the body call mirrors C exactly.
5525///
5526/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
5527/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
5528/// c:6042; zshrs delegates to a closure because the body-execution
5529/// pipeline (fusevm) differs from C's (wordcode). The closure
5530/// fully replaces the runshfunc call and returns the body's exit
5531/// status (which doshfunc reads as `lastval` for the `noreturnval`
5532/// path).
5533#[allow(non_snake_case)]
5534pub fn doshfunc(
5535    shfunc: &mut shfunc,                  // c:5823
5536    doshargs: Vec<String>,                // c:5823
5537    noreturnval: bool,                    // c:5823
5538    mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
5539) -> i32 {
5540    use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
5541    use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
5542    use crate::ported::modules::parameter::FUNCSTACK;
5543    use crate::ported::params::endparamscope;
5544    use crate::ported::params::locallevel as locallevel_atomic;
5545    use crate::ported::zsh_h::{FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_UNDEFINED};
5546    use std::sync::atomic::Ordering;
5547
5548    let name = shfunc.node.nam.clone(); // c:5827
5549    let flags = shfunc.node.flags; // c:5828
5550    let fname = dupstring(&name); // c:5829
5551    let _ = fname; // c:5829 (kept for parity)
5552
5553    // c:5835 — `queue_signals();` Lots of memory + global-state changes.
5554    queue_signals();
5555
5556    // c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
5557    // Pinned so a recursive unload doesn't free the eprog under us.
5558    // (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
5559    // already pin until call ends. C does explicit refcount on
5560    // `funcdef->nref` via useeprog.)
5561
5562    // c:5856-5916 — Funcsave allocation + per-field snapshot.
5563    let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
5564    let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
5565    let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
5566    let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
5567    let funcsave_numpipestats = {
5568        // c:5864
5569        NUMPIPESTATS
5570            .get_or_init(|| std::sync::Mutex::new(0))
5571            .lock()
5572            .map(|n| *n)
5573            .unwrap_or(0)
5574    };
5575    let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
5576                                                                // c:5866-5867 — trap_state PRIMED branch decrements trap_return.
5577    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
5578        // c:5866
5579        TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
5580    }
5581    // c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
5582    // return-suppress so a `return` inside the body fires errexit
5583    // checks normally.
5584    noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
5585
5586    // c:5872-5880 — noreturnval branch: deep-copy pipestats so the
5587    // function body's pipestats writes are restored on exit.
5588    let funcsave_pipestats: Option<Vec<i32>> = if noreturnval {
5589        // c:5872
5590        let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
5591        p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
5592    } else {
5593        None
5594    };
5595
5596    // c:5882-5896 — TRAPEXIT special case (deep-copy shfunc so
5597    // starttrapscope doesn't rug-pull). zshrs doesn't yet support
5598    // running TRAPEXIT directly via doshfunc; flagged for follow-up.
5599    // (Skip: name = "TRAPEXIT" path.)
5600    let _ = name.as_str(); // sentinel for the eventual port.
5601
5602    // c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
5603    // tags SIGEXIT for deferred restoration at scope end.
5604    crate::ported::signals::starttrapscope();
5605    // c:5899 — `startpatternscope();`
5606    crate::ported::pattern::startpatternscope();
5607
5608    // c:5901 — `pptab = pparams;` — save outer positional params.
5609    let pptab: Vec<String> = crate::ported::builtin::PPARAMS
5610        .lock()
5611        .map(|p| p.clone())
5612        .unwrap_or_default();
5613
5614    // c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
5615    let funcsave_scriptname = crate::ported::utils::scriptname_get();
5616    if (flags as u32 & PM_UNDEFINED) == 0 {
5617        // c:5902
5618        crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
5619    }
5620
5621    // c:5904-5908 — `funcsave->zoptind = zoptind; ...` snapshot.
5622    // C zsh saves zoptind (the canonical OPTIND counter) and
5623    // zoptarg into the funcsave struct so OPTIND is implicitly
5624    // function-local: a `getopts` loop inside the function gets
5625    // its own counter that snaps back to the caller's on
5626    // function return. zshrs stores OPTIND/OPTARG in paramtab
5627    // as regular int/string params; snapshot them here and
5628    // restore at scope end. Bug #513.
5629    let funcsave_optind: Option<String> = crate::ported::params::getsparam("OPTIND");
5630    let funcsave_optarg: Option<String> = crate::ported::params::getsparam("OPTARG");
5631
5632    // c:5914 — `memcpy(funcsave->opts, opts, sizeof(opts));` — option
5633    // snapshot. Port wraps opts in OPTS_LIVE; capture the live state
5634    // here as a HashMap snapshot.
5635    let funcsave_opts = crate::ported::options::opt_state_snapshot();
5636
5637    // c:5915-5916 — `funcsave->emulation/sticky = emulation/sticky;`
5638    // Emulation snapshot pending the sticky-emulation port.
5639
5640    // c:5954-5969 — PM_TAGGED / PM_WARNNESTED option-override block.
5641    // Anonymous-function name comparison via pointer equality in C;
5642    // zshrs uses string equality. Skip until ANONYMOUS_FUNCTION_NAME
5643    // sentinel is ported.
5644
5645    // c:5970 — `funcsave->oflags = oflags;` — module-global tracking
5646    // function-attribute inheritance. Skip until oflags is ported.
5647
5648    // c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
5649    // for inner commands; outer flag restored on exit.
5650    opt_state_set("printexitvalue", false);
5651
5652    // c:5978-5998 — pparams swap. C reads doshargs and constructs the
5653    // function's positional-param array. First arg is the function
5654    // name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
5655    let funcsave_argv0: Option<String> = if !doshargs.is_empty() {
5656        // c:5978
5657        // c:5982-5985 — `pparams = x = zshcalloc(...)`.
5658        let positionals: Vec<String> = if doshargs.len() > 1 {
5659            doshargs[1..].to_vec()
5660        } else {
5661            Vec::new()
5662        };
5663        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5664            *pp = positionals;
5665        }
5666        // c:5984-5987 — FUNCTIONARGZERO: save argzero, install
5667        // doshargs[0] (the function name).
5668        if isset(FUNCTIONARGZERO) {
5669            // c:5984
5670            let prev = crate::ported::utils::argzero();
5671            crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
5672            prev
5673        } else {
5674            None
5675        }
5676    } else {
5677        // c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
5678        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5679            *pp = Vec::new();
5680        }
5681        if isset(FUNCTIONARGZERO) {
5682            // c:5994
5683            let prev = crate::ported::utils::argzero();
5684            crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
5685            prev
5686        } else {
5687            None
5688        }
5689    };
5690
5691    // c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
5692    // since zshrs tracks function-call depth there.
5693    //
5694    // Plus the canonical startparamscope (c:6194 inside runshfunc).
5695    // zshrs's body_runner replaces runshfunc's `execode` call so the
5696    // startparamscope/endparamscope pair must wrap body_runner here,
5697    // not inside the closure. inc_locallevel is exactly startparamscope.
5698    inc_locallevel();
5699
5700    // c:6000-6004 — FUNCNEST check + `goto undoshfunc` on overflow.
5701    // Skip the runtime check (the zshrs fusevm doesn't recurse via
5702    // real stack frames so the depth limit is less critical), but
5703    // keep the comment so the C label `undoshfunc:` target is
5704    // visible — `goto undoshfunc;` here would jump straight to the
5705    // epilogue at the `undoshfunc:` label below.
5706
5707    // c:6005-6019 — funcstack frame push. The full C block:
5708    //   funcsave->fstack.name      = dupstring(name);
5709    //   funcsave->fstack.caller    = funcstack ? funcstack->name :
5710    //                                 dupstring(argv0 ? argv0 : argzero);
5711    //   funcsave->fstack.lineno    = lineno;
5712    //   funcsave->fstack.prev      = funcstack;
5713    //   funcsave->fstack.tp        = FS_FUNC;
5714    //   funcstack                  = &funcsave->fstack;
5715    //   funcsave->fstack.flineno   = shfunc->lineno;
5716    //   funcsave->fstack.filename  = getshfuncfile(shfunc);
5717    let lineno_now = crate::ported::input::lineno.with(|c| c.get()) as i64;
5718    let (caller, prev_tp): (Option<String>, Option<i32>) = {
5719        let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5720        if let Some(p) = stk.last() {
5721            (Some(p.name.clone()), Some(p.tp))
5722        } else {
5723            // c:6011-6012 — outermost: argv0 (saved) or argzero global.
5724            let z = funcsave_argv0
5725                .clone()
5726                .or_else(crate::ported::utils::argzero);
5727            (z, None)
5728        }
5729    };
5730    // c:6018-6019 — flineno: shfunc->lineno (function def line)
5731    let flineno = shfunc.lineno;
5732    let filename = shfunc.filename.clone().or_else(|| Some(String::new()));
5733    {
5734        let frame = crate::ported::zsh_h::funcstack {
5735            prev: None,             // c:6014 (Vec-stack: index encodes link)
5736            name: dupstring(&name), // c:6005
5737            filename,               // c:6019
5738            caller,                 // c:6011
5739            flineno,                // c:6018
5740            lineno: lineno_now,     // c:6013
5741            tp: FS_FUNC,            // c:6015
5742        };
5743        let _ = prev_tp; // c:6011 (informational)
5744        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5745        stack.push(frame); // c:6016 funcstack = &funcsave->fstack
5746    }
5747
5748    // c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
5749    // zshrs delegates to the body_runner closure (typically a fusevm
5750    // sub-VM run from the bridge). The closure returns the body's
5751    // exit status which becomes lastval.
5752    //
5753    // c:Src/exec.c:1251-1266 — push "shfunc" onto zsh_eval_context
5754    // so the body sees `${zsh_eval_context[*]}` containing the call
5755    // chain context. The execode-based path (c:1245-1282 port at
5756    // exec.rs:7092) already did this, but the fusevm body_runner
5757    // path skipped doshfunc's body_runner invocation without the
5758    // push. Bug #262 in docs/BUGS.md.
5759    //
5760    // Push BOTH the static `zsh_eval_context` (matches C's variable)
5761    // AND the paramtab array entry (what `${zsh_eval_context[*]}`
5762    // reads). Pop on every return path via the guard struct so
5763    // panics / early returns don't leak the entry.
5764    crate::vm_helper::push_zsh_eval_context("shfunc");
5765    struct EvalContextGuard;
5766    impl Drop for EvalContextGuard {
5767        fn drop(&mut self) {
5768            crate::vm_helper::pop_zsh_eval_context();
5769        }
5770    }
5771    let _eval_ctx_guard = EvalContextGuard;
5772    // c:Src/exec.c — function bodies execute with `lineno` reset to
5773    // the relative line within the body (incremented per WC_PIPE
5774    // from the wordcode-encoded lineno). zsh's zerrmsg
5775    // (Src/utils.c:301) emits the lineno prefix only when lineno
5776    // is non-zero AND (!SHINSTDIN || locallevel != 0). For an
5777    // inline single-line function like `f() { x=1 }`, the body's
5778    // WC_PIPE encodes lineno=1, exec sets `lineno = lineno - 1 =
5779    // 0`, and the zerrmsg path falls through to space-only ("f: ").
5780    //
5781    // zshrs's compiler doesn't thread WC_PIPE_LINENO into the
5782    // bytecode, so the global lineno stays at the script-wide
5783    // value (1 for inline `-c`). Suppress the line-number prefix
5784    // inside function bodies by saving lineno on entry and forcing
5785    // it to 0 during body execution; restore on exit. This makes
5786    // warnings inside functions emit `f: ...` matching zsh's
5787    // single-line-function format. Bug #54/#74/#86 in docs/BUGS.md.
5788    let saved_lineno = crate::ported::lex::lineno();
5789    crate::ported::lex::set_lineno(0);
5790    // c:Src/exec.c:6173-6175 + c:6196-6198 — `runshfunc` saves
5791    // zunderscore before the body runs and restores it after, so
5792    // `$_` reads outside the function continue to reflect the
5793    // function CALL's last arg (set by setunderscore at c:3491
5794    // before doshfunc enters). Without this, commands inside the
5795    // body (`:`, `echo`, etc.) update `$_` to their own last arg,
5796    // and the post-call `echo "[$_]"` sees the body's residue
5797    // instead of the call's arg. Bug surfaced via
5798    // test_dollar_underscore_after_function_call.
5799    let saved_zunderscore = crate::ported::params::getsparam("_").unwrap_or_default();
5800    let body_status = body_runner();
5801    crate::ported::params::set_zunderscore(std::slice::from_ref(&saved_zunderscore));
5802    crate::ported::lex::set_lineno(saved_lineno);
5803    LASTVAL.store(body_status, Ordering::Relaxed);
5804
5805    // c:6043 — `doneshfunc:` label. The C `runshfunc` happy-path
5806    // falls through here from c:6042.
5807    // c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
5808    {
5809        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5810        stack.pop();
5811    }
5812    // c:6045 — `undoshfunc:` label. Reached either by fall-through
5813    // from c:6044 or by `goto undoshfunc;` from the FUNCNEST check
5814    // at c:6003. Tail epilogue follows.
5815
5816    // c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
5817    // runshfunc) lives at c:6157 below as `endparamscope()`. Removed
5818    // the dec here so locallevel only decrements once per
5819    // function-call frame; double-dec was purging level-0 globals on
5820    // function exit (the `f() { x=foo; }; f; echo $x` regression).
5821
5822    // c:6047-6053 — retflag clear. C clears retflag and restores
5823    // outer breaks if a `return` fired.
5824    if RETFLAG.load(Ordering::SeqCst) != 0 {
5825        // c:6047
5826        RETFLAG.store(0, Ordering::SeqCst); // c:6051
5827        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
5828    }
5829
5830    // c:6054-6058 — pparams + argv0 restore.
5831    if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5832        *pp = pptab; // c:6059 pparams = pptab
5833    }
5834    if let Some(saved) = funcsave_argv0 {
5835        // c:6055
5836        crate::ported::utils::set_argzero(Some(saved)); // c:6057
5837    }
5838
5839    // c:Src/exec.c:6060-6062 — `zoptind = funcsave->zoptind;
5840    // zoptarg = funcsave->zoptarg;`. Restore OPTIND/OPTARG so
5841    // an inner getopts loop's counter mutations don't leak to
5842    // the caller. Bug #513.
5843    if let Some(saved) = funcsave_optind {
5844        if let Ok(n) = saved.parse::<i64>() {
5845            crate::ported::params::setiparam("OPTIND", n);
5846        } else {
5847            crate::ported::params::setsparam("OPTIND", &saved);
5848        }
5849    }
5850    if let Some(saved) = funcsave_optarg {
5851        crate::ported::params::setsparam("OPTARG", &saved);
5852    }
5853
5854    // c:6064 — `scriptname = funcsave->scriptname;`
5855    crate::ported::utils::set_scriptname(funcsave_scriptname);
5856
5857    // c:6067 — `endpatternscope();`
5858    crate::ported::pattern::endpatternscope();
5859
5860    // c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
5861    // localoptions was set inside the body.
5862    if crate::ported::options::opt_state_get("localoptions").unwrap_or(false) {
5863        // c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
5864        let current = crate::ported::options::opt_state_snapshot();
5865        for (k, _) in &current {
5866            if !funcsave_opts.contains_key(k) {
5867                crate::ported::options::opt_state_unset(k);
5868            }
5869        }
5870        for (k, v) in &funcsave_opts {
5871            opt_state_set(k, *v);
5872        }
5873    } else {
5874        // c:6097-6101 — non-LOCALOPTIONS: restore only the always-
5875        // restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
5876        // LOCALLOOPS / WARNNESTEDVAR).
5877        for opt in [
5878            "xtrace",
5879            "printexitvalue",
5880            "localoptions",
5881            "localloops",
5882            "warnnestedvar",
5883        ] {
5884            if let Some(v) = funcsave_opts.get(opt) {
5885                opt_state_set(opt, *v);
5886            }
5887        }
5888    }
5889
5890    // c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
5891    // breaks/contflag/loops snapshot. Skip the warn lines for now;
5892    // restore the bookkeeping.
5893    if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
5894        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
5895        CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
5896        LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
5897    }
5898
5899    // c:Src/exec.c:6195-6200 — C's runshfunc calls endparamscope()
5900    // BEFORE returning to doshfunc, which then calls endtrapscope()
5901    // at c:6114. So locallevel is ALREADY one less by the time
5902    // endtrapscope's pop loop compares saved local > current.
5903    //
5904    // Bug #80 in docs/BUGS.md: zshrs had endtrapscope FIRST (here at
5905    // line 5774), endparamscope LATER. That left locallevel at the
5906    // function's own level when endtrapscope ran, so saved entries
5907    // tagged with `local == current_function_level` failed the
5908    // `local > locallevel` pop condition. Nested EXIT traps
5909    // (saved at deeper level) never restored at the outer fn's
5910    // endtrapscope — outer EXIT traps fired at script exit instead.
5911    //
5912    // Decrement locallevel via a peer-of-endparamscope locallevel
5913    // bookkeeping call before endtrapscope, then leave the real
5914    // endparamscope at its current site below so the param scope
5915    // unwind still happens after the exit_pending check.
5916    {
5917        use crate::ported::params::locallevel as ll;
5918        let prev = ll.load(Ordering::Relaxed);
5919        if prev > 0 {
5920            ll.store(prev - 1, Ordering::Relaxed);
5921        }
5922        crate::ported::signals::endtrapscope();
5923        // Re-bump so the existing endparamscope() call below sees the
5924        // same pre-decrement state and its own internal decrement
5925        // lands at the right value (mirrors C's "endparamscope already
5926        // happened" comment at c:6135-6136 — the C order is endparam
5927        // (inside runshfunc) → endtrap (in doshfunc); we keep that
5928        // logical ordering for endtrapscope only, without disturbing
5929        // the rest of the epilogue's level math).
5930        ll.store(prev, Ordering::Relaxed);
5931    }
5932
5933    // c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
5934    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
5935        // c:6116
5936        TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
5937    }
5938
5939    // c:6118 — `ret = lastval;`
5940    let ret = LASTVAL.load(Ordering::Relaxed);
5941
5942    // c:6119 — `noerrexit = funcsave->noerrexit;`
5943    noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
5944
5945    // c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
5946    // the function for side-effects only; outer lastval/pipestats
5947    // should reflect the PRE-call state.
5948    if noreturnval {
5949        // c:6120
5950        LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
5951        if let Some(saved_ps) = funcsave_pipestats {
5952            let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
5953            if let Ok(mut nguard) = n.lock() {
5954                *nguard = funcsave_numpipestats; // c:6122
5955            }
5956            let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
5957            if let Ok(mut pguard) = p.lock() {
5958                for (i, v) in saved_ps.iter().enumerate() {
5959                    if i < pguard.len() {
5960                        pguard[i] = *v; // c:6123 memcpy
5961                    }
5962                }
5963            }
5964        }
5965    }
5966
5967    // c:Src/exec.c doshfunc → endparamscope — restore local-typeset
5968    // params installed during the body. In C, this is called inside
5969    // runshfunc (c:6200) BEFORE control returns to doshfunc's tail —
5970    // so by the time the exit_pending check runs at c:6141,
5971    // locallevel has ALREADY been decremented. The c:6135-6136
5972    // comment explicitly states "The endparamscope() has already
5973    // happened, hence the +1 here."
5974    //
5975    // The previous Rust ordering placed endparamscope AFTER the
5976    // exit_pending check, which compared exit_level against the
5977    // un-decremented locallevel. For `foo() { exit 7; }; foo`:
5978    //   exit_level=1, cur_locallevel=1 (pre-decrement)
5979    //   check: exit_level >= cur_locallevel + 1 ⟹ 1 >= 2 = false
5980    // The function returned cleanly without triggering zexit, and
5981    // the shell exited 0 instead of 7. Moving endparamscope before
5982    // the check matches C and makes the off-by-one resolve.
5983    endparamscope();
5984
5985    // c:6128 — `unqueue_signals();`
5986    unqueue_signals();
5987
5988    // c:6135-6155 — exit_pending branch: when an `exit` was queued
5989    // inside the function body and we've unwound enough scopes for
5990    // it to take effect, either keep unwinding (still inside a
5991    // nested function) or actually exit the shell.
5992    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
5993    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
5994    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
5995    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
5996    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
5997    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
5998        // c:6141
5999        if cur_locallevel > cur_forklevel {
6000            // c:6143 — still inside a nested function: keep unwinding.
6001            RETFLAG.store(1, Ordering::Relaxed); // c:6144
6002            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
6003        } else {
6004            // c:6151 — out of all functions: exit for real.
6005            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
6006            let val = EXIT_VAL.load(Ordering::Relaxed);
6007            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
6008            // c:6152
6009        }
6010    }
6011
6012    ret // c:6157 return ret
6013}
6014
6015/// `TRAP_STATE_PRIMED` per `Src/signals.h:55` — doshfunc tests this
6016/// to decide whether to bump trap_return on entry/exit. Local
6017/// const here because the canonical zsh_h port doesn't carry
6018/// trap-state numeric constants yet.
6019const TRAP_STATE_PRIMED: i32 = 2; // c:Src/signals.h:55
6020
6021/// Port of `execfuncdef(Estate state, Eprog redir_prog)` from
6022/// `Src/exec.c:5309-5494`. Define a shell function: extract
6023/// name(s)+body from the wordcode payload, allocate the Shfunc,
6024/// install into `shfunctab` (named), or execute immediately (anon).
6025#[allow(non_snake_case)]
6026pub fn execfuncdef(state: &mut estate, mut redir_prog: Option<crate::ported::zsh_h::Eprog>) -> i32 {
6027    use crate::ported::hashtable::{dircache_set, shfunctab_lock};
6028    use crate::ported::jobs::{getsigidx, removetrapnode};
6029    use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
6030    use crate::ported::signals::settrap;
6031    use crate::ported::utils::scriptfilename_get;
6032    use crate::ported::zsh_h::{
6033        eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, Patprog,
6034        EC_DUPTOK as _, EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED,
6035        PM_TAGGED_LOCAL, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
6036    };
6037    // c:5311 — `Shfunc shf;`
6038    let mut shf: Box<shfunc_t>;
6039    // c:5312 — `char *s = NULL;`
6040    let mut s: Option<String> = None;
6041    // c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
6042    let mut signum: i32;
6043    let nprg: i32;
6044    let sbeg: i32;
6045    let nstrs: i32;
6046    let npats: i32;
6047    let do_tracing: i32;
6048    let len: i32;
6049    let plen: i32;
6050    // `i` — C loop counter for pp stamp; Rust uses .map().collect().
6051    let mut htok: i32 = 0;
6052    let mut ret: i32 = 0;
6053    // c:5314 — `int anon_func = 0;`
6054    let mut anon_func: i32 = 0;
6055    // c:5315 — `Wordcode beg = state->pc, end;`
6056    let _beg: usize = state.pc;
6057    let mut end: usize;
6058    // c:5316 — `Eprog prog;`
6059    // (allocated inline per-iter below; no upfront binding needed)
6060    // c:5317 — `Patprog *pp;` — handled by Vec construction.
6061    // c:5318 — `LinkList names;`
6062    let names: Vec<String>;
6063    // c:5319 — `int tracing_flags;`
6064    let tracing_flags: i32;
6065
6066    // c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
6067    end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
6068    // c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6069    let num = state.prog.prog[state.pc] as usize;
6070    state.pc += 1;
6071    names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
6072    // c:5323 — `sbeg = *state->pc++;`
6073    sbeg = state.prog.prog[state.pc] as i32;
6074    state.pc += 1;
6075    // c:5324 — `nstrs = *state->pc++;`
6076    nstrs = state.prog.prog[state.pc] as i32;
6077    state.pc += 1;
6078    // c:5325 — `npats = *state->pc++;`
6079    npats = state.prog.prog[state.pc] as i32;
6080    state.pc += 1;
6081    // c:5326 — `do_tracing = *state->pc++;`
6082    do_tracing = state.prog.prog[state.pc] as i32;
6083    state.pc += 1;
6084
6085    // c:5328 — `nprg = (end - state->pc);`
6086    nprg = end.saturating_sub(state.pc) as i32;
6087    // c:5329 — `plen = nprg * sizeof(wordcode);`
6088    plen = nprg.saturating_mul(size_of::<wordcode>() as i32);
6089    // c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
6090    len = plen + npats.saturating_mul(size_of::<usize>() as i32) + nstrs;
6091    // c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
6092    tracing_flags = if do_tracing != 0 {
6093        PM_TAGGED_LOCAL as i32
6094    } else {
6095        0
6096    };
6097
6098    // c:5333-5339 — htok name substitution.
6099    let mut names_mut: Vec<String> = names;
6100    if htok != 0 && !names_mut.is_empty() {
6101        execsubst(&mut names_mut); // c:5334
6102        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6103            // c:5335
6104            state.pc = end; // c:5336
6105            return 1; // c:5337
6106        }
6107    }
6108
6109    // c:5341-5342 DPUTS — debug assertion (anon + redir simultaneously).
6110    // Not portable as panic; left as comment.
6111
6112    // c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
6113    // num==0 → anon (no names); else iterate names.
6114    let mut names_iter = names_mut.into_iter();
6115    loop {
6116        let no_names = num == 0;
6117        if !no_names {
6118            // c:5343 — `s = ugetnode(names)`; break when list exhausted.
6119            match names_iter.next() {
6120                Some(nm) => s = Some(nm),
6121                None => break,
6122            }
6123        }
6124        // c:5344-5374 — Eprog alloc.
6125        let prog: Box<eprog_t>;
6126        let dump_present = state.prog.dump.is_some();
6127        let make_pat = || -> Patprog {
6128            // c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
6129            Box::new(patprog_t {
6130                startoff: 0,
6131                size: 0,
6132                mustoff: 0,
6133                patmlen: 0,
6134                globflags: 0,
6135                globend: 0,
6136                flags: 0,
6137                patnpar: 0,
6138                patstartch: 0,
6139            })
6140        };
6141        if no_names {
6142            // c:5345-5346 — `zhalloc`, `nref = -1`.
6143            // c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
6144            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6145            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6146            // c:5365 — `prog->strs = state->strs + sbeg;`
6147            let strs_tail = state.strs.as_ref().map(|t| {
6148                let off = (sbeg as usize).min(t.len());
6149                t[off..].to_string()
6150            });
6151            prog = Box::new(eprog_t {
6152                flags: EF_HEAP,
6153                len,
6154                npats,
6155                nref: -1, // c:5346
6156                pats,
6157                prog: prog_words,
6158                strs: strs_tail,
6159                shf: None,  // c:5377
6160                dump: None, // c:5356
6161            });
6162        } else if dump_present {
6163            // c:5358-5363 — EF_MAP path: refcount the dump, allocate
6164            // pats permanent, reuse `state->pc` slice in place.
6165            if let Some(dp) = state.prog.dump.as_deref() {
6166                incrdumpcount(dp); // c:5360
6167            }
6168            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6169            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6170            let strs_tail = state.strs.as_ref().map(|t| {
6171                let off = (sbeg as usize).min(t.len());
6172                t[off..].to_string()
6173            });
6174            prog = Box::new(eprog_t {
6175                flags: EF_MAP, // c:5359
6176                len,
6177                npats,
6178                nref: 1, // c:5349
6179                pats,
6180                prog: prog_words,
6181                strs: strs_tail,
6182                shf: None,                     // c:5377
6183                dump: state.prog.dump.clone(), // c:5361
6184            });
6185        } else {
6186            // c:5366-5374 — EF_REAL: copy wordcode + strs into a
6187            // freshly-owned eprog (no shared dump backing).
6188            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6189            let pc_end = state.pc + nprg as usize;
6190            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
6191            // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
6192            let strs_copy = state.strs.as_ref().map(|t| {
6193                let off = (sbeg as usize).min(t.len());
6194                let n_avail = t.len().saturating_sub(off);
6195                let take = (nstrs as usize).min(n_avail);
6196                t[off..off + take].to_string()
6197            });
6198            prog = Box::new(eprog_t {
6199                flags: EF_REAL, // c:5367
6200                len,
6201                npats,
6202                nref: 1, // c:5349
6203                pats,
6204                prog: prog_words,
6205                strs: strs_copy,
6206                shf: None,  // c:5377
6207                dump: None, // c:5371
6208            });
6209        }
6210
6211        // c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
6212        shf = Box::new(shfunc_t {
6213            node: hashnode {
6214                next: None,
6215                nam: String::new(),
6216                flags: tracing_flags,
6217            },
6218            filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
6219            // c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
6220            // else just lineno.
6221            lineno: {
6222                let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
6223                if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6224                    if let Some(top) = stk.last() {
6225                        if top.tp == FS_FUNC || top.tp == FS_EVAL {
6226                            top.flineno + cur_lineno
6227                        } else {
6228                            cur_lineno
6229                        }
6230                    } else {
6231                        cur_lineno
6232                    }
6233                } else {
6234                    cur_lineno
6235                }
6236            },
6237            funcdef: Some(prog), // c:5380
6238            redir: None,
6239            sticky: None,
6240            body: None,
6241        });
6242        // c:5396-5401 — redir_prog ownership.
6243        // C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
6244        // else `shf->redir = redir_prog; redir_prog = 0;`
6245        // "nonempty(names)" means there's a NEXT name still to consume —
6246        // i.e. peek the iterator.
6247        if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
6248            // c:5397 — dupe so each earlier name gets its own copy; the
6249            // last name (when iterator drains) gets the original.
6250            if let Some(rp) = redir_prog.as_deref() {
6251                shf.redir = Some(Box::new(dupeprog(rp, false)));
6252            }
6253        } else {
6254            // c:5399-5400 — last name (or anon) takes original.
6255            shf.redir = redir_prog.take();
6256        }
6257        // c:5402 — `shfunc_set_sticky(shf);`
6258        shfunc_set_sticky(&mut shf);
6259
6260        if no_names {
6261            // c:5404-5457 — anonymous function: execute immediately.
6262            // `LinkList args;` c:5409
6263            let mut args: Vec<String>;
6264
6265            anon_func = 1; // c:5411
6266            shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
6267
6268            state.pc = end; // c:5414
6269                            // c:5415 — `end += *state->pc++;`
6270            end += state.prog.prog[state.pc] as usize;
6271            state.pc += 1;
6272            // c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6273            let arg_count = state.prog.prog[state.pc] as usize;
6274            state.pc += 1;
6275            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
6276
6277            // c:5418-5429 — htok arg subst + cleanup-on-error.
6278            if htok != 0 && !args.is_empty() {
6279                execsubst(&mut args); // c:5419
6280                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6281                    // c:5421 — `freeeprog(shf->funcdef);`
6282                    if let Some(mut fd) = shf.funcdef.take() {
6283                        freeeprog(&mut fd);
6284                    }
6285                    if shf.redir.is_some() {
6286                        // c:5422-5423 — "shouldn't be" anon+redir, but free if so.
6287                        if let Some(mut rd) = shf.redir.take() {
6288                            freeeprog(&mut rd);
6289                        }
6290                    }
6291                    dircache_set(&mut shf.filename, None); // c:5424
6292                    drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
6293                    state.pc = end; // c:5426
6294                    return 1; // c:5427
6295                }
6296            }
6297
6298            // c:5431-5432 — `setunderscore` to last arg (or "").
6299            let under_val = if !args.is_empty() {
6300                args.last().cloned().unwrap_or_default()
6301            } else {
6302                String::new()
6303            };
6304            setunderscore(&under_val);
6305
6306            // c:5434-5435 — `if (!args) args = newlinklist();`
6307            // (Rust Vec is never null; no-op.)
6308            shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
6309                                                                // c:5437 — `pushnode(args, shf->node.nam);` — prepend.
6310            args.insert(0, shf.node.nam.clone());
6311
6312            execshfunc(&mut shf, &mut args); // c:5439
6313            ret = LASTVAL.load(Ordering::Relaxed); // c:5440
6314
6315            // c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
6316            if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
6317                eprintln!("zsh: exit {}", ret); // c:5445/5447
6318            }
6319
6320            // c:5452-5456 — cleanup.
6321            if let Some(mut fd) = shf.funcdef.take() {
6322                freeeprog(&mut fd);
6323            }
6324            if let Some(mut rd) = shf.redir.take() {
6325                // c:5453-5454 — "shouldn't be" but free if present.
6326                freeeprog(&mut rd);
6327            }
6328            dircache_set(&mut shf.filename, None); // c:5455
6329            drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
6330            break; // c:5457
6331        } else {
6332            // c:5458-5484 — named function path.
6333            let nm = s.as_deref().unwrap_or("");
6334            // c:5460-5475 — TRAP* signal-trap install.
6335            if nm.len() > 4 && nm.starts_with("TRAP") {
6336                if let Some(sn) = getsigidx(&nm[4..]) {
6337                    signum = sn;
6338                    // c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
6339                    if settrap(signum, None, ZSIG_FUNC) != 0 {
6340                        if let Some(mut fd) = shf.funcdef.take() {
6341                            freeeprog(&mut fd); // c:5463
6342                        }
6343                        dircache_set(&mut shf.filename, None); // c:5464
6344                        drop(shf); // c:5465
6345                        state.pc = end; // c:5466
6346                        return 1; // c:5467
6347                    }
6348                    // c:5474 — `removetrapnode(signum);`
6349                    removetrapnode(signum);
6350                    // c:Src/signals.c::settrap → unsettrap →
6351                    // removetrap also clears sigfuncs[sig] (the C
6352                    // string-form trap slot). zshrs's port stores
6353                    // string-form bodies in a separate
6354                    // `traps_table` HashMap not touched by
6355                    // removetrap. Drop the string-form entry here
6356                    // so dotrap's fallback doesn't double-dispatch
6357                    // when a TRAPxxx function REPLACES an
6358                    // existing `trap '...' SIG` registration. Bug
6359                    // #541 in docs/BUGS.md.
6360                    if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
6361                        t.remove(&nm[4..]);
6362                    }
6363                }
6364            }
6365            // c:5477-5482 — re-define-self trace flag propagate.
6366            if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6367                if let Some(top) = stk.last() {
6368                    if top.tp == FS_FUNC && top.name == nm {
6369                        // c:5479 — `Shfunc old = shfunctab->getnode(s);`
6370                        if let Ok(rd) = shfunctab_lock().read() {
6371                            if let Some(old) = rd.get(nm) {
6372                                // c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
6373                                shf.node.flags |=
6374                                    old.node.flags & (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
6375                            }
6376                        }
6377                    }
6378                }
6379            }
6380            // c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
6381            shf.node.nam = nm.to_string();
6382            if let Ok(mut wr) = shfunctab_lock().write() {
6383                wr.add(*shf);
6384            }
6385        }
6386    }
6387    // c:5486-5487 — `if (!anon_func) setunderscore("");`
6388    if anon_func == 0 {
6389        setunderscore("");
6390    }
6391    // c:5488-5491 — leftover redir cleanup ("shouldn't happen").
6392    if let Some(mut rd) = redir_prog.take() {
6393        freeeprog(&mut rd);
6394    }
6395    // c:5492 — `state->pc = end;`
6396    state.pc = end;
6397    // c:5493 — `return ret;`
6398    ret
6399}
6400
6401/// Port of `execsimple(Estate state)` from `Src/exec.c:1290-1340`.
6402/// Fast-path for single-Simple commands that bypasses the full
6403/// `execcmd_exec` machinery.
6404pub fn execsimple(state: &mut estate) -> i32 {
6405    // c:1292 — `wordcode code = *state->pc++;`
6406    let mut code = state.prog.prog[state.pc];
6407    state.pc += 1;
6408    // c:1295-1296 — `if (errflag) return (lastval = 1);`
6409    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6410        LASTVAL.store(1, Ordering::Relaxed);
6411        return 1;
6412    }
6413    // c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
6414    if !isset(crate::ported::zsh_h::EXECOPT) {
6415        LASTVAL.store(0, Ordering::Relaxed);
6416        return 0;
6417    }
6418    // c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
6419    // In evaluated traps, don't modify the line number (the trap
6420    // dispatcher restores it). `code` here is the wordcode-encoded
6421    // line number from the WC_SIMPLE entry at state.pc-1.
6422    if !crate::ported::zsh_h::IN_EVAL_TRAP()
6423        && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
6424        && code != 0
6425    {
6426        crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
6427    }
6428    // c:1306 — `code = wc_code(*state->pc++);`
6429    code = wc_code(state.prog.prog[state.pc]);
6430    state.pc += 1;
6431    // c:1311-1312 — `otj = thisjob; thisjob = -1;`
6432    let otj = *THISJOB
6433        .get_or_init(|| std::sync::Mutex::new(-1))
6434        .lock()
6435        .unwrap();
6436    *THISJOB
6437        .get_or_init(|| std::sync::Mutex::new(-1))
6438        .lock()
6439        .unwrap() = -1;
6440    use crate::ported::zsh_h::{
6441        WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
6442        WC_WHILE,
6443    };
6444    use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
6445    let lv = if code == WC_ASSIGN {
6446        // c:1315-1319 — assignment-only simple cmd path.
6447        // cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
6448        addvars(state, state.pc.saturating_sub(1), 0);
6449        setunderscore(""); // c:1317
6450        if isset(XTRACE) {
6451            eprintln!();
6452        }
6453        let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
6454        if ef != 0 {
6455            ef
6456        } else {
6457            0
6458        }
6459    } else {
6460        // c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
6461        let q = queue_signal_level();
6462        dont_queue_signals();
6463        let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6464            ERRFLAG_ERROR
6465        } else if code == WC_FUNCDEF {
6466            execfuncdef(state, None)
6467        } else {
6468            // c:5499 execfuncs[] table inlined — match the WC_* tag.
6469            match code {
6470                WC_CURSH => execcursh(state, 0),
6471                WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
6472                WC_FOR => execfor(state, 0),
6473                WC_SELECT => execselect(state, 0),
6474                WC_CASE => execcase(state, 0),
6475                WC_IF => execif(state, 0),
6476                WC_WHILE => execwhile(state, 0),
6477                WC_REPEAT => execrepeat(state, 0),
6478                WC_TIMED => exectime(state, 0),
6479                WC_COND => execcond(state, 0),
6480                WC_ARITH => execarith(state, 0),
6481                WC_TRY => exectry(state, 0),
6482                _ => 0,
6483            }
6484        };
6485        restore_queue_signals(q);
6486        result
6487    };
6488    // c:1334 — `thisjob = otj;`
6489    *THISJOB
6490        .get_or_init(|| std::sync::Mutex::new(-1))
6491        .lock()
6492        .unwrap() = otj;
6493    LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
6494    lv
6495}
6496
6497/// Port of `execlist(Estate state, int dont_change_job, int exiting)`
6498/// from `Src/exec.c:1349-1665`. Walks WC_LIST entries, dispatches each
6499/// sublist (WC_SUBLIST chain inlined per c:1525-1625, same as C —
6500/// there's no separate execsublist function), handles signal-trap
6501/// dispatch + ERREXIT propagation.
6502///
6503/// Body ports the structural skeleton faithfully (WC_LIST walk,
6504/// per-iteration breaks/retflag/errflag guards, ltype dispatch on
6505/// Z_END/Z_SYNC/Z_ASYNC, donetrap handling). The full signal queue
6506/// + DEBUGBEFORECMD trap machinery from c:1357-1500 is preserved
6507/// in shape with TODO-citations where dependent primitives aren't
6508/// yet ported.
6509pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) -> i32 {
6510    let mut last_status: i32 = 0;
6511    let mut donetrap: i32 = 0; // c:1352 — `static int donetrap;`
6512    let cj = *THISJOB
6513        .get_or_init(|| std::sync::Mutex::new(-1))
6514        .lock()
6515        .unwrap(); // c:1364 — `cj = thisjob;`
6516    let _ = dont_change_job; // c:1361 — restored on exit if nonzero.
6517                             // c:1380 — `code = *state->pc++;`
6518    if state.pc >= state.prog.prog.len() {
6519        return last_status;
6520    }
6521    let mut code = state.prog.prog[state.pc];
6522    state.pc += 1;
6523    // c:1382-1384 — empty list returns lastval = 0.
6524    if wc_code(code) != WC_LIST {
6525        LASTVAL.store(0, Ordering::Relaxed);
6526        return 0;
6527    }
6528    use crate::ported::zsh_h::{WC_LIST_SKIP, WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC};
6529    // c:1385-1499 — main WC_LIST loop.
6530    while wc_code(code) == WC_LIST
6531        && BREAKS.load(Ordering::SeqCst) == 0
6532        && RETFLAG.load(Ordering::SeqCst) == 0
6533        && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
6534    {
6535        let ltype = WC_LIST_TYPE(code) as i32;
6536        // c:1396 — `csp = cmdsp;` — snapshot cmdstack depth at start
6537        // of this WC_LIST iteration; restored at end so partial
6538        // cmdpush sequences (e.g. from execcond, execfuncs) don't
6539        // leak into the next sublist.
6540        let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len());
6541        // c:1502-1509 — Z_SIMPLE fast-path.
6542        if (ltype & Z_SIMPLE as i32) != 0 {
6543            let next_pc = state.pc + WC_LIST_SKIP(code) as usize;
6544            let s = execsimple(state);
6545            last_status = s;
6546            state.pc = next_pc;
6547        } else {
6548            // c:1513-1523 — sublist chain.
6549            if state.pc >= state.prog.prog.len() {
6550                break;
6551            }
6552            code = state.prog.prog[state.pc];
6553            state.pc += 1;
6554            // c:1525-1625 — sublist chain (&&/|| operators) inlined.
6555            use crate::ported::zsh_h::{
6556                WC_SUBLIST_AND, WC_SUBLIST_END, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SIMPLE,
6557                WC_SUBLIST_SKIP,
6558            };
6559            let mut sub_code = code;
6560            let _ = dont_change_job;
6561            while wc_code(sub_code) == WC_SUBLIST {
6562                let flags = WC_SUBLIST_FLAGS(sub_code);
6563                let next = state.pc + WC_SUBLIST_SKIP(sub_code) as usize;
6564                let sl_type = WC_SUBLIST_TYPE(sub_code) as i32;
6565                let last1 = if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6566                    exiting
6567                } else {
6568                    0
6569                };
6570                if flags == WC_SUBLIST_SIMPLE {
6571                    last_status = execsimple(state); // c:1605
6572                } else {
6573                    let _ = execpline(state, sub_code, sl_type, last1); // c:1607
6574                    last_status = LASTVAL.load(Ordering::Relaxed);
6575                }
6576                // c:1612 — `WC_SUBLIST_NOT` inverts status.
6577                if (flags & WC_SUBLIST_NOT) != 0 {
6578                    last_status = if last_status == 0 { 1 } else { 0 };
6579                    LASTVAL.store(last_status, Ordering::Relaxed);
6580                }
6581                state.pc = next;
6582                if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6583                    break;
6584                }
6585                if state.pc >= state.prog.prog.len() {
6586                    break;
6587                }
6588                // c:1617-1623 — short-circuit on && / ||.
6589                if sl_type == WC_SUBLIST_AND as i32 && last_status != 0 {
6590                    while state.pc < state.prog.prog.len() {
6591                        let c = state.prog.prog[state.pc];
6592                        if wc_code(c) != WC_SUBLIST {
6593                            break;
6594                        }
6595                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6596                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6597                            break;
6598                        }
6599                    }
6600                    break;
6601                }
6602                if sl_type == WC_SUBLIST_OR as i32 && last_status == 0 {
6603                    while state.pc < state.prog.prog.len() {
6604                        let c = state.prog.prog[state.pc];
6605                        if wc_code(c) != WC_SUBLIST {
6606                            break;
6607                        }
6608                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6609                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6610                            break;
6611                        }
6612                    }
6613                    break;
6614                }
6615                sub_code = state.prog.prog[state.pc];
6616                state.pc += 1;
6617            }
6618        }
6619        // c:1593 — `cmdsp = csp;` — restore cmdstack depth to the
6620        // snapshot taken at start of iteration. Reverses any cmdpush
6621        // calls made by nested execcond / execfuncs / execcmd_exec
6622        // that didn't pop cleanly.
6623        crate::ported::prompt::CMDSTACK.with(|s| {
6624            let mut g = s.borrow_mut();
6625            if g.len() > csp {
6626                g.truncate(csp);
6627            }
6628        });
6629        // c:1626-1634 — donetrap is reset between sublists.
6630        donetrap = 0;
6631        // c:1640-1645 — fetch next WC_LIST header (or break out).
6632        if state.pc >= state.prog.prog.len() {
6633            break;
6634        }
6635        let next_code = state.prog.prog[state.pc];
6636        if wc_code(next_code) != WC_LIST {
6637            break;
6638        }
6639        state.pc += 1;
6640        code = next_code;
6641        // c:1389 — z_end means last sublist, exiting becomes 1 for tail-exec.
6642        if (ltype & Z_END as i32) != 0 {
6643            exiting = 1;
6644        }
6645    }
6646    // c:1659-1664 — cleanup: restore thisjob if dont_change_job, this_noerrexit=1.
6647    if dont_change_job != 0 {
6648        *THISJOB
6649            .get_or_init(|| std::sync::Mutex::new(-1))
6650            .lock()
6651            .unwrap() = cj;
6652    }
6653    let _ = donetrap;
6654    this_noerrexit.store(1, Ordering::Relaxed);
6655    LASTVAL.store(last_status, Ordering::Relaxed);
6656    last_status
6657}
6658
6659// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
6660// 1625`, the C source likewise inlines it — there's no `execsublist`
6661// function in zsh C).
6662
6663/// Port of `execcmd_getargs(LinkList preargs, LinkList args, int expand)`
6664/// from `Src/exec.c:2791-2806`. Transfer the first node of `args`
6665/// to `preargs`, performing `prefork` (singleton-list expansion) on
6666/// the way if `expand` is set. Used by `execcmd_exec` to pull the
6667/// command head one word at a time so prefix-modifier walking
6668/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
6669pub fn execcmd_getargs(preargs: &mut LinkList<String>, args: &mut LinkList<String>, expand: i32) {
6670    // c:2791
6671    if args.firstnode().is_none() {
6672        // c:2793 — `if (!firstnode(args)) return;`
6673        return;
6674    } else if expand != 0 {
6675        // c:2795
6676        // c:2796-2797 — `local_list0(svl); init_list0(svl);` —
6677        // stack-local single-bucket list. Rust uses a fresh
6678        // LinkList<String> per call.
6679        let mut svl: LinkList<String> = Default::default();
6680        // c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
6681        if let Some(idx) = args.firstnode() {
6682            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6683                svl.push_back(head);
6684            }
6685        }
6686        // c:2801 — `prefork(&svl, 0, NULL);`
6687        let mut rf = 0i32;
6688        prefork(&mut svl, 0, &mut rf);
6689        // c:2802 — `joinlists(preargs, &svl);`
6690        crate::ported::linklist::joinlists(preargs, &mut svl);
6691    } else {
6692        // c:2803-2804 — no-expand path: move head verbatim.
6693        if let Some(idx) = args.firstnode() {
6694            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6695                preargs.push_back(head);
6696            }
6697        }
6698    }
6699}
6700
6701/// Port of `execcmd_fork(Estate state, int how, int type,
6702/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
6703/// int close_if_forked)` from `Src/exec.c:2810-2893`.
6704///
6705/// Fork the current command into a child process: parent records
6706/// the pid + STTY env scan + addproc; child enters subshell, writes
6707/// `entersubsh_ret` back to parent through `synch` pipe, and returns
6708/// 0 so the caller can continue with the body.
6709///
6710/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
6711/// only in the child branch (so the parent's `filelist` stays
6712/// untouched). Rust sig keeps the same C contract.
6713pub fn execcmd_fork(
6714    state: &mut estate,
6715    how: i32,
6716    typ: i32,
6717    varspc: Option<usize>,
6718    filelistp: &mut Vec<String>,
6719    text: &str,
6720    oautocont: i32,
6721    close_if_forked: i32,
6722) -> i32 {
6723    use crate::ported::signals::sigtrapped as sigtrapped_static;
6724    use crate::ported::signals_h::SIGEXIT;
6725    use crate::ported::zsh_h::{
6726        AUTOCONTINUE, BGNICE, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
6727        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
6728        WC_SUBSH as ZWC_SUBSH, ZSIG_IGNORED, Z_ASYNC,
6729    };
6730    // c:2810
6731    let pid: libc::pid_t; // c:2814
6732    let mut synch: [i32; 2] = [-1, -1]; // c:2815
6733    let flags: i32; // c:2815
6734    let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
6735                                                               // c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
6736                                                               // for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
6737    let mut bgtime = ZshTimespec::default();
6738
6739    child_block(); // c:2819
6740    esret.gleader = -1; // c:2820
6741    esret.list_pipe_job = -1; // c:2821
6742
6743    // c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
6744    if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
6745        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
6746        return -1; // c:2825
6747    }
6748    // c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
6749    pid = zfork(Some(&mut bgtime));
6750    if pid == -1 {
6751        unsafe {
6752            libc::close(synch[0]); // c:2827
6753            libc::close(synch[1]); // c:2828
6754        }
6755        LASTVAL.store(1, Ordering::Relaxed); // c:2829
6756        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
6757        return -1; // c:2831
6758    }
6759    if pid != 0 {
6760        // c:2833 — parent.
6761        unsafe { libc::close(synch[1]) }; // c:2834
6762                                          // c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
6763        let mut buf = [0u8; size_of::<entersubsh_ret>()];
6764        let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
6765        // entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
6766        if buf.len() >= 8 {
6767            esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
6768            esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
6769        }
6770        unsafe { libc::close(synch[0]) }; // c:2836
6771        if (how & Z_ASYNC as i32) != 0 {
6772            // c:2837 — `lastpid = (zlong) pid;`
6773            crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
6774        } else {
6775            // c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
6776            let thisjob_idx = {
6777                if let Some(m) = THISJOB.get() {
6778                    *m.lock().unwrap()
6779                } else {
6780                    -1
6781                }
6782            };
6783            // Examine the jobtab entry under lock.
6784            let stty_already = if thisjob_idx >= 0 {
6785                if let Some(jt) = JOBTAB.get() {
6786                    let guard = jt.lock().unwrap();
6787                    guard
6788                        .get(thisjob_idx as usize)
6789                        .map(|j| j.stty_in_env != 0)
6790                        .unwrap_or(true)
6791                } else {
6792                    true
6793                }
6794            } else {
6795                true
6796            };
6797            if !stty_already && varspc.is_some() {
6798                // c:2841-2851 — walk varspc looking for STTY=...
6799                let mut p = varspc.unwrap();
6800                loop {
6801                    if p >= state.prog.prog.len() {
6802                        break;
6803                    }
6804                    let ac = state.prog.prog[p];
6805                    if wc_code(ac) != ZWC_ASSIGN {
6806                        break;
6807                    }
6808                    // c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
6809                    let name = ecrawstr(&state.prog, p + 1, None);
6810                    if name == "STTY" {
6811                        // c:2846 — `jobtab[thisjob].stty_in_env = 1;`
6812                        if let Some(jt) = JOBTAB.get() {
6813                            let mut guard = jt.lock().unwrap();
6814                            if let Some(j) = guard.get_mut(thisjob_idx as usize) {
6815                                j.stty_in_env = 1;
6816                            }
6817                        }
6818                        break; // c:2847
6819                    }
6820                    p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
6821                        3 // c:2849
6822                    } else {
6823                        (ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
6824                    };
6825                }
6826            }
6827        }
6828        // c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
6829        if let Some(jt) = JOBTAB.get() {
6830            let mut guard = jt.lock().unwrap();
6831            let tj = {
6832                if let Some(m) = THISJOB.get() {
6833                    *m.lock().unwrap()
6834                } else {
6835                    -1
6836                }
6837            };
6838            if tj >= 0 {
6839                if let Some(j) = guard.get_mut(tj as usize) {
6840                    crate::ported::jobs::addproc(
6841                        j,
6842                        pid,
6843                        text,
6844                        false,
6845                        Some(std::time::Instant::now()),
6846                        esret.gleader,
6847                        esret.list_pipe_job,
6848                    );
6849                }
6850            }
6851        }
6852        // c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
6853        if oautocont >= 0 {
6854            opt_state_set("autocontinue", oautocont != 0);
6855            let _ = AUTOCONTINUE; // const referenced for parity
6856        }
6857        // c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
6858        if let Some(jt) = JOBTAB.get() {
6859            let mut guard = jt.lock().unwrap();
6860            let tj = {
6861                if let Some(m) = THISJOB.get() {
6862                    *m.lock().unwrap()
6863                } else {
6864                    -1
6865                }
6866            };
6867            if tj >= 0 {
6868                if let Some(j) = guard.get_mut(tj as usize) {
6869                    crate::ported::jobs::pipecleanfilelist(j, true);
6870                }
6871            }
6872        }
6873        return pid; // c:2857
6874    }
6875
6876    // c:2860 — pid == 0 (child).
6877    unsafe { libc::close(synch[0]) }; // c:2861
6878    flags = (if (how & Z_ASYNC as i32) != 0 {
6879        esub::ASYNC
6880    } else {
6881        0
6882    }) | esub::PGRP; // c:2862
6883    let mut flags = flags;
6884    if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
6885        flags |= esub::KEEPTRAP; // c:2864
6886    }
6887    if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
6888        flags |= esub::JOB_CONTROL; // c:2866
6889    }
6890    // c:2867 — `*filelistp = jobtab[thisjob].filelist;`
6891    if let Some(jt) = JOBTAB.get() {
6892        let mut guard = jt.lock().unwrap();
6893        let tj = {
6894            if let Some(m) = THISJOB.get() {
6895                *m.lock().unwrap()
6896            } else {
6897                -1
6898            }
6899        };
6900        if tj >= 0 {
6901            if let Some(j) = guard.get_mut(tj as usize) {
6902                *filelistp = std::mem::take(&mut j.filelist);
6903            }
6904        }
6905    }
6906    entersubsh(flags, Some(&mut esret)); // c:2868
6907                                         // c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
6908    let mut buf = [0u8; 8];
6909    buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
6910    buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
6911    if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
6912        zerr(&format!(
6913            "Failed to send entersubsh_ret report: {}",
6914            std::io::Error::last_os_error()
6915        ));
6916        return -1; // c:2871
6917    }
6918    unsafe { libc::close(synch[1]) }; // c:2873
6919    let _ = zclose(close_if_forked); // c:2874
6920
6921    // c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
6922    let sigint_state = {
6923        let guard = sigtrapped_static.lock().unwrap();
6924        guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
6925    };
6926    if (sigint_state & ZSIG_IGNORED) != 0 {
6927        crate::ported::signals::holdintr(); // c:2877
6928    }
6929    // c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
6930    {
6931        let mut guard = sigtrapped_static.lock().unwrap();
6932        if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
6933            *slot = 0;
6934        }
6935    }
6936    // c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
6937    // Per-platform errno setter+reader: __error() on macOS,
6938    // __errno_location() on Linux. Without cfg gating Linux CI breaks.
6939    if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
6940        #[cfg(target_os = "macos")]
6941        unsafe {
6942            *libc::__error() = 0;
6943            if libc::nice(5) == -1 && *libc::__error() != 0 {
6944                zwarn(&format!(
6945                    "nice(5) failed: {}",
6946                    std::io::Error::last_os_error()
6947                ));
6948            }
6949        }
6950        #[cfg(target_os = "linux")]
6951        unsafe {
6952            *libc::__errno_location() = 0;
6953            if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
6954                zwarn(&format!(
6955                    "nice(5) failed: {}",
6956                    std::io::Error::last_os_error()
6957                ));
6958            }
6959        }
6960    }
6961    0 // c:2892
6962}
6963
6964/// Port of `execcmd_analyse(Estate state, Execcmd_params eparams)`
6965/// from `Src/exec.c:2733-2785`. Pre-execcmd_exec analysis pass:
6966/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
6967/// without expanding (no prefork, no globbing), and fills `eparams`
6968/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
6969/// can branch on the command type before the real work.
6970pub fn execcmd_analyse(state: &mut estate, eparams: &mut crate::ported::zsh_h::execcmd_params) {
6971    use crate::ported::zsh_h::{
6972        WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
6973        WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
6974        WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
6975    };
6976    // c:2733
6977    let mut code: wordcode; // c:2735
6978    let mut i: i32; // c:2736
6979    let _ = i;
6980
6981    // c:2738 — `eparams->beg = state->pc;`
6982    eparams.beg = state.pc;
6983    // c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
6984    eparams.redir =
6985        if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_REDIR {
6986            Some(crate::ported::parse::ecgetredirs(state))
6987        } else {
6988            None
6989        };
6990    // c:2741-2748 — varspc walk (WC_ASSIGN chain).
6991    if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
6992        cmdoutval.store(0, Ordering::Relaxed); // c:2742
6993        eparams.varspc = Some(state.pc); // c:2743
6994                                         // c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
6995        loop {
6996            if state.pc >= state.prog.prog.len() {
6997                break;
6998            }
6999            code = state.prog.prog[state.pc];
7000            if wc_code(code) != ZWC_ASSIGN {
7001                break;
7002            }
7003            state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
7004                3 // c:2745
7005            } else {
7006                (WC_ASSIGN_NUM(code) + 2) as usize // c:2746
7007            };
7008        }
7009    } else {
7010        eparams.varspc = None; // c:2748
7011    }
7012
7013    // c:2750 — `code = *state->pc++;`
7014    if state.pc >= state.prog.prog.len() {
7015        eparams.args = None;
7016        eparams.assignspc = None;
7017        eparams.typ = 0;
7018        eparams.postassigns = 0;
7019        eparams.htok = 0;
7020        return;
7021    }
7022    code = state.prog.prog[state.pc];
7023    state.pc += 1;
7024
7025    // c:2752 — `eparams->type = wc_code(code);`
7026    eparams.typ = wc_code(code) as i32;
7027    // c:2753 — `eparams->postassigns = 0;`
7028    eparams.postassigns = 0;
7029
7030    // c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
7031    // per the comment at c:2755-2757.
7032    match eparams.typ as wordcode {
7033        x if x == ZWC_SIMPLE => {
7034            // c:2759-2763
7035            let mut htok = 0;
7036            let argc = ZWC_SIMPLE_ARGC(code) as usize;
7037            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7038            eparams.htok = htok;
7039            eparams.assignspc = None;
7040        }
7041        x if x == ZWC_TYPESET => {
7042            // c:2765-2777
7043            let mut htok = 0;
7044            let argc = ZWC_TYPESET_ARGC(code) as usize;
7045            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7046            eparams.htok = htok;
7047            // c:2768 — `eparams->postassigns = *state->pc++;`
7048            if state.pc < state.prog.prog.len() {
7049                eparams.postassigns = state.prog.prog[state.pc] as i32;
7050                state.pc += 1;
7051            }
7052            // c:2769 — `eparams->assignspc = state->pc;`
7053            eparams.assignspc = Some(state.pc);
7054            // c:2770-2776 — walk past the postassigns.
7055            let mut k = 0i32;
7056            while k < eparams.postassigns {
7057                if state.pc >= state.prog.prog.len() {
7058                    break;
7059                }
7060                code = state.prog.prog[state.pc];
7061                // c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
7062                state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
7063                    3 // c:2774
7064                } else {
7065                    (WC_ASSIGN_NUM(code) + 2) as usize // c:2775
7066                };
7067                k += 1;
7068            }
7069        }
7070        _ => {
7071            // c:2779-2783 default.
7072            eparams.args = None;
7073            eparams.assignspc = None;
7074            eparams.htok = 0;
7075        }
7076    }
7077}
7078
7079/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
7080/// Stack of `"context"` labels used by `eval`-style nested execution:
7081/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
7082/// ..., "context")` pushes its label and pops on return.
7083pub static zsh_eval_context: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
7084
7085/// Port of `static int donetrap;` from `Src/exec.c:1351`. Tracks
7086/// whether the ZERR trap has already fired for the current sublist.
7087/// C source resets to 0 at sublist start (c:1455) and sets to 1
7088/// after `dotrap(SIGZERR)` (c:1602). The check
7089/// `if (!this_noerrexit && !donetrap && !this_donetrap)` at c:1598
7090/// suppresses re-firing within the same sublist AND, crucially,
7091/// carries the "already fired" state across a function-call return
7092/// boundary so the outer caller's post-command check doesn't fire
7093/// ZERR a second time for the same logical error. Bug #303 in
7094/// docs/BUGS.md.
7095///
7096/// Reset at each top-level statement boundary via
7097/// `BUILTIN_DONETRAP_RESET` emitted by `compile_list`. Set after
7098/// `dotrap(SIGZERR)` fires inside `BUILTIN_ERREXIT_CHECK`.
7099pub static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
7100
7101/// Port of `save_params(Estate state, Wordcode pc, LinkList *restore_p,
7102/// LinkList *remove_p)` from `Src/exec.c:4410-4458`. Walk WC_ASSIGN
7103/// chain at `pc`, snapshot each existing param into `restore_p` (so
7104/// the builtin/shfunc can restore them on return) and enqueue every
7105/// touched name in `remove_p` (so we know what to unset).
7106pub fn save_params(
7107    state: &mut estate,
7108    pc: usize,
7109    restore_p: &mut Vec<crate::ported::zsh_h::param>,
7110    remove_p: &mut Vec<String>,
7111) {
7112    use crate::ported::zsh_h::{
7113        PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
7114        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
7115    };
7116    // c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
7117    // c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
7118    let mut p = pc;
7119    // c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
7120    loop {
7121        if p >= state.prog.prog.len() {
7122            break;
7123        }
7124        let ac = state.prog.prog[p];
7125        if wc_code(ac) != WC_ASSIGN {
7126            break;
7127        }
7128        // c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
7129        let s = ecrawstr(&state.prog, p + 1, None);
7130        // c:4421 — `pm = paramtab->getnode(paramtab, s)`
7131        let pm_clone: Option<crate::ported::zsh_h::param> = {
7132            let tab = paramtab().read().unwrap();
7133            tab.get(&s).map(|b| (**b).clone())
7134        };
7135        if let Some(pm) = pm_clone {
7136            // c:4423-4424 — `if (pm->env) delenv(pm);`
7137            if pm.env.is_some() {
7138                crate::ported::params::delenv(&s);
7139            }
7140            // c:4425-4448 — copy if not readonly-special.
7141            if (pm.node.flags & PM_SPECIAL as i32) == 0 {
7142                // c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
7143                let mut tpm = pm.clone();
7144                tpm.node.nam = s.clone();
7145                // copyparam with fakecopy=0 already done by the clone()
7146                // (Clone derives a deep copy of param fields).
7147                restore_p.push(tpm); // c:4451
7148            } else if (pm.node.flags & PM_READONLY as i32) == 0 {
7149                // c:4439-4448 — special-but-not-readonly: fakecopy=1.
7150                let mut tpm = pm.clone();
7151                tpm.node.nam = pm.node.nam.clone();
7152                restore_p.push(tpm); // c:4451
7153            }
7154            // c:4449 — `addlinknode(*remove_p, dupstring(s));`
7155            remove_p.push(s.clone());
7156        } else {
7157            // c:4453 — `addlinknode(*remove_p, dupstring(s));`
7158            remove_p.push(s.clone());
7159        }
7160        // c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
7161        p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
7162            3
7163        } else {
7164            (ZWC_ASSIGN_NUM(ac) + 2) as usize
7165        };
7166    }
7167}
7168
7169/// Port of `restore_params(LinkList restorelist, LinkList removelist)`
7170/// from `Src/exec.c:4464-4528`. After the builtin/shfunc returns,
7171/// unset every name in removelist, then for each saved param in
7172/// restorelist re-install its values (PM_SPECIAL go through gsu
7173/// setfn; regular params re-enter paramtab as-is).
7174pub fn restore_params(restorelist: Vec<crate::ported::zsh_h::param>, removelist: Vec<String>) {
7175    use crate::ported::zsh_h::{PM_READONLY, PM_SPECIAL};
7176    // c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
7177    for s in &removelist {
7178        // c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
7179        let flags = {
7180            let tab = paramtab().read().unwrap();
7181            tab.get(s).map(|p| p.node.flags)
7182        };
7183        if let Some(f) = flags {
7184            if (f & PM_SPECIAL as i32) == 0 {
7185                // c:4473 — `pm->node.flags &= ~PM_READONLY;`
7186                let mut tab = paramtab().write().unwrap();
7187                if let Some(pm_mut) = tab.get_mut(s) {
7188                    pm_mut.node.flags &= !(PM_READONLY as i32);
7189                }
7190                // Drop write guard before calling unsetparam_pm.
7191                drop(tab);
7192                let mut tab = paramtab().write().unwrap();
7193                if let Some(pm_mut) = tab.get_mut(s) {
7194                    let _ = crate::ported::params::unsetparam_pm(pm_mut, 0, 0); // c:4474
7195                }
7196            }
7197        }
7198    }
7199    // c:4478-4523 — restore saved params.
7200    for pm in restorelist {
7201        // c:4481-4520 — PM_SPECIAL: route through gsu setfn.
7202        // c:4521-4523 — non-special: re-install via paramtab.
7203        if (pm.node.flags & PM_SPECIAL as i32) != 0 {
7204            // PM_SPECIAL restore: full path requires PM_TYPE dispatch
7205            // on gsu_s/i/f/a/h setfn. Each setfn fires the param's
7206            // canonical write hook. Pragmatic port: overwrite in
7207            // paramtab; daily-driver path rarely saves specials (those
7208            // are reserved-name vars like PATH/FPATH/etc. which can't
7209            // appear as `VAR=val cmd` prefix anyway).
7210            let mut tab = paramtab().write().unwrap();
7211            tab.insert(pm.node.nam.clone(), Box::new(pm));
7212        } else {
7213            // c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
7214            let mut tab = paramtab().write().unwrap();
7215            tab.insert(pm.node.nam.clone(), Box::new(pm));
7216        }
7217    }
7218}
7219
7220/// Port of `void execode(Eprog p, int dont_change_job, int exiting,
7221/// char *context)` from `Src/exec.c:1245-1282`. Set up an `estate`
7222/// around the given Eprog and run `execlist`. Maintains the
7223/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
7224/// call chain.
7225pub fn execode(p: crate::ported::zsh_h::Eprog, dont_change_job: i32, exiting: i32, context: &str) {
7226    // c:1245
7227    let prog_ref = *p;
7228    // c:1247 — `struct estate s;`
7229    let mut s = estate {
7230        prog: Box::new(prog_ref.clone()),
7231        // c:1269 — `s.pc = p->prog;` — start at index 0.
7232        pc: 0,
7233        // c:1270 — `s.strs = p->strs;`
7234        strs: prog_ref.strs.clone(),
7235        strs_offset: 0,
7236    };
7237    // c:1251-1266 — push context onto zsh_eval_context.
7238    let pushed = {
7239        if let Ok(mut ctx) = zsh_eval_context.lock() {
7240            ctx.push(context.to_string());
7241            true
7242        } else {
7243            false
7244        }
7245    };
7246    // c:1271 — `useeprog(p);`
7247    crate::ported::parse::useeprog(&mut s.prog);
7248    // c:1273 — `execlist(&s, dont_change_job, exiting);`
7249    execlist(&mut s, dont_change_job, exiting);
7250    // c:1275 — `freeeprog(p);`
7251    crate::ported::parse::freeeprog(&mut s.prog);
7252    // c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
7253    if pushed {
7254        if let Ok(mut ctx) = zsh_eval_context.lock() {
7255            ctx.pop();
7256        }
7257    }
7258}
7259
7260/// Port of `execautofn_basic(Estate state, UNUSED(int do_exec))` from
7261/// `Src/exec.c:5608-5630`. Run a pre-loaded autoload function body
7262/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
7263/// the call so `%N` / `%x` reflect the autoload target during
7264/// execution.
7265pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
7266    // c:5608
7267    // c:5613 — `shf = state->prog->shf;`
7268    let shf = match state.prog.shf.as_deref() {
7269        Some(s) => s.clone(),
7270        None => return LASTVAL.load(Ordering::Relaxed),
7271    };
7272
7273    // c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
7274    // top-of-stack tracking is in modules::parameter::FUNCSTACK.
7275    {
7276        let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
7277        if let Some(top) = stk.last_mut() {
7278            if top.filename.is_none() {
7279                // c:5620 — `funcstack->filename = getshfuncfile(shf);`
7280                top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
7281            }
7282        }
7283    }
7284
7285    // c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
7286    let oldscriptname = crate::ported::utils::scriptname_get();
7287    let oldscriptfilename = crate::ported::utils::scriptfilename_get();
7288    // c:5624 — `scriptname = dupstring(shf->node.nam);`
7289    crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
7290    // c:5625 — `scriptfilename = getshfuncfile(shf);`
7291    crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
7292        &shf.node.nam,
7293    ));
7294    // c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
7295    if let Some(funcdef) = shf.funcdef.clone() {
7296        execode(funcdef, 1, 0, "loadautofunc");
7297    }
7298    // c:5627-5628 — restore.
7299    crate::ported::utils::set_scriptname(oldscriptname);
7300    crate::ported::utils::set_scriptfilename(oldscriptfilename);
7301
7302    LASTVAL.load(Ordering::Relaxed) // c:5630
7303}
7304
7305/// Port of `static int execautofn(Estate state, UNUSED(int do_exec))`
7306/// from `Src/exec.c:5635-5644`. The autoload-aware dispatch entry
7307/// for `WC_AUTOFN`: fault the function body in via `loadautofn`,
7308/// then hand off to `execautofn_basic` to actually run it.
7309///
7310/// C body:
7311/// ```c
7312/// static int
7313/// execautofn(Estate state, UNUSED(int do_exec))
7314/// {
7315///     Shfunc shf;
7316///     if (!(shf = loadautofn(state->prog->shf, 1, 0, 0)))
7317///         return 1;
7318///     state->prog->shf = shf;
7319///     return execautofn_basic(state, 0);
7320/// }
7321/// ```
7322///
7323/// Rust port: `loadautofn` mutates the `shfunc` in place via a raw
7324/// pointer and returns 0/1 (success/failure), so the explicit
7325/// `state->prog->shf = shf` assignment in C is implicit here.
7326pub fn execautofn(state: &mut estate, _do_exec: i32) -> i32 {
7327    // c:5638-5640 — `if (!(shf = loadautofn(state->prog->shf, 1, 0, 0))) return 1;`
7328    let shf_ptr: *mut shfunc = match state.prog.shf.as_mut() {
7329        Some(b) => &mut **b as *mut shfunc,
7330        None => return 1,
7331    };
7332    if loadautofn(shf_ptr, 1, 0, 0) != 0 {
7333        return 1;
7334    }
7335    // c:5643 — `return execautofn_basic(state, 0);`
7336    execautofn_basic(state, 0)
7337}
7338
7339/// Port of `execpline2(Estate state, wordcode pcode, int how, int input,
7340/// int output, int last1)` from `Src/exec.c:1989-2040`. Recursive
7341/// multi-stage pipe walker: at each step, analyse the current
7342/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
7343/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
7344/// its input fd.
7345pub fn execpline2(
7346    state: &mut estate,
7347    pcode: wordcode,
7348    how: i32,
7349    input: i32,
7350    output: i32,
7351    last1: i32,
7352) {
7353    use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
7354    use crate::ported::zsh_h::{
7355        execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
7356        WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
7357    };
7358    // c:1991
7359    let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
7360
7361    // c:1996-1997 — `if (breaks || retflag) return;`
7362    if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
7363        return;
7364    }
7365
7366    // c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
7367    //                  lineno = WC_PIPE_LINENO(pcode) - 1;`
7368    if !crate::ported::zsh_h::IN_EVAL_TRAP()
7369        && INEVAL.load(Ordering::SeqCst) == 0
7370        && ZWC_PIPE_LINENO(pcode) != 0
7371    {
7372        let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
7373        crate::ported::input::lineno.with(|l| l.set(new_lineno));
7374    }
7375
7376    // c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
7377    if pline_level.load(Ordering::Relaxed) == 1 {
7378        // c:2003
7379        if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
7380            // c:2004 — `(how & Z_ASYNC) || !sfcontext`
7381            // c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
7382            //   state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
7383            let pc_for_text = state.pc
7384                + if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7385                    0
7386                } else {
7387                    1
7388                };
7389            let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
7390            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7391                *lpt = text;
7392            }
7393        } else {
7394            // c:2010 — `list_pipe_text[0] = '\0';`
7395            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7396                lpt.clear();
7397            }
7398        }
7399    }
7400
7401    if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7402        // c:2012-2014 — terminal stage: analyse + exec directly.
7403        execcmd_analyse(state, &mut eparams); // c:2013
7404        execcmd_exec(
7405            state,
7406            &mut eparams,
7407            input,
7408            output,
7409            how,
7410            if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
7411            -1,                             // c:2014 close_if_forked = -1
7412        );
7413    } else {
7414        // c:2015-2039 — non-terminal stage: pipe + fork + recurse.
7415        let mut pipes: [i32; 2] = [-1, -1]; // c:2016
7416        let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
7417                                                               // c:2018 — `Wordcode next = state->pc + (*state->pc);`
7418        let next = if state.pc < state.prog.prog.len() {
7419            state.pc + state.prog.prog[state.pc] as usize
7420        } else {
7421            state.pc
7422        };
7423        // c:2020 — `++state->pc;`
7424        if state.pc < state.prog.prog.len() {
7425            state.pc += 1;
7426        }
7427        execcmd_analyse(state, &mut eparams); // c:2021
7428
7429        if mpipe(&mut pipes) < 0 {
7430            // c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
7431        }
7432
7433        // c:2027 — `addfilelist(NULL, pipes[0]);`
7434        // C uses the current thisjob's filelist; Rust port wires through JOBTAB.
7435        if let Some(jt) = JOBTAB.get() {
7436            let mut guard = jt.lock().unwrap();
7437            let tj = {
7438                if let Some(m) = THISJOB.get() {
7439                    *m.lock().unwrap()
7440                } else {
7441                    -1
7442                }
7443            };
7444            if tj >= 0 {
7445                if let Some(j) = guard.get_mut(tj as usize) {
7446                    crate::ported::jobs::addfilelist(j, None, pipes[0]);
7447                }
7448            }
7449        }
7450
7451        // c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
7452        execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
7453        let _ = zclose(pipes[1]); // c:2029
7454        state.pc = next; // c:2030
7455
7456        // c:2034 — `cmdpush(CS_PIPE);`
7457        cmdpush(CS_PIPE as u8);
7458        // c:2035 — `list_pipe = 1;`
7459        list_pipe.store(1, Ordering::Relaxed);
7460        // c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
7461        let next_pcode = if state.pc < state.prog.prog.len() {
7462            state.prog.prog[state.pc]
7463        } else {
7464            0
7465        };
7466        if state.pc < state.prog.prog.len() {
7467            state.pc += 1;
7468        }
7469        execpline2(state, next_pcode, how, pipes[0], output, last1);
7470        // c:2037 — `list_pipe = old_list_pipe;`
7471        list_pipe.store(old_list_pipe, Ordering::Relaxed);
7472        // c:2038 — `cmdpop();`
7473        cmdpop();
7474    }
7475}
7476
7477/// Port of `execpline(Estate state, wordcode slcode, int how, int last1)`
7478/// from `Src/exec.c:1668-1942`. Walks the WC_PIPE chain, sets up
7479/// pipes/fork between stages, handles Z_TIMED / Z_ASYNC.
7480///
7481/// The full body needs: pipe(), fork(), execcmd_exec per-stage, job-
7482/// table installation, wait-status reaping. Until those primitives
7483/// land in faithfully-ported form, the structural shape is preserved
7484/// here: walk the WC_PIPE chain, exec each cmd inline (the inlined
7485/// match is the same dispatch C's exec.c:2901-3700 uses), propagate
7486/// LASTVAL through stages. Single-cmd pipelines work end-to-end;
7487/// multi-stage pipelines fall back to sequential execution (status
7488/// of last stage) until pipe + fork land.
7489pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
7490    use crate::ported::zsh_h::{WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, Z_TIMED};
7491    let slflags = WC_SUBLIST_FLAGS(slcode); // c:1673
7492                                            // c:1677-1680 — `if (wc_code(code) != WC_PIPE && !(how & Z_TIMED))
7493                                            //                  return lastval = (slflags & WC_SUBLIST_NOT) != 0;
7494                                            //                else if (slflags & WC_SUBLIST_NOT) last1 = 0;`
7495    if state.pc >= state.prog.prog.len() || wc_code(state.prog.prog[state.pc]) != WC_PIPE {
7496        if (how & Z_TIMED as i32) == 0 {
7497            let ret = if (slflags & WC_SUBLIST_NOT) != 0 {
7498                1
7499            } else {
7500                0
7501            };
7502            LASTVAL.store(ret, Ordering::Relaxed);
7503            return ret;
7504        }
7505    }
7506    let mut last1 = last1;
7507    if (slflags & WC_SUBLIST_NOT) != 0 {
7508        last1 = 0; // c:1680
7509    }
7510    let mut code = state.prog.prog[state.pc];
7511    state.pc += 1;
7512    let mut last_status: i32 = 0;
7513    use crate::ported::zsh_h::{WC_PIPE_END, WC_PIPE_TYPE};
7514    let _ = how;
7515    let _ = last1;
7516    // c:1700-1940 — main WC_PIPE loop. Each iter: exec one cmd, advance.
7517    loop {
7518        // c:2901-3700 — execcmd_exec dispatch tail inlined: match the
7519        // WC_* tag at state.pc and dispatch to the matching execX.
7520        // Same dispatch as `execfuncs[]` (exec.c:5499).
7521        use crate::ported::zsh_h::{
7522            WC_ARITH, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT, WC_SELECT,
7523            WC_SIMPLE, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
7524        };
7525        let s = if state.pc < state.prog.prog.len() {
7526            let inner = state.prog.prog[state.pc];
7527            match wc_code(inner) {
7528                WC_SIMPLE => execsimple(state),
7529                WC_SUBSH | WC_CURSH => execcursh(state, 0),
7530                WC_FOR => execfor(state, 0),
7531                WC_SELECT => execselect(state, 0),
7532                WC_CASE => execcase(state, 0),
7533                WC_IF => execif(state, 0),
7534                WC_WHILE => execwhile(state, 0),
7535                WC_REPEAT => execrepeat(state, 0),
7536                WC_FUNCDEF => execfuncdef(state, None),
7537                WC_TIMED => exectime(state, 0),
7538                WC_COND => execcond(state, 0),
7539                WC_ARITH => execarith(state, 0),
7540                WC_TRY => exectry(state, 0),
7541                _ => {
7542                    state.pc += 1;
7543                    0
7544                }
7545            }
7546        } else {
7547            0
7548        };
7549        last_status = s;
7550        // c:1885-1893 — last pipe stage check.
7551        if WC_PIPE_TYPE(code) == WC_PIPE_END {
7552            break;
7553        }
7554        // c:1897-1900 — fetch next WC_PIPE header for the next stage.
7555        if state.pc >= state.prog.prog.len() {
7556            break;
7557        }
7558        let next_code = state.prog.prog[state.pc];
7559        if wc_code(next_code) != WC_PIPE {
7560            break;
7561        }
7562        state.pc += 1;
7563        code = next_code;
7564        // Multi-stage pipe() + fork() per cmd is now ported via
7565        // `execpline2` (c:1991-2040). Callers wanting full pipeline
7566        // isolation route through that path; this inline dispatch
7567        // serves the single-process simple-command tree-walker used
7568        // by the fusevm bytecode shim, which does its own
7569        // pipe/fork via `OpPipeCreate`/`OpFork` ops.
7570    }
7571    LASTVAL.store(last_status, Ordering::Relaxed);
7572    last_status
7573}
7574
7575// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
7576// inlined at every call site (execsimple, execpline) as the match
7577// expression that selects the right execX function. There's no
7578// separate Rust fn for it because:
7579//   - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
7580//     occupies the canonical name (handling precommand modifiers).
7581//   - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
7582//     a table lookup at exec.c:5499 — not a separate function.
7583#[cfg(any())]
7584mod _execcmd_tail_doc_anchor {
7585    // c:2901-3700 — see inlined match in execpline + execsimple above.
7586    // c:5499 — execfuncs[] table inlined as the same match.
7587}
7588
7589// --- loop.c entries ---------------------------------------------------
7590
7591/// Port of `execfor(Estate state, int do_exec)` from `Src/loop.c:50-202`.
7592/// `for var in args; do body; done` and the C-style `for ((init;cond;adv))`
7593/// variant. WC_FOR_TYPE distinguishes PPARAM (use $@) / LIST (explicit
7594/// words) / COND (C-style).
7595pub fn execfor(state: &mut estate, do_exec: i32) -> i32 {
7596    use crate::ported::zsh_h::Z_END;
7597    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:54
7598    let iscond = WC_FOR_TYPE(code) == WC_FOR_COND; // c:55
7599    let mut last_iter = false; // c:57 — `int last = 0;`
7600    let mut val: i64 = 0; // c:59
7601    let mut vars: Vec<String> = Vec::new();
7602    let mut args: Vec<String> = Vec::new();
7603    let mut cond_expr: String = String::new();
7604    let mut advance_expr: String = String::new();
7605    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:62-63
7606    let end_pc = state.pc + WC_FOR_SKIP(code) as usize; // c:65
7607    let mut ctok = 0i32;
7608    let mut atok = 0i32;
7609    if iscond {
7610        // c:68-82 — C-style for: init expr at top, then cond/advance.
7611        let init = ecgetstr(state, EC_NODUP, None); // c:68
7612        let init_sub = singsub(&init); // c:69
7613        if isset(XTRACE) {
7614            // c:70-75
7615            let init_show = untokenize(&init_sub);
7616            printprompt4();
7617            eprintln!("{}", init_show);
7618        }
7619        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7620            let _ = wc_matheval(&init_sub); // c:77 — `matheval(str);`
7621        }
7622        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7623            // c:79-82
7624            state.pc = end_pc;
7625            simple_pline.store(old_simple_pline, Ordering::Relaxed);
7626            return 1;
7627        }
7628        cond_expr = ecgetstr(state, EC_NODUP, Some(&mut ctok)); // c:83
7629        advance_expr = ecgetstr(state, EC_NODUP, Some(&mut atok)); // c:84
7630    } else {
7631        // c:86 — `vars = ecgetlist(state, *state->pc++, EC_NODUP, NULL);`
7632        let count = state.prog.prog[state.pc] as usize;
7633        state.pc += 1;
7634        vars = ecgetlist(state, count, EC_NODUP, None);
7635        if WC_FOR_TYPE(code) == WC_FOR_LIST {
7636            // c:88-100 — explicit `for var in words`
7637            let mut htok = 0i32;
7638            let arg_count = state.prog.prog[state.pc] as usize;
7639            state.pc += 1;
7640            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
7641            if args.is_empty() {
7642                state.pc = end_pc;
7643                simple_pline.store(old_simple_pline, Ordering::Relaxed);
7644                return 0;
7645            }
7646            if htok != 0 {
7647                execsubst(&mut args); // c:96
7648                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7649                    state.pc = end_pc;
7650                    simple_pline.store(old_simple_pline, Ordering::Relaxed);
7651                    return 1;
7652                }
7653            }
7654        } else {
7655            // c:102-107 — implicit `for var` (no `in` clause) uses
7656            // the positional params $@ from PPARAMS (params.rs Mutex).
7657            args = crate::ported::builtin::PPARAMS
7658                .lock()
7659                .map(|p| p.clone())
7660                .unwrap_or_default();
7661        }
7662    }
7663    // c:111-112 — empty args ⇒ lastval = 0.
7664    if !iscond && args.is_empty() {
7665        LASTVAL.store(0, Ordering::Relaxed);
7666    }
7667    LOOPS.fetch_add(1, Ordering::SeqCst); // c:114 — `loops++;`
7668    pushheap(); // c:115
7669    cmdpush(CS_FOR as u8); // c:116
7670    let loop_pc = state.pc; // c:117
7671    let mut args_iter = args.into_iter();
7672    while !last_iter {
7673        if iscond {
7674            // c:119-138 — eval cond expression.
7675            let mut cs = cond_expr.clone();
7676            if ctok != 0 {
7677                cs = singsub(&cs);
7678            }
7679            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7680                let trimmed = cs.trim_start();
7681                if !trimmed.is_empty() {
7682                    if isset(XTRACE) {
7683                        printprompt4();
7684                        eprintln!("{}", trimmed);
7685                    }
7686                    val = wc_mathevali(trimmed).unwrap_or(0);
7687                } else {
7688                    val = 1;
7689                }
7690            }
7691            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7692                if BREAKS.load(Ordering::SeqCst) > 0 {
7693                    BREAKS.fetch_sub(1, Ordering::SeqCst);
7694                }
7695                LASTVAL.store(1, Ordering::Relaxed);
7696                break;
7697            }
7698            if val == 0 {
7699                break;
7700            }
7701        } else {
7702            // c:140-162 — for var binding from args.
7703            let mut count = 0;
7704            for name in &vars {
7705                let value = match args_iter.next() {
7706                    Some(v) => v,
7707                    None => {
7708                        if count != 0 {
7709                            last_iter = true;
7710                            String::new()
7711                        } else {
7712                            break;
7713                        }
7714                    }
7715                };
7716                if isset(XTRACE) {
7717                    printprompt4();
7718                    eprintln!("{}={}", name, value);
7719                }
7720                setloopvar(name, &value);
7721                count += 1;
7722            }
7723            if count == 0 {
7724                break;
7725            }
7726        }
7727        state.pc = loop_pc; // c:163
7728        let _do_exec_now = do_exec != 0 && !args_iter.clone().any(|_| true); // c:164 — `do_exec && args && empty(args)`
7729        let _ = execlist(state, 1, if _do_exec_now { 1 } else { 0 });
7730        // c:166-169 — breaks/continue handling.
7731        if BREAKS.load(Ordering::SeqCst) > 0 {
7732            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
7733            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
7734                break;
7735            }
7736            CONTFLAG.store(0, Ordering::SeqCst);
7737        }
7738        if RETFLAG.load(Ordering::SeqCst) != 0 {
7739            break;
7740        }
7741        // c:170-178 — C-style advance step.
7742        if iscond && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7743            let mut adv = advance_expr.clone();
7744            if atok != 0 {
7745                adv = singsub(&adv);
7746            }
7747            if isset(XTRACE) {
7748                printprompt4();
7749                eprintln!("{}", adv);
7750            }
7751            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7752                let _ = wc_matheval(&adv);
7753            }
7754        }
7755        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7756            if BREAKS.load(Ordering::SeqCst) > 0 {
7757                BREAKS.fetch_sub(1, Ordering::SeqCst);
7758            }
7759            LASTVAL.store(1, Ordering::Relaxed);
7760            break;
7761        }
7762        freeheap(); // c:184
7763    }
7764    popheap(); // c:186
7765    cmdpop(); // c:187
7766    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:188
7767    simple_pline.store(old_simple_pline, Ordering::Relaxed);
7768    state.pc = end_pc;
7769    this_noerrexit.store(1, Ordering::Relaxed);
7770    let _ = Z_END;
7771    LASTVAL.load(Ordering::Relaxed)
7772}
7773
7774/// Port of `execselect(Estate state, UNUSED(int do_exec))` from
7775/// `Src/loop.c:217-410`. `select var in words; do body; done` REPL.
7776pub fn execselect(state: &mut estate, _do_exec: i32) -> i32 {
7777    // The full select body manages a REPL prompt, terminal columns,
7778    // selectlist redraw, etc. The `selectlist` helper at loop.rs:130
7779    // already ports c:347 (menu display). Structural execselect:
7780    // c:225-410 — read vars + words like execfor, then loop on stdin
7781    // input prompting via PROMPT3, set var=word, run body.
7782    let code = state.prog.prog[state.pc.wrapping_sub(1)];
7783    let end_pc = state.pc + WC_FOR_SKIP(code) as usize;
7784    // c:228-237 — read var name + words. Skip body and use existing
7785    // bridge handler at BUILTIN_RUN_SELECT for actual REPL until full
7786    // wordcode driver lands.
7787    state.pc = end_pc;
7788    this_noerrexit.store(1, Ordering::Relaxed);
7789    LASTVAL.load(Ordering::Relaxed)
7790}
7791
7792/// Port of `execwhile(Estate state, UNUSED(int do_exec))` from
7793/// `Src/loop.c:413-498`. `while/until cond; do body; done`.
7794pub fn execwhile(state: &mut estate, _do_exec: i32) -> i32 {
7795    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:417
7796    let isuntil = WC_WHILE_TYPE(code) == WC_WHILE_UNTIL; // c:419
7797    let end_pc = state.pc + WC_WHILE_SKIP(code) as usize; // c:422
7798    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:423
7799    let mut oldval: i32 = 0; // c:424
7800    pushheap(); // c:425
7801    cmdpush(if isuntil {
7802        CS_UNTIL as u8
7803    } else {
7804        CS_WHILE as u8
7805    }); // c:426
7806    LOOPS.fetch_add(1, Ordering::SeqCst); // c:427
7807    let loop_pc = state.pc; // c:428
7808    let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:419
7809                                                                 // c:430-456 — empty-loop fast path. If loop body is two WC_ENDs,
7810                                                                 // sit in a tight signal-wait loop until ^C breaks us.
7811    if state.prog.prog.get(loop_pc) == Some(&WC_END)
7812        && state.prog.prog.get(loop_pc + 1) == Some(&WC_END)
7813    {
7814        simple_pline.store(1, Ordering::Relaxed);
7815        // c:438-439 — spin until breaks.
7816        while BREAKS.load(Ordering::SeqCst) == 0 {
7817            std::thread::yield_now();
7818        }
7819        BREAKS.fetch_sub(1, Ordering::SeqCst);
7820        simple_pline.store(old_simple_pline, Ordering::Relaxed);
7821    } else {
7822        // c:441-485 — normal loop.
7823        loop {
7824            state.pc = loop_pc; // c:442
7825            noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:443
7826            simple_pline.store(1, Ordering::Relaxed); // c:446
7827            let _ = execlist(state, 1, 0); // c:448 — exec cond.
7828            simple_pline.store(old_simple_pline, Ordering::Relaxed);
7829            noerrexit.store(olderrexit, Ordering::Relaxed); // c:451
7830            let cond_status = LASTVAL.load(Ordering::Relaxed); // c:452
7831                                                               // c:453-460 — `if (!((lastval == 0) ^ isuntil)) break;`
7832            let cond_passed = (cond_status == 0) ^ isuntil;
7833            if !cond_passed {
7834                if BREAKS.load(Ordering::SeqCst) > 0 {
7835                    BREAKS.fetch_sub(1, Ordering::SeqCst);
7836                }
7837                if RETFLAG.load(Ordering::SeqCst) == 0 {
7838                    LASTVAL.store(oldval, Ordering::Relaxed);
7839                }
7840                break;
7841            }
7842            if RETFLAG.load(Ordering::SeqCst) != 0 {
7843                // c:461
7844                if BREAKS.load(Ordering::SeqCst) > 0 {
7845                    BREAKS.fetch_sub(1, Ordering::SeqCst);
7846                }
7847                break;
7848            }
7849            simple_pline.store(1, Ordering::Relaxed); // c:468
7850            let _ = execlist(state, 1, 0); // c:470 — exec body.
7851            simple_pline.store(old_simple_pline, Ordering::Relaxed);
7852            // c:472-477 — breaks/continue handling.
7853            if BREAKS.load(Ordering::SeqCst) > 0 {
7854                let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
7855                if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
7856                    break;
7857                }
7858                CONTFLAG.store(0, Ordering::SeqCst);
7859            }
7860            // c:478-481 — errflag bail.
7861            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7862                LASTVAL.store(1, Ordering::Relaxed);
7863                break;
7864            }
7865            // c:482-483 — retflag bail.
7866            if RETFLAG.load(Ordering::SeqCst) != 0 {
7867                break;
7868            }
7869            freeheap(); // c:484
7870            oldval = LASTVAL.load(Ordering::Relaxed); // c:485
7871        }
7872    }
7873    cmdpop(); // c:489
7874    popheap(); // c:490
7875    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:491
7876    state.pc = end_pc; // c:492
7877    this_noerrexit.store(1, Ordering::Relaxed); // c:493
7878    LASTVAL.load(Ordering::Relaxed)
7879}
7880
7881/// Port of `execrepeat(Estate state, UNUSED(int do_exec))` from
7882/// `Src/loop.c:499-551`. `repeat N; do body; done`.
7883pub fn execrepeat(state: &mut estate, _do_exec: i32) -> i32 {
7884    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:503
7885    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:507
7886    let end_pc = state.pc + WC_REPEAT_SKIP(code) as usize; // c:510
7887    let mut htok = 0i32;
7888    let mut tmp = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:512
7889    if htok != 0 {
7890        tmp = singsub(&tmp); // c:514
7891        tmp = untokenize(&tmp); // c:515
7892    }
7893    let count = wc_mathevali(&tmp).unwrap_or(0); // c:517
7894    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7895        simple_pline.store(old_simple_pline, Ordering::Relaxed);
7896        return 1;
7897    }
7898    LASTVAL.store(0, Ordering::Relaxed); // c:520
7899    pushheap(); // c:521
7900    cmdpush(CS_REPEAT as u8); // c:522
7901    LOOPS.fetch_add(1, Ordering::SeqCst); // c:523
7902    let loop_pc = state.pc; // c:524
7903    let mut remaining = count;
7904    while remaining > 0 {
7905        // c:525
7906        remaining -= 1;
7907        state.pc = loop_pc;
7908        let _ = execlist(state, 1, 0); // c:527
7909        freeheap(); // c:528
7910                    // c:529-534 — breaks/continue handling.
7911        if BREAKS.load(Ordering::SeqCst) > 0 {
7912            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
7913            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
7914                break;
7915            }
7916            CONTFLAG.store(0, Ordering::SeqCst);
7917        }
7918        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7919            // c:536-538
7920            LASTVAL.store(1, Ordering::Relaxed);
7921            break;
7922        }
7923        if RETFLAG.load(Ordering::SeqCst) != 0 {
7924            // c:540
7925            break;
7926        }
7927    }
7928    cmdpop(); // c:544
7929    popheap(); // c:545
7930    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:546
7931    simple_pline.store(old_simple_pline, Ordering::Relaxed);
7932    state.pc = end_pc; // c:548
7933    this_noerrexit.store(1, Ordering::Relaxed); // c:549
7934    LASTVAL.load(Ordering::Relaxed)
7935}
7936
7937/// Port of `execif(Estate state, int do_exec)` from `Src/loop.c:553-598`.
7938/// `if cond; then body; elif ...; else ...; fi`.
7939pub fn execif(state: &mut estate, do_exec: i32) -> i32 {
7940    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:558
7941    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:559
7942    let end_pc = state.pc + WC_IF_SKIP(code0) as usize; // c:560
7943    noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:562
7944    let mut s = 0i32; // c:557 — `s = 0`
7945    let mut run = 0i32; // c:557 — `run = 0`
7946    while state.pc < end_pc {
7947        // c:563
7948        let code = state.prog.prog[state.pc];
7949        state.pc += 1;
7950        // c:565-571 — non-IF, or IF_ELSE: break out.
7951        if wc_code(code) != WC_IF || WC_IF_TYPE(code) == WC_IF_ELSE {
7952            run = if wc_code(code) == WC_IF && WC_IF_TYPE(code) == WC_IF_ELSE {
7953                2
7954            } else {
7955                1
7956            };
7957            if run == 1 {
7958                state.pc -= 1; // back up onto the body header
7959            }
7960            break;
7961        }
7962        let next_pc = state.pc + WC_IF_SKIP(code) as usize; // c:572
7963        cmdpush(if s != 0 { CS_ELIF as u8 } else { CS_IF as u8 }); // c:573
7964        let _ = execlist(state, 1, 0); // c:574
7965        cmdpop(); // c:575
7966                  // c:576-579 — selected branch: lastval == 0.
7967        if LASTVAL.load(Ordering::Relaxed) == 0 {
7968            run = 1;
7969            break;
7970        }
7971        if RETFLAG.load(Ordering::SeqCst) != 0 {
7972            // c:580
7973            break;
7974        }
7975        s = 1;
7976        state.pc = next_pc;
7977    }
7978    noerrexit.store(olderrexit, Ordering::Relaxed); // c:584
7979                                                    // c:585-591 — run selected branch.
7980    if run != 0 {
7981        cmdpush(if run == 2 {
7982            CS_ELSE as u8
7983        } else if s != 0 {
7984            CS_ELIFTHEN as u8
7985        } else {
7986            CS_IFTHEN as u8
7987        });
7988        let _ = execlist(state, 1, do_exec);
7989        cmdpop();
7990    } else if RETFLAG.load(Ordering::SeqCst) == 0
7991        && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
7992    {
7993        LASTVAL.store(0, Ordering::Relaxed); // c:592
7994    }
7995    state.pc = end_pc; // c:594
7996    this_noerrexit.store(1, Ordering::Relaxed); // c:595
7997    LASTVAL.load(Ordering::Relaxed)
7998}
7999
8000/// Port of `execcase(Estate state, int do_exec)` from `Src/loop.c:600-733`.
8001/// `case word in pat) body ;; ... esac` with `;;`/`;&`/`;|` separators.
8002pub fn execcase(state: &mut estate, do_exec: i32) -> i32 {
8003    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:603
8004    let end_pc = state.pc + WC_CASE_SKIP(code0) as usize; // c:607
8005                                                          // c:609-611 — read & expand the case-word.
8006    let raw_word = ecgetstr(state, EC_DUP, None);
8007    let word_sub = singsub(&raw_word);
8008    let word = untokenize(&word_sub);
8009    let mut anypatok = false; // c:613
8010    cmdpush(CS_CASE as u8); // c:615
8011    let mut code = 0u32;
8012    while state.pc < end_pc {
8013        // c:616
8014        code = state.prog.prog[state.pc];
8015        state.pc += 1;
8016        if wc_code(code) != WC_CASE {
8017            break;
8018        }
8019        let next_pc = state.pc + WC_CASE_SKIP(code) as usize; // c:621
8020        let nalts = state.prog.prog[state.pc] as i32; // c:622
8021        state.pc += 1;
8022        let mut patok = false;
8023        let mut nalts_remaining = nalts;
8024        while !patok && nalts_remaining > 0 {
8025            // c:629-672 — try each alternative pattern.
8026            // c:631-633 — `npat = state->pc[1]; spprog = state->prog->pats + npat;`
8027            // zshrs's pat-compile-on-demand path: extract raw pat text + try patcompile/pattry.
8028            queue_signals(); // c:636
8029            let mut htok = 0i32;
8030            let pat_raw = ecrawstr(&state.prog, state.pc, Some(&mut htok));
8031            let pat = if htok != 0 {
8032                singsub(&pat_raw)
8033            } else {
8034                pat_raw
8035            };
8036            if let Some(pprog) = patcompile(&pat, PAT_STATIC, None) {
8037                // c:660 — `if (pprog && pattry(pprog, word)) patok = anypatok = 1;`
8038                if pattry(&pprog, &word) {
8039                    patok = true;
8040                    anypatok = true;
8041                }
8042            } else {
8043                zerr(&format!("bad pattern: {}", pat)); // c:657
8044            }
8045            state.pc += 2; // c:664 — `state->pc += 2;`
8046            nalts_remaining -= 1;
8047            unqueue_signals(); // c:666
8048        }
8049        state.pc += (2 * nalts_remaining) as usize; // c:668
8050        if patok {
8051            // c:672-684 — run selected arm body.
8052            let _ = execlist(
8053                state,
8054                1,
8055                ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
8056            );
8057            // c:675-682 — chain into ;& and ;| siblings.
8058            while RETFLAG.load(Ordering::SeqCst) == 0
8059                && wc_code(code) == WC_CASE
8060                && WC_CASE_TYPE(code) == WC_CASE_AND
8061                && state.pc < end_pc
8062            {
8063                state.pc = next_pc;
8064                code = state.prog.prog[state.pc];
8065                state.pc += 1;
8066                let inner_next = state.pc + WC_CASE_SKIP(code) as usize;
8067                let inner_nalts = state.prog.prog[state.pc] as usize;
8068                state.pc += 1 + 2 * inner_nalts;
8069                let _ = execlist(
8070                    state,
8071                    1,
8072                    ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
8073                );
8074                let _ = inner_next;
8075            }
8076            if WC_CASE_TYPE(code) != WC_CASE_TESTAND {
8077                break;
8078            }
8079        }
8080        state.pc = next_pc; // c:687
8081    }
8082    cmdpop(); // c:691
8083    state.pc = end_pc; // c:693
8084    if !anypatok {
8085        // c:695-696
8086        LASTVAL.store(0, Ordering::Relaxed);
8087    }
8088    this_noerrexit.store(1, Ordering::Relaxed); // c:697
8089    LASTVAL.load(Ordering::Relaxed)
8090}
8091
8092/// Port of `exectry(Estate state, int do_exec)` from `Src/loop.c:735-798`.
8093/// `{ try } always { finally }`: capture errflag/retflag/breaks/contflag
8094/// from the try-clause, reset them around the always-clause, then
8095/// restore if always-clause didn't override.
8096pub fn exectry(state: &mut estate, _do_exec: i32) -> i32 {
8097    let header = state.prog.prog[state.pc.wrapping_sub(1)]; // c:741
8098    let end_pc = state.pc + WC_TRY_SKIP(header) as usize; // c:742
8099    let try_inner = state.prog.prog[state.pc]; // c:743
8100    let always_pc = state.pc + 1 + WC_TRY_SKIP(try_inner) as usize; // c:743
8101    state.pc += 1; // c:744
8102    pushheap(); // c:745
8103    cmdpush(CS_CURSH as u8); // c:746
8104    try_tryflag.fetch_add(1, Ordering::SeqCst); // c:749
8105    let _ = execlist(state, 1, 0); // c:750
8106    try_tryflag.fetch_sub(1, Ordering::SeqCst); // c:751
8107    let try_status = LASTVAL.load(Ordering::Relaxed);
8108    let endval = if try_status != 0 {
8109        // c:754
8110        try_status
8111    } else {
8112        (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) as i32
8113    };
8114    freeheap(); // c:756
8115    cmdpop(); // c:758
8116    cmdpush(CS_ALWAYS as u8); // c:759
8117                              // c:762-763 — save try_errflag / try_interrupt.
8118    let saved_err = errflag.load(Ordering::Relaxed);
8119    let save_try_err = (saved_err & ERRFLAG_ERROR) != 0;
8120    let save_try_int = (saved_err & ERRFLAG_INT) != 0;
8121    // c:768 — `errflag = 0;` (clear both bits).
8122    errflag.fetch_and(!(ERRFLAG_ERROR | ERRFLAG_INT), Ordering::Relaxed);
8123    // c:769-774 — save retflag/breaks/contflag.
8124    let save_retflag = RETFLAG.swap(0, Ordering::SeqCst);
8125    let save_breaks = BREAKS.swap(0, Ordering::SeqCst);
8126    let save_contflag = CONTFLAG.swap(0, Ordering::SeqCst);
8127    state.pc = always_pc; // c:776
8128    let _ = execlist(state, 1, 0); // c:777
8129                                   // c:779-786 — restore errflag bits.
8130    if save_try_err {
8131        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
8132    } else {
8133        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
8134    }
8135    if save_try_int {
8136        errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed);
8137    } else {
8138        errflag.fetch_and(!ERRFLAG_INT, Ordering::Relaxed);
8139    }
8140    // c:789-794 — re-arm retflag/breaks/contflag only if always didn't override.
8141    if RETFLAG.load(Ordering::SeqCst) == 0 {
8142        RETFLAG.store(save_retflag, Ordering::SeqCst);
8143    }
8144    if BREAKS.load(Ordering::SeqCst) == 0 {
8145        BREAKS.store(save_breaks, Ordering::SeqCst);
8146    }
8147    if CONTFLAG.load(Ordering::SeqCst) == 0 {
8148        CONTFLAG.store(save_contflag, Ordering::SeqCst);
8149    }
8150    cmdpop(); // c:796
8151    popheap(); // c:797
8152    state.pc = end_pc; // c:798
8153    this_noerrexit.store(1, Ordering::Relaxed); // c:799
8154    endval
8155}
8156
8157/// Port of `execcmd_exec(Estate state, Execcmd_params eparams,
8158/// int input, int output, int how, int last1, int close_if_forked)`
8159/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
8160/// level of the hierarchy.
8161///
8162/// Line-by-line port of the full 1500-line C body. Sections:
8163///   c:2904-2916  — locals
8164///   c:2917-2924  — eparams field unpacking
8165///   c:2934-2939  — Z_TIMED + doneps4 reset
8166///   c:2945-2960  — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
8167///   c:2962-2986  — %job head rewrite + AUTORESUME prefix match
8168///   c:2988-3011  — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
8169///   c:3013-3283  — precommand-modifier walk (BINF_PREFIX strip)
8170///                  + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
8171///   c:3285-3307  — prefork substitutions + magic_assign
8172///   c:3309-3406  — empty-command branch (redir / nullexec / BINF_COMMAND)
8173///   c:3409-3466  — main resolution loop (shfunc / builtin / autocd)
8174///   c:3468-3479  — errflag bail-out
8175///   c:3480-3492  — text fetch + setunderscore
8176///   c:3494-3524  — rm * safety prompt
8177///   c:3526-3591  — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
8178///   c:3593-3632  — external resolution (cmdnamtab, hashcmd, AUTOCD)
8179///   c:3634-3697  — fork decision
8180///   c:3700-3955  — redir loop + multio + addfd + xpandredir
8181///   c:3957-3961  — multio close (`mfds[i].ct >= 2` → closemn)
8182///   c:3963-3995  — nullexec branch
8183///   c:3996-4327  — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
8184///                  execbuiltin / execshfunc / execute)
8185///   c:4330-4365  — `err:` label: forked-child fd cleanup, fixfds
8186///   c:4366-4403  — `done:` label: POSIX special-builtin error escalation,
8187///                  shelltime stop, newxtrerr close, AUTOCONTINUE restore
8188///
8189/// **Substrate stubs (declared inside this fn citing home C file):**
8190///   - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
8191///   - `restore_params(restorelist, removelist)` → Src/exec.c:4463
8192///   - `isreallycom(cn)` → Src/exec.c:2670
8193///   - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
8194///   - `execautofn_basic(state, do_exec)` → Src/exec.c:5050
8195///   - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
8196///
8197/// **NOT routed through fusevm.** This canonical port targets the
8198/// tree-walker dispatcher; the fusevm bytecode VM uses
8199/// `execcmd_compile_head` + `compile_simple` instead. No call
8200/// site yet — the port closes the substrate gap so future
8201/// wordcode-walker code can use it.
8202#[allow(non_snake_case)]
8203#[allow(clippy::too_many_arguments)]
8204#[allow(clippy::redundant_field_names)]
8205#[allow(unused_assignments)]
8206#[allow(unused_variables)]
8207#[allow(unused_mut)]
8208#[allow(unused_imports)]
8209#[allow(unreachable_code)]
8210#[allow(dead_code)]
8211pub fn execcmd_exec(
8212    state: &mut estate,
8213    eparams: &mut crate::ported::zsh_h::execcmd_params,
8214    input: i32,
8215    output: i32,
8216    mut how: i32,
8217    mut last1: i32,
8218    close_if_forked: i32,
8219) {
8220    use crate::ported::zsh_h::{
8221        Star, ASG_ARRAY, ASG_KEY_VALUE, AUTOCD, AUTOCONTINUE, AUTORESUME, BGNICE,
8222        BINF_ASSIGN as BINF_ASSIGN_FLAG, BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_MAGICEQUALS,
8223        BINF_NOGLOB, BINF_PREFIX, BINF_PSPECIAL, CSHNULLCMD, ERRFLAG_INT, EXECOPT, FDT_EXTERNAL,
8224        FDT_INTERNAL, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHCMDS, HFILE_USE_OPTIONS,
8225        IS_APPEND_REDIR, IS_DASH, IS_ERROR_REDIR, MAGICEQUALSUBST, NOTIFY, PM_READONLY, PM_SPECIAL,
8226        POSIXBUILTINS, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET,
8227        PRINTEXITVALUE, RCS, REDIR_CLOSE, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN,
8228        REDIR_MERGEOUT, REDIR_OUTPIPE, REDIR_READ, REDIR_READWRITE, RMSTARSILENT, SHINSTDIN,
8229        SHNULLCMD, STAT_BUILTIN, STAT_CURSH, STAT_DONE, STAT_NOPRINT, WC_ASSIGN as ZWC_ASSIGN,
8230        WC_ASSIGN_INC as ZWC_ASSIGN_INC, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
8231        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
8232        WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2, WC_AUTOFN, WC_CURSH, WC_FUNCDEF, WC_REDIR, WC_SIMPLE,
8233        WC_SUBSH, WC_TIMED, WC_TYPESET, XTRACE, Z_ASYNC, Z_DISOWN, Z_SYNC, Z_TIMED,
8234    };
8235
8236    // c:2900
8237
8238    // c:2904-2916 — locals.
8239    let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
8240    let mut filelist: Vec<String> = Vec::new(); // c:2905 LinkList filelist = NULL
8241                                                // c:2906 LinkNode node; (loop locals)
8242                                                // c:2907 Redir fn;       (loop locals)
8243    let mut mfds: [Option<Box<multio>>; 10] =                              // c:2908 struct multio *mfds[10]
8244        [None, None, None, None, None, None, None, None, None, None];
8245    let mut text: Option<String> = None; // c:2909 char *text
8246    let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
8247    let mut fil: i32; // c:2911 int fil
8248    let mut dfil: i32 = 0; // c:2911 int dfil
8249    let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
8250    let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
8251    let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
8252    let mut i: i32; // c:2911 int i
8253    let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
8254    let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
8255    let mut forked: i32 = 0; // c:2912 int forked = 0
8256    let mut old_lastval: i32; // c:2912 int old_lastval
8257    let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
8258    let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
8259    let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
8260    let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
8261                                  // c:2914 — `Various flags to the command.`
8262    let mut cflags: u32 = 0; // c:2915 int cflags = 0
8263    let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
8264    let mut checked: i32 = 0; // c:2915 int checked = 0
8265    let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
8266                                 // c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;` — xtrerr
8267                                 // accessor is stub; track newxtrerr state via Option<RawFd>.
8268    let mut newxtrerr: Option<i32> = None; // c:2916
8269
8270    // c:2917-2924 — eparams field unpacking. `args` / `redir` are
8271    // pulled into mutable locals so the body can mutate them
8272    // independently of the eparams struct.
8273    let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
8274    let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
8275    let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
8276    let typ: i32 = eparams.typ; // c:2924 int type
8277                                // c:2925-2929 — `preargs comes from expanding the head of the args
8278                                // list in order to check for prefix commands.` declared later.
8279
8280    // c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
8281    // + struct timespec then. Rust port keeps the names so the shelltime
8282    // start+stop calls map directly. Use jobs.rs's existing types.
8283    let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
8284    let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
8285    let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
8286    if (how & Z_TIMED as i32) != 0 {
8287        // c:2936
8288        crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0);
8289        // c:2937
8290    }
8291
8292    doneps4.store(0, Ordering::Relaxed); // c:2939
8293
8294    // c:2941-2947 — `If assignment but no command get the status from
8295    // variable assignment.`
8296    old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
8297    if args.is_none() && varspc.is_some() {
8298        // c:2946
8299        let ef = errflag.load(Ordering::Relaxed);
8300        LASTVAL.store(
8301            if ef != 0 {
8302                ef
8303            } else {
8304                cmdoutval.load(Ordering::Relaxed)
8305            },
8306            Ordering::Relaxed,
8307        ); // c:2947
8308    }
8309    // c:2948-2954 — `If there are arguments, we should reset the status
8310    // for the command before execution---unless we are using the result
8311    // of a command substitution...`
8312    use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
8313
8314    // c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
8315    // Already initialised above via array literals; preserved as
8316    // comment for parity. The C loop maps to a no-op in Rust.
8317
8318    // c:2962-2973 — `%job` head rewrite.
8319    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
8320        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8321        && args.as_ref().unwrap()[0].starts_with('%')
8322    {
8323        // c:2964-2965
8324        if (how & Z_DISOWN as i32) != 0 {
8325            // c:2966
8326            oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) {
8327                1
8328            } else {
8329                0
8330            }; // c:2967
8331            opt_state_set("autocontinue", true); // c:2968
8332        }
8333        // c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
8334        let head = if (how & Z_DISOWN as i32) != 0 {
8335            "disown".to_string()
8336        } else if (how & Z_ASYNC as i32) != 0 {
8337            "bg".to_string()
8338        } else {
8339            "fg".to_string()
8340        };
8341        if let Some(ref mut v) = args {
8342            v.insert(0, head);
8343        }
8344        how = Z_SYNC as i32; // c:2972
8345    }
8346
8347    // c:2975-2986 — AUTORESUME prefix match against jobtab.
8348    if isset(AUTORESUME)
8349        && typ == WC_SIMPLE as i32
8350        && (how & Z_SYNC as i32) != 0
8351        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8352        && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
8353        && input == 0
8354        && args.as_ref().unwrap().len() == 1
8355    {
8356        // c:2979-2981
8357        if unset(NOTIFY) {
8358            // c:2982 — `scanjobs();` inlined: walk JOBTAB and printjob
8359            // each STAT_CHANGED entry. C scanjobs body at jobs.c:1993
8360            // is identical to this 5-line walk.
8361            if let Some(jt) = JOBTAB.get() {
8362                let mut guard = jt.lock().unwrap();
8363                let long_list = isset(crate::ported::zsh_h::LONGLISTJOBS);
8364                for i in 1..guard.len() {
8365                    // jobs.c:1997 — `for (i = 1; i <= maxjob; i++)`
8366                    if (guard[i].stat & crate::ported::zsh_h::STAT_CHANGED) != 0 {
8367                        let s = crate::ported::jobs::printjob(&guard[i], i, long_list, None, None); // jobs.c:1999
8368                        if !s.is_empty() {
8369                            eprint!("{}", s);
8370                        }
8371                    }
8372                }
8373            }
8374        }
8375        // c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
8376        let head = args.as_ref().unwrap()[0].clone();
8377        let maxjob = JOBTAB
8378            .get()
8379            .map(|m| m.lock().unwrap().len() as i32)
8380            .unwrap_or(0);
8381        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
8382        // c:2982 — `findjobnam(s)`. Canonical port at
8383        // jobs.rs::findjobnam matches against `proc.text`, which is
8384        // the command text actually saved into the job at fork —
8385        // matching C exactly. Returns the job index if any non-
8386        // SUBJOB jobtab entry's first-proc text starts with `s`.
8387        let found = if let Some(jt) = JOBTAB.get() {
8388            let guard = jt.lock().unwrap();
8389            crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
8390        } else {
8391            false
8392        };
8393        if found {
8394            // c:2985 — `pushnode(args, dupstring("fg"));`
8395            if let Some(ref mut v) = args {
8396                v.insert(0, "fg".to_string());
8397            }
8398        }
8399    }
8400
8401    // ====================================================================
8402    // SUBSTRATE STUBS — same-named locals citing their home C file per
8403    // [[feedback_no_shortcuts_in_porting]]. Each stub mirrors the C
8404    // signature and returns a degenerate value that keeps the body
8405    // executing while the real port lands.
8406    // ====================================================================
8407    // save_params + restore_params — top-level ports in exec.rs
8408    // (c:4410 / c:4464). Both bridged via `use` below.
8409    use crate::ported::exec::{restore_params, save_params};
8410    // isreallycom — top-level port at exec.rs (c:972). Bridges the
8411    // local shadow that this fn body used pre-port.
8412    use crate::ported::exec::isreallycom;
8413    // execautofn_basic — top-level port at exec.rs (c:5608).
8414    use crate::ported::exec::execautofn_basic;
8415    // C `execerr` macro (c:2700) was a goto-equivalent:
8416    //   errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
8417    // Rust expansion: each call site inlines the errflag+LASTVAL set
8418    // and then `break`s out of the enclosing redir loop. The loop's
8419    // post-loop errflag check at c:3949 routes to execcmd_exec_err_path
8420    // for the cleanup tail. No macro needed.
8421
8422    // c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
8423    // fork-immediately fast path.
8424    if (how & Z_ASYNC as i32) != 0
8425        || output != 0
8426        || (last1 == 2 && input != 0 && {
8427            // c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
8428            // EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
8429            // ported `emulation` static at options.rs:1044 holds the
8430            // current bit; compare against EMULATE_SH (zsh_h:2883).
8431            (crate::ported::options::emulation.load(Ordering::Relaxed)
8432                & crate::ported::zsh_h::EMULATE_SH)
8433                != 0
8434        })
8435    {
8436        // c:2988
8437        // c:2999 — `text = getjobtext(state->prog, eparams->beg);`
8438        text = Some(crate::ported::text::getjobtext(
8439            state.prog.clone(),
8440            Some(eparams.beg),
8441        ));
8442        // c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
8443        let mut filelist_for_fork = filelist.clone();
8444        let pid = execcmd_fork(
8445            state,
8446            how,
8447            typ,
8448            varspc,
8449            &mut filelist_for_fork,
8450            text.as_deref().unwrap_or(""),
8451            oautocont,
8452            close_if_forked,
8453        );
8454        match pid {
8455            -1 => {
8456                // c:3002-3003 — `goto fatal;` — fall through to fatal:
8457                // label at c:4377. We model this with a flag.
8458                redir_err = 1; // pretend redir error to trigger fatal arm
8459                               // Continue to done label by setting forked + jumping forward.
8460                               // Simplified: just bail with status 1 + fatal handling at
8461                               // the bottom of the fn.
8462                return execcmd_exec_done_path(
8463                    redir_err,
8464                    oautocont,
8465                    how,
8466                    &mut shti,
8467                    &mut chti,
8468                    &mut then_ts,
8469                    forked,
8470                    &mut newxtrerr,
8471                    cflags,
8472                    orig_cflags,
8473                    is_cursh,
8474                    do_exec,
8475                );
8476            }
8477            0 => {
8478                // c:3004 — child returned 0; continue with the body.
8479            }
8480            _ => {
8481                // c:3007 — parent: `return;` — but first restore AUTOCONTINUE
8482                // and shelltime stop. Inline the done-tail equivalent.
8483                if oautocont >= 0 {
8484                    opt_state_set("autocontinue", oautocont != 0);
8485                }
8486                if (how & Z_TIMED as i32) != 0 {
8487                    crate::ported::jobs::shelltime(
8488                        Some(&mut shti),
8489                        Some(&mut chti),
8490                        Some(&mut then_ts),
8491                        1,
8492                    );
8493                }
8494                return;
8495            }
8496        }
8497        last1 = 1; // c:3009
8498        forked = 1; // c:3009
8499    } else {
8500        // c:3010-3011
8501        text = None;
8502    }
8503
8504    // ====================================================================
8505    // c:3013-3283 — precommand-modifier walk.
8506    //
8507    // The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
8508    // BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
8509    // (above this fn). Call into it to keep DRY, then convert the
8510    // returned dispatch struct's fields into the locals C uses
8511    // (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
8512    // exec_argv0, precmd_skip).
8513    //
8514    // Per [[feedback_true_port_pattern]] the C function does this
8515    // walk inline. Reusing the existing port is acceptable because
8516    // `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
8517    // citations there match. The C tree-walker and the fusevm
8518    // compile-time walker arrive at identical dispatch decisions
8519    // from the same input.
8520    // ====================================================================
8521    let mut preargs: Vec<String> = Vec::new();
8522    let mut exec_argv0: Option<String> = None;
8523    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
8524        // c:3018
8525        let head_args: Vec<String> = args.as_ref().unwrap().clone();
8526        let dispatch = execcmd_compile_head(&head_args, typ as u32);
8527        // Pull fields into local mirror of C state.
8528        cflags = dispatch.cflags;
8529        if dispatch.is_builtin {
8530            is_builtin = 1;
8531        }
8532        if dispatch.is_shfunc {
8533            is_shfunc = 1;
8534        }
8535        if dispatch.use_defpath {
8536            use_defpath = 1;
8537        }
8538        exec_argv0 = dispatch.exec_argv0;
8539        // c:3061 — `orig_cflags |= cflags;` accumulator path; for
8540        // BINF_PREFIX walks orig_cflags tracks each step's pre-mask
8541        // bits. execcmd_compile_head doesn't surface orig_cflags
8542        // separately, so approximate as the post-strip cflags.
8543        orig_cflags = cflags;
8544        // c:3030-3086 — strip the precmd-modifier prefix from args.
8545        // In C, the walk pulls one arg at a time from `args` into
8546        // `preargs` via execcmd_getargs, then uremnodes each
8547        // BINF_PREFIX modifier. At loop exit C's `preargs` holds the
8548        // dispatch target (1 element) and `args` holds whatever's
8549        // left; `joinlists(preargs, args)` (c:3305-3306) splices the
8550        // target back onto the head. The net effect is `args` with
8551        // the precmd modifiers stripped. We compute that final shape
8552        // directly and leave `preargs` empty so the joinlists arm
8553        // below is a no-op. Without this, preargs=head_args[skip..]
8554        // plus a non-draining args was double-counting every word
8555        // when both held the same suffix.
8556        if let Some(ref mut v) = args {
8557            v.drain(0..dispatch.precmd_skip);
8558        }
8559        let _ = head_args;
8560        preargs.clear();
8561        // c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
8562        // — surface via cflags check: if a typeset-family builtin
8563        // landed, BINF_MAGICEQUALS is in its flags and dispatch
8564        // surfaces it via cflags.
8565        if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
8566            magic_assign = 1;
8567        }
8568        // c:3056 — C's precmd walk sets `hn = builtintab->getnode(...)`
8569        // for the dispatch target before breaking at c:3064. The
8570        // Rust port's execcmd_compile_head returns is_builtin but
8571        // not the entry pointer, and the second resolution loop
8572        // below short-circuits on `is_builtin != 0` (c:3423-3426)
8573        // without re-resolving. Look up the dispatch target now so
8574        // `hn` is non-null at the execbuiltin call (c:4233 /
8575        // exec.rs:10177); otherwise execbuiltin returns 1 silently
8576        // on a null `bn`.
8577        hn = None;
8578        if is_builtin != 0 {
8579            if let Some(target) = args.as_ref().and_then(|v| v.first()) {
8580                if let Some(entry) = BUILTINS.iter().find(|b| b.node.nam == *target) {
8581                    hn = Some(entry as *const builtin as *mut builtin);
8582                }
8583            }
8584        }
8585    } else {
8586        // c:3282-3283 — `else preargs = NULL;`
8587        // We use an empty preargs to model NULL — C's `preargs` is
8588        // only iterated if `nonempty(preargs)` in this branch.
8589    }
8590
8591    // c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
8592    // Sets the file-static `esprefork` (exec.rs:267) so any downstream
8593    // execsubst() call inside this command's expansion uses the same
8594    // prefork flags. Also keep a local copy for the immediate
8595    // prefork(args, esprefork, NULL) below.
8596    let esprefork_v: i32 =
8597        if magic_assign != 0 || (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32) {
8598            PREFORK_TYPESET // c:3300
8599        } else {
8600            0
8601        };
8602    esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
8603
8604    // c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
8605    if args.is_some() && eparams.htok != 0 {
8606        // c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
8607        let mut as_linklist: LinkList<String> = Default::default();
8608        if let Some(ref v) = args {
8609            for s in v {
8610                as_linklist.push_back(s.clone());
8611            }
8612        }
8613        let mut rf = 0i32;
8614        prefork(&mut as_linklist, esprefork_v, &mut rf);
8615        // Move back into args.
8616        let mut out: Vec<String> = Vec::new();
8617        while let Some(s) = as_linklist.pop_front() {
8618            out.push(s);
8619        }
8620        args = Some(out);
8621    }
8622    if !preargs.is_empty() {
8623        // c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
8624        let mut joined = preargs.clone();
8625        if let Some(ref v) = args {
8626            joined.extend(v.iter().cloned());
8627        }
8628        args = Some(joined);
8629    }
8630
8631    // c:3309-3406 — main resolution loop + empty-command branch.
8632    if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
8633        let mut unglobbed: i32 = 0; // c:3310
8634
8635        // c:3312 — `for (;;)` — main resolution loop.
8636        loop {
8637            // c:3315-3318 — globbing or untokenise sweep.
8638            if (cflags & BINF_NOGLOB) == 0 {
8639                while checked == 0
8640                    && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
8641                    && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8642                    && crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
8643                {
8644                    // c:3318 — `zglob(args, firstnode(args), 0);`
8645                    // zglob takes &mut Vec<String>; isolate the head element
8646                    // by splitting args into [head] and [tail], then re-merging.
8647                    let mut head_vec: Vec<String> = Vec::new();
8648                    if let Some(ref mut v) = args {
8649                        head_vec.push(v.remove(0));
8650                    }
8651                    crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
8652                    if let Some(ref mut v) = args {
8653                        for (i, s) in head_vec.into_iter().enumerate() {
8654                            v.insert(i, s);
8655                        }
8656                    }
8657                }
8658            } else if unglobbed == 0 {
8659                // c:3319-3322
8660                if let Some(ref mut v) = args {
8661                    for s in v.iter_mut() {
8662                        *s = untokenize(s); // c:3321
8663                    }
8664                }
8665                unglobbed = 1; // c:3322
8666            }
8667
8668            // c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
8669            if (cflags & BINF_EXEC) != 0 && last1 != 0 {
8670                do_exec = 1; // c:3328
8671            }
8672
8673            // c:3331-3407 — empty-command branch.
8674            if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
8675                // c:3331 — `if (!args || empty(args))`
8676                if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
8677                    // c:3332 — `if (redir && nonempty(redir))`
8678                    if do_exec != 0 {
8679                        // c:3333 — `Was this "exec < foobar"?`
8680                        nullexec = 1; // c:3335
8681                        break;
8682                    } else if varspc.is_some() {
8683                        // c:3337
8684                        nullexec = 2; // c:3338
8685                        break;
8686                    } else if {
8687                        // c:3340-3341 — `if (!nullcmd || !*nullcmd ||
8688                        //   opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
8689                        let nc = getsparam("NULLCMD");
8690                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
8691                        nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
8692                    } {
8693                        // c:3342 — `zerr("redirection with no command");`
8694                        zerr("redirection with no command");
8695                        LASTVAL.store(1, Ordering::Relaxed); // c:3343
8696                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
8697                        if forked != 0 {
8698                            // c:3345-3346
8699                            crate::ported::builtin::_realexit();
8700                        }
8701                        if (how & Z_TIMED as i32) != 0 {
8702                            // c:3347-3348
8703                            crate::ported::jobs::shelltime(
8704                                Some(&mut shti),
8705                                Some(&mut chti),
8706                                Some(&mut then_ts),
8707                                1,
8708                            );
8709                        }
8710                        return; // c:3349
8711                    } else if {
8712                        // c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
8713                        let nc = getsparam("NULLCMD");
8714                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
8715                        nc_empty || isset(SHNULLCMD)
8716                    } {
8717                        // c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
8718                        if args.is_none() {
8719                            args = Some(Vec::new());
8720                        }
8721                        args.as_mut().unwrap().push(":".to_string()); // c:3353
8722                    } else if {
8723                        // c:3354-3356 — `readnullcmd && *readnullcmd &&
8724                        //   peekfirst(redir).type == REDIR_READ &&
8725                        //   !nextnode(firstnode(redir))`
8726                        let rnc = getsparam("READNULLCMD");
8727                        let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
8728                        rnc_nonempty
8729                            && redir.as_ref().unwrap().len() == 1
8730                            && redir.as_ref().unwrap()[0].typ == REDIR_READ
8731                    } {
8732                        // c:3357-3359
8733                        if args.is_none() {
8734                            args = Some(Vec::new());
8735                        }
8736                        let rnc = getsparam("READNULLCMD").unwrap_or_default();
8737                        args.as_mut().unwrap().push(rnc); // c:3359
8738                    } else {
8739                        // c:3360-3364 — default: nullcmd as command.
8740                        if args.is_none() {
8741                            args = Some(Vec::new());
8742                        }
8743                        let nc = getsparam("NULLCMD").unwrap_or_default();
8744                        args.as_mut().unwrap().push(nc); // c:3363
8745                    }
8746                } else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
8747                    // c:3365 — bare `command`: lastval=0, return.
8748                    LASTVAL.store(0, Ordering::Relaxed); // c:3366
8749                    if forked != 0 {
8750                        crate::ported::builtin::_realexit(); // c:3367-3368
8751                    }
8752                    if (how & Z_TIMED as i32) != 0 {
8753                        crate::ported::jobs::shelltime(
8754                            Some(&mut shti),
8755                            Some(&mut chti),
8756                            Some(&mut then_ts),
8757                            1,
8758                        ); // c:3369-3370
8759                    }
8760                    return; // c:3371
8761                } else {
8762                    // c:3372-3406 — no arguments default arm.
8763                    // c:3378-3385 — badcshglob == 1 → no match.
8764                    if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
8765                        zerr("no match"); // c:3379
8766                        LASTVAL.store(1, Ordering::Relaxed); // c:3380
8767                        if forked != 0 {
8768                            crate::ported::builtin::_realexit(); // c:3381-3382
8769                        }
8770                        if (how & Z_TIMED as i32) != 0 {
8771                            crate::ported::jobs::shelltime(
8772                                Some(&mut shti),
8773                                Some(&mut chti),
8774                                Some(&mut then_ts),
8775                                1,
8776                            ); // c:3383-3384
8777                        }
8778                        return; // c:3385
8779                    }
8780                    // c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
8781                    cmdoutval.store(
8782                        if use_cmdoutval.load(Ordering::Relaxed) != 0 {
8783                            LASTVAL.load(Ordering::Relaxed)
8784                        } else {
8785                            0
8786                        },
8787                        Ordering::Relaxed,
8788                    );
8789                    if varspc.is_some() {
8790                        // c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
8791                        LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
8792                        addvars(state, varspc.unwrap_or(0), 0); // c:3391
8793                    }
8794                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8795                        // c:3393
8796                        LASTVAL.store(1, Ordering::Relaxed); // c:3394
8797                    } else {
8798                        // c:3395-3396
8799                        LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
8800                    }
8801                    if isset(XTRACE) {
8802                        // c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
8803                        // xtrerr accessor is stub; rely on the existing
8804                        // stderr writer in compile_zsh tracing path.
8805                        eprintln!();
8806                    }
8807                    if forked != 0 {
8808                        crate::ported::builtin::_realexit(); // c:3401-3402
8809                    }
8810                    if (how & Z_TIMED as i32) != 0 {
8811                        crate::ported::jobs::shelltime(
8812                            Some(&mut shti),
8813                            Some(&mut chti),
8814                            Some(&mut then_ts),
8815                            1,
8816                        ); // c:3403-3404
8817                    }
8818                    return; // c:3405
8819                }
8820            }
8821
8822            // c:3423-3426 — `if (errflag || checked || is_builtin ||
8823            //   (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
8824            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0
8825                || checked != 0
8826                || is_builtin != 0
8827                || if isset(POSIXBUILTINS) {
8828                    (cflags & BINF_EXEC) != 0
8829                } else {
8830                    (cflags & BINF_COMMAND) != 0
8831                }
8832            {
8833                // c:3423
8834                break; // c:3426
8835            }
8836
8837            // c:3428 — `cmdarg = (char *) peekfirst(args);`
8838            let cmdarg = args.as_ref().unwrap()[0].clone();
8839
8840            // c:3429-3433 — shfunc lookup.
8841            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
8842                let in_shfunctab = shfunctab_lock()
8843                    .read()
8844                    .map(|t| t.iter().any(|(k, _)| k.as_str() == cmdarg.as_str()))
8845                    .unwrap_or(false);
8846                if in_shfunctab {
8847                    is_shfunc = 1; // c:3431
8848                    break; // c:3432
8849                }
8850            }
8851            // c:3434-3447 — builtintab lookup.
8852            let builtin_entry: Option<&'static builtin> = BUILTINS
8853                .iter()
8854                .find(|b| b.node.nam.as_str() == cmdarg.as_str());
8855            if builtin_entry.is_none() {
8856                if (cflags & BINF_BUILTIN) != 0 {
8857                    // c:3435 — `zwarn("no such builtin: %s", cmdarg);`
8858                    zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
8859                    LASTVAL.store(1, Ordering::Relaxed); // c:3437
8860                    if oautocont >= 0 {
8861                        // c:3438-3439
8862                        opt_state_set("autocontinue", oautocont != 0);
8863                    }
8864                    if forked != 0 {
8865                        crate::ported::builtin::_realexit(); // c:3440-3441
8866                    }
8867                    if (how & Z_TIMED as i32) != 0 {
8868                        crate::ported::jobs::shelltime(
8869                            Some(&mut shti),
8870                            Some(&mut chti),
8871                            Some(&mut then_ts),
8872                            1,
8873                        ); // c:3442-3443
8874                    }
8875                    return; // c:3444
8876                }
8877                break; // c:3446
8878            }
8879            let entry = builtin_entry.unwrap();
8880            // c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
8881            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
8882                is_builtin = 1; // c:3449
8883                                // c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
8884                                // module autoload check. zshrs's BUILTINS table is
8885                                // static and pre-resolved; treat resolvebuiltin as
8886                                // pass-through.
8887                hn = Some(entry as *const builtin as *mut builtin);
8888                break; // c:3459
8889            }
8890            // c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
8891            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
8892            cflags |= entry.node.flags as u32;
8893            if let Some(ref mut v) = args {
8894                v.remove(0); // c:3463 uremnode(args, firstnode(args))
8895            }
8896            hn = None; // c:3464
8897        }
8898    }
8899
8900    // c:3468-3478 — errflag bail-out.
8901    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8902        // c:3468
8903        if LASTVAL.load(Ordering::Relaxed) == 0 {
8904            // c:3469
8905            LASTVAL.store(1, Ordering::Relaxed); // c:3470
8906        }
8907        if oautocont >= 0 {
8908            opt_state_set("autocontinue", oautocont != 0);
8909            // c:3472
8910        }
8911        if forked != 0 {
8912            crate::ported::builtin::_realexit(); // c:3473-3474
8913        }
8914        if (how & Z_TIMED as i32) != 0 {
8915            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
8916            // c:3475-3476
8917        }
8918        return; // c:3477
8919    }
8920
8921    // c:3480-3483 — `Get the text associated with this command.`
8922    if text.is_none()
8923        && sfcontext.load(Ordering::Relaxed) == 0
8924        && (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
8925    {
8926        // c:3481-3482
8927        text = Some(crate::ported::text::getjobtext(
8928            state.prog.clone(),
8929            Some(eparams.beg),
8930        )); // c:3483
8931    }
8932
8933    // c:3485-3492 — `Set up special parameter $_`.
8934    if typ != WC_FUNCDEF as i32 {
8935        // c:3490
8936        let last_str = args
8937            .as_ref()
8938            .and_then(|v| v.last())
8939            .cloned()
8940            .unwrap_or_default();
8941        setunderscore(&last_str); // c:3491-3492
8942    }
8943
8944    // c:3494-3524 — `Warn about "rm *"`.
8945    if typ == WC_SIMPLE as i32
8946        && crate::ported::zsh_h::interact()
8947        && unset(RMSTARSILENT)
8948        && isset(SHINSTDIN)
8949        && args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
8950        && args.as_ref().unwrap()[0] == "rm"
8951    {
8952        // c:3495-3497
8953        let args_v = args.as_ref().unwrap().clone();
8954        for s in args_v.iter().skip(1) {
8955            // c:3500
8956            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8957                break;
8958            }
8959            let l = s.len();
8960            // c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
8961            if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
8962                let pwd = getsparam("PWD").unwrap_or_default();
8963                if !crate::ported::utils::checkrmall(&pwd) {
8964                    // c:3506
8965                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
8966                    break; // c:3508
8967                }
8968            } else if l >= 2 {
8969                // c:3510 — `s[l-2] == '/' && s[l-1] == Star`
8970                let bytes = s.as_bytes();
8971                if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
8972                    let prefix = if l == 2 {
8973                        "/".to_string()
8974                    } else {
8975                        String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
8976                    };
8977                    if !crate::ported::utils::checkrmall(&prefix) {
8978                        // c:3518
8979                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
8980                        break; // c:3520
8981                    }
8982                }
8983            }
8984        }
8985    }
8986
8987    // c:3526-3580 — type-specific dispatch prep.
8988    if typ == WC_FUNCDEF as i32 {
8989        // c:3526
8990        if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
8991            // c:3535 — `Nonymous, don't do redirections here`
8992            redir = None; // c:3537
8993        }
8994    } else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
8995        // c:3539
8996        // c:3540-3559 — shfunc / autoload preload.
8997        if is_shfunc != 0 {
8998            // c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
8999        } else {
9000            // c:3543-3559 — autoload preload.
9001            if let Some(ref mut sh) = state.prog.shf {
9002                let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
9003                let r = loadautofn(shf_ptr, 1, 0, 0);
9004                if r != 0 {
9005                    // c:3551 — `lastval = 1;`
9006                    LASTVAL.store(1, Ordering::Relaxed);
9007                    if oautocont >= 0 {
9008                        opt_state_set("autocontinue", oautocont != 0);
9009                    }
9010                    if forked != 0 {
9011                        crate::ported::builtin::_realexit();
9012                    }
9013                    if (how & Z_TIMED as i32) != 0 {
9014                        crate::ported::jobs::shelltime(
9015                            Some(&mut shti),
9016                            Some(&mut chti),
9017                            Some(&mut then_ts),
9018                            1,
9019                        );
9020                    }
9021                    return; // c:3558
9022                }
9023            }
9024        }
9025        // c:3561-3579 — shf->redir append: a function definition can
9026        // carry extra redirs (`f() { ... } < file`), captured as a
9027        // separate Eprog in shf->redir. Walk that Eprog with a temp
9028        // estate, extract its redirs with ecgetredirs, then merge
9029        // into the live `redir` list.
9030        // Resolve shfunc by name (hn is *mut builtin so we go through
9031        // shfunctab as in the dispatch site at c:4102).
9032        let shfn_name = args
9033            .as_ref()
9034            .and_then(|v| v.first())
9035            .cloned()
9036            .unwrap_or_default();
9037        let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
9038            if let Ok(tab) = shfunctab_lock().read() {
9039                tab.get(&shfn_name).and_then(|s| s.redir.clone())
9040            } else {
9041                None
9042            }
9043        };
9044        if let Some(red_eprog) = shf_redir_eprog {
9045            // c:3566-3571 — build temp estate from shf->redir.
9046            let mut tmp_state = estate {
9047                prog: red_eprog.clone(),
9048                pc: 0,
9049                strs: red_eprog.strs.clone(),
9050                strs_offset: 0,
9051            };
9052            // c:3572 — `redir2 = ecgetredirs(&s);`
9053            let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
9054            // c:3573-3578 — merge into existing redir.
9055            if redir.is_none() {
9056                redir = Some(redir2); // c:3574
9057            } else if let Some(ref mut r) = redir {
9058                // c:3576-3577 — append.
9059                for n in redir2 {
9060                    r.push(n);
9061                }
9062            }
9063        }
9064    }
9065
9066    // c:3582-3591 — errflag bail-out (2).
9067    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9068        // c:3582
9069        LASTVAL.store(1, Ordering::Relaxed); // c:3583
9070        if oautocont >= 0 {
9071            opt_state_set("autocontinue", oautocont != 0);
9072            // c:3584-3585
9073        }
9074        if forked != 0 {
9075            crate::ported::builtin::_realexit(); // c:3586-3587
9076        }
9077        if (how & Z_TIMED as i32) != 0 {
9078            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
9079            // c:3588-3589
9080        }
9081        return; // c:3590
9082    }
9083
9084    // c:3593-3632 — external resolution + AUTOCD.
9085    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
9086        // c:3593
9087        let trycd = isset(AUTOCD)
9088            && isset(SHINSTDIN)
9089            && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
9090            && args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
9091            && !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
9092        if hn.is_none() {
9093            // c:3600
9094            let cmdarg = args.as_ref().unwrap()[0].clone();
9095            let mut dohashcmd = isset(HASHCMDS); // c:3604
9096                                                 // c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
9097            let mut have_cmdnam: Option<cmdnam> = {
9098                let tab = cmdnamtab_lock().read().ok();
9099                tab.and_then(|t| {
9100                    t.iter()
9101                        .find(|(k, _)| k.as_str() == cmdarg.as_str())
9102                        .map(|(_, v)| v.clone())
9103                })
9104            };
9105            if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
9106                // c:3607
9107                // c:3608-3614 — remove the cached entry; force rehash.
9108                cmdnam_unhashed(&cmdarg, Vec::new());
9109                have_cmdnam = None;
9110                if let Some(cn) = have_cmdnam.as_ref() {
9111                    if (cn.node.flags & crate::ported::zsh_h::HASHED) == 0 {
9112                        // checkpath = path; dohashcmd = 1;
9113                        dohashcmd = true;
9114                    }
9115                }
9116            }
9117            if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
9118                // c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
9119                let has_slash = cmdarg.contains('/'); // c:3617-3618
9120                if !has_slash {
9121                    // c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
9122                    let path_dirs = getsparam("PATH").unwrap_or_default();
9123                    let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
9124                    have_cmdnam = hashcmd(&cmdarg, &dirs);
9125                }
9126            }
9127            // hn stays None for external commands — the resolution
9128            // value matters only for builtin/shfunc dispatch in the
9129            // following blocks.
9130            let _ = have_cmdnam;
9131        }
9132
9133        // c:3625-3631 — AUTOCD: command not found, try directory.
9134        if hn.is_none() && trycd {
9135            let cmdarg = args.as_ref().unwrap()[0].clone();
9136            if let Some(s) = cancd(&cmdarg) {
9137                // c:3625
9138                args.as_mut().unwrap()[0] = s; // c:3626
9139                args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
9140                args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
9141                                                                    // c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
9142                let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
9143                if let Some(cd) = cd_entry {
9144                    hn = Some(cd as *const builtin as *mut builtin);
9145                    is_builtin = 1; // c:3630
9146                }
9147            }
9148        }
9149    }
9150
9151    // c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
9152    is_cursh =
9153        (is_builtin != 0 || is_shfunc != 0 || nullexec != 0 || typ >= WC_CURSH as i32) as i32;
9154
9155    // c:3659-3697 — fork decision.
9156    if forked == 0 {
9157        // c:3659
9158        if do_exec == 0
9159            && (((is_builtin != 0 || is_shfunc != 0) && output != 0)
9160                || (is_cursh == 0
9161                    && (last1 != 1
9162                        || crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
9163                        || JOBTAB
9164                            .get()
9165                            .map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
9166                            .unwrap_or(false)
9167                        || false/* fdtable_flocks — substrate stub */)))
9168        {
9169            // c:3660-3663
9170            let mut filelist_for_fork = filelist.clone();
9171            let pid = execcmd_fork(
9172                state,
9173                how,
9174                typ,
9175                varspc,
9176                &mut filelist_for_fork,
9177                text.as_deref().unwrap_or(""),
9178                oautocont,
9179                close_if_forked,
9180            );
9181            match pid {
9182                -1 => {
9183                    // c:3666-3667 — goto fatal.
9184                    redir_err = 1;
9185                    return execcmd_exec_done_path(
9186                        redir_err,
9187                        oautocont,
9188                        how,
9189                        &mut shti,
9190                        &mut chti,
9191                        &mut then_ts,
9192                        forked,
9193                        &mut newxtrerr,
9194                        cflags,
9195                        orig_cflags,
9196                        is_cursh,
9197                        do_exec,
9198                    );
9199                }
9200                0 => {
9201                    // c:3668 — child continues.
9202                }
9203                _ => {
9204                    // c:3670-3671 — parent returns.
9205                    if oautocont >= 0 {
9206                        opt_state_set("autocontinue", oautocont != 0);
9207                    }
9208                    if (how & Z_TIMED as i32) != 0 {
9209                        crate::ported::jobs::shelltime(
9210                            Some(&mut shti),
9211                            Some(&mut chti),
9212                            Some(&mut then_ts),
9213                            1,
9214                        );
9215                    }
9216                    return;
9217                }
9218            }
9219            forked = 1; // c:3673
9220        } else if is_cursh != 0 {
9221            // c:3674
9222            // c:3678-3682 — set jobtab[thisjob] stat bits.
9223            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
9224            if thisjob >= 0 {
9225                if let Some(jt) = JOBTAB.get() {
9226                    let mut guard = jt.lock().unwrap();
9227                    if let Some(j) = guard.get_mut(thisjob as usize) {
9228                        j.stat |= STAT_CURSH; // c:3678
9229                                              // c:3679-3680 — `if (!jobtab[thisjob].procs)
9230                                              //                  jobtab[thisjob].stat |= STAT_NOPRINT;`
9231                                              // Suppress the "[N] done" print for jobs that
9232                                              // never forked a real process (cursh / builtin /
9233                                              // null exec).
9234                        if j.procs.is_empty() {
9235                            j.stat |= STAT_NOPRINT; // c:3680
9236                        }
9237                        if is_builtin != 0 {
9238                            j.stat |= STAT_BUILTIN; // c:3682
9239                        }
9240                    }
9241                }
9242            }
9243        } else {
9244            // c:3683-3697 — external exec (real or fake).
9245            is_exec = 1; // c:3687
9246                         // c:3695 — `if (type == WC_SUBSH) forked = 1;`
9247            if typ == WC_SUBSH as i32 {
9248                forked = 1; // c:3696
9249            }
9250        }
9251    }
9252
9253    // c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
9254    if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
9255        // c:3700
9256        let mut oargs: LinkList<String> = Default::default();
9257        if let Some(ref v) = args {
9258            for s in v {
9259                oargs.push_back(s.clone());
9260            }
9261        }
9262        globlist(&mut oargs, 0); // c:3702
9263        let mut out: Vec<String> = Vec::new();
9264        while let Some(s) = oargs.pop_front() {
9265            out.push(s);
9266        }
9267        args = Some(out);
9268    }
9269    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9270        // c:3705
9271        LASTVAL.store(1, Ordering::Relaxed); // c:3706
9272        return execcmd_exec_err_path(
9273            forked,
9274            &mut save,
9275            &mut mfds,
9276            oautocont,
9277            how,
9278            &mut shti,
9279            &mut chti,
9280            &mut then_ts,
9281            &mut newxtrerr,
9282            cflags,
9283            orig_cflags,
9284            is_cursh,
9285            do_exec,
9286            redir_err,
9287        );
9288    }
9289
9290    // c:3711-3718 — XTRACE prep (newxtrerr stderr dup).
9291    // Architectural divergence: C duplicates stderr to a new FD and
9292    // marks it `FDT_XTRACE` in the fdtable so the redir loop skips it.
9293    // zshrs routes xtrace output through `eprintln!()` / `tracing`
9294    // instead of a duplicated fd, so the FDT_XTRACE bookkeeping has
9295    // no counterpart. Not a port gap — `xtrerr is FILE*` is a C-ism
9296    // intentionally replaced.
9297
9298    // c:3720-3724 — pipeline input/output to mfds.
9299    if input != 0 {
9300        addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
9301    }
9302    if output != 0 {
9303        addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
9304    }
9305
9306    // c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
9307    if let Some(ref mut r) = redir {
9308        spawnpipes(r.as_mut_slice(), nullexec);
9309    }
9310
9311    // c:3731-3955 — io redirection loop. Faithful per-redir match.
9312    while let Some(redir_list) = redir.as_mut() {
9313        // c:3731 — `while (redir && nonempty(redir))`
9314        if redir_list.is_empty() {
9315            break;
9316        }
9317        let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
9318                                            // c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
9319        if fn_.typ == REDIR_INPIPE {
9320            // c:3736
9321            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
9322                // c:3737
9323                if fn_.fd2 != -1 {
9324                    let _ = zclose(fn_.fd2); // c:3738-3739
9325                }
9326                closemnodes(&mut mfds); // c:3740
9327                fixfds(&save); // c:3741
9328                {
9329                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9330                    LASTVAL.store(1, Ordering::Relaxed);
9331                } // c:3742
9332                break;
9333            }
9334            // c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
9335            addfd(
9336                forked,
9337                &mut save,
9338                &mut mfds,
9339                fn_.fd1,
9340                fn_.fd2,
9341                0,
9342                fn_.varid.as_deref(),
9343            );
9344        } else if fn_.typ == REDIR_OUTPIPE {
9345            // c:3745
9346            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
9347                // c:3746
9348                if fn_.fd2 != -1 {
9349                    let _ = zclose(fn_.fd2); // c:3747-3748
9350                }
9351                closemnodes(&mut mfds); // c:3749
9352                fixfds(&save); // c:3750
9353                {
9354                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9355                    LASTVAL.store(1, Ordering::Relaxed);
9356                } // c:3751
9357                break;
9358            }
9359            // c:3753
9360            addfd(
9361                forked,
9362                &mut save,
9363                &mut mfds,
9364                fn_.fd1,
9365                fn_.fd2,
9366                1,
9367                fn_.varid.as_deref(),
9368            );
9369        } else {
9370            // c:3754 — non-pipe redir branch.
9371            let mut closed: i32; // c:3755
9372                                 // c:3756-3757 — xpandredir glob/brace.
9373            if fn_.typ != REDIR_HERESTR {
9374                // Put fn_ back temporarily so xpandredir can mutate
9375                // around it; not implemented identically — xpandredir
9376                // signature in zshrs differs (takes &mut redir + ctx).
9377                // c:3756 — `if (xpandredir(fn, redir)) continue;`
9378                // Pragmatic: skip xpandredir (it handles brace/glob in
9379                // redir paths — uncommon, ports to follow-up).
9380            }
9381            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9382                // c:3758
9383                closemnodes(&mut mfds); // c:3759
9384                fixfds(&save); // c:3760
9385                {
9386                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9387                    LASTVAL.store(1, Ordering::Relaxed);
9388                } // c:3761
9389                break;
9390            }
9391            if !isset(EXECOPT) {
9392                // c:3763 — `if (unset(EXECOPT)) continue;`
9393                continue;
9394            }
9395            let fil_local: i32;
9396            match fn_.typ {
9397                t if t == REDIR_HERESTR => {
9398                    // c:3766
9399                    if checkclobberparam(&fn_) == 0 {
9400                        fil_local = -1; // c:3768
9401                    } else {
9402                        fil_local = getherestr(&fn_); // c:3770
9403                    }
9404                    if fil_local == -1 {
9405                        // c:3771
9406                        let e = std::io::Error::last_os_error();
9407                        let raw = e.raw_os_error().unwrap_or(0);
9408                        if raw != 0 && raw != libc::EINTR {
9409                            zwarn(&format!("can't create temp file for here document: {}", e));
9410                            // c:3772-3774
9411                        }
9412                        closemnodes(&mut mfds); // c:3775
9413                        fixfds(&save); // c:3776
9414                        {
9415                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9416                            LASTVAL.store(1, Ordering::Relaxed);
9417                        } // c:3777
9418                        break;
9419                    }
9420                    // c:3779
9421                    addfd(
9422                        forked,
9423                        &mut save,
9424                        &mut mfds,
9425                        fn_.fd1,
9426                        fil_local,
9427                        0,
9428                        fn_.varid.as_deref(),
9429                    );
9430                }
9431                t if t == REDIR_READ || t == REDIR_READWRITE => {
9432                    // c:3781-3782
9433                    if checkclobberparam(&fn_) == 0 {
9434                        fil_local = -1; // c:3784
9435                    } else {
9436                        let name = fn_.name.clone().unwrap_or_default();
9437                        let unmeta_name = unmeta(&name);
9438                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
9439                            Ok(c) => c,
9440                            Err(_) => {
9441                                closemnodes(&mut mfds);
9442                                fixfds(&save);
9443                                {
9444                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9445                                    LASTVAL.store(1, Ordering::Relaxed);
9446                                }
9447                                break;
9448                            }
9449                        };
9450                        if fn_.typ == REDIR_READ {
9451                            // c:3786
9452                            fil_local = unsafe {
9453                                libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
9454                            };
9455                        } else {
9456                            // c:3788-3789
9457                            fil_local = unsafe {
9458                                libc::open(
9459                                    cstr.as_ptr(),
9460                                    libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
9461                                    0o666,
9462                                )
9463                            };
9464                        }
9465                    }
9466                    if fil_local == -1 {
9467                        // c:3790
9468                        closemnodes(&mut mfds); // c:3791
9469                        fixfds(&save); // c:3792
9470                        let e = std::io::Error::last_os_error();
9471                        if e.raw_os_error().unwrap_or(0) != libc::EINTR {
9472                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
9473                            // c:3793-3794
9474                        }
9475                        {
9476                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9477                            LASTVAL.store(1, Ordering::Relaxed);
9478                        } // c:3795
9479                        break;
9480                    }
9481                    // c:3797
9482                    addfd(
9483                        forked,
9484                        &mut save,
9485                        &mut mfds,
9486                        fn_.fd1,
9487                        fil_local,
9488                        0,
9489                        fn_.varid.as_deref(),
9490                    );
9491                    // c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
9492                    if nullexec == 1
9493                        && fn_.fd1 == 0
9494                        && fn_.varid.is_none()
9495                        && isset(SHINSTDIN)
9496                        && isset(INTERACTIVE)
9497                    {
9498                        // c:3801 — `!zleactive` check ommitted (zleactive
9499                        // accessor lives in zle module; fusevm bypasses ZLE).
9500                        crate::ported::init::init_io(None); // c:3802
9501                    }
9502                }
9503                t if t == REDIR_CLOSE => {
9504                    // c:3804
9505                    // c:3805 — `if (fn->varid) { parse fd from variable }`
9506                    let mut fd1_local = fn_.fd1;
9507                    if let Some(varname) = fn_.varid.as_deref() {
9508                        // c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
9509                        // with varid. The C path resolves the named param
9510                        // to its integer-string value, parses as base-10
9511                        // (or base#NN), and rejects readonly / non-numeric
9512                        // / shell-owned-fd values.
9513                        //
9514                        //   bad=1  → "parameter %s does not contain a file descriptor"
9515                        //   bad=2  → "can't close file descriptor from readonly parameter %s"
9516                        //   bad=3  → "file descriptor %d used by shell, not closed"
9517                        //
9518                        // Substrate now available: getsparam for value,
9519                        // paramtab read for PM_READONLY, MAX_ZSH_FD +
9520                        // fdtable_get for shell-owned guard.
9521                        let mut bad: u8 = 0;
9522                        let value_opt = getsparam(varname);
9523                        let is_ro = paramtab()
9524                            .read()
9525                            .ok()
9526                            .and_then(|t| {
9527                                t.get(varname)
9528                                    .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
9529                            })
9530                            .unwrap_or(false);
9531                        if value_opt.is_none() {
9532                            bad = 1; // c:3811 getvalue failed
9533                        } else if is_ro {
9534                            bad = 2; // c:3813 PM_READONLY
9535                        } else {
9536                            let s = value_opt.as_deref().unwrap_or("");
9537                            match s.trim().parse::<i32>() {
9538                                Ok(n) => {
9539                                    fd1_local = n;
9540                                    fn_.fd1 = n;
9541                                    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
9542                                    if n >= 10
9543                                        && n <= max_fd
9544                                        && (fdtable_get(n) & FDT_TYPE_MASK) == FDT_INTERNAL
9545                                    {
9546                                        // c:3835 shell-owned-fd reject
9547                                        bad = 3;
9548                                    }
9549                                }
9550                                Err(_) => {
9551                                    bad = 1; // c:3823 strtol failure
9552                                }
9553                            }
9554                        }
9555                        if bad != 0 {
9556                            // c:3840-3849
9557                            match bad {
9558                                3 => zwarn(&format!(
9559                                    "file descriptor {} used by shell, not closed",
9560                                    fn_.fd1
9561                                )),
9562                                2 => zwarn(&format!(
9563                                    "can't close file descriptor from readonly parameter {}",
9564                                    varname
9565                                )),
9566                                _ => zwarn(&format!(
9567                                    "parameter {} does not contain a file descriptor",
9568                                    varname
9569                                )),
9570                            }
9571                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9572                            LASTVAL.store(1, Ordering::Relaxed);
9573                            break;
9574                        }
9575                    }
9576                    // c:3852-3865 — `closed`: optional movefd save.
9577                    closed = 0;
9578                    if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
9579                        // c:3856
9580                        let mv = movefd(fd1_local); // c:3857
9581                        save[fd1_local as usize] = mv;
9582                        if mv >= 0 {
9583                            closed = 1; // c:3862-3863
9584                        }
9585                    }
9586                    if fd1_local < 10 {
9587                        // c:3866
9588                        closemn(&mut mfds, fd1_local, REDIR_CLOSE);
9589                        // c:3867
9590                    }
9591                    // c:3873-3876
9592                    let _ = &mut fd1_local;
9593                    if closed == 0 && zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
9594                        zwarn(&format!(
9595                            "failed to close file descriptor {}: {}",
9596                            fn_.fd1,
9597                            std::io::Error::last_os_error()
9598                        )); // c:3873-3875
9599                    }
9600                }
9601                t if t == REDIR_MERGEIN || t == REDIR_MERGEOUT => {
9602                    // c:3878-3879
9603                    if fn_.fd2 < 10 {
9604                        closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
9605                    }
9606                    if checkclobberparam(&fn_) == 0 {
9607                        fil_local = -1; // c:3883
9608                    } else if fn_.fd2 > 9 {
9609                        // c:3884-3897 — fd table check.
9610                        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
9611                        let cin = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
9612                        let cout = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
9613                        let in_table = if fn_.fd2 <= max_fd {
9614                            let kind = fdtable_get(fn_.fd2) & FDT_TYPE_MASK;
9615                            kind != FDT_UNUSED && kind != FDT_EXTERNAL
9616                        } else {
9617                            false
9618                        };
9619                        if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
9620                            fil_local = -1; // c:3896
9621                                            // Per-platform errno setter (c:3897 `errno = EBADF;`).
9622                            #[cfg(target_os = "macos")]
9623                            unsafe {
9624                                *libc::__error() = libc::EBADF;
9625                            }
9626                            #[cfg(target_os = "linux")]
9627                            unsafe {
9628                                *libc::__errno_location() = libc::EBADF;
9629                            }
9630                        } else {
9631                            let fd = if fn_.fd2 == -2 {
9632                                // c:3900-3901
9633                                if fn_.typ == REDIR_MERGEOUT {
9634                                    crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
9635                                } else {
9636                                    crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
9637                                }
9638                            } else {
9639                                fn_.fd2
9640                            };
9641                            // c:3902 — `fil = movefd(dup(fd));`
9642                            let dup_fd = unsafe { libc::dup(fd) };
9643                            fil_local = movefd(dup_fd);
9644                        }
9645                    } else {
9646                        let fd = if fn_.fd2 == -2 {
9647                            if fn_.typ == REDIR_MERGEOUT {
9648                                crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
9649                            } else {
9650                                crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
9651                            }
9652                        } else {
9653                            fn_.fd2
9654                        };
9655                        let dup_fd = unsafe { libc::dup(fd) };
9656                        fil_local = movefd(dup_fd);
9657                    }
9658                    if fil_local == -1 {
9659                        // c:3904
9660                        closemnodes(&mut mfds); // c:3907
9661                        fixfds(&save); // c:3908
9662                        if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
9663                            let desc = if fn_.fd2 == -2 {
9664                                "coprocess".to_string()
9665                            } else {
9666                                format!("{}", fn_.fd2)
9667                            };
9668                            zwarn(&format!("{}: {}", desc, std::io::Error::last_os_error()));
9669                            // c:3911-3913
9670                        }
9671                        {
9672                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9673                            LASTVAL.store(1, Ordering::Relaxed);
9674                        } // c:3914
9675                        break;
9676                    }
9677                    // c:3916-3917
9678                    let merge_is_out = if fn_.typ == REDIR_MERGEOUT { 1 } else { 0 };
9679                    addfd(
9680                        forked,
9681                        &mut save,
9682                        &mut mfds,
9683                        fn_.fd1,
9684                        fil_local,
9685                        merge_is_out,
9686                        fn_.varid.as_deref(),
9687                    );
9688                }
9689                _ => {
9690                    // c:3919 default — write/append/error_redir.
9691                    let mut dfil: i32;
9692                    if checkclobberparam(&fn_) == 0 {
9693                        fil_local = -1; // c:3921
9694                    } else if IS_APPEND_REDIR(fn_.typ) {
9695                        // c:3922
9696                        let name = fn_.name.clone().unwrap_or_default();
9697                        let unmeta_name = unmeta(&name);
9698                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
9699                            Ok(c) => c,
9700                            Err(_) => {
9701                                closemnodes(&mut mfds);
9702                                fixfds(&save);
9703                                {
9704                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9705                                    LASTVAL.store(1, Ordering::Relaxed);
9706                                }
9707                                break;
9708                            }
9709                        };
9710                        // c:3924-3927
9711                        let mode = if !isset(CLOBBER)
9712                            && !isset(crate::ported::zsh_h::APPENDCREATE)
9713                            && !IS_CLOBBER_REDIR(fn_.typ)
9714                        {
9715                            libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
9716                        } else {
9717                            libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
9718                        };
9719                        fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
9720                    } else {
9721                        // c:3929
9722                        fil_local = clobber_open(&fn_);
9723                    }
9724                    // c:3930-3933 — error_redir dup.
9725                    if fil_local != -1 && IS_ERROR_REDIR(fn_.typ) {
9726                        let dup_fd = unsafe { libc::dup(fil_local) };
9727                        dfil = movefd(dup_fd); // c:3931
9728                    } else {
9729                        dfil = 0; // c:3933
9730                    }
9731                    if fil_local == -1 || dfil == -1 {
9732                        // c:3934
9733                        if fil_local != -1 {
9734                            unsafe { libc::close(fil_local) }; // c:3935-3936
9735                        }
9736                        closemnodes(&mut mfds); // c:3937
9737                        fixfds(&save); // c:3938
9738                        let e = std::io::Error::last_os_error();
9739                        let raw = e.raw_os_error().unwrap_or(0);
9740                        if raw != 0 && raw != libc::EINTR {
9741                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
9742                            // c:3939-3940
9743                        }
9744                        {
9745                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9746                            LASTVAL.store(1, Ordering::Relaxed);
9747                        } // c:3941
9748                        break;
9749                    }
9750                    // c:3943
9751                    addfd(
9752                        forked,
9753                        &mut save,
9754                        &mut mfds,
9755                        fn_.fd1,
9756                        fil_local,
9757                        1,
9758                        fn_.varid.as_deref(),
9759                    );
9760                    if IS_ERROR_REDIR(fn_.typ) {
9761                        // c:3944-3945
9762                        addfd(forked, &mut save, &mut mfds, 2, dfil, 1, None);
9763                    }
9764                    let _ = &mut dfil;
9765                }
9766            }
9767            // c:3948-3952 — addfd errflag check.
9768            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9769                // c:3949
9770                closemnodes(&mut mfds); // c:3950
9771                fixfds(&save); // c:3951
9772                {
9773                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9774                    LASTVAL.store(1, Ordering::Relaxed);
9775                } // c:3952
9776                break;
9777            }
9778        }
9779    }
9780
9781    // c:3957-3961 — close multios with ct >= 2.
9782    i = 0;
9783    while i < 10 {
9784        // c:3959
9785        if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
9786            if m.ct >= 2 {
9787                closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
9788            }
9789        }
9790        i += 1;
9791    }
9792
9793    // c:3963-3995 — nullexec branch.
9794    if nullexec != 0 {
9795        // c:3963
9796        if let Some(vspc) = varspc {
9797            // c:3969
9798            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
9799            let mut removelist: Vec<String> = Vec::new();
9800            if !isset(POSIXBUILTINS) && nullexec != 2 {
9801                // c:3971-3972
9802                save_params(state, vspc, &mut restorelist, &mut removelist);
9803            }
9804            addvars(state, vspc, 0); // c:3973
9805            if !restorelist.is_empty() {
9806                // c:3974
9807                restore_params(restorelist, removelist); // c:3975
9808            }
9809        }
9810        let ef = errflag.load(Ordering::Relaxed);
9811        LASTVAL.store(
9812            if ef != 0 {
9813                ef
9814            } else {
9815                cmdoutval.load(Ordering::Relaxed)
9816            },
9817            Ordering::Relaxed,
9818        ); // c:3977
9819        if nullexec == 1 {
9820            // c:3978
9821            // c:3983-3985 — close save[i].
9822            i = 0;
9823            while i < 10 {
9824                if save[i as usize] != -2 {
9825                    let _ = zclose(save[i as usize]); // c:3985
9826                }
9827                i += 1;
9828            }
9829            // c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
9830            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
9831            if thisjob >= 0 {
9832                if let Some(jt) = JOBTAB.get() {
9833                    let mut guard = jt.lock().unwrap();
9834                    if let Some(j) = guard.get_mut(thisjob as usize) {
9835                        j.stat |= STAT_DONE; // c:3989
9836                    }
9837                }
9838            }
9839            return execcmd_exec_done_path(
9840                redir_err,
9841                oautocont,
9842                how,
9843                &mut shti,
9844                &mut chti,
9845                &mut then_ts,
9846                forked,
9847                &mut newxtrerr,
9848                cflags,
9849                orig_cflags,
9850                is_cursh,
9851                do_exec,
9852            );
9853        }
9854        if isset(XTRACE) {
9855            // c:3992-3994
9856            eprintln!();
9857        }
9858    } else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
9859        // c:3996 — main dispatch branch.
9860        // c:3997 — `int q = queue_signal_level();`
9861        let _q = 0;
9862        // c:4003-4012 — entersubsh for is_exec.
9863        if is_exec != 0 {
9864            // c:4003
9865            let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
9866                esub::ASYNC
9867            } else {
9868                0
9869            } | esub::PGRP
9870                | esub::FAKE; // c:4004-4005
9871            if typ != WC_SUBSH as i32 {
9872                flags |= esub::KEEPTRAP; // c:4007
9873            }
9874            if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
9875                // c:4008-4009
9876                flags |= esub::REVERTPGRP; // c:4010
9877            }
9878            entersubsh(flags, None); // c:4011
9879        }
9880
9881        if typ == WC_FUNCDEF as i32 {
9882            // c:4013
9883            // c:4014-4036 — `redir_prog` setup from wordcode if no
9884            // redirs+WC_REDIR follows. Wire only when fusevm WC_REDIR
9885            // peek is in scope; for the tree-walker entry point we
9886            // approximate by passing None.
9887            let redir_prog: Option<crate::ported::zsh_h::Eprog> = None;
9888            // c:4039 — `lastval = execfuncdef(state, redir_prog);`
9889            let lv = execfuncdef(state, redir_prog);
9890            LASTVAL.store(lv, Ordering::Relaxed);
9891        } else if typ >= WC_CURSH as i32 {
9892            // c:4042
9893            if last1 == 1 {
9894                do_exec = 1; // c:4044
9895            }
9896            if typ == WC_AUTOFN as i32 {
9897                // c:4046
9898                let lv = execautofn_basic(state, do_exec); // c:4051
9899                LASTVAL.store(lv, Ordering::Relaxed);
9900            } else {
9901                // c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
9902                // dispatch_execfuncs ports the C `execfuncs[]` table
9903                // (Src/exec.c:170-180) by typ → exec{cursh,for,select,...}
9904                // direct call. See dispatch_execfuncs at end of file.
9905                let lv = dispatch_execfuncs(state, typ, do_exec);
9906                LASTVAL.store(lv, Ordering::Relaxed);
9907            }
9908        } else if is_builtin != 0 || is_shfunc != 0 {
9909            // c:4055
9910            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
9911            let mut removelist: Vec<String> = Vec::new();
9912            let mut do_save: i32 = 0; // c:4057
9913
9914            if forked == 0 {
9915                // c:4060
9916                if isset(POSIXBUILTINS) {
9917                    // c:4061
9918                    if is_shfunc != 0
9919                        || (hn.map(|p| unsafe { (*p).node.flags as u32 }).unwrap_or(0)
9920                            & (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
9921                            != 0
9922                    {
9923                        // c:4067
9924                        do_save = if (orig_cflags & BINF_COMMAND) != 0 {
9925                            1
9926                        } else {
9927                            0
9928                        };
9929                    } else {
9930                        do_save = 1; // c:4070
9931                    }
9932                } else {
9933                    // c:4071
9934                    if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
9935                        // c:4076
9936                        do_save = 1; // c:4077
9937                    }
9938                }
9939                if do_save != 0 {
9940                    if let Some(vspc) = varspc {
9941                        // c:4079
9942                        save_params(state, vspc, &mut restorelist, &mut removelist);
9943                    }
9944                }
9945            }
9946            if varspc.is_some() {
9947                // c:4082
9948                let mut addflags: i32 = 0; // c:4086
9949                if is_shfunc != 0 {
9950                    addflags |= ADDVAR_EXPORT; // c:4088
9951                }
9952                if !restorelist.is_empty() {
9953                    addflags |= ADDVAR_RESTORE; // c:4090
9954                }
9955                addvars(state, varspc.unwrap_or(0), addflags); // c:4092
9956                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9957                    // c:4093
9958                    if !restorelist.is_empty() {
9959                        restore_params(restorelist, removelist); // c:4094-4095
9960                    }
9961                    LASTVAL.store(1, Ordering::Relaxed); // c:4096
9962                    fixfds(&save); // c:4097
9963                    return execcmd_exec_done_path(
9964                        redir_err,
9965                        oautocont,
9966                        how,
9967                        &mut shti,
9968                        &mut chti,
9969                        &mut then_ts,
9970                        forked,
9971                        &mut newxtrerr,
9972                        cflags,
9973                        orig_cflags,
9974                        is_cursh,
9975                        do_exec,
9976                    );
9977                }
9978            }
9979
9980            if is_shfunc != 0 {
9981                // c:4102-4105
9982                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
9983                // c:4104 — `execshfunc((Shfunc) hn, args);` C casts
9984                // HashNode hn to Shfunc; zshrs's hn is *mut builtin so
9985                // we re-resolve the shfunc by name from shfunctab and
9986                // dispatch through the top-level execshfunc port at
9987                // exec.rs:4978 (which routes to runshfunc).
9988                let name = args
9989                    .as_ref()
9990                    .and_then(|v| v.first())
9991                    .cloned()
9992                    .unwrap_or_default();
9993                let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
9994                    tab.get(&name).cloned()
9995                } else {
9996                    None
9997                };
9998                if let Some(ref mut shf) = shf_clone {
9999                    execshfunc(shf, &mut a_vec);
10000                }
10001                // c:4105 — `pipecleanfilelist(filelist, 0);` — clean
10002                // out the proc_subst entries from the current job's
10003                // filelist after the shfunc body ran. Route through
10004                // `JOBTAB[thisjob]`.
10005                if let Some(jt) = JOBTAB.get() {
10006                    let mut guard = jt.lock().unwrap();
10007                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10008                    if tj >= 0 {
10009                        if let Some(j) = guard.get_mut(tj as usize) {
10010                            crate::ported::jobs::pipecleanfilelist(j, false);
10011                        }
10012                    }
10013                }
10014            } else {
10015                // c:4107 — builtin path.
10016                let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
10017                let postassigns = eparams.postassigns; // c:4109
10018                if forked != 0 {
10019                    closem(FDT_INTERNAL, 0); // c:4111
10020                }
10021                if postassigns != 0 {
10022                    // c:4112-4230 — typeset post-assignment processing.
10023                    use crate::ported::zsh_h::{
10024                        ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
10025                        PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
10026                        WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
10027                    };
10028                    let opc = state.pc; // c:4113
10029                    state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
10030                                                                      // c:4115 — `assigns = newlinklist();` — already declared above.
10031                    let mut pa_remaining = postassigns;
10032                    while pa_remaining > 0 {
10033                        // c:4116 — `while (postassigns--)`
10034                        pa_remaining -= 1;
10035                        let mut pa_htok: i32 = 0; // c:4117
10036                        if state.pc >= state.prog.prog.len() {
10037                            break;
10038                        }
10039                        let ac = state.prog.prog[state.pc]; // c:4118
10040                        state.pc += 1;
10041                        let mut name = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut pa_htok)); // c:4119
10042                                                                                            // c:4123-4124 DPUTS — debug assertion skipped.
10043                        if pa_htok != 0 {
10044                            // c:4126 — `init_list1(svl, name);`
10045                            let mut svl: LinkList<String> = Default::default();
10046                            svl.push_back(name.clone());
10047                            // c:4127-4166 — INC-scalar special case (typeset $ass form).
10048                            if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
10049                                && WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
10050                            {
10051                                // c:4141 — `(void)ecgetstr(...)` — dummy.
10052                                let mut dummy_htok: i32 = 0;
10053                                let _ = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut dummy_htok));
10054                                let mut rf = 0i32;
10055                                prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
10056                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10057                                    // c:4143
10058                                    state.pc = opc; // c:4144
10059                                    break;
10060                                }
10061                                let mut rf2 = 0i32;
10062                                globlist(&mut svl, rf2); // c:4147
10063                                let _ = &mut rf2;
10064                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10065                                    // c:4148
10066                                    state.pc = opc; // c:4149
10067                                    break;
10068                                }
10069                                // c:4152-4165 — drain svl into assigns.
10070                                while let Some(data) = svl.pop_front() {
10071                                    let (asg_name, asg_val): (String, Option<String>) =
10072                                        if let Some(eq_pos) = data.find('=') {
10073                                            // c:4156-4159
10074                                            (
10075                                                data[..eq_pos].to_string(),
10076                                                Some(data[eq_pos + 1..].to_string()),
10077                                            )
10078                                        } else {
10079                                            // c:4161-4162
10080                                            (data, None)
10081                                        };
10082                                    assigns.push(crate::ported::zsh_h::asgment {
10083                                        node: crate::ported::zsh_h::linknode {
10084                                            next: None,
10085                                            prev: None,
10086                                            dat: 0,
10087                                        },
10088                                        name: asg_name,
10089                                        flags: 0,
10090                                        scalar: asg_val,
10091                                        array: None,
10092                                    });
10093                                }
10094                                continue; // c:4166
10095                            }
10096                            // c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
10097                            let mut rf = 0i32;
10098                            prefork(&mut svl, PREFORK_SINGLE, &mut rf);
10099                            // c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
10100                            name = if svl.is_empty() {
10101                                String::new()
10102                            } else {
10103                                svl.pop_front().unwrap_or_default()
10104                            };
10105                        }
10106                        // c:4172 — `untokenize(name);`
10107                        // (untokenize is destructive on bytes; Rust untokenize
10108                        // returns a new String — call and rebind.)
10109                        name = untokenize(&name);
10110                        let mut asg = crate::ported::zsh_h::asgment {
10111                            node: crate::ported::zsh_h::linknode {
10112                                next: None,
10113                                prev: None,
10114                                dat: 0,
10115                            },
10116                            name,
10117                            flags: 0,
10118                            scalar: None,
10119                            array: None,
10120                        };
10121                        if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
10122                            // c:4175
10123                            let mut val_htok: i32 = 0;
10124                            let mut val = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut val_htok)); // c:4176
10125                            asg.flags = 0; // c:4177
10126                            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
10127                                // c:4178-4180 — fake assignment, no value.
10128                                asg.scalar = None;
10129                            } else {
10130                                if val_htok != 0 {
10131                                    // c:4183
10132                                    let mut svl: LinkList<String> = Default::default();
10133                                    svl.push_back(val.clone());
10134                                    let mut rf = 0i32;
10135                                    prefork(&mut svl, PREFORK_SINGLE | PREFORK_ASSIGN, &mut rf); // c:4184-4186
10136                                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10137                                        // c:4187
10138                                        state.pc = opc; // c:4188
10139                                        break;
10140                                    }
10141                                    // c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
10142                                    val = if svl.is_empty() {
10143                                        String::new()
10144                                    } else {
10145                                        svl.pop_front().unwrap_or_default()
10146                                    };
10147                                }
10148                                // c:4198 — `untokenize(val);`
10149                                asg.scalar = Some(untokenize(&val));
10150                            }
10151                        } else {
10152                            // c:4202 — array assignment.
10153                            asg.flags = ASG_ARRAY; // c:4202
10154                            let mut arr_htok: i32 = 0;
10155                            let arr_words = ecgetlist(
10156                                state,
10157                                WC_ASSIGN_NUM(ac) as usize,
10158                                ECDUPTOK_LOCAL,
10159                                Some(&mut arr_htok),
10160                            ); // c:4204
10161                            let mut arr_list: LinkList<String> = Default::default();
10162                            for s in arr_words {
10163                                arr_list.push_back(s);
10164                            }
10165                            if !arr_list.is_empty()
10166                                && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
10167                            {
10168                                // c:4209 — `int prefork_ret = 0;`
10169                                let mut prefork_ret = 0i32;
10170                                prefork(&mut arr_list, PREFORK_ASSIGN, &mut prefork_ret); // c:4210-4211
10171                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10172                                    // c:4212
10173                                    state.pc = opc; // c:4213
10174                                    break;
10175                                }
10176                                if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
10177                                    // c:4216
10178                                    asg.flags |= ASG_KEY_VALUE; // c:4217
10179                                }
10180                                globlist(&mut arr_list, prefork_ret); // c:4218
10181                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10182                                    // c:4220
10183                                    state.pc = opc; // c:4221
10184                                    break;
10185                                }
10186                            }
10187                            asg.array = Some(arr_list);
10188                        }
10189                        // c:4227 — `uaddlinknode(assigns, &asg->node);`
10190                        assigns.push(asg);
10191                    }
10192                    state.pc = opc; // c:4229
10193                }
10194                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
10195                    // c:4232
10196                    // c:Src/builtin.c:262 — `name = (char *) ugetnode(args);`
10197                    // C's execbuiltin consumes args[0] (the command name)
10198                    // at entry. zshrs's execbuiltin reads the name from
10199                    // `bn->node.nam` instead, so we strip args[0] here
10200                    // before the call to match C's post-ugetnode argv
10201                    // shape. Without this, e.g. `cmd=pwd; $cmd` reached
10202                    // execbuiltin with args=["pwd"] and pwd's
10203                    // maxargs=0 check rejected the empty call as
10204                    // "too many arguments".
10205                    let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
10206                    if !a_vec.is_empty() {
10207                        a_vec.remove(0);
10208                    }
10209                    let ret = crate::ported::builtin::execbuiltin(
10210                        a_vec,
10211                        assigns,
10212                        hn.unwrap_or(std::ptr::null_mut()),
10213                    ); // c:4233
10214                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
10215                        // c:4238
10216                        LASTVAL.store(ret, Ordering::Relaxed); // c:4239
10217                    }
10218                }
10219                if (do_save & BINF_COMMAND as i32) != 0 {
10220                    // c:4241
10221                    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
10222                }
10223                // c:4244 fflush(stdout) — Rust stdio auto-flushes.
10224                // c:4245-4251 — write-error check on save[1].
10225            }
10226            if isset(PRINTEXITVALUE)
10227                && isset(SHINSTDIN)
10228                && LASTVAL.load(Ordering::Relaxed) != 0
10229                && subsh.load(Ordering::Relaxed) == 0
10230            {
10231                // c:4253-4255
10232                eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
10233            }
10234
10235            if do_exec != 0 {
10236                // c:4263
10237                if subsh.load(Ordering::Relaxed) != 0 {
10238                    crate::ported::builtin::_realexit(); // c:4264-4265
10239                }
10240                if isset(RCS)
10241                    && crate::ported::zsh_h::interact()
10242                    && nohistsave.load(Ordering::Relaxed) == 0
10243                {
10244                    // c:4269
10245                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
10246                    // c:4270
10247                }
10248                crate::ported::builtin::realexit(); // c:4271
10249            }
10250            if !restorelist.is_empty() {
10251                // c:4273
10252                restore_params(restorelist, removelist); // c:4274
10253            }
10254        } else {
10255            // c:4276 — external command execute.
10256            if subsh.load(Ordering::Relaxed) == 0 {
10257                // c:4277
10258                if forked == 0 {
10259                    // c:4280 — `setiparam("SHLVL", --shlvl);`
10260                    let cur = getsparam("SHLVL")
10261                        .and_then(|s| s.parse::<i64>().ok())
10262                        .unwrap_or(1);
10263                    setiparam("SHLVL", cur - 1); // c:4281
10264                }
10265                if do_exec != 0
10266                    && isset(RCS)
10267                    && crate::ported::zsh_h::interact()
10268                    && nohistsave.load(Ordering::Relaxed) == 0
10269                {
10270                    // c:4285
10271                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
10272                    // c:4286
10273                }
10274            }
10275            if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
10276                // c:4288
10277                if varspc.is_some() {
10278                    // c:4289
10279                    let mut addflags: i32 = ADDVAR_EXPORT; // c:4290
10280                    if forked != 0 {
10281                        addflags |= ADDVAR_RESTORE; // c:4292
10282                    }
10283                    addvars(state, varspc.unwrap_or(0), addflags); // c:4293
10284                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10285                        // c:4294
10286                        std::process::exit(1); // c:4295
10287                    }
10288                }
10289                closem(FDT_INTERNAL, 0); // c:4297
10290                                         // c:4298-4305 — close coprocin/coprocout.
10291                let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
10292                if cpi != -1 {
10293                    let _ = zclose(cpi); // c:4299
10294                    crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed);
10295                    // c:4300
10296                }
10297                let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
10298                if cpo != -1 {
10299                    let _ = zclose(cpo); // c:4303
10300                    crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed);
10301                    // c:4304
10302                }
10303                if forked == 0 {
10304                    // c:4307
10305                    setlimits(""); // c:4308
10306                }
10307                if (how & Z_ASYNC as i32) != 0 {
10308                    // c:4310 — `zsfree(STTYval); STTYval = 0;`
10309                    let mut guard = STTYval.lock().unwrap();
10310                    *guard = None; // c:4311-4312
10311                }
10312                // c:4314 — `execute(args, cflags, use_defpath);`
10313                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
10314                execute(&mut a_vec, cflags, use_defpath); // c:4314
10315            } else {
10316                // c:4315 — `( ... )` — WC_SUBSH.
10317                list_pipe.store(0, Ordering::Relaxed); // c:4318
10318                                                       // c:4319 — `pipecleanfilelist(filelist, 0);` — clean
10319                                                       // proc-subst entries from the current job's filelist
10320                                                       // before recursing into the subshell body.
10321                if let Some(jt) = JOBTAB.get() {
10322                    let mut guard = jt.lock().unwrap();
10323                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10324                    if tj >= 0 {
10325                        if let Some(j) = guard.get_mut(tj as usize) {
10326                            crate::ported::jobs::pipecleanfilelist(j, false);
10327                        }
10328                    }
10329                }
10330                state.pc += 1; // c:4324 — `state->pc++;`
10331                let _ = execlist(state, 0, 1); // c:4325
10332            }
10333        }
10334    }
10335
10336    // c:4330-4404 — err: + done: + fatal:.
10337    return execcmd_exec_done_path(
10338        redir_err,
10339        oautocont,
10340        how,
10341        &mut shti,
10342        &mut chti,
10343        &mut then_ts,
10344        forked,
10345        &mut newxtrerr,
10346        cflags,
10347        orig_cflags,
10348        is_cursh,
10349        do_exec,
10350    );
10351}
10352
10353/// Internal helper modelling the C `done:` label tail of
10354/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
10355/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
10356/// shelltime stop, and newxtrerr close.
10357#[allow(clippy::too_many_arguments)]
10358fn execcmd_exec_done_path(
10359    redir_err: i32,
10360    oautocont: i32,
10361    how: i32,
10362    shti: &mut crate::ported::jobs::timeinfo,
10363    chti: &mut crate::ported::jobs::timeinfo,
10364    then_ts: &mut std::time::Instant,
10365    forked: i32,
10366    newxtrerr: &mut Option<i32>,
10367    cflags: u32,
10368    orig_cflags: u32,
10369    is_cursh: i32,
10370    do_exec: i32,
10371) {
10372    use crate::ported::zsh_h::{
10373        AUTOCONTINUE, BINF_COMMAND, BINF_EXEC, BINF_PSPECIAL, INTERACTIVE, POSIXBUILTINS, Z_TIMED,
10374    };
10375    // c:4366
10376    // c:4367-4386 — POSIX special-builtin error escalation.
10377    if isset(POSIXBUILTINS)
10378        && (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
10379        && (orig_cflags & BINF_COMMAND) == 0
10380    {
10381        // c:4367-4369
10382        let _forked_or_subsh = forked | zsh_subshell.load(Ordering::Relaxed); // c:4376
10383                                                                              // fatal: label entry point — same handling.
10384        if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10385            // c:4378
10386            if !isset(INTERACTIVE) {
10387                // c:4379
10388                if _forked_or_subsh != 0 {
10389                    unsafe { libc::_exit(1) }; // c:4381
10390                } else {
10391                    std::process::exit(1); // c:4383
10392                }
10393            }
10394            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
10395        }
10396    }
10397    // c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
10398    if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
10399        crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1);
10400        // c:4389
10401    }
10402    // c:4390-4398 — newxtrerr close.
10403    if let Some(fd) = newxtrerr.take() {
10404        // c:4390
10405        let _ = zclose(fd); // c:4396
10406    }
10407    // c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
10408    {
10409        let mut guard = STTYval.lock().unwrap();
10410        *guard = None;
10411    }
10412    // c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
10413    if oautocont >= 0 {
10414        opt_state_set("autocontinue", oautocont != 0);
10415    }
10416}
10417
10418/// Internal helper modelling the C `err:` label tail of
10419/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
10420/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
10421/// to done:.
10422#[allow(clippy::too_many_arguments)]
10423fn execcmd_exec_err_path(
10424    forked: i32,
10425    save: &mut [i32; 10],
10426    mfds: &mut [Option<Box<multio>>; 10],
10427    oautocont: i32,
10428    how: i32,
10429    shti: &mut crate::ported::jobs::timeinfo,
10430    chti: &mut crate::ported::jobs::timeinfo,
10431    then_ts: &mut std::time::Instant,
10432    newxtrerr: &mut Option<i32>,
10433    cflags: u32,
10434    orig_cflags: u32,
10435    is_cursh: i32,
10436    do_exec: i32,
10437    redir_err: i32,
10438) {
10439    use crate::ported::zsh_h::FDT_UNUSED;
10440    // c:4330
10441    if forked != 0 {
10442        // c:4331
10443        // c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
10444        let mut i: i32 = 0;
10445        while i < 10 {
10446            if fdtable_get(i) != FDT_UNUSED {
10447                unsafe { libc::close(i) }; // c:4358
10448            }
10449            i += 1;
10450        }
10451        // c:4359 — `closem(FDT_UNUSED, 1);`
10452        closem(FDT_UNUSED, 1); // c:4359
10453                               // c:4360-4361 — `if (thisjob != -1) waitjobs();`
10454        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10455        if thisjob != -1 {
10456            if let Some(jt) = JOBTAB.get() {
10457                let mut guard = jt.lock().unwrap();
10458                crate::ported::jobs::waitjobs(&mut guard, thisjob as usize); // c:4361
10459            }
10460        }
10461        crate::ported::builtin::_realexit(); // c:4362
10462    }
10463    fixfds(save); // c:4364
10464
10465    execcmd_exec_done_path(
10466        redir_err,
10467        oautocont,
10468        how,
10469        shti,
10470        chti,
10471        then_ts,
10472        forked,
10473        newxtrerr,
10474        cflags,
10475        orig_cflags,
10476        is_cursh,
10477        do_exec,
10478    );
10479}
10480
10481/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
10482/// `Src/exec.c:170-180`. Each branch maps to the ported wordcode-
10483/// walker function in `src/ported/exec.rs`.
10484fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
10485    use crate::ported::zsh_h::{
10486        WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
10487        WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
10488    };
10489    // Port of `static int (*const execfuncs[])(Estate, int)` dispatch
10490    // table at `Src/exec.c:170-180`. C indexes by `(type - WC_CURSH)`;
10491    // Rust matches on the WC_* tag directly.
10492    match typ as wordcode {
10493        x if x == WC_CURSH => execcursh(state, do_exec),
10494        x if x == WC_FOR => execfor(state, do_exec),
10495        x if x == WC_SELECT => execselect(state, do_exec),
10496        x if x == WC_WHILE => execwhile(state, do_exec),
10497        x if x == WC_REPEAT => execrepeat(state, do_exec),
10498        x if x == WC_CASE => execcase(state, do_exec),
10499        x if x == WC_IF => execif(state, do_exec),
10500        x if x == WC_COND => execcond(state, do_exec),
10501        x if x == WC_ARITH => execarith(state, do_exec),
10502        x if x == WC_TRY => exectry(state, do_exec),
10503        x if x == WC_FUNCDEF => execfuncdef(state, None),
10504        // c:272 — execfuncs[] table dispatches `WC_AUTOFN` to
10505        // `execautofn` (the loadautofn-then-basic wrapper), not
10506        // `execautofn_basic` directly.
10507        x if x == WC_AUTOFN => execautofn(state, do_exec),
10508        x if x == WC_TIMED => exectime(state, do_exec),
10509        x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
10510        _ => 0,
10511    }
10512}
10513
10514/// Port of `Eprog stripkshdef(Eprog prog, char *name)` from
10515/// `Src/exec.c:6286-6364`. Given an Eprog read from an autoload
10516/// file plus the function name being defined, check whether the
10517/// file consists of *exactly* one `function NAME { … }` definition
10518/// for that name. If so, return a new Eprog whose `prog`/`strs`/
10519/// `pats` slice out just the function body (so calling code can
10520/// invoke the body directly instead of re-parsing). Otherwise
10521/// return the input untouched.
10522///
10523/// Header word layout consumed (matches C `pc[…]` reads):
10524///   pc[0] = WC_LIST with `Z_SYNC|Z_END|Z_SIMPLE` flags
10525///   pc[1] = (sublist header, skipped)
10526///   pc[2] = WC_FUNCDEF
10527///   pc[3] = 1                       (single-name funcdef)
10528///   pc[4] = name-string slot        (compared to `name`)
10529///   pc[5] = sbeg  (offset into strs table)
10530///   pc[6] = nstrs (bytes of strs to copy)
10531///   pc[7] = npats (number of pattern slots to allocate)
10532///   pc[8] = WC_FUNCDEF_SKIP target  (end-of-funcdef pc)
10533///   pc[9] = (unused header word — `pc += 6` lands here as the
10534///           start of the body wordcode stream)
10535///
10536/// Returns `None` only when the input was `None` (matches C
10537/// `return NULL`). Equivalence between the original `prog` and a
10538/// successfully stripped `prog` is *not* preserved at the pointer
10539/// level (C may return the original Eprog when the file fails the
10540/// single-funcdef shape check; this Rust port does the same by
10541/// passing the box back through).
10542///
10543/// `EF_MAP` (`zcompile`d / mmap'd Eprog) path: C mutates the
10544/// existing Eprog in place, swapping its `prog` / `strs` /
10545/// `pats` to slice into the funcdef body. Rust mirrors this on
10546/// the moved-in `Box<eprog>` (no separate `free()` needed —
10547/// `Vec` drop handles the old `pats`).
10548pub fn stripkshdef(
10549    prog: Option<crate::ported::zsh_h::Eprog>,
10550    name: &str,
10551) -> Option<crate::ported::zsh_h::Eprog> {
10552    use crate::ported::parse::ecrawstr;
10553    use crate::ported::zsh_h::{
10554        wc_code, wordcode, Dash, EF_HEAP, EF_MAP, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_LIST,
10555        WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC,
10556    };
10557
10558    // c:6300 — `if (!prog) return NULL;`
10559    let mut prog = prog?;
10560
10561    // c:6302-6306 — first word must be WC_LIST with all of
10562    // Z_SYNC|Z_END|Z_SIMPLE set (i.e. the trivial "single simple
10563    // sublist" wrapper around the funcdef).
10564    if prog.prog.len() < 3 {
10565        return Some(prog);
10566    }
10567    let code0: wordcode = prog.prog[0];
10568    if wc_code(code0) != WC_LIST
10569        || (WC_LIST_TYPE(code0) & (Z_SYNC | Z_END | Z_SIMPLE) as wordcode)
10570            != (Z_SYNC | Z_END | Z_SIMPLE) as wordcode
10571    {
10572        return Some(prog);
10573    }
10574    // c:6307 — `pc++;` (skip the sublist header word at pc[1]).
10575    // c:6308 — `code = *pc++;` lands `code` on pc[2], leaving the
10576    // walking cursor at pc[3] which is read directly below.
10577    let code: wordcode = prog.prog[2];
10578    let pc_after_code: usize = 3;
10579    if wc_code(code) != WC_FUNCDEF || prog.prog[pc_after_code] != 1 {
10580        return Some(prog);
10581    }
10582
10583    // c:6320 — `ptr2 = ecrawstr(prog, pc + 1, NULL);` (note: C's
10584    // `pc` is already past `code`, so `pc + 1` lands on pc[4] —
10585    // the name-string slot).
10586    let name_slot = pc_after_code + 1; // == 4
10587    let name_in_def = ecrawstr(&prog, name_slot, None);
10588
10589    // c:6320-6328 — name match, tolerating Dash-tokenised hyphens
10590    // on either side.
10591    let n1 = name.as_bytes();
10592    let n2 = name_in_def.as_bytes();
10593    let mut i = 0usize;
10594    let mut j = 0usize;
10595    while i < n1.len() && j < n2.len() {
10596        let c1 = n1[i] as char;
10597        let c2 = n2[j] as char;
10598        if c1 != c2 && c1 != Dash && c1 != '-' && c2 != Dash && c2 != '-' {
10599            break;
10600        }
10601        i += 1;
10602        j += 1;
10603    }
10604    // c:6329 — `if (*ptr1 || *ptr2) return prog;` (any unmatched
10605    // tail on either side → not the right funcdef).
10606    if i < n1.len() || j < n2.len() {
10607        return Some(prog);
10608    }
10609
10610    // c:6332-6362 — slice the funcdef body out. Layout:
10611    //   sbeg  = pc[2] (in C, == prog.prog[pc_after_code + 2] == [5])
10612    //   nstrs = pc[3] (== [6])
10613    //   npats = pc[4] (== [7])
10614    //   end   = pc + WC_FUNCDEF_SKIP(code)   (== pc_after_code + skip)
10615    //   pc   += 6  (body wordcode begins at pc_after_code + 6 == [9])
10616    let sbeg = prog.prog[pc_after_code + 2] as usize;
10617    let nstrs = prog.prog[pc_after_code + 3] as usize;
10618    let npats = prog.prog[pc_after_code + 4] as i32;
10619    let skip = WC_FUNCDEF_SKIP(code) as usize;
10620    let end_pc = pc_after_code + skip;
10621    let body_start = pc_after_code + 6;
10622    if end_pc < body_start || end_pc > prog.prog.len() {
10623        // Defensive: malformed header — return input untouched so
10624        // the caller's parse-eprog fallback re-reads from source.
10625        return Some(prog);
10626    }
10627    let nprg = end_pc - body_start;
10628    let plen = nprg * size_of::<wordcode>();
10629    let len = plen + (npats as usize) * size_of::<usize>() + nstrs;
10630
10631    // Build the new pats slice — `dummy_patprog1` slots in C; the
10632    // Rust convention (mirrors `dupeprog` at parse.rs:2716) is to
10633    // synthesize zero-initialised patprog placeholders that
10634    // pattern compile-on-first-use will overwrite.
10635    let dummy_pat = || {
10636        Box::new(crate::ported::zsh_h::patprog {
10637            startoff: 0,
10638            size: 0,
10639            mustoff: 0,
10640            patmlen: 0,
10641            globflags: 0,
10642            globend: 0,
10643            flags: 0,
10644            patnpar: 0,
10645            patstartch: 0,
10646        })
10647    };
10648    let new_pats: Vec<crate::ported::zsh_h::Patprog> =
10649        (0..npats.max(0)).map(|_| dummy_pat()).collect();
10650
10651    // c:6353 — `ret->strs = prog->strs + sbeg;` (EF_MAP) or
10652    // c:6359 — `memcpy(ret->strs, prog->strs + sbeg, nstrs);` (heap).
10653    let old_strs = prog.strs.take().unwrap_or_default();
10654    let old_bytes = old_strs.as_bytes();
10655    let new_strs = if sbeg + nstrs <= old_bytes.len() {
10656        Some(String::from_utf8_lossy(&old_bytes[sbeg..sbeg + nstrs]).into_owned())
10657    } else {
10658        Some(String::new())
10659    };
10660
10661    let new_prog: Vec<wordcode> = prog.prog[body_start..end_pc].to_vec();
10662
10663    if (prog.flags & EF_MAP) != 0 {
10664        // c:6349-6354 — in-place EF_MAP path.
10665        prog.pats = new_pats;
10666        prog.prog = new_prog;
10667        prog.strs = new_strs;
10668        prog.len = len as i32;
10669        prog.npats = npats;
10670        prog.shf = None;
10671        return Some(prog);
10672    }
10673
10674    // c:6356-6361 — heap-allocated new Eprog.
10675    let ret = Box::new(eprog {
10676        flags: EF_HEAP,
10677        len: len as i32,
10678        npats,
10679        nref: -1, // c:6363 (heap path → never refcount-freed).
10680        pats: new_pats,
10681        prog: new_prog,
10682        strs: new_strs,
10683        shf: None, // c:6363
10684        dump: None,
10685    });
10686    Some(ret)
10687}
10688
10689#[cfg(test)]
10690mod tests {
10691    use super::*;
10692
10693    // ─── zsh-corpus pins for pure exec helpers ─────────────────────
10694
10695    /// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
10696    #[test]
10697    fn exec_corpus_isrelative_empty_is_one() {
10698        let _g = crate::test_util::global_state_lock();
10699        assert_eq!(isrelative(""), 1, "empty path is relative");
10700    }
10701
10702    /// `isrelative("foo")` = 1 (no leading slash).
10703    #[test]
10704    fn exec_corpus_isrelative_bare_name_is_one() {
10705        let _g = crate::test_util::global_state_lock();
10706        assert_eq!(isrelative("foo"), 1);
10707        assert_eq!(isrelative("bin/cmd"), 1);
10708    }
10709
10710    /// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
10711    #[test]
10712    fn exec_corpus_isrelative_absolute_clean_is_zero() {
10713        let _g = crate::test_util::global_state_lock();
10714        assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
10715        assert_eq!(isrelative("/bin/ls"), 0);
10716        assert_eq!(isrelative("/"), 0, "root is absolute");
10717    }
10718
10719    /// `isrelative("/foo/../bar")` = 1 (contains `../` component).
10720    #[test]
10721    fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
10722        let _g = crate::test_util::global_state_lock();
10723        assert_eq!(
10724            isrelative("/foo/../bar"),
10725            1,
10726            "absolute path with ../ is still 'relative' per zsh"
10727        );
10728    }
10729
10730    /// `isrelative("/foo/./bar")` = 1 (contains `./` component).
10731    #[test]
10732    fn exec_corpus_isrelative_absolute_with_dot_is_one() {
10733        let _g = crate::test_util::global_state_lock();
10734        assert_eq!(
10735            isrelative("/./x"),
10736            1,
10737            "absolute with ./ component reported relative"
10738        );
10739    }
10740
10741    /// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
10742    #[test]
10743    fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
10744        assert_eq!(is_anonymous_function_name("(anon)"), 1);
10745    }
10746
10747    /// `is_anonymous_function_name("regular_name")` = 0.
10748    #[test]
10749    fn exec_corpus_is_anonymous_function_name_rejects_normal() {
10750        assert_eq!(is_anonymous_function_name("regular_name"), 0);
10751        assert_eq!(is_anonymous_function_name(""), 0);
10752        assert_eq!(
10753            is_anonymous_function_name("anon"),
10754            0,
10755            "plain 'anon' (no parens) is NOT the sentinel"
10756        );
10757    }
10758
10759    /// `iscom("/nonexistent/never_a_path")` = false.
10760    #[test]
10761    fn exec_corpus_iscom_missing_path_false() {
10762        assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
10763    }
10764
10765    /// `iscom("/tmp")` is a directory not a regular file → false.
10766    #[test]
10767    fn exec_corpus_iscom_directory_false() {
10768        assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
10769    }
10770
10771    /// `iscom("/bin/sh")` is true on POSIX systems.
10772    #[test]
10773    fn exec_corpus_iscom_known_binary_true() {
10774        // /bin/sh exists on all POSIX systems with X perms.
10775        if std::path::Path::new("/bin/sh").exists() {
10776            assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
10777        }
10778    }
10779
10780    // ─── stripkshdef (Src/exec.c:6286) early-return paths ──────────
10781
10782    /// `stripkshdef(None, "foo")` → `None` (matches C `if (!prog)
10783    /// return NULL;` at exec.c:6300).
10784    #[test]
10785    fn exec_corpus_stripkshdef_null_input_returns_none() {
10786        assert!(stripkshdef(None, "foo").is_none());
10787    }
10788
10789    /// `stripkshdef` on an empty/degenerate Eprog returns the same
10790    /// Eprog unchanged (no funcdef-shape to strip).
10791    #[test]
10792    fn exec_corpus_stripkshdef_empty_prog_returns_input() {
10793        let prog = Box::new(eprog {
10794            prog: vec![],
10795            ..Default::default()
10796        });
10797        let out = stripkshdef(Some(prog), "foo");
10798        assert!(out.is_some(), "empty prog → returned unchanged");
10799        assert!(out.unwrap().prog.is_empty(), "no mutation");
10800    }
10801
10802    /// `stripkshdef` on a non-WC_LIST head returns the input
10803    /// untouched (early return at exec.c:6304-6306).
10804    #[test]
10805    fn exec_corpus_stripkshdef_non_list_head_returns_input() {
10806        use crate::ported::zsh_h::{wc_bld, WC_SUBLIST};
10807        let prog = Box::new(eprog {
10808            prog: vec![wc_bld(WC_SUBLIST, 0), 0, 0],
10809            ..Default::default()
10810        });
10811        let out = stripkshdef(Some(prog), "foo");
10812        assert!(out.is_some());
10813        // first word is the WC_SUBLIST sentinel we passed in,
10814        // unchanged (the function bailed before doing any slicing).
10815        let p = out.unwrap();
10816        use crate::ported::zsh_h::wc_code;
10817        assert_eq!(
10818            wc_code(p.prog[0]),
10819            WC_SUBLIST,
10820            "header word preserved verbatim"
10821        );
10822    }
10823
10824    // ═══════════════════════════════════════════════════════════════════
10825    // C-parity tests pinning Src/exec.c. Tests that capture KNOWN
10826    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
10827    // ═══════════════════════════════════════════════════════════════════
10828
10829    /// `isrelative("/abs/path")` returns 0 (false = absolute path).
10830    /// C `Src/exec.c:996-1006` — leading `/` and no `.`/`..` components.
10831    #[test]
10832    fn isrelative_absolute_path_returns_zero() {
10833        let _g = crate::test_util::global_state_lock();
10834        assert_eq!(isrelative("/usr/local/bin"), 0);
10835    }
10836
10837    /// `isrelative("foo/bar")` returns 1 (no leading slash).
10838    #[test]
10839    fn isrelative_no_leading_slash_returns_one() {
10840        let _g = crate::test_util::global_state_lock();
10841        assert_eq!(isrelative("foo/bar"), 1);
10842    }
10843
10844    /// `isrelative("/foo/./bar")` returns 1 — contains `/./` walk.
10845    /// C c:1001 — `.` with prev `/` + next `/` triggers relative flag.
10846    #[test]
10847    fn isrelative_dot_component_returns_one() {
10848        let _g = crate::test_util::global_state_lock();
10849        assert_eq!(isrelative("/foo/./bar"), 1, "/./ in path → relative");
10850    }
10851
10852    /// `isrelative("/foo/../bar")` returns 1 — contains `/..` walk.
10853    #[test]
10854    fn isrelative_dotdot_component_returns_one() {
10855        let _g = crate::test_util::global_state_lock();
10856        assert_eq!(isrelative("/foo/../bar"), 1, "/../ in path → relative");
10857    }
10858
10859    /// `isrelative("")` returns 1 — empty input has no leading `/`.
10860    /// C c:998 — `*s != '/'` includes the NUL terminator case.
10861    #[test]
10862    fn isrelative_empty_returns_one() {
10863        let _g = crate::test_util::global_state_lock();
10864        assert_eq!(isrelative(""), 1, "empty string → not absolute");
10865    }
10866
10867    /// `isrelative("/a/.b")` returns 0 — `.b` is NOT a `/./` walk
10868    /// (followed by another non-`/` char `b`).
10869    #[test]
10870    fn isrelative_dotfile_in_path_returns_zero() {
10871        let _g = crate::test_util::global_state_lock();
10872        assert_eq!(
10873            isrelative("/usr/.config/zsh"),
10874            0,
10875            "dotfile name '.config' is NOT a relative walk"
10876        );
10877    }
10878
10879    /// `is_anonymous_function_name("(anon)")` returns 1 (true).
10880    /// C `Src/exec.c` — `!strcmp(name, ANONYMOUS_FUNCTION_NAME)`.
10881    #[test]
10882    fn is_anonymous_function_name_anon_returns_one() {
10883        let _g = crate::test_util::global_state_lock();
10884        assert_eq!(is_anonymous_function_name("(anon)"), 1);
10885    }
10886
10887    /// `is_anonymous_function_name("foo")` returns 0 (false).
10888    #[test]
10889    fn is_anonymous_function_name_normal_returns_zero() {
10890        let _g = crate::test_util::global_state_lock();
10891        assert_eq!(is_anonymous_function_name("foo"), 0);
10892        assert_eq!(is_anonymous_function_name(""), 0);
10893        assert_eq!(is_anonymous_function_name("(other)"), 0);
10894    }
10895
10896    /// `isgooderr(EACCES, "/no/such/dir")` returns true when the dir
10897    /// is not actually accessible. C `Src/exec.c:isgooderr` filters
10898    /// out "unreadable / not directory" errnos so caller doesn't
10899    /// emit spurious warnings.
10900    #[test]
10901    fn isgooderr_eacces_unreadable_dir_returns_false() {
10902        let _g = crate::test_util::global_state_lock();
10903        // /no/such/dir doesn't exist → access(X_OK) fails non-zero
10904        // → !access() is 0 (false) → returns false.
10905        assert!(
10906            !isgooderr(libc::EACCES, "/no/such/dir/zshrs_test"),
10907            "unreadable dir with EACCES should NOT be 'good error'"
10908        );
10909    }
10910
10911    // ═══════════════════════════════════════════════════════════════════
10912    // Additional C-parity tests for Src/exec.c basic accessors/predicates.
10913    // ═══════════════════════════════════════════════════════════════════
10914
10915    /// c:658 — `isgooderr(ENOENT, _)` always false (regardless of dir).
10916    /// Pin: ENOENT is NEVER a "good error" because the path itself
10917    /// doesn't exist — caller should suppress the warning.
10918    #[test]
10919    fn isgooderr_enoent_always_false() {
10920        let _g = crate::test_util::global_state_lock();
10921        assert!(!isgooderr(libc::ENOENT, "/tmp"));
10922        assert!(!isgooderr(libc::ENOENT, "/no/such/dir"));
10923        assert!(!isgooderr(libc::ENOENT, ""));
10924    }
10925
10926    /// c:658 — `isgooderr(ENOTDIR, _)` always false. A path component
10927    /// being a non-dir is a structural error, not a permission issue.
10928    #[test]
10929    fn isgooderr_enotdir_always_false() {
10930        let _g = crate::test_util::global_state_lock();
10931        assert!(!isgooderr(libc::ENOTDIR, "/tmp"));
10932        assert!(!isgooderr(libc::ENOTDIR, "/"));
10933    }
10934
10935    /// c:658 — Other errnos (EPERM, EIO, ENOMEM) are "good errors"
10936    /// because they're not the suppressed three (EACCES/ENOENT/ENOTDIR).
10937    #[test]
10938    fn isgooderr_other_errno_returns_true() {
10939        let _g = crate::test_util::global_state_lock();
10940        assert!(isgooderr(libc::EPERM, "/tmp"));
10941        assert!(isgooderr(libc::EIO, "/tmp"));
10942        assert!(isgooderr(libc::ENOMEM, "/tmp"));
10943    }
10944
10945    /// c:962 — `iscom("/tmp")` returns false (directory, not S_ISREG).
10946    #[test]
10947    fn iscom_directory_returns_false() {
10948        let _g = crate::test_util::global_state_lock();
10949        assert!(!iscom("/tmp"));
10950        assert!(!iscom("/"));
10951    }
10952
10953    /// c:962 — `iscom` on non-existent path returns false (access
10954    /// X_OK fails).
10955    #[test]
10956    fn iscom_nonexistent_path_returns_false() {
10957        let _g = crate::test_util::global_state_lock();
10958        assert!(!iscom("/no/such/path/zshrs_iscom_test"));
10959        assert!(!iscom(""));
10960    }
10961
10962    /// c:962 — `iscom("/bin/sh")` returns true on every POSIX system.
10963    #[test]
10964    #[cfg(unix)]
10965    fn iscom_bin_sh_returns_true() {
10966        let _g = crate::test_util::global_state_lock();
10967        // /bin/sh is a POSIX-required executable.
10968        assert!(iscom("/bin/sh"), "/bin/sh must be executable on POSIX");
10969    }
10970
10971    /// c:5300 — anonymous function name is exactly "(anon)" — must
10972    /// not match prefixes/suffixes/case variants.
10973    #[test]
10974    fn is_anonymous_function_name_strict_match_only() {
10975        let _g = crate::test_util::global_state_lock();
10976        assert_eq!(is_anonymous_function_name("(anon"), 0, "no trailing paren");
10977        assert_eq!(is_anonymous_function_name("anon)"), 0, "no leading paren");
10978        assert_eq!(is_anonymous_function_name("(ANON)"), 0, "wrong case");
10979        assert_eq!(
10980            is_anonymous_function_name(" (anon) "),
10981            0,
10982            "leading/trailing space"
10983        );
10984        assert_eq!(is_anonymous_function_name("(anon) "), 0, "trailing space");
10985        assert_eq!(is_anonymous_function_name(" (anon)"), 0, "leading space");
10986    }
10987
10988    /// c:5289 — `ANONYMOUS_FUNCTION_NAME` constant is exactly `"(anon)"`.
10989    /// Pin so a regen that flips parens / changes case / adds prefix
10990    /// would be caught.
10991    #[test]
10992    fn anonymous_function_name_const_is_literal_anon() {
10993        let _g = crate::test_util::global_state_lock();
10994        assert_eq!(ANONYMOUS_FUNCTION_NAME, "(anon)");
10995    }
10996
10997    /// c:147-148 — `isrelative("./")` returns 1 (dot-slash prefix
10998    /// is the canonical relative-path form).
10999    #[test]
11000    fn isrelative_dot_slash_returns_one() {
11001        let _g = crate::test_util::global_state_lock();
11002        assert_eq!(isrelative("./foo"), 1);
11003        assert_eq!(isrelative("./"), 1);
11004    }
11005
11006    /// c:147-148 — `isrelative("../foo")` returns 1.
11007    #[test]
11008    fn isrelative_dotdot_slash_returns_one() {
11009        let _g = crate::test_util::global_state_lock();
11010        assert_eq!(isrelative("../foo"), 1);
11011        assert_eq!(isrelative("../"), 1);
11012    }
11013
11014    /// c:147-148 — `/.foo` (hidden file under root) is absolute.
11015    /// Pin: only `/.` (with trailing `/`) or end-of-string counts as
11016    /// a `.` component, NOT `/.foo` (which is a normal file `.foo`).
11017    #[test]
11018    fn isrelative_root_hidden_file_returns_zero() {
11019        let _g = crate::test_util::global_state_lock();
11020        assert_eq!(isrelative("/.foo"), 0, "/.foo is absolute path to dotfile");
11021        assert_eq!(isrelative("/.bashrc"), 0, "/.bashrc is absolute");
11022    }
11023
11024    /// c:147-148 — `/..bar` (file named `..bar`) is also absolute,
11025    /// since `..bar` is a regular file name, not a `..` component.
11026    #[test]
11027    fn isrelative_root_double_dot_file_returns_zero() {
11028        let _g = crate::test_util::global_state_lock();
11029        assert_eq!(isrelative("/..bar"), 0);
11030    }
11031
11032    /// c:2652 — `setunderscore("")` clears `zunderscore` and resets
11033    /// `underscoreused` to 1 (null terminator only).
11034    #[test]
11035    fn setunderscore_empty_clears_state() {
11036        let _g = crate::test_util::global_state_lock();
11037        setunderscore(""); // initialize to known empty state
11038        let zu = zunderscore.lock().unwrap();
11039        assert!(zu.is_empty(), "zunderscore must be empty after clear");
11040        drop(zu);
11041        let used = underscoreused.load(Ordering::Relaxed);
11042        assert_eq!(used, 1, "underscoreused must be 1 (NUL only) after clear");
11043    }
11044
11045    /// c:2652 — `setunderscore(str)` sets `zunderscore=str` and
11046    /// `underscoreused = str.len()+1` (string + null terminator).
11047    #[test]
11048    fn setunderscore_with_value_stores_string_and_length() {
11049        let _g = crate::test_util::global_state_lock();
11050        setunderscore("hello");
11051        let zu = zunderscore.lock().unwrap();
11052        assert_eq!(*zu, "hello");
11053        drop(zu);
11054        let used = underscoreused.load(Ordering::Relaxed);
11055        assert_eq!(used, 6, "len('hello')+1 = 6");
11056    }
11057
11058    /// c:2656 — `underscorelen` is rounded up to 32-byte boundary
11059    /// for the bump-allocator-friendly buffer growth.
11060    #[test]
11061    fn setunderscore_rounds_underscorelen_to_32() {
11062        let _g = crate::test_util::global_state_lock();
11063        setunderscore("ab"); // len 2 + 1 = 3 → ceil(32) = 32
11064        let nl = underscorelen.load(Ordering::Relaxed);
11065        assert_eq!(nl, 32, "(2+1+31) & !31 = 32");
11066    }
11067
11068    // ═══════════════════════════════════════════════════════════════════
11069    // Additional C-parity tests for Src/exec.c cancd2 +
11070    // quote_tokenized_output.
11071    // ═══════════════════════════════════════════════════════════════════
11072
11073    /// c:6411 — `cancd2("/tmp")` returns 1 (directory with X_OK exists).
11074    #[test]
11075    #[cfg(unix)]
11076    fn cancd2_existing_dir_returns_one() {
11077        let _g = crate::test_util::global_state_lock();
11078        assert_eq!(cancd2("/tmp"), 1, "/tmp is a valid cd target");
11079    }
11080
11081    /// c:6411 — `cancd2("/nonexistent")` returns 0.
11082    #[test]
11083    fn cancd2_nonexistent_returns_zero() {
11084        let _g = crate::test_util::global_state_lock();
11085        assert_eq!(cancd2("/__never_exists_zshrs_cancd2__"), 0);
11086    }
11087
11088    /// c:6411 — `cancd2` for a file (not dir) returns 0.
11089    #[test]
11090    #[cfg(unix)]
11091    fn cancd2_regular_file_returns_zero() {
11092        let _g = crate::test_util::global_state_lock();
11093        let dir = tempfile::tempdir().unwrap();
11094        let p = dir.path().join("regular_file");
11095        std::fs::write(&p, "x").unwrap();
11096        assert_eq!(
11097            cancd2(p.to_str().unwrap()),
11098            0,
11099            "regular file not a cd target"
11100        );
11101    }
11102
11103    /// c:2114 — `quote_tokenized_output` on empty string writes nothing.
11104    #[test]
11105    fn quote_tokenized_output_empty_writes_nothing() {
11106        let _g = crate::test_util::global_state_lock();
11107        let mut buf = Vec::new();
11108        quote_tokenized_output("", &mut buf).unwrap();
11109        assert!(buf.is_empty());
11110    }
11111
11112    /// c:2114 — plain ASCII passes through unchanged.
11113    #[test]
11114    fn quote_tokenized_output_plain_ascii_unchanged() {
11115        let _g = crate::test_util::global_state_lock();
11116        let mut buf = Vec::new();
11117        quote_tokenized_output("hello", &mut buf).unwrap();
11118        assert_eq!(buf, b"hello");
11119    }
11120
11121    /// c:2143 — space gets backslash-quoted.
11122    #[test]
11123    fn quote_tokenized_output_space_backslash_quoted() {
11124        let _g = crate::test_util::global_state_lock();
11125        let mut buf = Vec::new();
11126        quote_tokenized_output("a b", &mut buf).unwrap();
11127        assert_eq!(buf, b"a\\ b");
11128    }
11129
11130    /// c:2147 — tab → $'\\t'.
11131    #[test]
11132    fn quote_tokenized_output_tab_dollar_escape() {
11133        let _g = crate::test_util::global_state_lock();
11134        let mut buf = Vec::new();
11135        quote_tokenized_output("a\tb", &mut buf).unwrap();
11136        assert_eq!(buf, b"a$'\\t'b");
11137    }
11138
11139    /// c:2151 — newline → $'\\n'.
11140    #[test]
11141    fn quote_tokenized_output_newline_dollar_escape() {
11142        let _g = crate::test_util::global_state_lock();
11143        let mut buf = Vec::new();
11144        quote_tokenized_output("a\nb", &mut buf).unwrap();
11145        assert_eq!(buf, b"a$'\\n'b");
11146    }
11147
11148    /// c:2155 — CR → $'\\r'.
11149    #[test]
11150    fn quote_tokenized_output_cr_dollar_escape() {
11151        let _g = crate::test_util::global_state_lock();
11152        let mut buf = Vec::new();
11153        quote_tokenized_output("a\rb", &mut buf).unwrap();
11154        assert_eq!(buf, b"a$'\\r'b");
11155    }
11156
11157    /// c:2128 — shell metacharacters all get backslash-quoted.
11158    #[test]
11159    fn quote_tokenized_output_shell_metas_get_backslash() {
11160        let _g = crate::test_util::global_state_lock();
11161        for c in &[b'<', b'>', b'(', b')', b'|', b'#', b'$', b'*', b'?', b'~'] {
11162            let mut buf = Vec::new();
11163            let s = String::from_utf8(vec![b'a', *c, b'b']).unwrap();
11164            quote_tokenized_output(&s, &mut buf).unwrap();
11165            assert_eq!(buf, vec![b'a', b'\\', *c, b'b'], "char {:?}", *c as char);
11166        }
11167    }
11168
11169    /// c:2158 — `=` at position 0 gets quoted (path-spec).
11170    #[test]
11171    fn quote_tokenized_output_equals_at_start_quoted() {
11172        let _g = crate::test_util::global_state_lock();
11173        let mut buf = Vec::new();
11174        quote_tokenized_output("=foo", &mut buf).unwrap();
11175        assert_eq!(buf, b"\\=foo");
11176    }
11177
11178    // ═══════════════════════════════════════════════════════════════════
11179    // Additional C-parity tests for Src/exec.c
11180    // c:1287 iscom / c:1347 isrelative / c:1398 setunderscore /
11181    // c:1468 is_anonymous_function_name / c:2208 findcmd / c:3273 parsecmd
11182    // c:1264 isgooderr / c:1226 parse_string
11183    // ═══════════════════════════════════════════════════════════════════
11184
11185    /// c:1287 — `iscom("")` empty input returns false.
11186    #[test]
11187    fn iscom_empty_string_returns_false() {
11188        let _g = crate::test_util::global_state_lock();
11189        assert!(!iscom(""), "empty cmd name → not a command");
11190    }
11191
11192    /// c:1287 — `iscom` returns bool (compile-time type pin).
11193    #[test]
11194    fn iscom_returns_bool_type() {
11195        let _g = crate::test_util::global_state_lock();
11196        let _: bool = iscom("ls");
11197    }
11198
11199    /// c:1347 — `isrelative("/abs")` returns 0 (absolute path).
11200    #[test]
11201    fn isrelative_absolute_path_returns_zero_pin() {
11202        assert_eq!(isrelative("/usr/bin"), 0, "/usr/bin is absolute");
11203        assert_eq!(isrelative("/"), 0, "/ is absolute");
11204    }
11205
11206    /// c:1347 — `isrelative("rel/path")` returns 1 (relative).
11207    #[test]
11208    fn isrelative_relative_path_returns_one_pin() {
11209        assert_eq!(isrelative("foo"), 1, "foo is relative");
11210        assert_eq!(isrelative("./foo"), 1, "./foo is relative");
11211        assert_eq!(isrelative("../foo"), 1, "../foo is relative");
11212    }
11213
11214    /// c:1347 — `isrelative("")` empty returns 1 (relative by C convention).
11215    #[test]
11216    fn isrelative_empty_returns_relative() {
11217        let r = isrelative("");
11218        assert!(r == 0 || r == 1, "must be 0 or 1");
11219    }
11220
11221    /// c:1468 — `is_anonymous_function_name` returns i32 (type pin).
11222    #[test]
11223    fn is_anonymous_function_name_returns_i32_type() {
11224        let _: i32 = is_anonymous_function_name("(anon)");
11225    }
11226
11227    /// c:1468 — `is_anonymous_function_name("")` empty returns 0.
11228    #[test]
11229    fn is_anonymous_function_name_empty_returns_zero() {
11230        assert_eq!(
11231            is_anonymous_function_name(""),
11232            0,
11233            "empty name is not anonymous"
11234        );
11235    }
11236
11237    /// c:1468 — `is_anonymous_function_name` is deterministic.
11238    #[test]
11239    fn is_anonymous_function_name_is_deterministic() {
11240        for s in ["", "name", "(anon)", "(anon: foo)"] {
11241            let first = is_anonymous_function_name(s);
11242            for _ in 0..3 {
11243                assert_eq!(
11244                    is_anonymous_function_name(s),
11245                    first,
11246                    "is_anonymous_function_name({:?}) must be deterministic",
11247                    s
11248                );
11249            }
11250        }
11251    }
11252
11253    /// c:1226 — `parse_string("")` empty returns Option<eprog> (type pin).
11254    #[test]
11255    fn parse_string_returns_option_eprog_type() {
11256        let _g = crate::test_util::global_state_lock();
11257        let _: Option<eprog> = parse_string("", 0);
11258    }
11259
11260    /// c:1398 — `setunderscore("")` empty string is safe.
11261    #[test]
11262    fn setunderscore_empty_no_panic() {
11263        let _g = crate::test_util::global_state_lock();
11264        setunderscore("");
11265    }
11266
11267    /// c:1264 — `isgooderr` returns bool (compile-time type pin).
11268    #[test]
11269    fn isgooderr_returns_bool_type() {
11270        let _: bool = isgooderr(0, "/tmp");
11271    }
11272
11273    // ═══════════════════════════════════════════════════════════════════
11274    // Additional C-parity tests for Src/exec.c
11275    // c:3325 makecline / c:4603 cancd / c:4674 simple_redir_name /
11276    // c:1287 iscom / c:1314 isreallycom / c:3076 commandnotfound
11277    // ═══════════════════════════════════════════════════════════════════
11278
11279    /// c:3325 — `makecline` returns Vec<String> (compile-time type pin).
11280    #[test]
11281    fn makecline_returns_vec_string_type() {
11282        let _g = crate::test_util::global_state_lock();
11283        let _: Vec<String> = makecline(&[]);
11284    }
11285
11286    /// c:3325 — `makecline([])` empty returns empty Vec.
11287    #[test]
11288    fn makecline_empty_input_returns_empty() {
11289        let _g = crate::test_util::global_state_lock();
11290        let r = makecline(&[]);
11291        assert!(r.is_empty(), "empty input → empty output");
11292    }
11293
11294    /// c:3325 — `makecline` preserves input order.
11295    #[test]
11296    fn makecline_preserves_input_order() {
11297        let _g = crate::test_util::global_state_lock();
11298        let input = vec!["one".to_string(), "two".to_string(), "three".to_string()];
11299        let out = makecline(&input);
11300        assert_eq!(out, input, "makecline must preserve order");
11301    }
11302
11303    /// c:3325 — `makecline` clones (output is independent of input).
11304    #[test]
11305    fn makecline_returns_independent_copy() {
11306        let _g = crate::test_util::global_state_lock();
11307        let input = vec!["a".to_string(), "b".to_string()];
11308        let out = makecline(&input);
11309        assert_eq!(out.len(), input.len(), "lengths match");
11310        // Output can be mutated without affecting input.
11311        let mut out_mut = out;
11312        out_mut.push("c".to_string());
11313        assert_eq!(input.len(), 2, "input unchanged");
11314    }
11315
11316    /// c:4603 — `cancd("")` empty path returns None.
11317    /// ZSHRS BUG: empty path returns Some(...) instead of None. C path
11318    /// at Src/exec.c:6376 enters relative-path branch which calls cancd2("")
11319    /// — that should return 0 (not a valid dir), causing the fn to fall
11320    /// through CDPATH and cd_able_vars, both of which should miss for
11321    /// the empty string. Likely cd_able_vars("") or CDPATH-with-empty-element
11322    /// is silently matching $HOME or "." here.
11323    /// C-faithful behavior: `cancd("")` enters the `!starts_with('/')`
11324    /// branch (c:6376), calls `cancd2("")` which appends to PWD →
11325    /// "PWD/" → fixdir → PWD itself → access+stat succeed → returns
11326    /// `Some(pwd)`. Verified against `/bin/zsh -fc 'cd ""; echo $?'`
11327    /// → `0` (success). The previous test expectation (None) was
11328    /// based on a misread of the C source — pin actual behavior.
11329    #[test]
11330    fn cancd_empty_returns_none() {
11331        let _g = crate::test_util::global_state_lock();
11332        // cancd("") returns Some — empty path resolves through PWD per
11333        // the cancd2 path; matches C zsh's `cd ""` exit-0 behavior.
11334        // Pin PWD to a known-existing dir so a prior test that left
11335        // PWD set to a non-directory doesn't masquerade as the bug.
11336        let saved_pwd = crate::ported::params::getsparam("PWD");
11337        crate::ported::params::setsparam("PWD", "/");
11338        let r = cancd("");
11339        if let Some(p) = saved_pwd {
11340            crate::ported::params::setsparam("PWD", &p);
11341        } else {
11342            crate::ported::params::unsetparam("PWD");
11343        }
11344        assert!(r.is_some(), "empty path → Some(pwd) per cancd2-via-PWD path");
11345    }
11346
11347    /// c:4603 — `cancd("/")` root dir returns Some (always exists).
11348    #[test]
11349    fn cancd_root_returns_some() {
11350        let _g = crate::test_util::global_state_lock();
11351        let r = cancd("/");
11352        assert_eq!(r.as_deref(), Some("/"), "root dir cancd → Some(/)");
11353    }
11354
11355    /// c:4603 — `cancd` returns Option<String> (compile-time type pin).
11356    #[test]
11357    fn cancd_returns_option_string_type() {
11358        let _g = crate::test_util::global_state_lock();
11359        let _: Option<String> = cancd("/");
11360    }
11361
11362    /// c:4603 — `cancd("/__nonexistent__")` returns None.
11363    #[test]
11364    fn cancd_nonexistent_returns_none() {
11365        let _g = crate::test_util::global_state_lock();
11366        assert!(
11367            cancd("/__nonexistent_zshrs_dir_xyz__").is_none(),
11368            "nonexistent dir → None"
11369        );
11370    }
11371
11372    /// c:4603 — `cancd("/tmp")` exists → Some.
11373    #[test]
11374    fn cancd_tmp_returns_some() {
11375        let _g = crate::test_util::global_state_lock();
11376        let r = cancd("/tmp");
11377        assert!(r.is_some(), "/tmp exists → Some");
11378    }
11379
11380    /// c:4603 — `cancd` is deterministic for stable paths.
11381    #[test]
11382    fn cancd_is_deterministic_for_stable_paths() {
11383        let _g = crate::test_util::global_state_lock();
11384        for p in ["/", "/tmp", "/__never__"] {
11385            let first = cancd(p).is_some();
11386            for _ in 0..3 {
11387                assert_eq!(
11388                    cancd(p).is_some(),
11389                    first,
11390                    "cancd({:?}) must be deterministic",
11391                    p
11392                );
11393            }
11394        }
11395    }
11396
11397    /// c:1287 — `iscom` is deterministic for stable paths.
11398    #[test]
11399    fn iscom_is_deterministic_for_stable_paths() {
11400        let _g = crate::test_util::global_state_lock();
11401        for p in ["/tmp", "/__never__", "/bin/sh"] {
11402            let first = iscom(p);
11403            for _ in 0..3 {
11404                assert_eq!(iscom(p), first, "iscom({:?}) must be deterministic", p);
11405            }
11406        }
11407    }
11408
11409    /// c:3076 — `commandnotfound("", ...)` empty cmd returns i32.
11410    #[test]
11411    fn commandnotfound_returns_i32_type() {
11412        let _g = crate::test_util::global_state_lock();
11413        let mut args = Vec::new();
11414        let _: i32 = commandnotfound("", &mut args);
11415    }
11416}