Skip to main content

zsh/ported/
exec.rs

1//! Faithful Rust ports of free functions and file-static globals from
2//! `Src/exec.c`. The wordcode-VM dispatch tree (`execlist` / `execpline`
3//! / `execcmd` / `execsimple` etc.) that drives execution in C zsh is
4//! NOT replicated here — zshrs runs the fusevm bytecode VM instead
5//! (see `src/vm_helper.rs` + `src/fusevm_bridge.rs`).
6//!
7//! What lives here are the parts of `Src/exec.c` that ARE faithful
8//! ports and don't depend on the C-side wordcode walker:
9//!
10//! - **`trap_state` / `trap_return` / `forklevel`** — file-static
11//!   integer globals from `Src/exec.c:134 / :155 / :1052`, exposed as
12//!   atomics shared between this module, `Src/signals.c`'s port at
13//!   `src/ported/signals.rs`, and `Src/params.c`'s port at
14//!   `src/ported/params.rs`.
15//! - **`gethere`** (`Src/exec.c:4573`) — turn a here-document into a
16//!   here-string. Called from the lexer port (`src/ported/lex.rs`).
17//! - **`getoutput`** (`Src/exec.c:4712`) — command-substitution body
18//!   runner. Called from the parameter-expansion port
19//!   (`src/ported/subst.rs`).
20//! - **`loadautofn`** + **`getfpfunc`** (`Src/exec.c:5050` / `:5260`)
21//!   — `$fpath` walker + autoload file installer. Called from
22//!   `bin_autoload` / `bin_functions -c` in `src/ported/builtin.rs`.
23//! - **`resolvebuiltin`** (`Src/exec.c:2703`) — module-autoload guard
24//!   used by the dispatch walk in `execcmd_exec`.
25//! - **`execcmd_compile_head`** — fusevm-bytecode-time head resolver
26//!   mirroring the head section (`c:2904-3275`) of C's `execcmd_exec`.
27//!   NOT a faithful port; the canonical 7-arg `execcmd_exec` port lives
28//!   alongside it.
29//! - **`execcmd_exec`** (`Src/exec.c:2900`) — canonical 7-arg port of
30//!   the C function (locals + dispatch walk through builtin/shfunc/external
31//!   invocation). Used by future tree-walker callers; the fusevm
32//!   bytecode flow goes through `execcmd_compile_head` instead.
33
34use std::os::unix::fs::PermissionsExt;
35use std::sync::atomic::Ordering;
36
37// `with_executor` import removed — all ShellExecutor reach-in calls
38// routed through `crate::ported::exec::*` fn-ptrs installed by
39// fusevm_bridge at startup. See memory feedback_no_exec_script_from_ported.
40use crate::ported::builtin::{cd_able_vars, fixdir, BUILTINS, DOPRINTDIR, EXIT_VAL, LASTVAL};
41use crate::ported::builtins::rlimits::setlimits;
42use crate::ported::builtins::sched::zleactive;
43use crate::ported::compat::zgettime_monotonic_if_available;
44use crate::ported::config_h::DEFAULT_PATH;
45use crate::ported::context::{zcontext_restore, zcontext_save};
46use crate::ported::hashtable::{
47    cmdnam_unhashed, cmdnamtab_lock, dircache_set, hashdir, pathchecked, shfunctab_lock,
48};
49use crate::ported::hist::{strinbeg, strinend};
50use crate::ported::init::{shout, underscorelen, underscoreused, zunderscore, SHTTY};
51use crate::ported::input::{inpop, inpush};
52use crate::ported::jobs::{expandjobtab, get_usage, release_pgrp, waitforpid, JOBTAB, THISJOB};
53use crate::ported::lex::{
54    hgetc, parsestr, tok, untokenize, ztokens, LEXERR, LEX_LEXSTOP, LEX_LINENO,
55};
56use crate::ported::mem::{dupstring, dyncat, popheap, pushheap};
57use crate::ported::modules::clone::mypgrp;
58use crate::ported::options::{dosetopt, opt_state_set, sticky};
59use crate::ported::params::{
60    endparamscope, getsparam, locallevel, paramtab, setiparam, zgetenv, zputenv,
61};
62use crate::ported::parse::{closedumps, ecrawstr, parse_list};
63use crate::ported::prompt::{cmdpop, cmdpush};
64use crate::ported::signals::{
65    intrap, queue_signals, settrap, signal_mask, signal_unblock, sigtrapped, trapisfunc,
66    traplocallevel, unqueue_signals, unsettrap,
67};
68use crate::ported::signals_h::{
69    child_block, child_unblock, dont_queue_signals, signal_default, signal_ignore, winch_unblock,
70    SIGCOUNT,
71};
72use crate::ported::subst::{quotesubst, singsub};
73use crate::ported::utils::{
74    errflag, fdtable_get, fdtable_set, gettempfile, gettempname, inc_locallevel, movefd, pathprog,
75    printprompt4, quotedzputs, redup, unmeta, unmetafy, write_loop, zclose, zerr, zwarn,
76    ERRFLAG_ERROR, MAX_ZSH_FD,
77};
78use crate::ported::zsh_h::{
79    builtin, cmdnam, emulation_options, eprog, execstack, funcwrap, hashnode, isset, jobfile, multio,
80    redir,
81    shfunc, unset, wc_code, Emulation_options, Inang, Inpar, Meta, Nularg, Outpar, Pound,
82    BINF_BUILTIN, BINF_CLEARENV, BINF_COMMAND, BINF_DASH, BINF_EXEC, BINF_PREFIX, CHASEDOTS,
83    CHASELINKS, CLOBBER, CLOBBEREMPTY, CS_CMDSUBST, ERRFLAG_INT, FDT_EXTERNAL, FDT_INTERNAL,
84    FDT_PROC_SUBST, FDT_SAVED_MASK, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHDIRS, INP_LINENO,
85    INTERACTIVE, IS_CLOBBER_REDIR, IS_DASH, JOBTEXTSIZE, MAX_PIPESTATS, MONITOR, MULTIOS,
86    MULTIOUNIT, PATHDIRS, PM_LOADDIR, PM_READONLY, PM_UNDEFINED, POSIXBUILTINS, POSIXJOBS,
87    POSIXTRAPS, REDIRF_FROM_HEREDOC, REDIR_CLOSE, REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE,
88    REDIR_OUTPIPE, USEZLE, VERBOSE, WC_LIST, WC_LIST_TYPE, WC_PIPE, WC_PIPE_END, WC_PIPE_TYPE,
89    WC_REDIR, WC_REDIR_TYPE, WC_REDIR_VARID, WC_SIMPLE, WC_SIMPLE_ARGC, WC_SUBLIST, WC_SUBLIST_END,
90    WC_SUBLIST_FLAGS, WC_SUBLIST_TYPE, WC_TYPESET, ZSIG_FUNC, ZSIG_IGNORED, Z_END,
91};
92use crate::ported::zsh_system_h::timespec as ZshTimespec;
93use crate::ported::ztype_h::{inull, itok};
94use crate::zsh_h::XTRACE;
95
96/// Port of the anonymous `enum { ... }` from `Src/exec.c:35-40`.
97/// Flag bits passed as the `addflags` argument to `addvars` /
98/// `addvarsfromargs`:
99///   - `ADDVAR_EXPORT`  (1<<0) — export each assignment for the
100///                                command `VAR=val cmd ...` form.
101///   - `ADDVAR_RESTORE` (1<<2) — the variable list is being restored
102///                                later (implicit local scope), so
103///                                suppress `ASSPM_WARN`.
104pub const ADDVAR_EXPORT: i32 = 1 << 0; // c:37 (Src/exec.c)
105/// `ADDVAR_RESTORE` constant.
106pub const ADDVAR_RESTORE: i32 = 1 << 2; // c:39 (Src/exec.c)
107
108/// Port of `int trap_state;` from `Src/exec.c:134`. Tracks whether
109/// a trap handler is currently being processed and, paired with
110/// `TRAP_RETURN` below, whether a `return` inside the trap should
111/// promote to `TRAP_STATE_FORCE_RETURN` to unwind the trap caller.
112///
113/// Values: `TRAP_STATE_INACTIVE = 0`, `TRAP_STATE_PRIMED = 1`,
114/// `TRAP_STATE_FORCE_RETURN = 2` (see `Src/zsh.h`).
115pub static TRAP_STATE: std::sync::atomic::AtomicI32 = // c:134 (Src/exec.c)
116    std::sync::atomic::AtomicI32::new(0);
117
118/// Port of `int trap_return;` from `Src/exec.c:155`. Carries the
119/// pending exit status from inside a trap; sentinel `-2` means
120/// "running an EXIT/DEBUG-style trap at the current level"
121/// (signals.c:1166). Promoted to the user's `return N` value by
122/// `bin_return` when POSIX-trap semantics apply (builtin.c:5852).
123pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = // c:155 (Src/exec.c)
124    std::sync::atomic::AtomicI32::new(0);
125
126/// Port of `int forklevel;` from `Src/exec.c:1052`. Records the
127/// `locallevel` at the most recent fork point (set at c:1221:
128/// `forklevel = locallevel;` inside `entersubsh()`). Used by:
129///   - `signals.c:808` SIGPIPE handler — `!forklevel` distinguishes
130///     the top-level shell from a forked subshell.
131///   - `exec.c:6146` — `if (locallevel > forklevel)` decides whether
132///     a function-defined trap should fire on this subshell exit.
133///   - `params.c:3724` — WARNCREATEGLOBAL nest-depth check.
134///
135/// Initialised to 0 (no fork has occurred yet). Set to `locallevel`
136/// at every `entersubsh()` entry per c:1221.
137pub static FORKLEVEL: std::sync::atomic::AtomicI32 = // c:1052 (Src/exec.c)
138    std::sync::atomic::AtomicI32::new(0);
139
140// =============================================================================
141// File-static globals from Src/exec.c. Bucket choices per PORT_PLAN.md:
142//   - Per-evaluator transient state → thread_local Cell (bucket 1)
143//   - Shell-wide shared state       → AtomicI32 / Mutex (bucket 2)
144// All names match C exactly. Surrounding doc-comments cite the C
145// declaration line.
146// =============================================================================
147
148/// Port of `int noerrexit;` from `Src/exec.c:72`. Bit-flags that
149/// suppress ERREXIT triggering on the next command(s). Bits:
150/// `NOERREXIT_EXIT` (in `if`/`while`/`until` test contexts),
151/// `NOERREXIT_RETURN` (after `return`), `NOERREXIT_UNTIL_EXEC`
152/// (until next exec'd command). Bucket-1 — per-evaluator (each
153/// recursive eval has its own suppression frame).
154pub static noerrexit: std::sync::atomic::AtomicI32 = // c:72 (Src/exec.c)
155    std::sync::atomic::AtomicI32::new(0);
156
157/// Port of `int this_noerrexit;` from `Src/exec.c:109`. When set,
158/// suppress ERREXIT for THIS one command only (consumed + cleared
159/// before the next command starts). Set by `execcursh` and the
160/// `((expr))` arith path so a 0-result doesn't trigger errexit.
161pub static this_noerrexit: std::sync::atomic::AtomicI32 = // c:109 (Src/exec.c)
162    std::sync::atomic::AtomicI32::new(0);
163
164/// Port of `mod_export int noerrs;` from `Src/exec.c:117`. When
165/// non-zero, suppress `zerr()` output (lex error reporting during
166/// `parse_string`, `parseopts` etc.). Saved/restored by
167/// `execsave`/`execrestore`.
168/// Port of `static char list_pipe_text[JOBTEXTSIZE]` from
169/// `Src/exec.c:463`. Holds the textual rendering of the in-flight
170/// pipe list; saved across nested execlist invocations at
171/// exec.c:1372-1380 (zeroed on entry, restored from
172/// `old_list_pipe_text` at c:1634-1638) and round-tripped through
173/// execsave/execrestore (c:6448 / c:6484). zshrs models it as a
174/// length-bounded String guarded by a Mutex — the C `char[80]` cap
175/// is a buffer-overflow guard, but matching length matters for the
176/// `jobs` builtin's pipe-list rendering.
177pub static LIST_PIPE_TEXT: std::sync::Mutex<String> = std::sync::Mutex::new(String::new()); // c:463 (Src/exec.c)
178
179pub static noerrs: std::sync::atomic::AtomicI32 = // c:117 (Src/exec.c)
180    std::sync::atomic::AtomicI32::new(0);
181
182/// Port of `int nohistsave;` from `Src/exec.c:122`. When non-zero,
183/// `addhistnode` no-ops so trap firings / `eval` invocations don't
184/// pollute `$HISTCMD`. Tracked alongside `noerrs` in the trap path.
185pub static nohistsave: std::sync::atomic::AtomicI32 = // c:122 (Src/exec.c)
186    std::sync::atomic::AtomicI32::new(0);
187
188/// Port of `int subsh;` from `Src/exec.c:160`. Subshell depth — bumped
189/// every time `entersubsh` forks a sub-shell, used by signal handling
190/// (different SIGINT semantics in subshells) and by `${$$}` (`$$`
191/// stays at the top-level pid).
192pub static subsh: std::sync::atomic::AtomicI32 = // c:160 (Src/exec.c)
193    std::sync::atomic::AtomicI32::new(0);
194
195/// Port of `mod_export int zsh_subshell;` from `Src/init.c:67`. Visible
196/// `$ZSH_SUBSHELL` parameter — incremented by `entersubsh()` each time
197/// the shell forks into a subshell (real or fake-exec). Distinct from
198/// `subsh` which records whether we ARE a subshell; `zsh_subshell` is
199/// the visible depth count.
200pub static zsh_subshell: std::sync::atomic::AtomicI32 = // c:67 (Src/init.c)
201    std::sync::atomic::AtomicI32::new(0);
202
203/// Port of `mod_export volatile int retflag;` from `Src/exec.c:165`.
204/// Set by `bin_return` to unwind the function-call stack. Cleared
205/// by `runshfunc` on entry, checked by `execlist`'s main loop.
206pub static retflag: std::sync::atomic::AtomicI32 = // c:165 (Src/exec.c)
207    std::sync::atomic::AtomicI32::new(0);
208
209/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
210/// recent `$(cmd)` command-substitution child. Used by exit-status
211/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
212/// the pid `waitpid`-d for it.
213pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
214    std::sync::atomic::AtomicI32::new(0);
215
216/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
217/// Pid of the most recent process-substitution child (`<(cmd)` /
218/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
219/// jobs aren't wait-collected by the parent until the fd is closed.
220pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
221    std::sync::atomic::AtomicI32::new(0);
222
223/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
224/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
225/// runs alongside a substitution.
226pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
227    std::sync::atomic::AtomicI32::new(0);
228
229/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
230/// `lastval` is updated from `cmdoutval` after the command
231/// (i.e. the command had substitutions whose exit status matters).
232pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
233    std::sync::atomic::AtomicI32::new(0);
234
235/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
236/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
237/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
238/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
239/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
240/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
241pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
242    std::sync::atomic::AtomicI32::new(0);
243
244/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
245/// currently-executing pipeline is the long-running pipe-into-loop
246/// shape (`cat foo | while read a; do ... done`) — drives the
247/// super/sub-job tracking documented in the famous `Allen Edeln…`
248/// comment block above this declaration in C.
249pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
250    std::sync::atomic::AtomicI32::new(0);
251
252/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
253/// dispatch of a "simple" pipeline (single-stage / no shell-construct
254/// tail) so the `list_pipe` machinery short-circuits.
255pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
256    std::sync::atomic::AtomicI32::new(0);
257
258/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
259/// PID of the sub-shell created to host the loop-after-pipe pattern;
260/// passed up the recursive `execlist` stack so the cat-job's super-
261/// job entry can record it.
262pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
263    std::sync::atomic::AtomicI32::new(0);
264
265/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
266/// `execpline` doesn't wait for the pipeline; used during the
267/// list_pipe sub-shell fork bookkeeping.
268pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
269    std::sync::atomic::AtomicI32::new(0);
270
271/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
272/// pipeline depth (counts nested pipelines within the current
273/// `execlist` call chain).
274pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
275    std::sync::atomic::AtomicI32::new(0);
276
277/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
278/// Set in the child after the list_pipe fork so the child knows to
279/// continue executing the loop body (vs the parent which records
280/// the pid + returns).
281pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
282    std::sync::atomic::AtomicI32::new(0);
283
284/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
285/// table index of the pipeline's first-stage job (the `cat` in
286/// `cat foo | while ...`).
287pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
288    std::sync::atomic::AtomicI32::new(0);
289
290/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
291/// `printprompt4` has emitted the `$PS4` prefix for the current
292/// xtrace command — prevents double-printing when an inner sub-eval
293/// also wants to xtrace.
294pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
295    std::sync::atomic::AtomicI32::new(0);
296
297/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
298///
299/// File-static "execsubst parameters" — callers (execcmd_exec at
300/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
301/// uses them as the `flags` arg to prefork() and the gate on
302/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
303/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
304/// when the dispatched builtin has `BINF_NOGLOB`.
305pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
306    std::sync::atomic::AtomicI32::new(0);
307pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
308    std::sync::atomic::AtomicI32::new(1);
309
310/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
311/// of the linked exec-context save stack — `execsave` pushes a frame
312/// before signal-handler / trap dispatch; `execrestore` pops it
313/// afterwards so the interrupted command resumes with its state intact.
314pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
315    std::sync::Mutex::new(None);
316
317/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
318/// `stty` argument string captured by `addvars` when the command's
319/// inline env contains `STTY=...`. Applied by `execute` before fork
320/// + exec so the spawned program sees its tty configured. Reset to
321/// `None` after consumption to avoid infinite recursion.
322pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
323    std::sync::Mutex::new(None);
324
325/// Convert a here-document into a here-string. Line-by-line port of
326/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
327/// input stream via `hgetc()` until the terminator line is matched,
328/// returning the collected body as a string. `strp` is in/out: on
329/// entry the raw terminator (possibly with token markers + leading
330/// tabs); on return the munged terminator (after `quotesubst` +
331/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
332///
333/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
334/// Rust's `String` auto-grows so the OOM branch is effectively
335/// unreachable, but the return type stays `Option<String>` to mirror
336/// the C signature which can return NULL.
337///
338/// Port of `gethere(char **strp, int typ)` from `Src/exec.c:4573`.
339pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
340    // c:4573 (Src/exec.c)
341    let mut buf: String; // c:4575 char *buf
342    let mut bsiz: usize; // c:4576 int bsiz
343    let mut qt: i32 = 0; // c:4576 int qt = 0
344    let mut strip: i32 = 0; // c:4576 int strip = 0
345                            // c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
346                            // `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
347                            // C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
348                            // `s` (the loop iterator for the inull-scan) stays local to its
349                            // for-loop. `c` mirrors the C `char c`.
350    let mut t: usize; // c:4577 char *t
351    let mut c: Option<char>; // c:4577 char c
352    let mut str: String = strp.clone(); // c:4578 char *str = *strp
353
354    // c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
355    for s in str.bytes() {
356        if inull(s) {
357            // c:4581
358            qt = 1; // c:4582
359            break; // c:4583
360        }
361    }
362    str = quotesubst(&str); // c:4585
363    str = untokenize(&str); // c:4586
364    if typ == REDIR_HEREDOCDASH {
365        // c:4587
366        strip = 1; // c:4588
367                   // c:4589-4590 — while (*str == '\t') str++;
368        while str.starts_with('\t') {
369            str.remove(0);
370        }
371    }
372    *strp = str.clone(); // c:4592 *strp = str
373
374    // c:4593 — bptr = buf = zalloc(bsiz = 256);
375    bsiz = 256;
376    buf = String::with_capacity(bsiz);
377    let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
378
379    // c:4594 — for (;;)
380    loop {
381        t = buf.len(); // c:4595 t = bptr
382
383        // c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
384        loop {
385            c = hgetc();
386            if !(c == Some('\t') && strip != 0) {
387                break;
388            }
389        }
390
391        // c:4599 — for (;;) — inner body-read loop
392        loop {
393            // c:4600-4613 — buffer-growth realloc dance. Rust's
394            // String auto-grows; nothing to do.
395            // c:4614 — if (lexstop || c == '\n') break;
396            if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
397                break;
398            }
399            // c:4616 — if (!qt && c == '\\')
400            if qt == 0 && c == Some('\\') {
401                buf.push('\\'); // c:4617 *bptr++ = c
402                c = hgetc(); // c:4618
403                if c == Some('\n') {
404                    // c:4619
405                    buf.pop(); // c:4620 bptr--
406                    c = hgetc(); // c:4621
407                    continue; // c:4622
408                }
409            }
410            if let Some(ch) = c {
411                // c:4625 *bptr++ = c
412                buf.push(ch);
413            }
414            c = hgetc(); // c:4626
415        }
416        // c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
417
418        // c:4629-4630 — if (!strcmp(t, str)) break;
419        if &buf[t..] == str.as_str() {
420            break;
421        }
422        // c:4631-4634 — if (lexstop) { t = bptr; break; }
423        // C's ingetc sets `lexstop = 1` when input is exhausted
424        // (Src/input.c:289-292), so the flag check alone suffices
425        // there. zshrs's hgetc signals the same exhaustion by
426        // returning None WITHOUT setting LEX_LEXSTOP — treat both as
427        // the c:4631 condition, else an unterminated heredoc loops
428        // here forever appending '\n'. Gap #3 2026-06-12 (A04
429        // "Here-documents don't perform shell expansion" wedge).
430        if LEX_LEXSTOP.with(|f| f.get()) || c.is_none() {
431            t = buf.len();
432            break;
433        }
434        // c:4635 — *bptr++ = '\n';
435        buf.push('\n');
436    }
437    // c:4637 — *t = '\0';
438    buf.truncate(t);
439
440    // c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
441    // The C dance frees the realloc'd block and re-allocates via the
442    // string-heap allocator. Rust drops the old String when reassigned.
443    buf = dupstring(&buf);
444
445    if qt == 0 {
446        // c:4641
447        // c:4642 — int ef = errflag;
448        let ef = errflag.load(Ordering::Relaxed);
449        // c:4644 — parsestr(&buf);
450        if let Ok(parsed) = parsestr(&buf) {
451            buf = parsed;
452        }
453        // c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
454        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
455            let cur = errflag.load(Ordering::Relaxed);
456            errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
457        }
458    }
459    Some(buf) // c:4651 return buf
460}
461
462/// Port of `LinkList getoutput(char *cmd, int qt)` from
463/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
464/// active executor, then routes the captured stdout through
465/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
466///
467/// C return shape: `LinkList` of `char*`. Rust port returns
468/// `Vec<String>` (same shape, owned).
469///
470/// `qt` matches C exactly:
471///   - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
472///     entire output as a single-element vec. C c:4858-4862: if
473///     output empty, returns a single Nularg sentinel so callers
474///     see "empty value" rather than "no value".
475///   - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
476///     `spacesplit(buf, allownull=false)` per c:4865-4871.
477///
478/// Uses `with_executor` (panics on missing VM context), not
479/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
480/// `execpline` directly — there's no "no shell" code path. The
481/// silent-no-op pattern (return empty string when no executor) would
482/// mask catastrophic state corruption as "command produced no output",
483/// which is the failure mode the `subst.rs:496` warning block flags.
484/* $(...) */
485// c:4709
486/// `getoutput` — see implementation.
487pub fn getoutput(cmd: &str, qt: i32) -> Vec<String> {
488    // c:4713
489    // c:4715 — `Eprog prog;`
490    let prog: Option<eprog>;
491    // c:4716 — `int pipes[2];`  (collapsed: in-process executor; no fork)
492    // c:4717 — `pid_t pid;`     (collapsed)
493    let mut s: String; // c:4718
494                       // c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
495                       //                !sourcelevel && unset(INTERACTIVECOMMENTS));
496                       //                prog = parse_string(cmd, 0); nocomments = onc;`
497    let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
498    let new_nc = crate::ported::zsh_h::interact()
499        && crate::ported::init::sourcelevel.load(Ordering::Relaxed) == 0
500        && !isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
501    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
502    prog = parse_string(cmd, 0);
503    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
504
505    if prog.is_none() {
506        // c:4725
507        return Vec::new(); // c:4726 return NULL
508    }
509    let prog = prog.unwrap();
510
511    if !isset(crate::ported::zsh_h::EXECOPT) {
512        // c:4728
513        return Vec::new(); // c:4729 newlinklist()
514    }
515
516    // c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
517    if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
518        /* $(< word) */
519        // c:4732
520        s = red_name;
521        s = singsub(&s); // c:4737
522        if errflag.load(Ordering::Relaxed) != 0 {
523            return Vec::new(); // c:4739
524        }
525        let s = untokenize(&s); // c:4740
526        let path_meta = unmeta(&s); // c:4741 unmeta(s)
527        let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
528            Ok(c) => c,
529            Err(_) => return Vec::new(),
530        };
531        let stream = unsafe {
532            libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
533        };
534        if stream == -1 {
535            // c:4742 — `zwarn("%e: %s", errno, s);`
536            let errno = std::io::Error::last_os_error();
537            zerr(&format!("{}: {}", errno, s));
538            LASTVAL.store(1, Ordering::Relaxed);
539            cmdoutval.store(1, Ordering::Relaxed);
540            return Vec::new(); // c:4744
541        }
542        // c:4746 — `retval = readoutput(stream, qt, &readerror);`
543        let mut readerror: i32 = 0;
544        let retval = readoutput(stream, qt, &mut readerror); // c:4746
545        if readerror != 0 {
546            // c:4747
547            zerr(&format!(
548                "error when reading {}: {}", // c:4748
549                s,
550                std::io::Error::from_raw_os_error(readerror)
551            ));
552            LASTVAL.store(1, Ordering::Relaxed);
553            cmdoutval.store(1, Ordering::Relaxed);
554        }
555        return retval; // c:4751
556    }
557
558    // c:4753-4790 — Full fork path: mpipe + zfork + parent
559    // readoutput / waitforpid / child execode + _realexit. fusevm runs
560    // command substitution in-process, so the fork shape collapses to a
561    // synchronous executor call. C control points preserved as cites:
562    //   c:4753 mpipe       — handled by ShellExecutor pipe wiring
563    //   c:4758 child_block — no-op (no fork)
564    //   c:4760 zfork       — replaced by in-process exec
565    //   c:4768-4776 parent — equivalent to executor return
566    //   c:4778-4789 child  — entersubsh+execode+_realexit collapse
567    cmdoutval.store(0, Ordering::Relaxed); // c:4759
568    let buf = crate::ported::exec::run_command_substitution(cmd);
569    LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed); // c:4775
570
571    // c:4772 retval = readoutput — post-walk (c:4855-4871 tail) inlined.
572    let buf = buf.trim_end_matches('\n');
573    if qt != 0 {
574        if buf.is_empty() {
575            vec![String::from(Nularg)] // c:4859-4861
576        } else {
577            vec![buf.to_string()] // c:4863
578        }
579    } else {
580        crate::ported::utils::spacesplit(buf, false) // c:4865
581    }
582}
583
584/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
585/// int ignore_loaddir)` from `Src/exec.c:5050`. Walks `$fpath` for a
586/// file named `shf->node.nam`, reads it, installs the text body on
587/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
588///
589/// C body (abridged):
590///   1. `name = shf->node.nam`
591///   2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
592///   3. If !test_only && file found: parse → store eprog on
593///      `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
594///   4. Returns shf on success, NULL on failure.
595///
596/// Rust port: returns 0 = success, 1 = failure (matches the
597/// existing call-site convention in `bin_functions -c`). Stores
598/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
599/// `hashtable.rs:362`); the parser pass that converts text →
600/// Eprog runs lazily at first call site.
601/// Port of `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)` from `Src/exec.c:5682`.
602pub fn loadautofn(
603    shf: *mut shfunc, // c:5682 (Src/exec.c)
604    _ks: i32,
605    autol: i32,
606    _ignore_loaddir: i32,
607) -> i32 {
608    if shf.is_null() {
609        return 1;
610    }
611    // c:5054 — `name = shf->node.nam`.
612    let name = unsafe { (*shf).node.nam.clone() };
613    // c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
614    let mut dir_path: Option<String> = None;
615    let mut dump_hit: Option<(eprog, i32)> = None;
616    let path = match getfpfunc(&name, &mut dir_path, None, 0, &mut dump_hit) {
617        Some(p) => p,
618        None => {
619            // c:Src/exec.c:5713-5719 — file not found path. C:
620            //   `if (prog == &dummy_eprog) {
621            //        locallevel--;
622            //        zwarn("%s: function definition file not found",
623            //              shf->node.nam);
624            //        locallevel++;
625            //        popheap();
626            //        return NULL;
627            //    }`
628            // C's getfpfunc returns &dummy_eprog as the "not found"
629            // sentinel when test_only==0; loadautofn detects it and
630            // emits the diagnostic before returning NULL. Rust's
631            // getfpfunc returns Option::None for the same condition,
632            // so we emit the same diagnostic here. The locallevel
633            // dance is preserved as a comment because the Rust
634            // port's zwarn doesn't reference locallevel in the
635            // format string itself (the dance in C is only to keep
636            // the prefix line counter consistent with the function-
637            // body context). Bug #107 in docs/BUGS.md.
638            crate::ported::utils::zwarn(&format!(
639                "{}: function definition file not found",
640                name
641            ));
642            return 1; // c:5719 NULL
643        }
644    };
645    let _ = autol;
646    // Previously the Rust port treated this parameter as
647    // "test_only" and early-returned when set, so the `+X`
648    // call from `eval_autoload` (`loadautofn(shf, mode, 1, d)`)
649    // never actually loaded the file. C's parameter is `autol`
650    // (autoload mode), NOT a test-only flag — the C body
651    // unconditionally loads/parses regardless of autol. autol=1
652    // controls the EF_RUN / map-flag dance for the wordcode prog
653    // (c:5725-5749), but the loaded-body / PM_UNDEFINED-clear
654    // path runs in all cases. Removing the early-return so
655    // `autoload -U +X funcname` actually loads the body and
656    // `type funcname` reports `function from /path/file` instead
657    // of `autoload shell function`. Bug #160 in docs/BUGS.md.
658    // c:5100-5140 — read the file. C uses zopen + read + parse_string +
659    // execsave; Rust port stores raw text on the ShFunc and defers
660    // parse-to-Eprog until the first call.
661    //
662    // c:Src/exec.c:6238 / parse.c:3833 — when getfpfunc resolved the
663    // function out of a compiled `.zwc` dump, there is no source file
664    // to read; the wordcode Eprog came back through `dump_hit`. C
665    // executes that wordcode directly (`shf->funcdef = stripkshdef(
666    // prog, ...)`, exec.c:5753-5755). zshrs executes function bodies
667    // through the fusevm bytecode pipeline which consumes source
668    // text, so bridge wordcode → text with the canonical text.c
669    // renderer (`getpermtext`, ported at text.rs:189) — the same
670    // walker `functions NAME` printing uses for wordcode-backed
671    // funcdefs (C hashtable.c:954). The downstream
672    // `autoload_register_source` step (vm_helper.rs:3162) performs
673    // the `stripkshdef` shape decision, matching c:5725-5760.
674    // c:5706-5710 — the ksh-mode precedence chain:
675    //   `if (ksh == 1) { ksh = fksh; if (ksh == 1)
676    //        ksh = PM_KSHSTORED ? 2 : PM_ZSHSTORED ? 0 : 1; }`
677    // The dump header flag (FDHF_KSHLOAD/FDHF_ZSHLOAD via `*ksh`
678    // from try_dump_file) outranks the stub's PM_*STORED bits, which
679    // are only consulted when the dump says 1 (no explicit style).
680    // zshrs's load/register split (vm_helper's
681    // `autoload_register_source` makes the c:5725 ksh-vs-zsh
682    // decision later, from the tab entry's flags + KSHAUTOLOAD) —
683    // fold a decisive dump flag into the PM bits so the downstream
684    // decision sees the same precedence.
685    let dump_ksh = dump_hit.as_ref().map(|(_, k)| *k);
686    let body = match dump_hit {
687        Some((prog, _ksh)) => crate::ported::text::getpermtext(Box::new(prog), None, 0),
688        None => match std::fs::read_to_string(&path) {
689            Ok(t) => t,
690            Err(_) => return 1,
691        },
692    };
693    // c:Src/exec.c:5735/5757 — `loadautofnsetfile(shf, fdir)`. The
694    // helper stamps PM_LOADDIR alongside the filename when fdir is
695    // present, so `whence -v NAME` later concatenates the directory
696    // with `/NAME` (PM_LOADDIR branch at hashtable.rs:1350). zshrs's
697    // prior `shf->filename = dir_path` assignment skipped the flag
698    // → `type colors` printed `from /path/to/functions` instead of
699    // `from /path/to/functions/colors`. Mirror C exactly.
700    unsafe {
701        loadautofnsetfile(&mut *shf, dir_path.as_deref().or(Some(&path)));
702    }
703    // c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
704    unsafe {
705        (*shf).node.flags &= !(PM_UNDEFINED as i32);
706    }
707    // c:5706-5710 fold (see comment above): decisive dump style wins
708    // over the stub's stored-style bits.
709    let (ksh_on, ksh_off): (i32, i32) = match dump_ksh {
710        Some(2) => (
711            crate::ported::zsh_h::PM_KSHSTORED as i32,
712            crate::ported::zsh_h::PM_ZSHSTORED as i32,
713        ),
714        Some(0) => (
715            crate::ported::zsh_h::PM_ZSHSTORED as i32,
716            crate::ported::zsh_h::PM_KSHSTORED as i32,
717        ),
718        _ => (0, 0),
719    };
720    unsafe {
721        (*shf).node.flags = ((*shf).node.flags | ksh_on) & !ksh_off;
722    }
723    // Sync the body string into the Rust-side ShFunc table so the
724    // lazy-parse path can find it later.
725    if let Ok(mut tab) = shfunctab_lock().write() {
726        if let Some(existing) = tab.get_mut(&name) {
727            existing.body = Some(body);
728            existing.filename = dir_path;
729            existing.node.flags = (existing.node.flags | ksh_on) & !ksh_off;
730        } else {
731            tab.add(shfunc {
732                node: hashnode {
733                    next: None,
734                    nam: name.clone(),
735                    flags: ksh_on, // c:5706-5710 dump-style fold
736                },
737                filename: dir_path,
738                lineno: 0,
739                funcdef: None,
740                redir: None,
741                sticky: None,
742                body: Some(body),
743            });
744        }
745    }
746    0
747}
748
749/// Port of `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)` from Src/exec.c:6219. Walks `$fpath` (or the
750/// supplied `spec_path` slice) for a file named `name` and writes the
751/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
752/// Returns `Some(file_path)` on success, `None` when not found.
753///
754/// Per dir, the compiled-dump lookup runs FIRST (c:6238
755/// `try_dump_file(*pp, s, buf, ksh, test_only)`) — a directory
756/// digest `<dir>.zwc` or per-function `<dir>/<name>.zwc` wins over
757/// the plain file when newer (mtime logic inside try_dump_file,
758/// c:parse.c:3762-3784). On a dump hit the loaded program + ksh
759/// mode (C's `*ksh` out-param) are written through `dump_out` and
760/// the nominal `<dir>/<name>` path is returned; the caller must
761/// check `dump_out` before reading the returned path as a plain
762/// file.
763pub fn getfpfunc(
764    name: &str,
765    dir_path_out: &mut Option<String>, // c:6219 (Src/exec.c)
766    spec_path: Option<&[String]>,
767    test_only: i32, // c:6219 `int test_only`
768    dump_out: &mut Option<(eprog, i32)>, // c:6219 `int *ksh` + dump Eprog return
769) -> Option<String> {
770    // C reads $fpath via `getaparam("fpath")` (the param-table array form
771    // tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
772    // misses any in-script modification like `fpath=(/some/dir $fpath)`
773    // because that mutates the internal param table, not the inherited
774    // process env. Fall back to env only when the param table is empty
775    // (cold start before any param-table init).
776    let dirs: Vec<String> = match spec_path {
777        Some(s) => s.to_vec(),
778        None => crate::ported::params::getaparam("fpath")
779            .filter(|v| !v.is_empty())
780            .or_else(|| getsparam("FPATH").map(|v| v.split(':').map(String::from).collect()))
781            .or_else(|| {
782                std::env::var("FPATH")
783                    .ok()
784                    .map(|v| v.split(':').map(String::from).collect())
785            })
786            .unwrap_or_default(),
787    };
788    for dir in &dirs {
789        if dir.is_empty() {
790            continue;
791        }
792        let path = format!("{}/{}", dir, name); // c:6230 snprintf(buf, ..., "%s/%s", *pp, s)
793        // c:6238 — `if ((r = try_dump_file(*pp, s, buf, ksh, test_only)))`
794        // — the .zwc digest / per-function dump is tried BEFORE the
795        // plain file in each directory.
796        if let Some(hit) =
797            crate::ported::parse::try_dump_file(dir, name, &path, test_only != 0)
798        {
799            *dump_out = Some(hit);
800            *dir_path_out = Some(dir.clone()); // c:6240 `*fdir = *pp;`
801            return Some(path); // c:6241
802        }
803        if std::path::Path::new(&path).exists() {
804            // c:6245 access(buf, R_OK)
805            *dir_path_out = Some(dir.clone());
806            return Some(path);
807        }
808    }
809    None
810}
811
812/// Port of `resolvebuiltin(const char *cmdarg, HashNode hn)` from
813/// `Src/exec.c:2703`. Ensures that an autoload-stub builtin has its
814/// module loaded before the caller invokes its `handlerfunc`. If the
815/// stub has no handler, `ensurefeature` is asked to load the module
816/// and re-lookup the builtin node. C body (abridged):
817/// ```c
818/// if (!((Builtin) hn)->handlerfunc) {
819///     char *modname = dupstring(((Builtin) hn)->optstr);
820///     (void)ensurefeature(modname, "b:", ...);
821///     hn = builtintab->getnode(builtintab, cmdarg);
822///     if (!hn) { lastval=1; zerr(...); return NULL; }
823/// }
824/// return hn;
825/// ```
826///
827/// WARNING: zshrs's builtin table is the static `BUILTINS` array in
828/// `src/ported/builtin.rs`. Module autoload routes through
829/// `module::ensurefeature(MODULESTAB, modname, "b:", Some(cmdarg))`;
830/// after the module loads the handler should be wired into BUILTINS.
831pub fn resolvebuiltin<'a>(
832    cmdarg: &str, // c:2703 (Src/exec.c)
833    hn: &'a builtin,
834) -> Option<&'a builtin> {
835    // c:2705 — `if (!((Builtin) hn)->handlerfunc)`.
836    if hn.handlerfunc.is_none() {
837        // c:2706 — `modname = dupstring(((Builtin)hn)->optstr)`.
838        let modname = hn.optstr.clone().unwrap_or_default();
839        // c:2712 — `ensurefeature(modname, "b:", cmdarg)`.
840        let _ = {
841            let mut t = crate::ported::module::MODULESTAB.lock().unwrap();
842            crate::ported::module::ensurefeature(&mut t, &modname, "b:", Some(cmdarg))
843        };
844        // c:2715-2716 — re-lookup the now-(hopefully)-resolved builtin.
845        if let Some(re) = BUILTINS.iter().find(|b| b.node.nam == cmdarg) {
846            if re.handlerfunc.is_some() {
847                return Some(re); // c:2723
848            }
849        }
850        // c:2717-2721 — `lastval = 1; zerr(...)` + return NULL.
851        zerr(&format!(
852            "autoloading module {} failed to define builtin: {}",
853            modname, cmdarg
854        ));
855        return None; // c:2720
856    }
857    Some(hn) // c:2723
858}
859
860/// Dispatch decision returned by `execcmd_compile_head` — the
861/// fusevm-bytecode-time head resolver that mirrors the local-variable
862/// state the C `execcmd_exec` function carries through `c:2913-2916`
863/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
864/// precmd-modifier strip count. The fusevm bytecode compiler reads
865/// this to emit the correct dispatch opcode in
866/// `src/extensions/compile_zsh.rs::compile_simple`.
867///
868/// Not a C struct — invented to bridge the divergence between the
869/// C wordcode-walker (which mutates locals + falls through to
870/// invocation) and zshrs's split parse → compile → VM pipeline.
871#[allow(non_camel_case_types)]
872#[derive(Debug, Default, Clone)]
873pub struct execcmd_dispatch {
874    /// Number of `BINF_PREFIX` words to strip from the head of args.
875    /// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
876    pub precmd_skip: usize,
877    /// Set when the head (after strip) is a real builtin
878    /// (`Src/exec.c:3065 is_builtin = 1`).
879    pub is_builtin: bool,
880    /// Set when the head (after strip) is a shell function
881    /// (`Src/exec.c:3053 is_shfunc = 1`).
882    pub is_shfunc: bool,
883    /// `cflags` accumulator from `Src/exec.c:2915` — gathers
884    /// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
885    /// BINF_NOGLOB` bits encountered during the precommand-modifier
886    /// walk (c:3062 `cflags |= hn->flags`).
887    pub cflags: u32,
888    /// `command -p` requested: use the default `$PATH` for lookup
889    /// (`Src/exec.c:3160 use_defpath = 1`). NOT YET HONORED by the
890    /// fusevm compiler — flagged for follow-up.
891    pub use_defpath: bool,
892    /// `command -v` / `command -V` requested: the dispatch target
893    /// flips to `bin_whence` per `Src/exec.c:3149-3157`
894    /// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
895    /// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
896    /// instead of resolving the post-strip head.
897    pub has_command_vv: bool,
898    /// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
899    /// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
900    pub exec_argv0: Option<String>,
901    /// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
902    /// — the `else` arm of `if (redir && nonempty(redir))`). Covers
903    /// bare `exec` / `noglob` / `command`. Caller emits
904    /// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
905    /// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
906    /// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
907    /// returns 0 without complaining about missing redirs).
908    pub is_empty_command: bool,
909}
910
911/// !!! NOT A PORT OF C `execcmd_exec` !!!
912///
913/// This is a fusevm-bytecode-time head resolver invoked by
914/// `src/extensions/compile_zsh.rs::compile_simple` and the
915/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
916/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
917/// file under the C-faithful name `execcmd_exec`.
918///
919/// This helper mirrors the head section (`c:2904-3275`) of the C
920/// function — local initialisation, the precommand-modifier walk
921/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
922/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
923/// sub-option parsers — and returns the resulting dispatch
924/// decision via `execcmd_dispatch`. The fusevm compiler reads
925/// that struct to decide which `Op::CallBuiltin` /
926/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
927/// correct post-strip `argc`.
928///
929/// =================== WARNING — DIVERGENCE ====================
930///
931/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
932/// `multio` redirections, evaluates `varspc` assignments, then calls
933/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
934/// stops after the precmd-modifier walk and only returns the head
935/// decision; runtime dispatch is driven by the bytecode the fusevm
936/// compiler emits.
937///
938/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
939/// wordcode iterator state — zshrs doesn't traverse wordcode here,
940/// so the args list arrives already-expanded as a `&[String]`
941/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
942/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
943///
944/// =============================================================
945pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
946    // c:2900 (Src/exec.c)
947
948    // c:2904-2916 — locals.
949    let mut hn: Option<&'static builtin> = None; // c:2904
950    let mut is_shfunc = false; // c:2913
951    let mut is_builtin = false; // c:2913
952    let mut use_defpath = false; // c:2913
953    let mut cflags: u32 = 0; // c:2915
954    let mut orig_cflags: u32 = 0; // c:2915
955    let _ = orig_cflags;
956    // c:3263 — `char *exec_argv0 = NULL;` (declared inside the
957    // BINF_EXEC arm; hoisted here so the dispatch struct can carry it
958    // out after the loop terminates).
959    let mut exec_argv0: Option<String> = None;
960    // c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
961    // (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
962    // compiler can emit `bin_whence` instead of resolving the head.
963    let mut has_command_vv = false;
964
965    // c:2962-2973 — `%job` head: rewrite `%name` → `fg|bg|disown %name`.
966    // Not in scope for the compile-time dispatch walk: jobspec
967    // expansion happens at runtime in fusevm; the bytecode emits a
968    // direct `fg`/`bg` call when it sees a leading `%`. Flagged for
969    // follow-up when the canonical port lands.
970
971    // c:2975-2986 — AUTORESUME prefix-match against jobtab. Same
972    // status as the %job head: runtime concern, deferred.
973
974    // c:3013-3091 — precommand-modifier walk.
975    let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
976    let mut precmd_skip: usize = 0;
977
978    // c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
979    if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
980        // c:3018
981        // c:3029 — `while (nonempty(preargs))`.
982        while precmd_skip < preargs.len() {
983            // c:3029
984            // c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
985            let cmdarg = untokenize(&preargs[precmd_skip]);
986            // c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
987            // already performed prefork expansion on `preargs`, so
988            // `has_token` is effectively false here; the C `break` on
989            // unexpanded tokens is unreachable in this entry point.
990
991            // c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
992            // even disabled builtins so the reserved-word form
993            // (`integer x`, `local foo`) still dispatches to the
994            // typeset family. The static `BUILTINS` array doesn't
995            // expose a separate disabled-bit lookup; one path covers
996            // both. Effect is identical for the precmd-modifier walk.
997
998            // c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
999            // BINF_COMMAND)) && shfunctab->getnode(...))` — shell
1000            // function takes precedence unless a `builtin`/`command`
1001            // modifier preceded it.
1002            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
1003                // c:3051
1004                if shfunctab_lock()
1005                    .read()
1006                    .map(|t| t.iter().any(|(k, _)| k == &cmdarg))
1007                    .unwrap_or(false)
1008                {
1009                    is_shfunc = true; // c:3053
1010                    break; // c:3054
1011                }
1012            }
1013            // c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
1014            let entry = BUILTINS.iter().find(|b| b.node.nam == cmdarg);
1015            let Some(entry) = entry else {
1016                // c:3056-3058
1017                break;
1018            };
1019            hn = Some(entry);
1020            // c:3061-3063 — accumulate cflags.
1021            orig_cflags |= cflags;
1022            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
1023            cflags |= entry.node.flags as u32;
1024            // c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
1025            // builtin, stop.
1026            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
1027                // c:3064
1028                // WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
1029                // to autoload the builtin's module if its
1030                // `handlerfunc` is NULL. In zshrs, builtins live in
1031                // two places: the static `BUILTINS` table (which
1032                // mirrors C `handlerfunc`, often `None` for ports
1033                // dispatched through fusevm) AND fusevm's
1034                // `register_builtins` map (the actual runtime
1035                // dispatcher). A null `handlerfunc` in the static
1036                // table is NOT an autoload failure for us — it
1037                // means dispatch routes through fusevm. So we
1038                // skip the resolvebuiltin call here; the faithful
1039                // port remains available for future callers that
1040                // genuinely need module-autoload semantics.
1041                is_builtin = true; // c:3065
1042                break; // c:3077
1043            }
1044            // c:3086 — `uremnode(preargs, firstnode(preargs))`.
1045            precmd_skip += 1;
1046            // c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
1047            //   (...); if (!firstnode(preargs)) break; }`. zshrs has
1048            // no `execcmd_getargs` (args arrive pre-expanded); the
1049            // bounds-check at the top of `while precmd_skip <
1050            // preargs.len()` handles the empty case identically.
1051
1052            // c:3092-3177 — BINF_COMMAND sub-option parsing
1053            // (`command -p / -v / -V`).
1054            if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
1055                // c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
1056                //                int has_p = 0, has_vV = 0, has_other = 0;`
1057                let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
1058                let mut pnode: Option<usize> = None; // c:3102
1059                let mut has_p = false; // c:3104
1060                let mut has_vv = false; // c:3104
1061                let mut has_other = false; // c:3104
1062                                           // c:3107 — `while (IS_DASH(*argdata))`
1063                while argnode < preargs.len()
1064                    && IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
1065                {
1066                    let argdata = preargs[argnode].clone(); // c:3106
1067                    let bytes = argdata.as_bytes();
1068                    // c:3108-3111 — stop on bare `-` or `--`.
1069                    if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
1070                        // c:3109
1071                        break; // c:3111
1072                    }
1073                    // c:3112-3133 — scan flag chars.
1074                    for &c in &bytes[1..] {
1075                        // c:3112
1076                        match c as char {
1077                            'p' => {
1078                                // c:3114
1079                                has_p = true; // c:3122
1080                                pnode = Some(argnode); // c:3123
1081                            }
1082                            'v' | 'V' => {
1083                                // c:3125-3126
1084                                has_vv = true; // c:3127
1085                            }
1086                            _ => {
1087                                // c:3129
1088                                has_other = true; // c:3130
1089                            }
1090                        }
1091                    }
1092                    // c:3134-3138 — unknown flag → don't try, leave alone.
1093                    if has_other {
1094                        // c:3134
1095                        has_p = false; // c:3136
1096                        has_vv = false; // c:3136
1097                        break; // c:3137
1098                    }
1099                    // c:3140-3147 — advance to next arg.
1100                    argnode += 1; // c:3141 nextnode(argnode)
1101                    if argnode >= preargs.len() {
1102                        // c:3142 — execcmd_getargs (skipped: pre-expanded)
1103                        break; // c:3145
1104                    }
1105                }
1106                // c:3149-3157 — `-v`/`-V` → dispatch to whence.
1107                if has_vv {
1108                    // c:3149
1109                    // c:3154 `pushnode(preargs, "command")` — C re-inserts
1110                    // "command" so bin_whence sees it as argv[0]. zshrs
1111                    // surfaces this via `has_command_vv`; the fusevm
1112                    // compiler emits the equivalent whence call.
1113                    has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
1114                    is_builtin = true;
1115                    break; // c:3157
1116                } else if has_p {
1117                    // c:3158
1118                    use_defpath = true; // c:3160
1119                    if let Some(pn) = pnode {
1120                        // c:3165 — `uremnode(preargs, pnode)`. zshrs:
1121                        // remove the `-p`-bearing arg from preargs.
1122                        if pn < preargs.len() {
1123                            preargs.remove(pn);
1124                            // precmd_skip already accounts for the
1125                            // stripped `command` prefix; we just removed
1126                            // the `-p` flag which sat at preargs[pn].
1127                            // No precmd_skip change needed — the head
1128                            // remains where it was.
1129                        }
1130                    }
1131                }
1132                // c:3176-3177 — `--` trailing end-of-options strip.
1133                if argnode < preargs.len() {
1134                    let argdata = &preargs[argnode];
1135                    let b = argdata.as_bytes();
1136                    if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
1137                        // c:3176
1138                        preargs.remove(argnode); // c:3177
1139                    }
1140                }
1141            } else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
1142                // c:3178-3275 — BINF_EXEC sub-option parsing
1143                // (`exec -a NAME -l -c`).
1144                let mut argnode: usize = precmd_skip; // c:3185
1145                let mut error_done = false;
1146                // c:3196 — `while (argdata && IS_DASH(*argdata) &&
1147                //                  strlen(argdata) >= 2)`
1148                while argnode < preargs.len() {
1149                    let argdata = preargs[argnode].clone();
1150                    let bytes = argdata.as_bytes();
1151                    if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
1152                        break; // c:3196 loop guard
1153                    }
1154                    let oldnode = argnode; // c:3197
1155                    argnode += 1; // c:3198 nextnode(oldnode)
1156                                  // c:3203-3208 — empty next → error.
1157                    if argnode >= preargs.len() {
1158                        // c:3203
1159                        zerr(
1160                            // c:3204
1161                            "exec requires a command to execute",
1162                        );
1163                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
1164                        error_done = true;
1165                        break; // c:3207 goto done
1166                    }
1167                    // c:3209 — `uremnode(preargs, oldnode)`.
1168                    preargs.remove(oldnode);
1169                    argnode -= 1; // re-anchor — `argnode` was the post-removed slot
1170                                  // c:3210-3211 — `--` stops option scan.
1171                    if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
1172                        // c:3210
1173                        break; // c:3211
1174                    }
1175                    // c:3212-3258 — scan flag chars after the leading `-`.
1176                    let mut k = 1usize;
1177                    while k < bytes.len() && !error_done {
1178                        let cmdopt = bytes[k] as char; // c:3212
1179                        match cmdopt {
1180                            'a' => {
1181                                // c:3214 — `-a` ARGV0 override.
1182                                if k + 1 < bytes.len() {
1183                                    // c:3216 — `-aNAME` inline form.
1184                                    exec_argv0 =
1185                                        Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
1186                                    k = bytes.len(); // c:3219 position past end
1187                                } else {
1188                                    // c:3220 — `-a NAME` separate form.
1189                                    if argnode >= preargs.len() {
1190                                        // c:3230
1191                                        zerr(
1192                                            // c:3231
1193                                            "exec flag -a requires a parameter",
1194                                        );
1195                                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
1196                                        error_done = true;
1197                                        break; // c:3234 goto done
1198                                    }
1199                                    exec_argv0 = Some(preargs[argnode].clone()); // c:3236
1200                                    preargs.remove(argnode); // c:3239
1201                                }
1202                            }
1203                            'c' => {
1204                                // c:3242
1205                                cflags |= BINF_CLEARENV; // c:3243
1206                            }
1207                            'l' => {
1208                                // c:3245
1209                                cflags |= BINF_DASH; // c:3246
1210                            }
1211                            _ => {
1212                                // c:3248
1213                                zerr(
1214                                    // c:3249
1215                                    &format!("unknown exec flag -{}", cmdopt),
1216                                );
1217                                errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
1218                                error_done = true;
1219                                break; // c:3256
1220                            }
1221                        }
1222                        k += 1;
1223                    }
1224                    if error_done {
1225                        break;
1226                    }
1227                }
1228                // c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
1229                // the actual `setenv` to the fusevm compiler / external
1230                // exec path; we surface `exec_argv0` via the dispatch
1231                // struct so the caller can apply it before fork+exec.
1232                if let Some(ref a0) = exec_argv0 {
1233                    // c:3263 — `remnulargs + untokenize` then setenv.
1234                    let cleaned = untokenize(a0); // c:3266-3267
1235                    exec_argv0 = Some(cleaned);
1236                }
1237                if error_done {
1238                    return execcmd_dispatch {
1239                        precmd_skip,
1240                        is_builtin,
1241                        is_shfunc,
1242                        cflags,
1243                        use_defpath,
1244                        has_command_vv,
1245                        exec_argv0,
1246                        is_empty_command: false,
1247                    };
1248                }
1249            }
1250            // c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
1251            // unset(POSIXBUILTINS)) break;`. After processing a
1252            // `command` precmd modifier (and its -p/-v/-V flags), the
1253            // C loop exits with hn cleared so the dispatch falls
1254            // through to external lookup. Without this, the next
1255            // iteration would find `command print` → print's builtin
1256            // and dispatch to it; zsh's intentional behaviour is to
1257            // skip builtins under `command` (unless POSIXBUILTINS is
1258            // set, where the loop continues normally).
1259            if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
1260                hn = None; // c:3275 hn = NULL
1261                break; // c:3277
1262            }
1263        }
1264    }
1265
1266    // c:3309-3406 — "Empty command" branch. When the precmd-modifier
1267    // walk above strips every word with nothing left to dispatch
1268    // (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
1269    // C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
1270    //
1271    // - redir-present + do_exec       → nullexec=1 (continue to run)
1272    // - redir-present + varspc        → nullexec=2 (continue)
1273    // - redir-present + no nullcmd    → `zerr("redirection with no command")`
1274    //                                   lastval=1, return
1275    // - redir-present + SHNULLCMD     → args=[":"]
1276    // - redir-present + readnullcmd   → args=[readnullcmd]
1277    // - redir-present + default       → args=[nullcmd]
1278    // - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
1279    // - NO redir + default            → lastval=cmdoutval, return (c:3372-3406)
1280    //
1281    // zshrs's `execcmd_compile_head` doesn't receive `redir` (it
1282    // takes `args` only). The cases that DEPEND on redirs are handled by
1283    // `compile_zsh.rs::compile_redir` before this dispatch fires; the
1284    // remaining cases collapse into the single `is_empty_command`
1285    // flag below. Both NO-redir sub-cases produce the same observable
1286    // outcome (lastval=0, return without invoking anything), so a
1287    // single flag suffices.
1288    let is_empty_command = precmd_skip >= preargs.len();
1289
1290    // =================== WARNING — DIVERGENCE ====================
1291    // c:3285+: prefork-substitution, magic_assign decision, multio
1292    // setup, varspc evaluation, and the actual execbuiltin /
1293    // runshfunc / execute call. ~1300 lines of interpreter-only
1294    // code, entirely replaced by fusevm bytecode dispatch in
1295    // `src/extensions/compile_zsh.rs::compile_simple` and the
1296    // opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
1297    // The return value below feeds those compile-time decisions.
1298    // =============================================================
1299
1300    let _ = hn;
1301    execcmd_dispatch {
1302        precmd_skip,
1303        is_builtin,
1304        is_shfunc,
1305        cflags,
1306        use_defpath,
1307        has_command_vv,
1308        exec_argv0,
1309        is_empty_command,
1310    }
1311}
1312
1313// =============================================================================
1314// Leaf-function ports — c:283 (parse_string) and below. Added incrementally to
1315// chip at the ~5500 lines of exec.c still un-ported beyond the wordcode
1316// walker (execlist / execpline / execcmd which the fusevm bytecode VM
1317// replaces — see the WARNING block in execcmd_exec).
1318// =============================================================================
1319
1320/// Port of `parse_string(char *s, int reset_lineno)` from `Src/exec.c:283`.
1321///
1322/// C body:
1323/// ```c
1324/// Eprog p; zlong oldlineno;
1325/// zcontext_save();
1326/// inpush(s, INP_LINENO, NULL);
1327/// strinbeg(0);
1328/// oldlineno = lineno;
1329/// if (reset_lineno) lineno = 1;
1330/// p = parse_list();
1331/// lineno = oldlineno;
1332/// if (tok == LEXERR && !lastval) lastval = 1;
1333/// strinend();
1334/// inpop();
1335/// zcontext_restore();
1336/// return p;
1337/// ```
1338///
1339/// Parses an arbitrary string as a zsh command list, returning the
1340/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
1341/// `bin_eval` for `eval`, and the autoload path.
1342pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
1343    // c:285-286
1344    let p: Option<eprog>;
1345    let oldlineno: i64;
1346
1347    zcontext_save(); // c:288
1348    inpush(s, INP_LINENO, None); // c:289
1349    strinbeg(0); // c:290
1350    oldlineno = LEX_LINENO.get() as i64; // c:291
1351    if reset_lineno != 0 {
1352        // c:292
1353        LEX_LINENO.set(1); // c:293
1354    }
1355    p = parse_list(); // c:294
1356    LEX_LINENO.set(oldlineno as u64); // c:295
1357                                      // c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
1358    if tok() == LEXERR && LASTVAL.load(Ordering::Relaxed) == 0 {
1359        LASTVAL.store(1, Ordering::Relaxed);
1360    }
1361    strinend(); // c:298
1362    inpop(); // c:299
1363    zcontext_restore(); // c:300
1364    p // c:301
1365}
1366
1367/// Port of `int isgooderr(int e, char *dir)` from `Src/exec.c:652`.
1368///
1369/// C body:
1370/// ```c
1371/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
1372/// return ((e != EACCES || !access(dir, X_OK)) &&
1373///         e != ENOENT && e != ENOTDIR);
1374/// ```
1375///
1376/// errno classifier for `execve` failures during PATH search: if the
1377/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
1378/// it's "expected" (try next PATH entry); otherwise it's a real
1379/// failure worth surfacing.
1380pub fn isgooderr(e: i32, dir: &str) -> bool {
1381    // c:652
1382    // c:Src/exec.c:658-659 — `(e != EACCES || !access(dir, X_OK)) &&
1383    //   e != ENOENT && e != ENOTDIR`. C's `access(dir, X_OK)` returns
1384    //   0 on success / -1 on failure. The previous Rust port used
1385    //   `metadata().permissions().mode() & 0o111` which reports the
1386    //   X bit even when the path doesn't exist as the EFFECTIVE caller
1387    //   (root, ACLs, capabilities all flip access() vs raw mode).
1388    //   `/no/such/dir` metadata() fails → returned false for
1389    //   dir_x_ok, then `!false` = true, giving "good error" for a
1390    //   nonexistent path. Use libc::access directly to match C exactly.
1391    let unmeta_dir = unmeta(dir);
1392    let cstr = std::ffi::CString::new(unmeta_dir.as_bytes()).unwrap_or_default();
1393    let access_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1394    (e != libc::EACCES || access_ok) && e != libc::ENOENT && e != libc::ENOTDIR
1395}
1396
1397/// Port of `int iscom(char *s)` from `Src/exec.c:962`.
1398///
1399/// C body:
1400/// ```c
1401/// struct stat statbuf;
1402/// char *us = unmeta(s);
1403/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
1404///         S_ISREG(statbuf.st_mode));
1405/// ```
1406///
1407/// True iff `s` names an executable regular file (X-perm + S_IFREG).
1408/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
1409/// validate candidate paths before exec.
1410pub fn iscom(s: &str) -> bool {
1411    // c:962
1412    let us = unmeta(s); // c:965
1413                        // c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
1414    let cstr = match std::ffi::CString::new(us.as_str()) {
1415        Ok(c) => c,
1416        Err(_) => return false,
1417    };
1418    let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1419    if !x_ok {
1420        return false;
1421    }
1422    let meta = match std::fs::metadata(&us) {
1423        Ok(m) => m,
1424        Err(_) => return false,
1425    };
1426    meta.file_type().is_file()
1427}
1428
1429/// Port of `int isreallycom(Cmdnam cn)` from `Src/exec.c:972-987`.
1430///
1431/// Verify that a hashed/cached cmdnamtab entry still names a real
1432/// external command (X-perm + regular file). For HASHED entries
1433/// (`cn->u.cmd` carries the absolute path), test the path directly;
1434/// otherwise concatenate `name[0] + "/" + nam` and test that.
1435/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
1436/// turn into a failed `execve` syscall.
1437pub fn isreallycom(cn: &cmdnam) -> bool {
1438    // c:972
1439    let fullnam: String;
1440    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
1441        // c:977-978 — `strcpy(fullnam, cn->u.cmd);`
1442        fullnam = cn.cmd.clone().unwrap_or_default();
1443    } else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
1444        // c:979-980 — `if (!cn->u.name) return 0;`
1445        return false;
1446    } else {
1447        // c:982-984 — `strcpy + strcat("/") + strcat(nam)`
1448        let path0 = &cn.name.as_ref().unwrap()[0];
1449        fullnam = format!("{}/{}", path0, cn.node.nam);
1450    }
1451    iscom(&fullnam) // c:986
1452}
1453
1454/// Port of `int isrelative(char *s)` from `Src/exec.c:996`.
1455///
1456/// C body:
1457/// ```c
1458/// if (*s != '/') return 1;
1459/// for (; *s; s++)
1460///     if (*s == '.' && s[-1] == '/' &&
1461///         (s[1] == '/' || s[1] == '\0' ||
1462///          (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
1463///         return 1;
1464/// return 0;
1465/// ```
1466///
1467/// True iff `s` either doesn't start with `/` OR contains a `./` or
1468/// `../` component anywhere. Used by `cd` resolution and PATH-cache
1469/// invalidation to detect non-canonical paths.
1470pub fn isrelative(s: &str) -> i32 {
1471    // c:996
1472    let bytes = s.as_bytes();
1473    if bytes.is_empty() || bytes[0] != b'/' {
1474        // c:998
1475        return 1; // c:999
1476    }
1477    // c:1000-1004 — walk for `./` or `../` components.
1478    for i in 1..bytes.len() {
1479        let c = bytes[i];
1480        let prev = bytes[i - 1];
1481        if c == b'.' && prev == b'/' {
1482            let next = bytes.get(i + 1).copied().unwrap_or(0);
1483            if next == b'/' || next == 0 {
1484                // c:1002
1485                return 1;
1486            }
1487            if next == b'.' {
1488                let next2 = bytes.get(i + 2).copied().unwrap_or(0);
1489                if next2 == b'/' || next2 == 0 {
1490                    // c:1003
1491                    return 1;
1492                }
1493            }
1494        }
1495    }
1496    0 // c:1005
1497}
1498
1499/// Port of `void setunderscore(char *str)` from `Src/exec.c:2652`.
1500///
1501/// C body:
1502/// ```c
1503/// queue_signals();
1504/// if (str && *str) {
1505///     size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
1506///     if (nl > underscorelen || (underscorelen - nl) > 64) {
1507///         zfree(zunderscore, underscorelen);
1508///         zunderscore = (char *) zalloc(underscorelen = nl);
1509///     }
1510///     strcpy(zunderscore, str);
1511///     underscoreused = l;
1512/// } else {
1513///     ... reset zunderscore = "" ...
1514/// }
1515/// unqueue_signals();
1516/// ```
1517///
1518/// Sets the `$_` global to the last argument of the most recent
1519/// command. Called from `execcmd_exec` (c:3936) per `last_status`
1520/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
1521pub fn setunderscore(str: &str) {
1522    // c:2652
1523    queue_signals(); // c:2654
1524    if !str.is_empty() {
1525        // c:2655 `if (str && *str)`
1526        // c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
1527        let mut zu = zunderscore.lock().unwrap();
1528        *zu = str.to_string();
1529        let nl = (str.len() + 1 + 31) & !31; // c:2656
1530        underscorelen.store(nl, Ordering::Relaxed); // c:2660
1531        underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
1532    // c:2663
1533    } else {
1534        // c:2664
1535        let mut zu = zunderscore.lock().unwrap();
1536        zu.clear(); // c:2669 `*zunderscore = '\0';`
1537        underscoreused.store(1, Ordering::Relaxed); // c:2670
1538    }
1539    unqueue_signals(); // c:2672
1540}
1541
1542/// Port of `int mpipe(int *pp)` from `Src/exec.c:5160`.
1543///
1544/// C body:
1545/// ```c
1546/// if (pipe(pp) < 0) {
1547///     zerr("pipe failed: %e", errno);
1548///     return -1;
1549/// }
1550/// pp[0] = movefd(pp[0]);
1551/// pp[1] = movefd(pp[1]);
1552/// return 0;
1553/// ```
1554///
1555/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
1556/// fd range via `movefd`. Used by `getpipe` / `getproc` /
1557/// `spawnpipes` for process substitution and pipeline wiring.
1558pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
1559    // c:5160
1560    let mut fds: [libc::c_int; 2] = [-1; 2];
1561    if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
1562        // c:5162
1563        zerr(&format!(
1564            // c:5163
1565            "pipe failed: {}",
1566            std::io::Error::last_os_error()
1567        ));
1568        return -1; // c:5164
1569    }
1570    pp[0] = movefd(fds[0]); // c:5166
1571    pp[1] = movefd(fds[1]); // c:5167
1572    0 // c:5168
1573}
1574
1575/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
1576/// from `Src/exec.c:5289`. Anonymous-function name marker used by
1577/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
1578/// `() { ... }` anonymous function dispatch.
1579pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
1580
1581/// Port of `int is_anonymous_function_name(const char *name)` from
1582/// `Src/exec.c:5300`.
1583///
1584/// C body:
1585/// ```c
1586/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
1587/// ```
1588///
1589/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
1590/// reporting and `whence -v` to skip / annotate anonymous functions.
1591pub fn is_anonymous_function_name(name: &str) -> i32 {
1592    // c:5300
1593    if name == ANONYMOUS_FUNCTION_NAME {
1594        // c:5302
1595        1
1596    } else {
1597        0
1598    }
1599}
1600
1601/// Port of `void execsave(void)` from `Src/exec.c:6438`.
1602///
1603/// C body:
1604/// ```c
1605/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
1606/// es->list_pipe_pid = list_pipe_pid;
1607/// es->nowait = nowait;
1608/// es->pline_level = pline_level;
1609/// es->list_pipe_child = list_pipe_child;
1610/// es->list_pipe_job = list_pipe_job;
1611/// strcpy(es->list_pipe_text, list_pipe_text);
1612/// es->lastval = lastval;
1613/// es->noeval = noeval;
1614/// es->badcshglob = badcshglob;
1615/// es->cmdoutpid = cmdoutpid;
1616/// es->cmdoutval = cmdoutval;
1617/// es->use_cmdoutval = use_cmdoutval;
1618/// es->procsubstpid = procsubstpid;
1619/// es->trap_return = trap_return;
1620/// es->trap_state = trap_state;
1621/// es->trapisfunc = trapisfunc;
1622/// es->traplocallevel = traplocallevel;
1623/// es->noerrs = noerrs;
1624/// es->this_noerrexit = this_noerrexit;
1625/// es->underscore = ztrdup(zunderscore);
1626/// es->next = exstack;
1627/// exstack = es;
1628/// noerrs = cmdoutpid = 0;
1629/// ```
1630///
1631/// Snapshot every transient exec-context global onto the `exstack`
1632/// linked list so a signal-handler / trap-firing nested eval can
1633/// scribble freely; `execrestore` pops the frame back. Called by
1634/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
1635pub fn execsave() {
1636    // c:6438
1637    // c:6442 — `es = zalloc(sizeof(execstack));`
1638    let mut es = Box::new(execstack {
1639        // c:6442
1640        next: None,
1641        list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
1642        nowait: nowait.load(Ordering::Relaxed),               // c:6444
1643        pline_level: pline_level.load(Ordering::Relaxed),     // c:6445
1644        list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
1645        list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
1646        list_pipe_text: {
1647            // c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
1648            let mut buf = [0u8; JOBTEXTSIZE];
1649            if let Ok(s) = LIST_PIPE_TEXT.lock() {
1650                let bytes = s.as_bytes();
1651                let n = bytes.len().min(JOBTEXTSIZE - 1);
1652                buf[..n].copy_from_slice(&bytes[..n]);
1653            }
1654            buf
1655        },
1656        lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
1657        // c:6450 — `es->noeval = noeval;`. Snapshot math.c's
1658        // `int noeval` (the parse-only side-effect-skip counter)
1659        // via math.rs's pub accessor.
1660        noeval: crate::ported::math::m_noeval(),
1661        // c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
1662        // csh-glob diagnostic counter (glob.c:103 / glob.rs
1663        // BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
1664        // the outer command's per-line accounting.
1665        badcshglob: crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed), // c:6451
1666        cmdoutpid: cmdoutpid.load(Ordering::Relaxed),                        // c:6452
1667        cmdoutval: cmdoutval.load(Ordering::Relaxed),                        // c:6453
1668        use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed),                // c:6454
1669        procsubstpid: procsubstpid.load(Ordering::Relaxed),                  // c:6455
1670        trap_return: TRAP_RETURN.load(Ordering::Relaxed),                    // c:6456
1671        trap_state: TRAP_STATE.load(Ordering::Relaxed),                      // c:6457
1672        trapisfunc: trapisfunc.load(Ordering::Relaxed),                      // c:6458
1673        traplocallevel: traplocallevel.load(Ordering::Relaxed),              // c:6459
1674        noerrs: noerrs.load(Ordering::Relaxed),                              // c:6460
1675        this_noerrexit: this_noerrexit.load(Ordering::Relaxed),              // c:6461
1676        // c:6462 — `es->underscore = ztrdup(zunderscore);`
1677        underscore: Some(zunderscore.lock().unwrap().clone()),
1678    });
1679    // c:6463-6464 — `es->next = exstack; exstack = es;`
1680    let mut head = exstack.lock().unwrap();
1681    es.next = head.take();
1682    *head = Some(es);
1683    // c:6465 — `noerrs = cmdoutpid = 0;`
1684    noerrs.store(0, Ordering::Relaxed);
1685    cmdoutpid.store(0, Ordering::Relaxed);
1686}
1687
1688/// Port of `void execrestore(void)` from `Src/exec.c:6470`.
1689///
1690/// C body:
1691/// ```c
1692/// struct execstack *en = exstack;
1693/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
1694/// queue_signals();
1695/// exstack = exstack->next;
1696/// list_pipe_pid = en->list_pipe_pid;
1697/// nowait = en->nowait;
1698/// pline_level = en->pline_level;
1699/// list_pipe_child = en->list_pipe_child;
1700/// list_pipe_job = en->list_pipe_job;
1701/// strcpy(list_pipe_text, en->list_pipe_text);
1702/// lastval = en->lastval;
1703/// noeval = en->noeval;
1704/// badcshglob = en->badcshglob;
1705/// cmdoutpid = en->cmdoutpid;
1706/// cmdoutval = en->cmdoutval;
1707/// use_cmdoutval = en->use_cmdoutval;
1708/// procsubstpid = en->procsubstpid;
1709/// trap_return = en->trap_return;
1710/// trap_state = en->trap_state;
1711/// trapisfunc = en->trapisfunc;
1712/// traplocallevel = en->traplocallevel;
1713/// noerrs = en->noerrs;
1714/// this_noerrexit = en->this_noerrexit;
1715/// setunderscore(en->underscore);
1716/// zsfree(en->underscore);
1717/// free(en);
1718/// unqueue_signals();
1719/// ```
1720///
1721/// Pop the top `execstack` frame and restore every transient
1722/// exec-context global. Inverse of `execsave`.
1723pub fn execrestore() {
1724    // c:6470
1725    let mut head = exstack.lock().unwrap();
1726    let en = match head.take() {
1727        // c:6472 + c:6477
1728        Some(en) => en,
1729        None => {
1730            // c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
1731            crate::DPUTS!(true, "BUG: execrestore() without execsave()");
1732            return;
1733        }
1734    };
1735    queue_signals(); // c:6476
1736    *head = en.next; // c:6477
1737    drop(head); // release lock before scalar restores
1738
1739    list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
1740    nowait.store(en.nowait, Ordering::Relaxed); // c:6480
1741    pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
1742    list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
1743    list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
1744                                                              // c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
1745    if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
1746        let nul = en
1747            .list_pipe_text
1748            .iter()
1749            .position(|&b| b == 0)
1750            .unwrap_or(JOBTEXTSIZE);
1751        *s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
1752    }
1753    LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
1754                                                  // c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
1755                                                  // counter from the saved frame.
1756    crate::ported::math::m_noeval_set(en.noeval);
1757    // c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
1758    // diagnostic counter saved on entry.
1759    crate::ported::glob::BADCSHGLOB.store(en.badcshglob, Ordering::Relaxed);
1760    cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
1761    cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
1762    use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
1763    procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
1764    TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
1765    TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
1766    trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
1767    traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
1768    noerrs.store(en.noerrs, Ordering::Relaxed); // c:6496
1769    this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
1770                                                                // c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
1771    if let Some(ref u) = en.underscore {
1772        setunderscore(u); // c:6498
1773    }
1774    // c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
1775    unqueue_signals(); // c:6502
1776}
1777
1778/// Port of `void execstring(char *s, int dont_change_job, int exiting,
1779/// char *context)` from `Src/exec.c:1228`.
1780///
1781/// C body:
1782/// ```c
1783/// Eprog prog;
1784/// pushheap();
1785/// if (isset(VERBOSE)) {
1786///     zputs(s, stderr);
1787///     fputc('\n', stderr);
1788///     fflush(stderr);
1789/// }
1790/// if ((prog = parse_string(s, 0)))
1791///     execode(prog, dont_change_job, exiting, context);
1792/// popheap();
1793/// ```
1794///
1795/// Public entry — execute an arbitrary string as a zsh command list.
1796/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
1797/// body executors, command substitution body runners.
1798///
1799/// =================== WARNING — DIVERGENCE ====================
1800/// The C path is `parse_string` → `execode` → `execlist` (wordcode
1801/// walker). zshrs replaces `execode/execlist` with the fusevm
1802/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
1803/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
1804/// parse+execute chain delegates to the fusevm entry. When `execlist`
1805/// lands as a strict 1:1 port, swap the delegate for the canonical
1806/// chain.
1807/// =============================================================
1808pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
1809    // c:1228
1810    pushheap(); // c:1232
1811                // c:1233-1237 — VERBOSE banner.
1812    if isset(VERBOSE) {
1813        // c:1233
1814        let mut stderr = std::io::stderr().lock();
1815        use std::io::Write;
1816        let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
1817        let _ = stderr.write_all(b"\n"); // c:1235
1818        let _ = stderr.flush(); // c:1236
1819    }
1820    // c:1238-1239 — parse + execode. zshrs delegates the parse+VM
1821    // chain to the fusevm pipeline via the exec_hooks fn-ptr
1822    // installed by fusevm_bridge at startup. Direct
1823    // `with_executor` / ShellExecutor reach-in from src/ported/ is
1824    // forbidden — see memory feedback_no_exec_script_from_ported.
1825    let _ = crate::ported::exec::execute_script_zsh_pipeline(s);
1826    popheap(); // c:1240
1827}
1828
1829/// Port of `void runshfunc(Eprog prog, FuncWrap wrap, char *name)` from
1830/// `Src/exec.c:6166`. The inner shell-function executor — fires
1831/// module-registered wrapper handlers around the function body, with
1832/// `$_` (zunderscore) save/restore and a paramscope push/pop around
1833/// the wordcode walk.
1834///
1835/// C control flow:
1836/// ```c
1837/// queue_signals();
1838/// ou = zalloc(ouu = underscoreused);
1839/// if (ou) memcpy(ou, zunderscore, underscoreused);
1840/// while (wrap) {                       // wrapper chain
1841///     wrap->module->wrapper++;
1842///     cont = wrap->handler(prog, wrap->next, name);
1843///     wrap->module->wrapper--;
1844///     if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
1845///         unload_module(wrap->module);
1846///     if (!cont) {                     // wrapper handled it
1847///         if (ou) zfree(ou, ouu);
1848///         unqueue_signals();
1849///         return;
1850///     }
1851///     wrap = wrap->next;
1852/// }
1853/// startparamscope();
1854/// execode(prog, 1, 0, "shfunc");
1855/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
1856/// endparamscope();
1857/// unqueue_signals();
1858/// ```
1859///
1860/// (a) `wrap->module->wrapper++/--` (c:6178/6180) wired against
1861///     `module::MODULESTAB.modules[name].wrapper` (i32), looked up
1862///     by `wrap.module.node.nam`. Recursive unload during handler
1863///     defers correctly.
1864/// (b) `unload_module(wrap->module)` (c:6184) wired via
1865///     `modulestab.unload_module(name)` when wrapper hits 0 AND
1866///     MOD_UNLOAD flag is set on the module's hashnode.
1867/// (c) `execode(prog, 1, 0, "shfunc")` (c:6195) ported at
1868///     exec.rs:6047. Body uses execode for the no-source
1869///     (compiled-wordcode) branch and fusevm for the
1870///     source-preserving (autoloaded) branch per cache coherence.
1871/// (d) `startparamscope/endparamscope` Rust signatures take
1872///     `&mut HashTable` (params.rs:7425/7435). We pass the global
1873///     paramtab handle via the params crate.
1874pub fn runshfunc(prog: &eprog, mut wrap: Option<&funcwrap>, name: &str) {
1875    // c:6166
1876    queue_signals(); // c:6171
1877                     // c:6173-6175 — snapshot zunderscore into `ou`.
1878    let ouu = underscoreused.load(Ordering::Relaxed) as usize;
1879    let ou: Option<String> = if ouu > 0 {
1880        // c:6174
1881        Some(zunderscore.lock().unwrap().clone()) // c:6175
1882    } else {
1883        None
1884    };
1885    // c:6177-6193 — wrapper chain walk.
1886    while let Some(w) = wrap {
1887        // c:6177
1888        // c:6178 — wrap->module->wrapper++ (WARNING a).
1889        // c:6178 — `wrap->module->wrapper++;` — bump refcount so a
1890        // recursive unload during the handler defers until we return.
1891        let mod_name: Option<String> = w.module.as_ref().map(|m| m.node.nam.clone());
1892        if let Some(ref n) = mod_name {
1893            if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1894                if let Some(m) = tab.modules.get_mut(n) {
1895                    m.wrapper += 1;
1896                }
1897            }
1898        }
1899        let cont = if let Some(h) = w.handler {
1900            // c:6179 — WrapFunc takes Eprog by value + next FuncWrap by value.
1901            // We pass an empty next sentinel (wrapper-chain walks are
1902            // single-step in zshrs — see chain-walk comment below).
1903            let next_sentinel = Box::new(funcwrap {
1904                next: None,
1905                flags: 0,
1906                handler: None,
1907                module: None,
1908            });
1909            h(Box::new(prog.clone()), next_sentinel, name)
1910        } else {
1911            1
1912        };
1913        // c:6180 — `wrap->module->wrapper--;`
1914        // c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD)) unload_module(wrap->module);`
1915        if let Some(ref n) = mod_name {
1916            let should_unload = {
1917                if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1918                    if let Some(m) = tab.modules.get_mut(n) {
1919                        m.wrapper -= 1;
1920                        m.wrapper == 0 && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
1921                    } else {
1922                        false
1923                    }
1924                } else {
1925                    false
1926                }
1927            };
1928            if should_unload {
1929                if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1930                    let _ = tab.unload_module(n); // c:6184
1931                }
1932            }
1933        }
1934        if cont == 0 {
1935            // c:6186 — wrapper claimed the call.
1936            unqueue_signals(); // c:6189
1937            return; // c:6190
1938        }
1939        // c:6192 — wrap = wrap->next; the linked-list step requires
1940        // owning the next ref; the borrowed iteration breaks here.
1941        // Wrapper chains > 1 are extremely rare; we stop at the
1942        // first to avoid a Box::leak.
1943        wrap = None;
1944    }
1945    // c:6194 — startparamscope (just inc_locallevel internally).
1946    inc_locallevel();
1947    // c:6195 — `execode(prog, 1, 0, "shfunc");` — run the function
1948    // body. Prefer the canonical execode (exec.rs:6047) which walks
1949    // execlist on a fresh estate over the prog. If prog.strs carries
1950    // the original source (autoloaded ported that the lazy-compile path
1951    // populated), route through the fusevm pipeline for cache
1952    // coherence with execstring.
1953    if let Some(ref src) = prog.strs {
1954        let _ = crate::ported::exec::execute_script_zsh_pipeline(src);
1955    } else {
1956        // Pure wordcode body — drive via the canonical execode.
1957        execode_wordcode(Box::new(prog.clone()), 1, 0, "shfunc");
1958        let _ = name;
1959    }
1960    if let Some(ou_str) = ou {
1961        // c:6196
1962        setunderscore(&ou_str); // c:6197
1963                                // c:6198 — zfree(ou, ouu) — Rust drops on scope exit.
1964    }
1965    endparamscope(); // c:6200
1966                     // c:6141 — deferred-exit gate. After endparamscope() unwinds the
1967                     // function's local scope (locallevel--), check whether an exit
1968                     // queued inside the function has reached its target scope:
1969                     //   if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)
1970                     // The `+1` accounts for endparamscope having already happened
1971                     // here (locallevel is already one less than when exit_level was
1972                     // captured at c:5890). When the gate fires:
1973                     //   - locallevel > forklevel: still in a nested function — force
1974                     //     the outer frame to return too (retflag=1, breaks=loops).
1975                     //   - locallevel <= forklevel: out of all functions — actually
1976                     //     exit the shell now via zexit(exit_val, ZEXIT_NORMAL).
1977                     // `in_exit_trap` (c:Src/signals.c:63 — `int in_exit_trap;`) is the
1978                     // EXIT-trap reentry counter. dotrap at signals.c:1272/1277 wraps
1979                     // SIGEXIT handler dispatch with ++/--, so an exit issued FROM an
1980                     // EXIT trap shouldn't re-trigger the gate (or the trap would
1981                     // recurse). zshrs's signals::in_exit_trap is the canonical port
1982                     // surface — read it directly here.
1983    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
1984    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
1985    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
1986    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
1987    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
1988    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
1989        // c:6141
1990        if cur_locallevel > cur_forklevel {
1991            // c:6143 — still inside a nested function: keep unwinding.
1992            RETFLAG.store(1, Ordering::Relaxed); // c:6144
1993            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
1994        } else {
1995            // c:6151 — out of all functions: exit for real.
1996            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
1997            let val = EXIT_VAL.load(Ordering::Relaxed);
1998            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
1999            // c:6152
2000        }
2001    }
2002    unqueue_signals(); // c:6202
2003}
2004
2005/// Port of `Emulation_options sticky_emulation_dup(Emulation_options src,
2006/// int useheap)` from `Src/exec.c:5501`.
2007///
2008/// C body (`useheap` selects between heap-arena and permanent zalloc;
2009/// Rust collapses both into owned `Box` clones):
2010/// ```c
2011/// Emulation_options newsticky = useheap ?
2012///     hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
2013/// newsticky->emulation = src->emulation;
2014/// if (src->n_on_opts) {
2015///     size_t sz = src->n_on_opts * sizeof(*src->on_opts);
2016///     newsticky->n_on_opts = src->n_on_opts;
2017///     newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
2018///     memcpy(newsticky->on_opts, src->on_opts, sz);
2019/// }
2020/// if (src->n_off_opts) {
2021///     size_t sz = src->n_off_opts * sizeof(*src->off_opts);
2022///     newsticky->n_off_opts = src->n_off_opts;
2023///     newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
2024///     memcpy(newsticky->off_opts, src->off_opts, sz);
2025/// }
2026/// return newsticky;
2027/// ```
2028///
2029/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
2030/// at function-def time to snapshot the pending `sticky` global so
2031/// the function carries its own immutable copy.
2032pub fn sticky_emulation_dup(src: &emulation_options, _useheap: i32) -> Emulation_options {
2033    // c:5501
2034    // c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
2035    let mut newsticky = Box::new(emulation_options {
2036        emulation: src.emulation, // c:5505
2037        n_on_opts: 0,
2038        n_off_opts: 0,
2039        on_opts: Vec::new(),
2040        off_opts: Vec::new(),
2041    });
2042    // c:5506-5511 — copy on_opts.
2043    if src.n_on_opts != 0 {
2044        // c:5506
2045        newsticky.n_on_opts = src.n_on_opts; // c:5508
2046        newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
2047    }
2048    // c:5512-5517 — copy off_opts.
2049    if src.n_off_opts != 0 {
2050        // c:5512
2051        newsticky.n_off_opts = src.n_off_opts; // c:5514
2052        newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
2053    }
2054    newsticky // c:5519
2055}
2056
2057/// Port of `void shfunc_set_sticky(Shfunc shf)` from `Src/exec.c:5527`.
2058///
2059/// C body:
2060/// ```c
2061/// if (sticky)
2062///     shf->sticky = sticky_emulation_dup(sticky, 0);
2063/// else
2064///     shf->sticky = NULL;
2065/// ```
2066///
2067/// Stamp the function with the current pending sticky-emulation
2068/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
2069pub fn shfunc_set_sticky(shf: &mut shfunc) {
2070    // c:5527
2071    let sticky_guard = sticky.lock().unwrap();
2072    if let Some(ref s) = *sticky_guard {
2073        // c:5529
2074        shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
2075    } else {
2076        // c:5531
2077        shf.sticky = None; // c:5532
2078    }
2079}
2080
2081/// Port of `static char *search_defpath(char *cmd, char *pbuf, int plen)`
2082/// from `Src/exec.c:691`.
2083///
2084/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
2085/// Used by `command -p` to bypass the user's `$PATH` and search the
2086/// system default (`/bin:/usr/bin:...`).
2087pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
2088    // c:691
2089    // c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
2090    for ps in DEFAULT_PATH.split(':') {
2091        // c:695
2092        // c:697 — `if (*ps == '/')`.
2093        if !ps.starts_with('/') {
2094            continue;
2095        }
2096        // c:700-707 — PATH_MAX bounds check on `<dir>` segment.
2097        if ps.len() >= plen {
2098            // c:700 / c:704
2099            continue; // c:701 / c:705
2100        }
2101        // c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
2102        let full_len = ps.len() + 1 + cmd.len();
2103        if full_len >= plen {
2104            // c:709
2105            continue; // c:710
2106        }
2107        let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
2108                                             // c:712 — `if (iscom(pbuf)) return pbuf;`
2109        if iscom(&buf) {
2110            // c:712
2111            return Some(buf); // c:713
2112        }
2113    }
2114    None // c:716
2115}
2116
2117/// Port of `static int checkclobberparam(struct redir *f)` from
2118/// `Src/exec.c:2178`.
2119///
2120/// C body:
2121/// ```c
2122/// struct value vbuf; Value v;
2123/// char *s = f->varid; int fd;
2124/// if (!s) return 1;
2125/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
2126/// if (v->pm->node.flags & PM_READONLY) {
2127///     zwarn("can't allocate file descriptor to readonly parameter %s",
2128///           f->varid);
2129///     errno = 0;
2130///     return 0;
2131/// }
2132/// /* We can't clobber the value in the parameter if it's
2133///  * already an opened file descriptor */
2134/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
2135///     (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
2136///     fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
2137///     zwarn("can't clobber parameter %s containing file descriptor %d",
2138///          f->varid, fd);
2139///     errno = 0;
2140///     return 0;
2141/// }
2142/// return 1;
2143/// ```
2144///
2145/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
2146/// name) is writable and (under NOCLOBBER) doesn't currently hold an
2147/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
2148/// already emitted).
2149///
2150/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
2151/// NOCLOBBER is set and the param's value is the fd-number of an
2152/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
2153/// the existing fd doesn't get clobbered by the upcoming open(2).
2154pub fn checkclobberparam(f: &redir) -> i32 {
2155    // c:2178
2156    // c:2182 — `char *s = f->varid;`
2157    let s = match &f.varid {
2158        Some(v) => v.clone(),
2159        None => return 1, // c:2185-2186 — `if (!s) return 1;`
2160    };
2161    // c:2186 — `if (!(v = getvalue(&vbuf, &s, 0))) return 1;`
2162    let mut vbuf = crate::ported::zsh_h::value {
2163        pm: None,
2164        arr: Vec::new(),
2165        scanflags: 0,
2166        valflags: 0,
2167        start: 0,
2168        end: 0,
2169    };
2170    let mut cursor: &str = s.as_str();
2171    let v_opt = crate::ported::params::getvalue(Some(&mut vbuf), &mut cursor, 0);
2172    if v_opt.is_none() {
2173        return 1; // c:2187
2174    }
2175    // c:2188-2197 — readonly refusal via v->pm->node.flags.
2176    let readonly = vbuf
2177        .pm
2178        .as_ref()
2179        .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
2180        .unwrap_or(false);
2181    if readonly {
2182        // c:2191
2183        zwarn(&format!(
2184            // c:2192
2185            "can't allocate file descriptor to readonly parameter {}",
2186            s
2187        ));
2188        // c:2195 — `errno = 0;` not flagged as a system error.
2189        return 0; // c:2196
2190    }
2191    // c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
2192    // AND the param holds a valid fd that's already in our fdtable as
2193    // FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
2194    // open so we don't clobber it.
2195    if !isset(CLOBBER) {
2196        // c:2201 — `getstrvalue(v)` — read the param's string form.
2197        let val_str = crate::ported::params::getstrvalue(Some(&mut vbuf));
2198        if let Ok(fd) = val_str.trim().parse::<i32>() {
2199            // c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
2200            let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2201            if fd >= 0 && fd <= max_fd {
2202                let kind = fdtable_get(fd);
2203                if kind == FDT_EXTERNAL {
2204                    zwarn(&format!("{}: file descriptor {} already open", s, fd)); // c:2206-2210
2205                    return 0; // c:2211
2206                }
2207            }
2208        }
2209    }
2210    1 // c:2214
2211}
2212
2213/// Port of `static int clobber_open(struct redir *f)` from
2214/// `Src/exec.c:2221`.
2215///
2216/// C body:
2217/// ```c
2218/// struct stat buf;
2219/// int fd, oerrno;
2220/// char *ufname = unmeta(f->name);
2221/// /* If clobbering, just open. */
2222/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
2223///     return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
2224/// /* If not clobbering, attempt to create file exclusively. */
2225/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
2226///     return fd;
2227/// /* If that fails, we are still allowed to open non-regular files. */
2228/// oerrno = errno;
2229/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
2230///     if (!fstat(fd, &buf)) {
2231///         if (!S_ISREG(buf.st_mode)) return fd;
2232///         /* CLOBBER_EMPTY allows re-use of empty regular files. */
2233///         if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
2234///     }
2235///     close(fd);
2236/// }
2237/// errno = oerrno;
2238/// return -1;
2239/// ```
2240///
2241/// Open the redir target for write with the NOCLOBBER rules:
2242/// - CLOBBER set or `>|` form → just open with O_TRUNC
2243/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
2244///   files (FIFOs, devices, sockets) OR empty regular files under
2245///   CLOBBEREMPTY.
2246pub fn clobber_open(f: &redir) -> i32 {
2247    // c:2221
2248    let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
2249    let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
2250        Ok(c) => c,
2251        Err(_) => return -1,
2252    };
2253    // c:2228-2230 — clobber path: just open + truncate.
2254    if isset(CLOBBER) || IS_CLOBBER_REDIR(f.typ) {
2255        // c:2228
2256        // c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
2257        let fd = unsafe {
2258            libc::open(
2259                ufname.as_ptr(),
2260                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
2261                0o666 as libc::c_uint,
2262            )
2263        };
2264        return fd; // c:2230
2265    }
2266    // c:2233-2235 — try O_EXCL create first.
2267    let fd = unsafe {
2268        // c:2233
2269        libc::open(
2270            ufname.as_ptr(),
2271            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
2272            0o666 as libc::c_uint,
2273        )
2274    };
2275    if fd >= 0 {
2276        return fd; // c:2235
2277    }
2278    // c:2240 — `oerrno = errno;` — save for restoration on the recover path.
2279    let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2280    // c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
2281    // OR (CLOBBEREMPTY && size == 0).
2282    let fd = unsafe {
2283        // c:2241
2284        libc::open(
2285            ufname.as_ptr(),
2286            libc::O_WRONLY | libc::O_NOCTTY,
2287            0o666 as libc::c_uint,
2288        )
2289    };
2290    if fd != -1 {
2291        let mut buf: libc::stat = unsafe { std::mem::zeroed() };
2292        if unsafe { libc::fstat(fd, &mut buf) } == 0 {
2293            // c:2242
2294            // c:2243-2244 — non-regular file: accept.
2295            if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
2296                // c:2243
2297                return fd; // c:2244
2298            }
2299            // c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
2300            if isset(CLOBBEREMPTY) && buf.st_size == 0 {
2301                // c:2256
2302                return fd; // c:2257
2303            }
2304        }
2305        unsafe {
2306            libc::close(fd);
2307        } // c:2259
2308    }
2309    // c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
2310    // "file exists" not the noisier "couldn't reopen" trailing errno.
2311    // Per-platform errno setter: __error() on macOS, __errno_location()
2312    // on Linux. Without cfg gating the build breaks on Linux (CI).
2313    #[cfg(target_os = "macos")]
2314    unsafe {
2315        *libc::__error() = oerrno;
2316    }
2317    #[cfg(target_os = "linux")]
2318    unsafe {
2319        *libc::__errno_location() = oerrno;
2320    }
2321    -1 // c:2263
2322}
2323
2324/// Port of `char *findcmd(char *arg0, int docopy, int default_path)`
2325/// from `Src/exec.c:897`. Walk `$PATH` (or DEFAULT_PATH under
2326/// `default_path=1`) for `arg0`, returning the matching path on
2327/// success. `_docopy` is the C source's "duplicate the result"
2328/// flag — Rust ownership covers it without an explicit copy step.
2329/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
2330/// `command -p`).
2331pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
2332    // c:897
2333    // c:903-908 — if (default_path) → search_defpath; return.
2334    if default_path != 0 {
2335        return search_defpath(arg0, libc::PATH_MAX as usize);
2336    }
2337    // c:912-913 — strlen(arg0) > PATH_MAX → NULL.
2338    if arg0.len() > libc::PATH_MAX as usize {
2339        return None;
2340    }
2341    // c:Src/exec.c:914-920 — `/`-bearing arg path resolution.
2342    //   if ((s = strchr(arg0, '/'))) {
2343    //       RET_IF_COM(arg0);   // ← unconditional accept on iscom hit
2344    //       if (arg0 == s || unset(PATHDIRS) ||
2345    //           !strncmp(arg0, "./", 2) ||
2346    //           !strncmp(arg0, "../", 3))
2347    //           return NULL;
2348    //   }
2349    // The Rust port had the iscom check gated on `starts_with('/')`,
2350    // so `type ./target/debug/zshrs` returned None even when the
2351    // file was executable. Bug #496 family.
2352    if arg0.contains('/') {
2353        if iscom(arg0) {
2354            return Some(arg0.to_string()); // c:915 RET_IF_COM
2355        }
2356        // c:916-919 — absolute OR PATHDIRS-off OR `./` / `../` →
2357        // give up here (no $PATH walk for these). Relative without
2358        // those prefixes falls through to the $PATH scan below for
2359        // the PATHDIRS=set case.
2360        if arg0.starts_with('/')
2361            || !isset(PATHDIRS)
2362            || arg0.starts_with("./")
2363            || arg0.starts_with("../")
2364        {
2365            return None;
2366        }
2367        // else fall through to PATH walk.
2368    }
2369    // c:943-951 — walk `path[]` (the shell `$path` array). Read $PATH
2370    // from paramtab so shell-private edits via `path=(...)` take
2371    // effect (not OS env only).
2372    let path = getsparam("PATH")?;
2373    for dir in path.split(':') {
2374        if dir.is_empty() {
2375            continue;
2376        }
2377        let candidate = format!("{}/{}", dir, arg0);
2378        if iscom(&candidate) {
2379            return Some(candidate);
2380        }
2381    }
2382    None // c:952
2383}
2384
2385/// Port of `static void addfd(int forked, int *save, struct multio **mfds,
2386///                             int fd1, int fd2, int rflag, char *varid)`
2387/// from `Src/exec.c:2397`.
2388///
2389/// C body (~100 lines, three branches):
2390/// ```c
2391/// if (varid) {
2392///     /* {varid}>file form — move fd above 10 and bind $varid to it */
2393/// } else if (!mfds[fd1] || unset(MULTIOS)) {
2394///     /* new multio OR MULTIOS off — first redir on this fd */
2395/// } else {
2396///     /* additional redir on a fd that's already a multio (split or extend) */
2397/// }
2398/// ```
2399///
2400/// Register `fd2` (already-open) as a redirection target for `fd1`.
2401/// Three branches: `varid` writes the moved fd to `$varid` and bumps
2402/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
2403/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
2404/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
2405/// via `mpipe`, or appends another fd to an already-split stream
2406/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
2407///
2408/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
2409/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
2410/// longer a hard cap (still 8 for the initial allocation, grown
2411/// on demand thereafter).
2412///
2413/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
2414/// stores the int value but doesn't expose a bitwise-OR setter; we
2415/// re-read + OR + re-store as two atomic-feeling steps.
2416pub fn addfd(
2417    forked: i32,
2418    save: &mut [i32; 10],
2419    mfds: &mut [Option<Box<multio>>; 10],
2420    fd1: i32,
2421    fd2: i32,
2422    rflag: i32,
2423    varid: Option<&str>,
2424) {
2425    // c:2397
2426    let mut pipes: [i32; 2] = [-1; 2]; // c:2400
2427
2428    // c:2402-2417 — `if (varid)` branch — {varid}>file shape.
2429    if let Some(vid) = varid {
2430        // c:2402
2431        let fd_moved = movefd(fd2); // c:2404
2432        if fd_moved == -1 {
2433            // c:2405
2434            zerr(&format!(
2435                // c:2406
2436                "cannot move fd {}: {}",
2437                fd2,
2438                std::io::Error::last_os_error()
2439            ));
2440            return; // c:2407
2441        }
2442        // c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
2443        fdtable_set(fd_moved, FDT_EXTERNAL);
2444        // c:2410 — `setiparam(varid, (zlong)fd1);`
2445        setiparam(vid, fd_moved as i64);
2446        // c:2415-2416 — `if (errflag) zclose(fd1);`
2447        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
2448            // c:2415
2449            let _ = zclose(fd_moved); // c:2416
2450        }
2451        return;
2452    }
2453    // c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
2454    let fd1u = fd1 as usize;
2455    if fd1u >= mfds.len() {
2456        return;
2457    }
2458    if mfds[fd1u].is_none() || unset(MULTIOS) {
2459        // c:2418
2460        if mfds[fd1u].is_none() {
2461            // c:2419 — `starting a new multio`
2462            // c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
2463            mfds[fd1u] = Some(Box::new(multio {
2464                ct: 0,
2465                rflag: 0,
2466                pipe: -1,
2467                // c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
2468                // grow on demand via push() below. Pre-fill MULTIOUNIT
2469                // slots with -1 so existing indexed writes (fds[0], fds[1])
2470                // still work without explicit resize().
2471                fds: vec![-1; MULTIOUNIT],
2472            }));
2473            // c:2421 — `if (!forked && save[fd1] == -2)`
2474            if forked == 0 && save[fd1u] == -2 {
2475                if fd1 == fd2 {
2476                    // c:2422
2477                    save[fd1u] = -1; // c:2423
2478                } else {
2479                    // c:2424
2480                    let fd_n = movefd(fd1); // c:2425
2481                    if fd_n < 0 {
2482                        // c:2430
2483                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2484                        if e != libc::EBADF {
2485                            // c:2431
2486                            zerr(&format!(
2487                                // c:2432
2488                                "cannot duplicate fd {}: {}",
2489                                fd1,
2490                                std::io::Error::from_raw_os_error(e)
2491                            ));
2492                            mfds[fd1u] = None; // c:2433
2493                            closemnodes(mfds); // c:2434
2494                            return; // c:2435
2495                        }
2496                    } else {
2497                        // c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
2498                        crate::DPUTS!(
2499                            fdtable_get(fd_n) != FDT_INTERNAL,
2500                            "Saved file descriptor not marked as internal"
2501                        );
2502                        // c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
2503                        let cur = fdtable_get(fd_n);
2504                        fdtable_set(fd_n, cur | FDT_SAVED_MASK);
2505                    }
2506                    save[fd1u] = fd_n; // c:2442
2507                }
2508            }
2509        }
2510        // c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
2511        // handled above; this is the non-varid branch.)
2512        let _ = redup(fd2, fd1);
2513        // c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
2514        if let Some(mn) = mfds[fd1u].as_mut() {
2515            mn.ct = 1; // c:2448
2516            mn.fds[0] = fd1; // c:2449
2517            mn.rflag = rflag; // c:2450
2518        }
2519    } else {
2520        // c:2451 — extend existing multio.
2521        // c:2452-2456 — rflag mismatch check.
2522        let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
2523        if cur_rflag != rflag {
2524            // c:2452
2525            zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
2526            closemnodes(mfds); // c:2454
2527            return; // c:2455
2528        }
2529        let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
2530        if cur_ct == 1 {
2531            // c:2457 — split the stream.
2532            // c:2458 — `int fdN = movefd(fd1);`
2533            let fd_n = movefd(fd1);
2534            if fd_n < 0 {
2535                // c:2459
2536                zerr(&format!(
2537                    // c:2460
2538                    "multio failed for fd {}: {}",
2539                    fd1,
2540                    std::io::Error::last_os_error()
2541                ));
2542                closemnodes(mfds); // c:2461
2543                return; // c:2462
2544            }
2545            if let Some(mn) = mfds[fd1u].as_mut() {
2546                mn.fds[0] = fd_n; // c:2464
2547            }
2548            // c:2465 — `fdN = movefd(fd2);`
2549            let fd_n2 = movefd(fd2);
2550            if fd_n2 < 0 {
2551                // c:2466
2552                zerr(&format!(
2553                    // c:2467
2554                    "multio failed for fd {}: {}",
2555                    fd2,
2556                    std::io::Error::last_os_error()
2557                ));
2558                closemnodes(mfds); // c:2468
2559                return; // c:2469
2560            }
2561            if let Some(mn) = mfds[fd1u].as_mut() {
2562                mn.fds[1] = fd_n2; // c:2471
2563            }
2564            // c:2472 — `mpipe(pipes)`
2565            if mpipe(&mut pipes) < 0 {
2566                // c:2472
2567                zerr(&format!(
2568                    // c:2473
2569                    "multio failed for fd {}: {}",
2570                    fd2,
2571                    std::io::Error::last_os_error()
2572                ));
2573                closemnodes(mfds); // c:2474
2574                return; // c:2475
2575            }
2576            // c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
2577            if let Some(mn) = mfds[fd1u].as_mut() {
2578                mn.pipe = pipes[(1 - rflag) as usize];
2579            }
2580            // c:2478 — `redup(pipes[rflag], fd1);`
2581            let _ = redup(pipes[rflag as usize], fd1);
2582            // c:2479 — `mfds[fd1]->ct = 2;`
2583            if let Some(mn) = mfds[fd1u].as_mut() {
2584                mn.ct = 2;
2585            }
2586        } else {
2587            // c:2480 — extend already-split stream.
2588            // c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
2589            //                              sizeof + ct*sizeof(int));`
2590            // Rust's `Vec<i32>` grows on demand; ensure capacity for the
2591            // new slot before the indexed write below.
2592            if let Some(mn) = mfds[fd1u].as_mut() {
2593                while mn.fds.len() <= cur_ct as usize {
2594                    mn.fds.push(-1);
2595                }
2596            }
2597            // c:2487 — `if ((fdN = movefd(fd2)) < 0)`
2598            let fd_n = movefd(fd2);
2599            if fd_n < 0 {
2600                zerr(&format!(
2601                    // c:2488
2602                    "multio failed for fd {}: {}",
2603                    fd2,
2604                    std::io::Error::last_os_error()
2605                ));
2606                closemnodes(mfds); // c:2489
2607                return; // c:2490
2608            }
2609            // c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
2610            if let Some(mn) = mfds[fd1u].as_mut() {
2611                let slot = mn.ct as usize;
2612                if slot < mn.fds.len() {
2613                    mn.fds[slot] = fd_n;
2614                    mn.ct += 1;
2615                }
2616            }
2617        }
2618    }
2619}
2620
2621/// Port of `static void closemn(struct multio **mfds, int fd, int type)`
2622/// from `Src/exec.c:2273`.
2623///
2624/// C body (abridged — the meat is the fork-into-tee-or-cat child):
2625/// ```c
2626/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
2627///     struct multio *mn = mfds[fd];
2628///     char buf[TCBUFSIZE]; int len, i;
2629///     pid_t pid; struct timespec bgtime;
2630///     child_block();
2631///     if ((pid = zfork(&bgtime))) {
2632///         for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
2633///         zclose(mn->pipe);
2634///         if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
2635///         mn->ct = 1; mn->fds[0] = fd;
2636///         addproc(pid, NULL, 1, &bgtime, -1, -1);
2637///         child_unblock(); return;
2638///     }
2639///     /* pid == 0 (child) */
2640///     opts[INTERACTIVE] = 0;
2641///     dont_queue_signals();
2642///     child_unblock();
2643///     closeallelse(mn);
2644///     if (mn->rflag) {
2645///         /* tee process: read mn->pipe, write each mn->fds[i] */
2646///     } else {
2647///         /* cat process: read each mn->fds[i], write mn->pipe */
2648///     }
2649///     _exit(0);
2650/// } else if (fd >= 0 && type == REDIR_CLOSE)
2651///     mfds[fd] = NULL;
2652/// ```
2653///
2654/// Success-path close of a multio. For ct>=2 (multiple-output
2655/// redirection), forks a tee/cat child that proxies bytes between
2656/// the original fd and the per-output fds. Single-output multios
2657/// (ct=1) skip the fork entirely and just clear the slot.
2658///
2659/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
2660/// tee/cat child in the current job's auxprocs.
2661pub fn closemn(mfds: &mut [Option<Box<multio>>; 10], fd: i32, type_: i32) {
2662    // c:2273
2663    // c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
2664    let needs_tee = fd >= 0
2665        && (fd as usize) < mfds.len()
2666        && mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
2667    if needs_tee {
2668        // c:2275
2669        // Take the multio out of the slot so we can move pieces into
2670        // the child without aliasing the slot.
2671        let mn = mfds[fd as usize].take().unwrap();
2672        let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
2673                                   // c:2287 — `child_block();` block SIGCHLD before fork race.
2674        child_block();
2675        // c:2288 — `pid = zfork(&bgtime);`
2676        let mut bgtime = ZshTimespec {
2677            tv_sec: 0,
2678            tv_nsec: 0,
2679        };
2680        let pid = zfork(Some(&mut bgtime));
2681        if pid != 0 {
2682            // c:2288 parent branch
2683            // c:2289-2290 — close all per-output fds.
2684            for i in 0..mn.ct as usize {
2685                if i < mn.fds.len() {
2686                    let _ = zclose(mn.fds[i]); // c:2290
2687                }
2688            }
2689            let _ = zclose(mn.pipe); // c:2291
2690            if pid == -1 {
2691                // c:2292
2692                // c:2293 — `mfds[fd] = NULL;` already done via .take()
2693                child_unblock(); // c:2294
2694                return; // c:2295
2695            }
2696            // c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
2697            let mut mn_back = mn;
2698            mn_back.ct = 1; // c:2297
2699            mn_back.fds[0] = fd; // c:2298
2700            mfds[fd as usize] = Some(mn_back);
2701            // c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
2702            // the tee/cat child in the current job's auxprocs (aux=true).
2703            if let Some(jt) = JOBTAB.get() {
2704                let mut guard = jt.lock().unwrap();
2705                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
2706                if tj >= 0 {
2707                    if let Some(j) = guard.get_mut(tj as usize) {
2708                        crate::ported::jobs::addproc(
2709                            j,
2710                            pid,
2711                            "",
2712                            true,
2713                            Some(std::time::Instant::now()),
2714                            -1,
2715                            -1,
2716                        );
2717                    }
2718                }
2719            }
2720            let _ = bgtime;
2721            child_unblock(); // c:2300
2722            return; // c:2301
2723        }
2724        // c:2303 — child branch (pid == 0).
2725        opt_state_set("interactive", false); // c:2304
2726        dont_queue_signals(); // c:2305
2727        child_unblock(); // c:2306
2728        closeallelse(&mn); // c:2307
2729                           // c:2308-2333 — tee or cat loop.
2730        if mn.rflag != 0 {
2731            // c:2308 — `mn->rflag` set → tee process
2732            // c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
2733            loop {
2734                let len = unsafe {
2735                    libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2736                };
2737                if len == 0 {
2738                    break;
2739                }
2740                if len < 0 {
2741                    // c:2311
2742                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2743                    if e == libc::EINTR {
2744                        // c:2312
2745                        continue;
2746                    } else {
2747                        break; // c:2315
2748                    }
2749                }
2750                // c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
2751                for i in 0..mn.ct as usize {
2752                    if i >= mn.fds.len() {
2753                        break;
2754                    }
2755                    if write_loop(mn.fds[i], &buf[..len as usize]).is_err() {
2756                        break; // c:2319
2757                    }
2758                }
2759            }
2760        } else {
2761            // c:2321 — cat process
2762            for i in 0..mn.ct as usize {
2763                if i >= mn.fds.len() {
2764                    break;
2765                }
2766                // c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
2767                loop {
2768                    let len = unsafe {
2769                        libc::read(mn.fds[i], buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2770                    };
2771                    if len == 0 {
2772                        break;
2773                    }
2774                    if len < 0 {
2775                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2776                        // c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
2777                        if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
2778                            continue;
2779                        } else {
2780                            break; // c:2329
2781                        }
2782                    }
2783                    // c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
2784                    if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
2785                        break; // c:2332
2786                    }
2787                }
2788            }
2789        }
2790        // c:2335 — `_exit(0);`
2791        unsafe {
2792            libc::_exit(0);
2793        }
2794    } else if fd >= 0 && type_ == REDIR_CLOSE {
2795        // c:2336
2796        // c:2337 — `mfds[fd] = NULL;`
2797        if (fd as usize) < mfds.len() {
2798            mfds[fd as usize] = None;
2799        }
2800    }
2801}
2802
2803/// Port of `static void closemnodes(struct multio **mfds)` from
2804/// `Src/exec.c:2344`.
2805///
2806/// C body:
2807/// ```c
2808/// int i, j;
2809/// for (i = 0; i < 10; i++)
2810///     if (mfds[i]) {
2811///         for (j = 0; j < mfds[i]->ct; j++)
2812///             zclose(mfds[i]->fds[j]);
2813///         mfds[i] = NULL;
2814///     }
2815/// ```
2816///
2817/// Failure-path cleanup: close every fd stashed in any of the 10
2818/// multio slots and null the slot. Called from `execcmd_exec` when
2819/// a redirect setup fails partway through and we need to roll back.
2820pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
2821    // c:2344
2822    for i in 0..10 {
2823        // c:2348
2824        if let Some(mn) = mfds[i].take() {
2825            // c:2349
2826            for j in 0..mn.ct as usize {
2827                // c:2350
2828                if j < mn.fds.len() {
2829                    let _ = zclose(mn.fds[j]); // c:2351
2830                }
2831            }
2832            // c:2352 — `mfds[i] = NULL;` — handled by .take() above.
2833        }
2834    }
2835}
2836
2837/// Port of `static void closeallelse(struct multio *mn)` from
2838/// `Src/exec.c:2358`.
2839///
2840/// C body:
2841/// ```c
2842/// int i, j;
2843/// long openmax;
2844/// openmax = fdtable_size;
2845/// for (i = 0; i < openmax; i++)
2846///     if (mn->pipe != i) {
2847///         for (j = 0; j < mn->ct; j++)
2848///             if (mn->fds[j] == i) break;
2849///         if (j == mn->ct)
2850///             zclose(i);
2851///     }
2852/// ```
2853///
2854/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
2855/// stashed in `mn->fds`. Called inside the multio tee/cat child
2856/// process to release every fd the parent had open — only the pipe
2857/// + per-output fds stay alive for the read/write loop.
2858pub fn closeallelse(mn: &multio) {
2859    // c:2358
2860    // c:2363 — `openmax = fdtable_size;`. zshrs models fdtable as a
2861    // Vec; use MAX_ZSH_FD as the upper bound (fdtable_size grows past
2862    // max_zsh_fd in C but every slot past it is FDT_UNUSED anyway).
2863    let openmax = MAX_ZSH_FD.load(Ordering::Relaxed) + 1; // c:2363
2864    for i in 0..openmax {
2865        // c:2365
2866        if mn.pipe == i {
2867            // c:2366
2868            continue;
2869        }
2870        // c:2367-2369 — scan mn->fds[] for i; skip-close if found.
2871        let mut found = false;
2872        for j in 0..mn.ct as usize {
2873            // c:2367
2874            if j < mn.fds.len() && mn.fds[j] == i {
2875                // c:2368
2876                found = true;
2877                break; // c:2369
2878            }
2879        }
2880        // c:2370-2371 — `if (j == mn->ct) zclose(i);`
2881        if !found {
2882            let _ = zclose(i); // c:2371
2883        }
2884    }
2885}
2886
2887/// Port of `static void fixfds(int *save)` from `Src/exec.c:4523`.
2888///
2889/// C body:
2890/// ```c
2891/// int old_errno = errno;
2892/// int i;
2893/// for (i = 0; i != 10; i++)
2894///     if (save[i] != -2)
2895///         redup(save[i], i);
2896/// errno = old_errno;
2897/// ```
2898///
2899/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
2900/// means "no save was made for this fd"; any other value is the
2901/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
2902/// caller's errno across the loop so a downstream caller diagnoses
2903/// the original failure, not a noisy dup2 errno.
2904pub fn fixfds(save: &[i32; 10]) {
2905    // c:4523
2906    let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
2907    for i in 0..10i32 {
2908        // c:4528 — `for (i = 0; i != 10; i++)`
2909        if save[i as usize] != -2 {
2910            // c:4529
2911            redup(save[i as usize], i); // c:4530
2912        }
2913    }
2914    // c:4531 — `errno = old_errno;`
2915    #[cfg(target_os = "macos")]
2916    unsafe {
2917        *libc::__error() = old_errno;
2918    }
2919    #[cfg(target_os = "linux")]
2920    unsafe {
2921        *libc::__errno_location() = old_errno;
2922    }
2923}
2924
2925/// Port of `mod_export void closem(int how, int all)` from `Src/exec.c:4546`.
2926///
2927/// C body:
2928/// ```c
2929/// int i;
2930/// for (i = 10; i <= max_zsh_fd; i++)
2931///     if (fdtable[i] != FDT_UNUSED &&
2932///         (all || (fdtable[i] != FDT_PROC_SUBST &&
2933///                  fdtable[i] != FDT_EXTERNAL)) &&
2934///         (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
2935///         if (i == SHTTY) SHTTY = -1;
2936///         zclose(i);
2937///     }
2938/// ```
2939///
2940/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
2941/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
2942/// type filter); otherwise only fds whose low-nibble type equals
2943/// `how` are closed. `all == 0` preserves user-visible fds
2944/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
2945/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
2946/// reference if we just closed the controlling tty.
2947pub fn closem(how: i32, all: i32) {
2948    // c:4546
2949    let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
2950    for i in 10i32..=max {
2951        // c:4550
2952        let kind = fdtable_get(i); // c:4551 fdtable[i]
2953        if kind == FDT_UNUSED {
2954            // c:4551
2955            continue;
2956        }
2957        // c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
2958        if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
2959            continue;
2960        }
2961        // c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
2962        if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
2963            continue;
2964        }
2965        // c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
2966        if i == SHTTY.load(Ordering::Relaxed) {
2967            // c:4560
2968            SHTTY.store(-1, Ordering::Relaxed); // c:4561
2969        }
2970        // c:4562 — `zclose(i);`
2971        let _ = zclose(i);
2972    }
2973}
2974
2975/// Port of `Cmdnam hashcmd(char *arg0, char **pp)` from
2976/// `Src/exec.c:1010`.
2977///
2978/// C body:
2979/// ```c
2980/// Cmdnam cn;
2981/// char *s, buf[PATH_MAX+1];
2982/// char **pq;
2983/// if (*arg0 == '/') return NULL;
2984/// for (; *pp; pp++)
2985///     if (**pp == '/') {
2986///         s = buf;
2987///         struncpy(&s, *pp, PATH_MAX);
2988///         *s++ = '/';
2989///         if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
2990///         strcpy(s, arg0);
2991///         if (iscom(buf)) break;
2992///     }
2993/// if (!*pp) return NULL;
2994/// cn = (Cmdnam) zshcalloc(sizeof *cn);
2995/// cn->node.flags = 0;
2996/// cn->u.name = pp;
2997/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
2998/// if (isset(HASHDIRS)) {
2999///     for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
3000///     pathchecked = pp + 1;
3001/// }
3002/// return cn;
3003/// ```
3004///
3005/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
3006/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
3007/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
3008/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
3009/// so subsequent commands hit the cache.
3010///
3011/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
3012/// `None` if `arg0` is absolute or no PATH entry contains it.
3013pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
3014    // c:1010
3015    // c:1016 — `if (*arg0 == '/') return NULL;`
3016    if arg0.starts_with('/') {
3017        return None; // c:1017
3018    }
3019    // c:1018-1028 — walk pp[] for first matching absolute entry.
3020    let mut found_idx: Option<usize> = None;
3021    for (i, dir) in pp.iter().enumerate() {
3022        // c:1018
3023        if !dir.starts_with('/') {
3024            // c:1019
3025            continue;
3026        }
3027        // c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
3028        if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
3029            // c:1023
3030            continue; // c:1024
3031        }
3032        let buf = format!("{}/{}", dir, arg0); // c:1025
3033        if iscom(&buf) {
3034            // c:1026
3035            found_idx = Some(i);
3036            break; // c:1027
3037        }
3038    }
3039    // c:1030-1031 — `if (!*pp) return NULL;`
3040    let pp_idx = match found_idx {
3041        Some(i) => i,
3042        None => return None, // c:1031
3043    };
3044    // c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
3045    let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
3046    let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
3047                                                // c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
3048    if let Ok(mut tab) = cmdnamtab_lock().write() {
3049        tab.add(cn.clone());
3050    }
3051    // c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
3052    // including the matching one, then bump pathchecked past it.
3053    if isset(HASHDIRS) {
3054        // c:1038
3055        let start = pathchecked.load(Ordering::Relaxed); // c:1039
3056        for pq in start..=pp_idx {
3057            // c:1039
3058            if pq < pp.len() {
3059                hashdir(&pp[pq], pq); // c:1040
3060            }
3061        }
3062        pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
3063    }
3064    Some(cn) // c:1044
3065}
3066
3067/// Port of `static pid_t zfork(struct timespec *ts)` from
3068/// `Src/exec.c:349`.
3069///
3070/// C body:
3071/// ```c
3072/// pid_t pid;
3073/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
3074///     zerr("job table full");
3075///     return -1;
3076/// }
3077/// if (ts) zgettime_monotonic_if_available(ts);
3078/// queue_signals();
3079/// pid = fork();
3080/// unqueue_signals();
3081/// if (pid == -1) {
3082///     zerr("fork failed: %e", errno);
3083///     return -1;
3084/// }
3085/// #ifdef HAVE_GETRLIMIT
3086/// if (!pid) setlimits(NULL);
3087/// #endif
3088/// return pid;
3089/// ```
3090///
3091/// fork(2) wrapper with jobtab capacity check + child rlimit
3092/// re-application. Used by every subshell-spawning path: pipelines,
3093/// process substitution, async commands, command substitution.
3094pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
3095    // c:349
3096    let pid: libc::pid_t;
3097
3098    // c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
3099    let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
3100    let thisjob = *thisjob_lock.lock().unwrap();
3101    if thisjob != -1 {
3102        // c:356
3103        let needed = (thisjob + 1) as usize;
3104        let needs_expand = JOBTAB
3105            .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3106            .lock()
3107            .map(|t| needed >= t.len().saturating_sub(1))
3108            .unwrap_or(false);
3109        if needs_expand {
3110            let mut tab = JOBTAB.get().unwrap().lock().unwrap();
3111            if !expandjobtab(&mut tab, needed) {
3112                // c:357
3113                zerr("job table full"); // c:357
3114                return -1; // c:358
3115            }
3116        }
3117    }
3118    // c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
3119    if let Some(ts) = ts {
3120        zgettime_monotonic_if_available(ts);
3121    }
3122    // c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
3123    queue_signals(); // c:368
3124    pid = unsafe { libc::fork() }; // c:369
3125    unqueue_signals(); // c:370
3126                       // c:371-374 — fork failure.
3127    if pid == -1 {
3128        // c:371
3129        zerr(&format!(
3130            // c:372
3131            "fork failed: {}",
3132            std::io::Error::last_os_error()
3133        ));
3134        return -1; // c:373
3135    }
3136    // c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
3137    #[cfg(unix)]
3138    if pid == 0 {
3139        // c:376
3140        let _ = setlimits(""); // c:378
3141    }
3142    pid // c:380
3143}
3144
3145/// Port of `void loadautofnsetfile(Shfunc shf, char *fdir)` from
3146/// `Src/exec.c:5657`.
3147///
3148/// C body:
3149/// ```c
3150/// if (!(shf->node.flags & PM_LOADDIR) ||
3151///     strcmp(shf->filename, fdir) != 0) {
3152///     dircache_set(&shf->filename, NULL);
3153///     if (fdir) {
3154///         shf->node.flags |= PM_LOADDIR;
3155///         dircache_set(&shf->filename, fdir);
3156///     } else {
3157///         shf->node.flags &= ~PM_LOADDIR;
3158///         shf->filename = ztrdup(shf->node.nam);
3159///     }
3160/// }
3161/// ```
3162///
3163/// Update `shf->filename` to the autoload directory `fdir`. Routes
3164/// through the refcounted `dircache_set` so identical directory
3165/// strings are shared across shfunc table entries.
3166pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
3167    // c:5657
3168    // c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
3169    let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
3170    let same = match (&shf.filename, fdir) {
3171        (Some(a), Some(b)) => a == b,
3172        _ => false,
3173    };
3174    if !loaddir || !same {
3175        // c:5664
3176        // c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
3177        dircache_set(&mut shf.filename, None);
3178        if let Some(fdir) = fdir {
3179            // c:5668
3180            shf.node.flags |= PM_LOADDIR as i32; // c:5670
3181            dircache_set(&mut shf.filename, Some(fdir)); // c:5671
3182        } else {
3183            // c:5672
3184            shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
3185            shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
3186        }
3187    }
3188}
3189
3190/// Port of `int commandnotfound(char *arg0, LinkList args)` from
3191/// `Src/exec.c:669`.
3192///
3193/// C body:
3194/// ```c
3195/// Shfunc shf = (Shfunc)
3196///     shfunctab->getnode(shfunctab, "command_not_found_handler");
3197/// if (!shf) {
3198///     lastval = 127;
3199///     return 1;
3200/// }
3201/// pushnode(args, arg0);
3202/// lastval = doshfunc(shf, args, 1);
3203/// return 0;
3204/// ```
3205///
3206/// Look up the user-defined `command_not_found_handler` shfunc and
3207/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
3208/// 1 if no handler (so caller emits the standard "command not found"
3209/// error). Sets `$?` to 127 in the no-handler path.
3210pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
3211    // c:669
3212    // c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
3213    let has_handler = shfunctab_lock()
3214        .read()
3215        .map(|t| t.get("command_not_found_handler").is_some())
3216        .unwrap_or(false);
3217    if !has_handler {
3218        // c:674
3219        LASTVAL.store(127, Ordering::Relaxed); // c:675
3220        return 1; // c:676
3221    }
3222    // c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
3223    // is the first positional arg per C convention).
3224    args.insert(0, arg0.to_string());
3225    args.insert(0, "command_not_found_handler".to_string());
3226    // c:680 — `lastval = doshfunc(shf, args, 1);`. Direct doshfunc
3227    // call mirrors C — body_runner routes through the host body-only
3228    // entry so the function body runs once inside doshfunc's scope.
3229    let shf_clone: Option<shfunc> = shfunctab_lock()
3230        .read()
3231        .ok()
3232        .and_then(|t| t.get("command_not_found_handler").cloned());
3233    if let Some(mut shf) = shf_clone {
3234        let body_args = args.clone();
3235        let body_runner = move || -> i32 {
3236            crate::ported::exec::run_function_body(
3237                "command_not_found_handler",
3238                &body_args[1..],
3239            )
3240            .unwrap_or(0)
3241        };
3242        let lv = doshfunc(&mut shf, args.clone(), true, body_runner);
3243        LASTVAL.store(lv, Ordering::Relaxed);
3244    }
3245    0 // c:681
3246}
3247
3248/// Port of `char *namedpipe(void)` from `Src/exec.c:5001`.
3249///
3250/// C body (#ifdef HAVE_FIFOS branch):
3251/// ```c
3252/// char *tnam = gettempname(NULL, 1);
3253/// if (!tnam) {
3254///     zerr("failed to create named pipe: %e", errno);
3255///     return NULL;
3256/// }
3257/// if (mkfifo(tnam, 0600) < 0) {
3258///     zerr("failed to create named pipe: %s, %e", tnam, errno);
3259///     return NULL;
3260/// }
3261/// return tnam;
3262/// ```
3263///
3264/// Create a FIFO with a unique name for process substitution. Used by
3265/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
3266pub fn namedpipe() -> Option<String> {
3267    // c:5001
3268    let tnam = gettempname(None, true); // c:5003
3269    let tnam = match tnam {
3270        Some(t) => t,
3271        None => {
3272            // c:5005
3273            zerr(&format!(
3274                // c:5006
3275                "failed to create named pipe: {}",
3276                std::io::Error::last_os_error()
3277            ));
3278            return None; // c:5007
3279        }
3280    };
3281    // c:5010 — `mkfifo(tnam, 0600)`.
3282    let cstr = match std::ffi::CString::new(tnam.as_str()) {
3283        Ok(c) => c,
3284        Err(_) => return None,
3285    };
3286    if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
3287        // c:5010
3288        zerr(&format!(
3289            // c:5014
3290            "failed to create named pipe: {}, {}",
3291            tnam,
3292            std::io::Error::last_os_error()
3293        ));
3294        return None; // c:5015
3295    }
3296    Some(tnam) // c:5017
3297}
3298
3299/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
3300///
3301/// C body:
3302/// ```c
3303/// char *str;
3304/// Eprog prog;
3305/// for (str = cmd + 2; *str && *str != Outpar; str++);
3306/// if (!*str || cmd[1] != Inpar) {
3307///     char *errstr = dupstrpfx(cmd, 2);
3308///     untokenize(errstr);
3309///     zerr("unterminated `%s...)'", errstr);
3310///     return NULL;
3311/// }
3312/// *str = '\0';
3313/// if (eptr) *eptr = str+1;
3314/// if (!(prog = parse_string(cmd + 2, 0))) {
3315///     zerr("parse error in process substitution");
3316///     return NULL;
3317/// }
3318/// return prog;
3319/// ```
3320///
3321/// Port of `static LinkList readoutput(int in, int qt, int *readerror)`
3322/// from `Src/exec.c:4805`. Drain a command-substitution pipe fd and
3323/// return the captured output split per `qt`.
3324///
3325/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
3326/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
3327/// per c:4861).
3328/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
3329/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
3330///
3331/// `readerror` is set to the errno on read failure, 0 on clean EOF.
3332pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
3333    // c:4805
3334    let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
3335    let mut readret: isize = 0; // c:4818 readret tracks last read return
3336                                // c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
3337                                // dance keeps SIGCHLD live so the foreground process can be reaped
3338                                // while we drain. zshrs's in-process command-sub runs without the
3339                                // queue (no fork), but the C call surface is preserved for parity.
3340    dont_queue_signals(); // c:4824
3341    child_unblock(); // c:4825
3342    let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
3343    loop {
3344        // c:4826
3345        // c:4828 — `readret = read(in, inbuf, 64);`
3346        let r = unsafe { libc::read(in_fd, inbuf.as_mut_ptr() as *mut libc::c_void, inbuf.len()) };
3347        readret = r as isize;
3348        if readret <= 0 {
3349            // c:4829
3350            if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
3351                // c:4830 — `if (readret < 0 && errno == EINTR) continue;`
3352                continue;
3353            }
3354            break; // c:4832
3355        }
3356        // c:4835 — `for (bufptr = inbuf; bufptr < inbuf + readret; bufptr++)`
3357        for i in 0..(readret as usize) {
3358            let c = inbuf[i];
3359            if crate::ported::ztype_h::imeta(c) {
3360                // c:4837 — `if (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; }`
3361                buf.push(Meta as u8); // c:4838
3362                buf.push(c ^ 32); // c:4839 (Meta-encoded payload)
3363            } else {
3364                buf.push(c); // c:4848 *ptr++ = c
3365            }
3366        }
3367    }
3368    child_block(); // c:4854
3369                   // c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
3370    *readerror = if readret < 0 {
3371        std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
3372    } else {
3373        0
3374    };
3375    // c:4857 — `close(in);`
3376    unsafe {
3377        libc::close(in_fd);
3378    }
3379    // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
3380    while buf.last() == Some(&b'\n') {
3381        buf.pop();
3382    }
3383    // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
3384    let s = String::from_utf8_lossy(&buf).into_owned();
3385    if qt != 0 {
3386        // c:4861
3387        if buf.is_empty() {
3388            return vec![String::from(Nularg)]; // c:4862
3389        }
3390        return vec![s]; // c:4864
3391    }
3392    // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
3393    let mut words = crate::ported::utils::spacesplit(&s, false); // c:4867
3394    if isset(crate::ported::zsh_h::GLOBSUBST) {
3395        // c:4870
3396        for w in words.iter_mut() {
3397            crate::ported::glob::shtokenize(w); // c:4870
3398        }
3399    }
3400    words
3401}
3402
3403/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
3404/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
3405/// remainder is the command up to the matching `Outpar`. Returns the
3406/// parsed Eprog (and writes the post-`)` cursor through `eptr`).
3407pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
3408    // c:4878
3409    let bytes = cmd.as_bytes();
3410    // c:4883 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
3411    if bytes.len() < 2 {
3412        return None;
3413    }
3414    let mut str_idx: usize = 2;
3415    while str_idx < bytes.len() && (bytes[str_idx] as char) != Outpar {
3416        str_idx += 1;
3417    }
3418    // c:4884 — `if (!*str || cmd[1] != Inpar)`.
3419    if str_idx >= bytes.len() || (bytes[1] as char) != Inpar {
3420        // c:4884
3421        let errstr = if bytes.len() >= 2 {
3422            untokenize(&cmd[..2]) // c:4891-4892
3423        } else {
3424            String::new()
3425        };
3426        zerr(&format!("unterminated `{}...)'", errstr)); // c:4893
3427        return None; // c:4894
3428    }
3429    // c:4896 — `*str = '\0';` — cmd[str_idx] becomes the terminator.
3430    // c:4897-4898 — `if (eptr) *eptr = str + 1;`
3431    if let Some(p) = eptr {
3432        *p = str_idx + 1;
3433    }
3434    // c:4899 — `parse_string(cmd + 2, 0)`.
3435    let body = &cmd[2..str_idx];
3436    let prog = parse_string(body, 0);
3437    if prog.is_none() {
3438        // c:4899
3439        zerr("parse error in process substitution"); // c:4900
3440        return None; // c:4901
3441    }
3442    prog // c:4903
3443}
3444
3445/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
3446/// front of a script when probing for a `#!` shebang line.
3447pub const POUNDBANGLIMIT: usize = 128;
3448
3449/// Port of `static char **makecline(LinkList list)` from `Src/exec.c:2046`.
3450///
3451/// Builds the argv array from a command's args list. The C version
3452/// allocates with a 4-slot prepad (2 reserved at the front for the
3453/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
3454/// doesn't need this since we rebuild the Vec on shebang re-exec
3455/// (see zexecve WARNING e).
3456///
3457/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
3458/// (stderr), preceded by the PS4 prefix when first command of the line.
3459pub fn makecline(list: &[String]) -> Vec<String> {
3460    // c:2046
3461    if isset(XTRACE) {
3462        // c:2055
3463        if doneps4.load(Ordering::Relaxed) == 0 {
3464            // c:2056
3465            printprompt4(); // c:2057
3466        }
3467        let mut first = true;
3468        let mut err = std::io::stderr().lock();
3469        use std::io::Write;
3470        for s in list.iter() {
3471            // c:2059
3472            if !first {
3473                let _ = err.write_all(b" "); // c:2063
3474            }
3475            first = false;
3476            let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
3477        }
3478        let _ = err.write_all(b"\n"); // c:2065
3479        let _ = err.flush(); // c:2066
3480    }
3481    list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
3482}
3483
3484/// Port of `static void execute(LinkList args, int flags, int defpath)`
3485/// from `Src/exec.c:723`. The canonical "child runs the simple
3486/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
3487/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
3488/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
3489/// commandnotfound-handler fallback and the final exit-code escape
3490/// (127 not-found / 126 noperm).
3491///
3492/// =================== WARNING — DIVERGENCE ====================
3493/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
3494///     fast-path wired via cmdnamtab_lock(); jumps direct to
3495///     `cn.cmd` absolute path before the $PATH scan. Unhashed
3496///     cursor-walk (c:830-846) still falls to the full $PATH scan;
3497///     observable behavior matches C when the hash hit is HASHED.
3498/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
3499///     not-yet-ported `doshfunc` for the `command_not_found_handler`
3500///     shell function. Already routes through executor dispatch
3501///     (see exec.rs:2783).
3502/// (c) `_realexit()` (c:810, 874) — bare `std::process::exit`.
3503/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
3504///     FD_CLOEXEC platform default (macOS, Linux).
3505/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
3506///     `defpath` (`command -p`) walks DEFAULT_PATH via
3507///     search_defpath (already ported).
3508/// =============================================================
3509pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
3510    // c:723
3511    let mut eno: i32 = 0;
3512    let mut ee: i32; // c:729
3513    let mut arg0 = if args.is_empty() {
3514        return;
3515    } else {
3516        args[0].clone()
3517    }; // c:731
3518       // c:733-748 — STTY pre-exec handling.
3519    {
3520        let mut stty = STTYval.lock().unwrap();
3521        if let Some(s) = stty.take() {
3522            // c:738 — STTYval = 0 to break recursion.
3523            if !s.is_empty()
3524                && unsafe { libc::isatty(0) } != 0
3525                && unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
3526            {
3527                drop(stty);
3528                let cmd = format!("stty {}", s); // c:739
3529                execstring(&cmd, 1, 0, "stty"); // c:743
3530            }
3531        }
3532    }
3533    // c:752-763 — ARGV0 override.
3534    if let Some(z) = zgetenv("ARGV0") {
3535        args[0] = z.clone(); // c:753
3536        unsafe {
3537            let key = std::ffi::CString::new("ARGV0").unwrap();
3538            libc::unsetenv(key.as_ptr()); // c:760
3539        }
3540        arg0 = args[0].clone();
3541    } else if (flags & BINF_DASH) != 0 {
3542        // c:764 — `BINF_DASH` prepends `-`.
3543        args[0] = format!("-{}", arg0); // c:767-768
3544        arg0 = args[0].clone();
3545    }
3546    let argv = makecline(args); // c:771
3547    let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
3548        Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
3549    } else {
3550        None
3551    };
3552    let newenvp = newenvp_owned.as_deref();
3553    closem(FDT_XTRACE, 0); // c:779
3554                           // c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
3555    child_unblock(); // c:786
3556    if arg0.len() >= libc::PATH_MAX as usize {
3557        // c:787
3558        zerr(&format!("command too long: {}", arg0)); // c:788
3559        unsafe {
3560            libc::_exit(1);
3561        } // c:789
3562    }
3563    // c:791-801 — slash in arg0 → direct exec.
3564    if let Some(slash_pos) = arg0.find('/') {
3565        let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
3566        let is_dot = arg0.starts_with('.')
3567            && (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
3568        if slash_pos == 0 || unset(PATHDIRS) || is_dot {
3569            // c:794
3570            zerr(&format!(
3571                "{}: {}",
3572                std::io::Error::from_raw_os_error(lerrno),
3573                arg0
3574            )); // c:797
3575            let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
3576                126
3577            } else {
3578                127
3579            };
3580            unsafe {
3581                libc::_exit(code);
3582            } // c:798
3583        }
3584    }
3585    if defpath != 0 {
3586        // c:804 — `command -p` default-path search.
3587        let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
3588            Some(p) => p, // c:808
3589            None => {
3590                if commandnotfound(&arg0, args) == 0 {
3591                    // c:809
3592                    unsafe {
3593                        libc::_exit(LASTVAL.load(Ordering::Relaxed));
3594                    }
3595                }
3596                zerr(&format!("command not found: {}", arg0)); // c:811
3597                unsafe {
3598                    libc::_exit(127);
3599                } // c:812
3600            }
3601        };
3602        ee = zexecve(&pbuf, &argv, newenvp); // c:815
3603        let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3604        if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3605            // c:819
3606            eno = ee;
3607        }
3608    } else {
3609        // c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
3610        // jump straight to the absolute path stored in `cn.cmd`,
3611        // skipping the full $PATH scan (one exec attempt vs N).
3612        // c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
3613        let hashed_path: Option<String> = {
3614            let tab = cmdnamtab_lock().read().ok();
3615            tab.and_then(|t| {
3616                t.get(&arg0).and_then(|cn| {
3617                    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
3618                        // c:827-828 — `strcpy(nn, cn->u.cmd);`
3619                        cn.cmd.clone()
3620                    } else {
3621                        None
3622                    }
3623                })
3624            })
3625        };
3626        if let Some(nn) = hashed_path {
3627            // c:848 — `ee = zexecve(nn, argv, newenvp);`
3628            ee = zexecve(&nn, &argv, newenvp);
3629            let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3630            if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3631                eno = ee;
3632            }
3633            // If the hashed entry's exec failed without a "good" error,
3634            // we still need the $PATH fallback — fall through.
3635            if eno == 0 && ee != 0 {
3636                // Reset for the $PATH scan below.
3637                ee = 0;
3638            }
3639        }
3640        // c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
3641        // optimization but C also walks the rest of `path` if the hashed
3642        // exec failed with a non-"good" error).
3643        let path_str = getsparam("PATH").unwrap_or_default();
3644        for pp in path_str.split(':') {
3645            if pp.is_empty() || pp == "." {
3646                // c:856
3647                ee = zexecve(&arg0, &argv, newenvp); // c:857
3648                if isgooderr(ee, pp) {
3649                    eno = ee;
3650                }
3651            } else {
3652                // c:860
3653                let candidate = format!("{}/{}", pp, arg0); // c:861-864
3654                ee = zexecve(&candidate, &argv, newenvp); // c:865
3655                if isgooderr(ee, pp) {
3656                    eno = ee;
3657                }
3658            }
3659        }
3660    }
3661    // c:871-881 — final error reporting.
3662    if eno != 0 {
3663        // c:871
3664        zerr(&format!(
3665            "{}: {}",
3666            std::io::Error::from_raw_os_error(eno),
3667            arg0
3668        )); // c:872
3669    } else if commandnotfound(&arg0, args) == 0 {
3670        // c:873
3671        unsafe {
3672            libc::_exit(LASTVAL.load(Ordering::Relaxed));
3673        } // c:874
3674    } else {
3675        zerr(&format!("command not found: {}", arg0)); // c:876
3676    }
3677    let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
3678        126
3679    } else {
3680        127
3681    }; // c:881
3682    unsafe {
3683        libc::_exit(code);
3684    }
3685}
3686
3687/// Port of `static int zexecve(char *pth, char **argv, char **newenvp)`
3688/// from `Src/exec.c:504`. Wraps `execve(2)` with:
3689///   - `$_` env var stamped to absolute `pth` (c:514-520)
3690///   - winch signal unblock right before the syscall (c:527)
3691///   - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
3692///     bytes, parses a `#!interp arg` shebang and re-execs the
3693///     interpreter (c:534-628). For `ENOEXEC` with no shebang,
3694///     binary-safety check then falls back to `/bin/sh script` per
3695///     POSIX (c:588-628).
3696///
3697/// Returns `errno` from the failing exec — execve only returns on
3698/// failure, so success means the calling process is already replaced.
3699///
3700/// =================== WARNING — DIVERGENCE ====================
3701/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
3702///     string; Rust uses a stack `String` (consumed by `zputenv`).
3703/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
3704///     unconditionally as a no-op when FD_CLOEXEC is platform default.
3705/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
3706///     c:639-642 — handled implicitly via &str ↔ CString.
3707/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
3708///     drop the metafy and pass byte ranges to zerr directly.
3709/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
3710///     (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
3711///     `Vec<String>` with interp + optional arg + original argv tail
3712///     since Vec doesn't expose negative indexing.
3713/// (f) `environ` is FFI-loaded only when `newenvp` is None.
3714/// =============================================================
3715pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
3716    // c:504
3717    use std::ffi::CString;
3718    // c:514-520 — `_=pth` env stamping.
3719    let pth_abs = if pth.starts_with('/') {
3720        // c:516
3721        pth.to_string() // c:517
3722    } else {
3723        // c:518
3724        format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
3725    };
3726    zputenv(&format!("_={}", pth_abs)); // c:520
3727    closedumps(); // c:522
3728    winch_unblock(); // c:527
3729    let cpth = match CString::new(pth) {
3730        Ok(c) => c,
3731        Err(_) => return libc::ENOENT,
3732    };
3733    let cargs: Vec<CString> = argv
3734        .iter()
3735        .filter_map(|a| CString::new(a.as_str()).ok())
3736        .collect();
3737    let mut argv_ptrs: Vec<*const libc::c_char> = cargs.iter().map(|c| c.as_ptr()).collect();
3738    argv_ptrs.push(std::ptr::null());
3739    let env_holder: Vec<CString>;
3740    let env_ptrs: Vec<*const libc::c_char>;
3741    let envp: *const *const libc::c_char = match newenvp {
3742        Some(env) => {
3743            env_holder = env
3744                .iter()
3745                .filter_map(|e| CString::new(e.as_str()).ok())
3746                .collect();
3747            env_ptrs = {
3748                let mut v: Vec<*const libc::c_char> =
3749                    env_holder.iter().map(|c| c.as_ptr()).collect();
3750                v.push(std::ptr::null());
3751                v
3752            };
3753            env_ptrs.as_ptr()
3754        }
3755        None => unsafe {
3756            extern "C" {
3757                static environ: *const *const libc::c_char;
3758            }
3759            environ
3760        },
3761    };
3762    unsafe {
3763        libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
3764    }
3765    let eno = std::io::Error::last_os_error()
3766        .raw_os_error()
3767        .unwrap_or(libc::ENOEXEC); // c:534
3768    if eno == libc::ENOEXEC || eno == libc::ENOENT {
3769        // c:534
3770        let fd = unsafe { libc::open(cpth.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:538
3771        if fd < 0 {
3772            return std::io::Error::last_os_error()
3773                .raw_os_error()
3774                .unwrap_or(libc::ENOENT); // c:634
3775        }
3776        let mut buf = vec![0u8; POUNDBANGLIMIT + 1]; // c:541
3777        let ct = unsafe {
3778            libc::read(
3779                fd,
3780                buf.as_mut_ptr() as *mut libc::c_void,
3781                POUNDBANGLIMIT as libc::size_t,
3782            )
3783        }; // c:542
3784        unsafe {
3785            libc::close(fd);
3786        } // c:543
3787        if ct >= 0 {
3788            // c:544
3789            let ct = ct as usize;
3790            if ct >= 2 && buf[0] == b'#' && buf[1] == b'!' {
3791                // c:545
3792                let mut t0 = 0;
3793                while t0 < ct && buf[t0] != b'\n' {
3794                    t0 += 1;
3795                } // c:546-548
3796                if t0 == ct {
3797                    // c:549
3798                    zerr(&format!(
3799                        // c:550
3800                        "{}: bad interpreter: {}: {}",
3801                        pth,
3802                        String::from_utf8_lossy(&buf[2..t0.min(ct)]),
3803                        std::io::Error::from_raw_os_error(eno)
3804                    ));
3805                } else {
3806                    // c:552
3807                    while t0 > 0 && (buf[t0] == b' ' || buf[t0] == b'\t' || buf[t0] == b'\n') {
3808                        buf[t0] = 0;
3809                        t0 -= 1;
3810                    } // c:553-554
3811                    let mut ptr_lo: usize = 2;
3812                    while ptr_lo < buf.len() && buf[ptr_lo] == b' ' {
3813                        ptr_lo += 1;
3814                    } // c:555
3815                    let ptr2_lo = ptr_lo;
3816                    let mut ptr_hi = ptr2_lo;
3817                    while ptr_hi < buf.len() && buf[ptr_hi] != 0 && buf[ptr_hi] != b' ' {
3818                        ptr_hi += 1;
3819                    } // c:556
3820                    let interp_str = String::from_utf8_lossy(&buf[ptr2_lo..ptr_hi]).into_owned();
3821                    if eno == libc::ENOENT {
3822                        // c:557 — pathprog rewrite path.
3823                        let pprog = if !interp_str.starts_with('/') {
3824                            // c:561
3825                            pathprog(&interp_str).map(|p| p.display().to_string())
3826                        } else {
3827                            None
3828                        };
3829                        if let Some(pprog) = pprog {
3830                            // c:562
3831                            let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3832                            argv_new.push(interp_str.clone()); // c:564
3833                            if ptr_hi >= buf.len() || buf[ptr_hi] == 0 {
3834                                argv_new.push(pth.to_string());
3835                            } else {
3836                                // c:567
3837                                let mut rest_lo = ptr_hi + 1;
3838                                while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3839                                    rest_lo += 1;
3840                                }
3841                                let mut rest_hi = rest_lo;
3842                                while rest_hi < buf.len() && buf[rest_hi] != 0 {
3843                                    rest_hi += 1;
3844                                }
3845                                let arg_str =
3846                                    String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3847                                argv_new.push(arg_str);
3848                                argv_new.push(pth.to_string());
3849                            }
3850                            for orig in argv.iter().skip(1) {
3851                                argv_new.push(orig.clone());
3852                            }
3853                            winch_unblock(); // c:565/c:570
3854                            return zexecve(&pprog, &argv_new, newenvp); // c:566/c:571
3855                        }
3856                        zerr(&format!(
3857                            // c:574
3858                            "{}: bad interpreter: {}: {}",
3859                            pth,
3860                            interp_str,
3861                            std::io::Error::from_raw_os_error(eno)
3862                        ));
3863                    } else if ptr_hi < buf.len() && buf[ptr_hi] != 0 {
3864                        // c:576
3865                        let mut rest_lo = ptr_hi + 1;
3866                        while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3867                            rest_lo += 1;
3868                        }
3869                        let mut rest_hi = rest_lo;
3870                        while rest_hi < buf.len() && buf[rest_hi] != 0 {
3871                            rest_hi += 1;
3872                        }
3873                        let arg_str = String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3874                        let mut argv_new: Vec<String> =
3875                            vec![interp_str.clone(), arg_str, pth.to_string()];
3876                        for orig in argv.iter().skip(1) {
3877                            argv_new.push(orig.clone());
3878                        }
3879                        winch_unblock(); // c:580
3880                        return zexecve(&interp_str, &argv_new, newenvp); // c:581
3881                    } else {
3882                        // c:582
3883                        let mut argv_new: Vec<String> = vec![interp_str.clone(), pth.to_string()];
3884                        for orig in argv.iter().skip(1) {
3885                            argv_new.push(orig.clone());
3886                        }
3887                        winch_unblock(); // c:584
3888                        return zexecve(&interp_str, &argv_new, newenvp); // c:585
3889                    }
3890                }
3891            } else if eno == libc::ENOEXEC {
3892                // c:588 — binary-safety + /bin/sh fallback.
3893                let nul_pos = buf[..ct].iter().position(|&b| b == 0); // c:597
3894                let isbinary = match nul_pos {
3895                    None => false, // c:598
3896                    Some(npos) => {
3897                        let mut has_letter = false;
3898                        let mut binary = true;
3899                        for &b in &buf[..npos] {
3900                            // c:602-609
3901                            if (b as char).is_ascii_lowercase() || b == b'$' || b == b'`' {
3902                                has_letter = true;
3903                            }
3904                            if has_letter && b == b'\n' {
3905                                binary = false; // c:606
3906                                break;
3907                            }
3908                        }
3909                        binary
3910                    }
3911                };
3912                if !isbinary {
3913                    // c:611
3914                    let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3915                    argv_new.push("sh".to_string()); // c:625
3916                    if !argv.is_empty() && (argv[0].starts_with('-') || argv[0].starts_with('+')) {
3917                        argv_new.push("-".to_string()); // c:623
3918                    }
3919                    for orig in argv.iter() {
3920                        argv_new.push(orig.clone());
3921                    }
3922                    winch_unblock(); // c:626
3923                    return zexecve("/bin/sh", &argv_new, newenvp); // c:627
3924                }
3925            }
3926        }
3927    }
3928    eno // c:643
3929}
3930
3931/// Port of `char *getoutputfile(char *cmd, char **eptr)` from
3932/// `Src/exec.c:4910` — `=(cmd)` process substitution.
3933///
3934/// Substitutes the cmd's stdout into a temp file, returns the
3935/// filename. Optimised path: `=(<<<heredoc-str)` writes the
3936/// heredoc body directly without a fork.
3937///
3938/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
3939///     so the temp file gets cleaned at job exit.
3940/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
3941///     Behavior matches the `full=0` case anyway.
3942/// (c) `entersubsh` is ported at exec.rs:3934 — wire it here when
3943///     re-routing the fork path away from setsid-only fallback.
3944/// (d) `execode` is now ported (exec.rs:6047) — the body still
3945///     re-feeds through fusevm for cache coherence with execstring.
3946/// (e) `_realexit` flushes stdio + jobs + history. We use bare
3947///     `std::process::exit(0)` for now.
3948/// (f) TMPSUFFIX link()-rename block (c:4951-4958) deferred; rare
3949///     `setopt suffix_alias` interaction with =(…).
3950pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
3951    // c:4910
3952    let bytes = cmd.as_bytes();
3953    let _ = bytes;
3954    // c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
3955    let mut ends_at: usize = 0;
3956    let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
3957    if let Some(p) = eptr {
3958        *p = ends_at;
3959    }
3960    let mut nam = gettempname(None, true)?; // c:4924
3961                                            // c:4927 — `simple_redir_name` opt for `=(<<<str)`.
3962    let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
3963        // c:4933
3964        let mut sub = singsub(&raw); // c:4933
3965        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
3966            // c:4934
3967            String::new() // c:4935 — sentinel; checked below
3968        } else {
3969            sub = untokenize(&sub); // c:4937
3970            dyncat(&sub, "\n") // c:4938
3971        }
3972    });
3973    if let Some(ref sv) = s {
3974        if sv.is_empty() {
3975            s = None;
3976        }
3977    }
3978    if s.is_none() {
3979        // c:4942
3980        child_block(); // c:4943
3981    }
3982    // c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
3983    let c_nam = match std::ffi::CString::new(nam.clone()) {
3984        Ok(c) => c,
3985        Err(_) => {
3986            if s.is_none() {
3987                child_unblock();
3988            }
3989            return None;
3990        }
3991    };
3992    let fd = unsafe {
3993        libc::open(
3994            c_nam.as_ptr(),
3995            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
3996            0o600 as libc::c_uint,
3997        )
3998    };
3999    if fd < 0 {
4000        // c:4945
4001        zerr(&format!(
4002            "process substitution failed: {}",
4003            std::io::Error::last_os_error()
4004        )); // c:4946
4005        if s.is_none() {
4006            child_unblock(); // c:4948
4007        }
4008        return None; // c:4949
4009    }
4010    // c:4951-4958 — TMPSUFFIX link block (see WARNING f).
4011    // c:4960 — `addfilelist(nam, 0);` — register temp file in current
4012    // job's filelist so it's unlinked at job exit (not relying on the
4013    // OS temp-reaper).
4014    if let Some(jt) = JOBTAB.get() {
4015        let mut guard = jt.lock().unwrap();
4016        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4017        if tj >= 0 {
4018            if let Some(j) = guard.get_mut(tj as usize) {
4019                crate::ported::jobs::addfilelist(j, Some(&nam), 0);
4020            }
4021        }
4022    }
4023    if let Some(sv) = s {
4024        // c:4962 — optimised here-string write path.
4025        let mut buf: Vec<u8> = sv.into_bytes();
4026        let _len = unmetafy(&mut buf); // c:4965
4027        let _ = write_loop(fd, &buf); // c:4966
4028        unsafe {
4029            libc::close(fd);
4030        } // c:4967
4031        return Some(nam); // c:4968
4032    }
4033    // c:4971 — `cmdoutpid = pid = zfork(NULL)`.
4034    let pid = zfork(None);
4035    cmdoutpid.store(pid, Ordering::Relaxed);
4036    if pid == -1 {
4037        // c:4972
4038        unsafe {
4039            libc::close(fd);
4040        } // c:4973
4041        child_unblock(); // c:4974
4042        return Some(nam); // c:4975
4043    } else if pid != 0 {
4044        // c:4976 — parent.
4045        unsafe {
4046            libc::close(fd);
4047        } // c:4977
4048        let _ = waitforpid(pid); // c:4978
4049        cmdoutval.store(0, Ordering::Relaxed); // c:4979
4050        return Some(nam); // c:4980
4051    }
4052    // c:4983 — child.
4053    closem(FDT_UNUSED, 0); // c:4984
4054    let _ = redup(fd, 1); // c:4985
4055    entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
4056    cmdpush(CS_CMDSUBST as u8); // c:4987
4057                                // c:4988 — execode — WARNING (d).
4058    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4059    let body = if body_end > 2 && body_end <= cmd.len() {
4060        &cmd[2..body_end]
4061    } else {
4062        ""
4063    };
4064    let _ = crate::ported::exec::execute_script_zsh_pipeline(body);
4065    cmdpop(); // c:4989
4066    unsafe {
4067        libc::close(1);
4068    } // c:4990
4069      // _realexit — WARNING (e)
4070    std::process::exit(0); // c:4991
4071    #[allow(unreachable_code)]
4072    {
4073        // c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
4074        let _ = &mut nam;
4075        unsafe {
4076            libc::kill(libc::getpid(), libc::SIGKILL);
4077        }
4078        None
4079    }
4080}
4081
4082/// Port of `char *getproc(char *cmd, char **eptr)` from
4083/// `Src/exec.c:5025` — `<(cmd)` / `>(cmd)` process substitution
4084/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
4085///
4086/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
4087///     path c:5037-5064) is omitted; modern Linux/macOS both
4088///     provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
4089///     unused here.
4090/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
4091///     the current job (c:5141-5142).
4092/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
4093///     c:5087.
4094/// (d) `entersubsh` is ported at exec.rs:3934 — wired below at
4095///     c:5063 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`).
4096/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
4097///     through fusevm for cache coherence.
4098/// (f) `_realexit` flushes stdio + jobs + history. We use bare
4099///     `std::process::exit(LASTVAL)` for now.
4100/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
4101pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4102    // c:5025
4103    let bytes = cmd.as_bytes();
4104    let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4105        1 // c:5032 — `<(...)` writer-side child
4106    } else {
4107        0
4108    };
4109    // c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
4110    // proc subst needs a host job to attach the child to.
4111    let tj_check = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4112    if tj_check == -1 {
4113        zerr(&format!("process substitution {} cannot be used here", cmd)); // c:5069
4114        return None; // c:5070
4115    }
4116    // c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
4117    let mut ends_at: usize = 0;
4118    let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
4119    if let Some(p) = eptr {
4120        *p = ends_at;
4121    }
4122    let mut pipes: [i32; 2] = [-1; 2];
4123    if mpipe(&mut pipes) < 0 {
4124        // c:5075
4125        return None;
4126    }
4127    let mut bgtime: ZshTimespec = libc::timespec {
4128        tv_sec: 0,
4129        tv_nsec: 0,
4130    };
4131    let pid = zfork(Some(&mut bgtime)); // c:5077
4132    if pid != 0 {
4133        // c:5077 — parent path.
4134        let pnam = format!("/dev/fd/{}", pipes[(1 - out) as usize]); // c:5078
4135        let _ = zclose(pipes[out as usize]); // c:5079
4136        if pid == -1 {
4137            // c:5080
4138            let _ = zclose(pipes[(1 - out) as usize]); // c:5082
4139            return None; // c:5083
4140        }
4141        let fd = pipes[(1 - out) as usize]; // c:5085
4142        fdtable_set(fd, FDT_PROC_SUBST); // c:5086
4143                                         // c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
4144                                         // pipe fd in the current job's filelist so it's closed at job exit.
4145        if let Some(jt) = JOBTAB.get() {
4146            let mut guard = jt.lock().unwrap();
4147            let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4148            if tj >= 0 {
4149                if let Some(j) = guard.get_mut(tj as usize) {
4150                    crate::ported::jobs::addfilelist(j, None, fd);
4151                }
4152            }
4153        }
4154        // c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
4155        // record the proc-subst writer-side child in the job's
4156        // auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
4157        // child), C omits the addproc — symmetric here.
4158        if out == 0 {
4159            if let Some(jt) = JOBTAB.get() {
4160                let mut guard = jt.lock().unwrap();
4161                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4162                if tj >= 0 {
4163                    if let Some(j) = guard.get_mut(tj as usize) {
4164                        crate::ported::jobs::addproc(
4165                            j,
4166                            pid,
4167                            "",
4168                            true,
4169                            Some(std::time::Instant::now()),
4170                            -1,
4171                            -1,
4172                        );
4173                    }
4174                }
4175            }
4176        }
4177        procsubstpid.store(pid, Ordering::Relaxed); // c:5092
4178        return Some(pnam); // c:5093
4179    }
4180    // c:5095 — child.
4181    entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
4182    let _ = redup(pipes[out as usize], out); // c:5096
4183    closem(FDT_UNUSED, 0); // c:5097
4184    cmdpush(CS_CMDSUBST as u8); // c:5100
4185    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4186    let body = if body_end > 2 && body_end <= cmd.len() {
4187        &cmd[2..body_end]
4188    } else {
4189        ""
4190    };
4191    let _ = crate::ported::exec::execute_script_zsh_pipeline(body);
4192    cmdpop(); // c:5102
4193    let _ = zclose(out); // c:5103
4194    std::process::exit(LASTVAL.load(Ordering::Relaxed)); // c:5104
4195}
4196
4197/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
4198/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
4199pub mod esub {
4200    // c:1056
4201    /// `ASYNC` constant.
4202    pub const ASYNC: i32 = 0x01; // c:1058
4203    /// `PGRP` constant.
4204    pub const PGRP: i32 = 0x02; // c:1063
4205    /// `KEEPTRAP` constant.
4206    pub const KEEPTRAP: i32 = 0x04; // c:1065
4207    /// `FAKE` constant.
4208    pub const FAKE: i32 = 0x08; // c:1067
4209    /// `REVERTPGRP` constant.
4210    pub const REVERTPGRP: i32 = 0x10; // c:1069
4211    /// `NOMONITOR` constant.
4212    pub const NOMONITOR: i32 = 0x20; // c:1071
4213    /// `JOB_CONTROL` constant.
4214    pub const JOB_CONTROL: i32 = 0x40; // c:1073
4215}
4216
4217/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
4218/// Out-arg used by `entersubsh()` to hand back the group-leader pid
4219/// and the list-pipe job index the parent should track. Only filled
4220/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
4221/// groups).
4222#[allow(non_camel_case_types)]
4223#[derive(Default)]
4224pub struct entersubsh_ret {
4225    pub gleader: i32,       // c:1122
4226    pub list_pipe_job: i32, // c:1123
4227}
4228
4229/// Port of `static void entersubsh(int flags, struct entersubsh_ret *retp)`
4230/// from `Src/exec.c:1083`. Called by every child fork to switch the
4231/// process into subshell mode: traps reset, monitor disabled, signals
4232/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
4233/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
4234///
4235/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
4236///     1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
4237///     ESUB_PGRP+sync path establishes pipeline group-leadership
4238///     (list_pipe_job inherit or thisjob-as-leader), filling
4239///     entersubsh_ret with the chosen gleader + list_pipe_job index.
4240/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
4241///     `clearjobtab(&mut JobTable, monitor)`; we get the global table
4242///     via a TABLE handle similar to other jobs.rs entries.
4243/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
4244/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
4245///     mypgrp` — direct C parity (jobs.rs:3406 provides the call).
4246/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
4247///     uses `opts_set_off(USEZLE)`; zleactive is the atomic in
4248///     builtins/sched.rs.
4249/// =============================================================
4250pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
4251    // c:1083
4252    let monitor: i32;
4253    let job_control_ok: i32;
4254    // c:1088-1092 — reset traps unless KEEPTRAP.
4255    if (flags & esub::KEEPTRAP) == 0 {
4256        // c:1088
4257        for sig in 0..=SIGCOUNT {
4258            // c:1089
4259            let st = {
4260                let guard = sigtrapped.lock().unwrap();
4261                guard.get(sig as usize).copied().unwrap_or(0)
4262            };
4263            let func_set = (st & ZSIG_FUNC) != 0; // c:1090
4264            let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
4265            if !func_set && !posix_ignored {
4266                unsettrap(sig); // c:1092
4267            }
4268        }
4269    }
4270    monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
4271    job_control_ok = if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
4272        // c:1094
4273        1
4274    } else {
4275        0
4276    };
4277    EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
4278    if (flags & esub::NOMONITOR) != 0 {
4279        // c:1096
4280        dosetopt(MONITOR, 0, 0); // c:1097
4281    }
4282    if !isset(MONITOR) {
4283        // c:1098
4284        if (flags & esub::ASYNC) != 0 {
4285            // c:1099
4286            let _ = settrap(libc::SIGINT, None, 0); // c:1100
4287            let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
4288            if unsafe { libc::isatty(0) } != 0 {
4289                // c:1102
4290                unsafe {
4291                    libc::close(0);
4292                } // c:1103
4293                let devnull = std::ffi::CString::new("/dev/null").unwrap();
4294                if unsafe { libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) } != 0 {
4295                    // c:1104
4296                    zerr(&format!(
4297                        // c:1105
4298                        "can't open /dev/null: {}",
4299                        std::io::Error::last_os_error()
4300                    ));
4301                    unsafe {
4302                        libc::_exit(1);
4303                    } // c:1106
4304                }
4305            }
4306        }
4307    } else if (flags & esub::PGRP) != 0 {
4308        // c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
4309        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4310        if thisjob != -1 {
4311            let lpj = list_pipe_job.load(Ordering::Relaxed);
4312            let lp = list_pipe.load(Ordering::Relaxed);
4313            let lpc = list_pipe_child.load(Ordering::Relaxed);
4314            if let Some(jt) = JOBTAB.get() {
4315                let mut guard = jt.lock().unwrap();
4316                let lpj_gleader = guard.get(lpj as usize).map(|j| j.gleader).unwrap_or(0);
4317                if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
4318                    // c:1111-1124 — inherit list_pipe_job's group leader.
4319                    let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
4320                        || (unsafe { libc::killpg(lpj_gleader, 0) } == -1
4321                            && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH))
4322                    {
4323                        // c:1115-1117 — primary group leader gone; this child becomes leader.
4324                        let new_gl = if lpc != 0 {
4325                            mypgrp.load(Ordering::Relaxed)
4326                        } else {
4327                            unsafe { libc::getpid() }
4328                        };
4329                        if let Some(j) = guard.get_mut(lpj as usize) {
4330                            j.gleader = new_gl;
4331                        }
4332                        if let Some(j) = guard.get_mut(thisjob as usize) {
4333                            j.gleader = new_gl;
4334                        }
4335                        unsafe { libc::setpgid(0, new_gl) };
4336                        if (flags & esub::ASYNC) == 0 {
4337                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
4338                        }
4339                        new_gl
4340                    } else {
4341                        lpj_gleader
4342                    };
4343                    if let Some(r) = retp {
4344                        if (flags & esub::ASYNC) == 0 {
4345                            r.gleader = pgid; // c:1122
4346                            r.list_pipe_job = lpj; // c:1123
4347                        }
4348                    }
4349                } else {
4350                    // c:1126-1151 — standard group-leader-takeover path.
4351                    let thisjob_gleader =
4352                        guard.get(thisjob as usize).map(|j| j.gleader).unwrap_or(0);
4353                    if thisjob_gleader == 0 || unsafe { libc::setpgid(0, thisjob_gleader) } == -1 {
4354                        let new_gl = unsafe { libc::getpid() };
4355                        if let Some(j) = guard.get_mut(thisjob as usize) {
4356                            j.gleader = new_gl; // c:1138
4357                        }
4358                        if lpj != thisjob {
4359                            let lpj_was_unset = guard
4360                                .get(lpj as usize)
4361                                .map(|j| j.gleader == 0)
4362                                .unwrap_or(true);
4363                            if lpj_was_unset {
4364                                if let Some(j) = guard.get_mut(lpj as usize) {
4365                                    j.gleader = new_gl; // c:1140-1141
4366                                }
4367                            }
4368                        }
4369                        unsafe { libc::setpgid(0, new_gl) }; // c:1142
4370                        if (flags & esub::ASYNC) == 0 {
4371                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
4372                            if let Some(r) = retp {
4373                                r.gleader = new_gl; // c:1146
4374                                if lpj != thisjob {
4375                                    r.list_pipe_job = lpj; // c:1148
4376                                }
4377                            }
4378                        }
4379                    }
4380                }
4381            }
4382        } else {
4383            // No real job slot; basic setpgid fallback.
4384            unsafe { libc::setpgid(0, 0) };
4385        }
4386    }
4387    if (flags & esub::FAKE) == 0 {
4388        // c:1153
4389        subsh.store(1, Ordering::Relaxed); // c:1154
4390    }
4391    // c:1161 — `zsh_subshell++;` regardless of FAKE.
4392    zsh_subshell.fetch_add(1, Ordering::Relaxed);
4393    // c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
4394    if (flags & esub::REVERTPGRP) != 0
4395        && unsafe { libc::getpid() } == mypgrp.load(Ordering::Relaxed)
4396    {
4397        release_pgrp(); // c:1163
4398    }
4399    *shout.lock().unwrap() = 0; // c:1164 — shout = NULL
4400    if (flags & esub::NOMONITOR) != 0 {
4401        // c:1165
4402        signal_ignore(libc::SIGTTOU); // c:1171
4403        signal_ignore(libc::SIGTTIN); // c:1172
4404        signal_ignore(libc::SIGTSTP); // c:1173
4405    } else if job_control_ok == 0 {
4406        // c:1174
4407        signal_default(libc::SIGTTOU); // c:1181
4408        signal_default(libc::SIGTTIN); // c:1182
4409        signal_default(libc::SIGTSTP); // c:1183
4410    }
4411    let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
4412    if interact {
4413        signal_default(libc::SIGTERM); // c:1186
4414        let int_st = sigtrapped
4415            .lock()
4416            .unwrap()
4417            .get(libc::SIGINT as usize)
4418            .copied()
4419            .unwrap_or(0);
4420        if (int_st & ZSIG_IGNORED) == 0 {
4421            // c:1187
4422            signal_default(libc::SIGINT); // c:1188
4423        }
4424        let pipe_st = sigtrapped
4425            .lock()
4426            .unwrap()
4427            .get(libc::SIGPIPE as usize)
4428            .copied()
4429            .unwrap_or(0);
4430        if pipe_st == 0 {
4431            // c:1189
4432            signal_default(libc::SIGPIPE); // c:1190
4433        }
4434    }
4435    let quit_st = sigtrapped
4436        .lock()
4437        .unwrap()
4438        .get(libc::SIGQUIT as usize)
4439        .copied()
4440        .unwrap_or(0);
4441    if (quit_st & ZSIG_IGNORED) == 0 {
4442        // c:1192
4443        signal_default(libc::SIGQUIT); // c:1193
4444    }
4445    // c:1202-1205 — unblock any trapped signals while in `intrap`.
4446    if intrap.load(Ordering::Relaxed) != 0 {
4447        // c:1202
4448        for sig in 1..=SIGCOUNT {
4449            let st = sigtrapped
4450                .lock()
4451                .unwrap()
4452                .get(sig as usize)
4453                .copied()
4454                .unwrap_or(0);
4455            if st != 0 && st != ZSIG_IGNORED {
4456                // c:1204
4457                let m = signal_mask(sig);
4458                let _ = signal_unblock(&m); // c:1205
4459            }
4460        }
4461    }
4462    if job_control_ok == 0 {
4463        // c:1206
4464        dosetopt(MONITOR, 0, 0); // c:1207
4465    }
4466    dosetopt(USEZLE, 0, 0); // c:1208
4467    zleactive.store(0, Ordering::Relaxed); // c:1209
4468                                           // c:1214-1217 — close saved fds.
4469    let max = MAX_ZSH_FD.load(Ordering::Relaxed);
4470    for i in 10..=max {
4471        if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
4472            // c:1215
4473            let _ = zclose(i); // c:1216
4474        }
4475    }
4476    // c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
4477    // at jobs.rs:1695 which handles ALL the C body including the
4478    // oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
4479    let mut dummy_table = crate::exec_jobs::JobTable::new();
4480    crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
4481    let _ = get_usage(); // c:1220
4482    FORKLEVEL.store(
4483        // c:1221 — `forklevel = locallevel;`
4484        locallevel.load(Ordering::Relaxed),
4485        Ordering::Relaxed,
4486    );
4487}
4488
4489/// Port of `static int getpipe(char *cmd, int nullexec)` from
4490/// `Src/exec.c:5119`.
4491///
4492/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
4493/// pipe pair: parent gets back the readable (`<(...)`) or writable
4494/// (`>(...)`) end as an fd; child runs the substituted command with
4495/// its stdio redirected into the other end.
4496///
4497/// ```c
4498/// Eprog prog;
4499/// int pipes[2], out = *cmd == Inang;
4500/// pid_t pid;
4501/// struct timespec bgtime;
4502/// char *ends;
4503/// if (!(prog = parsecmd(cmd, &ends))) return -1;
4504/// if (*ends) { zerr("invalid syntax..."); return -1; }
4505/// if (mpipe(pipes) < 0) return -1;
4506/// if ((pid = zfork(&bgtime))) {
4507///     zclose(pipes[out]);
4508///     if (pid == -1) { zclose(pipes[!out]); return -1; }
4509///     if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
4510///     procsubstpid = pid;
4511///     return pipes[!out];
4512/// }
4513/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
4514/// redup(pipes[out], out);
4515/// closem(FDT_UNUSED, 0);
4516/// cmdpush(CS_CMDSUBST);
4517/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
4518/// cmdpop();
4519/// _realexit();
4520/// ```
4521///
4522/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
4523///     procsubst pid recording site (c:5141-5142) earlier this
4524///     session; the child IS now recorded in `JOBTAB[thisjob]`.
4525/// (b) `entersubsh` IS now ported (exec.rs:3934) including the
4526///     ESUB_PGRP pipeline group-leadership path — wired this
4527///     session for getpipe's `entersubsh(ESUB_ASYNC|ESUB_PGRP|
4528///     ESUB_NOMONITOR, NULL)` call.
4529/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
4530///     can route through execode for the parsed eprog. Currently
4531///     this caller still uses the fusevm pipeline for cache
4532///     coherence with execstring; switch over when the wordcode
4533///     walker becomes the primary path.
4534/// (d) `_realexit()` flushes stdio + jobs + history. We use bare
4535///     `std::process::exit(lastval)` for now.
4536pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
4537    // c:5119
4538    let bytes = cmd.as_bytes();
4539    let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4540        1 // c:5122 — `<(...)` reads from child, child writes to fd 1
4541    } else {
4542        0 // `>(...)` — child reads from fd 0
4543    };
4544    let mut ends_at: usize = 0;
4545    let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
4546    if prog.is_none() {
4547        // c:5127
4548        return -1; // c:5128
4549    }
4550    // c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
4551    if ends_at < bytes.len() && bytes[ends_at] != 0 {
4552        zerr("invalid syntax for process substitution in redirection"); // c:5130
4553        return -1; // c:5131
4554    }
4555    let mut pipes: [i32; 2] = [-1; 2];
4556    if mpipe(&mut pipes) < 0 {
4557        // c:5133
4558        return -1;
4559    }
4560    // c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
4561    let mut bgtime: ZshTimespec = libc::timespec {
4562        tv_sec: 0,
4563        tv_nsec: 0,
4564    };
4565    let pid = zfork(Some(&mut bgtime)); // c:5135
4566    if pid != 0 {
4567        // c:5135 — parent.
4568        let _ = zclose(pipes[out as usize]); // c:5136
4569        if pid == -1 {
4570            // c:5137
4571            let _ = zclose(pipes[(1 - out) as usize]); // c:5138
4572            return -1; // c:5139
4573        }
4574        // c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
4575        if nullexec == 0 {
4576            if let Some(jt) = JOBTAB.get() {
4577                let mut guard = jt.lock().unwrap();
4578                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4579                if tj >= 0 {
4580                    if let Some(j) = guard.get_mut(tj as usize) {
4581                        crate::ported::jobs::addproc(
4582                            j,
4583                            pid,
4584                            "",
4585                            true, // aux=1 for proc subst
4586                            Some(std::time::Instant::now()),
4587                            -1,
4588                            -1,
4589                        );
4590                    }
4591                }
4592            }
4593        }
4594        procsubstpid.store(pid, Ordering::Relaxed); // c:5143
4595        return pipes[(1 - out) as usize]; // c:5144
4596    }
4597    // c:5146 — child path.
4598    entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
4599    let _ = redup(pipes[out as usize], out); // c:5147
4600    closem(FDT_UNUSED, 0); // c:5148
4601    cmdpush(CS_CMDSUBST as u8); // c:5149
4602                                // c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
4603    let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4604    let body = if body_end > 2 && body_end <= bytes.len() {
4605        &cmd[2..body_end]
4606    } else {
4607        ""
4608    };
4609    let _ = crate::ported::exec::execute_script_zsh_pipeline(body);
4610    cmdpop(); // c:5151
4611              // c:5152 — _realexit() — WARNING (d).
4612    std::process::exit(LASTVAL.load(Ordering::Relaxed));
4613}
4614
4615/// Port of `static void spawnpipes(LinkList l, int nullexec)` from
4616/// `Src/exec.c:5184`.
4617///
4618/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
4619/// entry fires `getpipe(name, nullexec || varid)` and stashes the
4620/// resulting fd into `f->fd2`.
4621///
4622/// ```c
4623/// LinkNode n;
4624/// Redir f;
4625/// char *str;
4626/// n = firstnode(l);
4627/// for (; n; incnode(n)) {
4628///     f = (Redir) getdata(n);
4629///     if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
4630///         str = f->name;
4631///         f->fd2 = getpipe(str, nullexec || f->varid);
4632///     }
4633/// }
4634/// ```
4635///
4636/// =================== WARNING — DIVERGENCE ====================
4637/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
4638/// in place of `LinkList`. The walk is identical; the only behavior
4639/// difference is that LinkList iteration in C lets callers splice
4640/// nodes mid-walk — we never do that here so it's a no-op divergence.
4641/// =============================================================
4642pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
4643    // c:5184
4644    for f in l.iter_mut() {
4645        // c:5191
4646        if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
4647            // c:5193
4648            let str_ = f.name.clone().unwrap_or_default(); // c:5194
4649            let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
4650                1
4651            } else {
4652                nullexec
4653            };
4654            f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
4655        }
4656    }
4657}
4658
4659/// Port of `static int cancd2(char *s)` from `Src/exec.c:6411`.
4660///
4661/// C body:
4662/// ```c
4663/// struct stat buf;
4664/// char *us, *us2 = NULL;
4665/// int ret;
4666/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
4667///     if (*s != '/')
4668///         us = tricat(pwd[1] ? pwd : "", "/", s);
4669///     else
4670///         us = ztrdup(s);
4671///     fixdir(us2 = us);
4672/// } else
4673///     us = unmeta(s);
4674/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
4675/// if (us2) free(us2);
4676/// return ret;
4677/// ```
4678///
4679/// True iff `s` is a directory we can `cd` into (X-perm). With
4680/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
4681/// (joining with PWD if relative) so `cd /foo/bar/..` works without
4682/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
4683pub fn cancd2(s: &str) -> i32 {
4684    // c:6411
4685    let us: String;
4686    // c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
4687    let chasedots = isset(CHASEDOTS); // c:6422
4688    let chaselinks = isset(CHASELINKS);
4689    if !chasedots && !chaselinks {
4690        // c:6422
4691        // c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
4692        let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
4693        let mut raw = if !s.starts_with('/') {
4694            // c:6423
4695            format!(
4696                "{}/{}",
4697                if pwd_str.len() > 1 { &pwd_str[..] } else { "" },
4698                s
4699            )
4700        } else {
4701            s.to_string()
4702        };
4703        // c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
4704        raw = fixdir(&raw);
4705        us = raw;
4706    } else {
4707        // c:6428
4708        us = unmeta(s); // c:6429
4709    }
4710    // c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
4711    let cstr = match std::ffi::CString::new(us.as_str()) {
4712        Ok(c) => c,
4713        Err(_) => return 0,
4714    };
4715    if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
4716        return 0;
4717    }
4718    let meta = match std::fs::metadata(&us) {
4719        Ok(m) => m,
4720        Err(_) => return 0,
4721    };
4722    if !meta.file_type().is_dir() {
4723        return 0;
4724    }
4725    1
4726}
4727
4728/// Port of `char *cancd(char *s)` from `Src/exec.c:6370`.
4729///
4730/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
4731/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
4732/// it, else `None`.
4733///
4734/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
4735/// `doprintdir = -1` when a non-trivial path is found (so `cd`
4736/// echoes the resolved path).
4737pub fn cancd(s: &str) -> Option<String> {
4738    // c:6370
4739    // c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
4740    //                (s[1]=='.' && (s[2]=='/' || !s[2])))`.
4741    let bytes = s.as_bytes();
4742    let nocdpath = bytes.first().copied() == Some(b'.')
4743        && (bytes.get(1).copied() == Some(b'/')
4744            || bytes.get(1).is_none()
4745            || (bytes.get(1).copied() == Some(b'.')
4746                && (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
4747    // c:6376 — `if (*s != '/')` branch.
4748    if !s.starts_with('/') {
4749        // c:6376
4750        // c:6379-6380 — `if (cancd2(s)) return s;`
4751        if cancd2(s) != 0 {
4752            return Some(s.to_string());
4753        }
4754        // c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
4755        let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
4756        if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
4757            return None; // c:6382
4758        }
4759        // c:6383-6397 — CDPATH walk.
4760        if !nocdpath {
4761            let cdpath_str = getsparam("CDPATH").unwrap_or_default();
4762            for cp in cdpath_str.split(':') {
4763                // c:6384
4764                let sbuf = if !cp.is_empty() {
4765                    format!("{}/{}", cp, s) // c:6386
4766                } else {
4767                    s.to_string() // c:6391
4768                };
4769                if cancd2(&sbuf) != 0 {
4770                    // c:6393
4771                    DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
4772                    return Some(sbuf); // c:6395
4773                }
4774            }
4775        }
4776        // c:6398-6403 — `cd_able_vars()` fallback.
4777        if let Some(t) = cd_able_vars(s) {
4778            // c:6398
4779            if cancd2(&t) != 0 {
4780                // c:6399
4781                DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
4782                return Some(t); // c:6401
4783            }
4784        }
4785        return None; // c:6404
4786    }
4787    // c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
4788    if cancd2(s) != 0 {
4789        Some(s.to_string())
4790    } else {
4791        None
4792    }
4793}
4794
4795/// Port of `char *simple_redir_name(Eprog prog, int redir_type)` from
4796/// `Src/exec.c:4689`.
4797///
4798/// Test if an Eprog encodes a single simple-command consisting of a
4799/// SINGLE redirection of the requested type with NO command body
4800/// (the `cat < foo` shape). When true, returns the redir target name
4801/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
4802/// `open(2)` instead of fork+pipe+exec.
4803///
4804/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
4805/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
4806/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
4807/// same shape — this port replicates the same offset reads.
4808pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
4809    // c:4689
4810    let pc = &prog.prog;
4811    // c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
4812    // offsets matching C's `pc[0]..pc[6]` checks.
4813    if pc.len() < 7 {
4814        return None;
4815    }
4816
4817    if wc_code(pc[0]) != WC_LIST
4818        || (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0  // c:4695
4819        || wc_code(pc[1]) != WC_SUBLIST
4820        || WC_SUBLIST_FLAGS(pc[1]) != 0  // c:4696
4821        || WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END  // c:4697
4822        || wc_code(pc[2]) != WC_PIPE
4823        || WC_PIPE_TYPE(pc[2]) != WC_PIPE_END  // c:4698
4824        || wc_code(pc[3]) != WC_REDIR
4825        || WC_REDIR_TYPE(pc[3]) != redir_type  // c:4699
4826        || WC_REDIR_VARID(pc[3]) != 0  // c:4700
4827        || pc[4] != 0  // c:4701
4828        || wc_code(pc[6]) != WC_SIMPLE
4829        || WC_SIMPLE_ARGC(pc[6]) != 0
4830    // c:4702
4831    {
4832        return None; // c:4706
4833    }
4834    // c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
4835    Some(dupstring(&ecrawstr(prog, 5, None)))
4836}
4837
4838/// Port of `int getherestr(struct redir *fn)` from `Src/exec.c:4655`.
4839///
4840/// C body:
4841/// ```c
4842/// char *s, *t;
4843/// int fd, len;
4844/// t = fn->name;
4845/// singsub(&t);
4846/// untokenize(t);
4847/// unmetafy(t, &len);
4848/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
4849///     t[len++] = '\n';
4850/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
4851///     return -1;
4852/// write_loop(fd, t, len);
4853/// close(fd);
4854/// fd = open(s, O_RDONLY | O_NOCTTY);
4855/// unlink(s);
4856/// return fd;
4857/// ```
4858///
4859/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
4860/// tempfile, then re-open read-only and unlink — gives the consumer a
4861/// read fd whose backing file is already cleaned up.
4862pub fn getherestr(fn_: &redir) -> i32 {
4863    // c:4655
4864    let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
4865    t = singsub(&t); // c:4661
4866    t = untokenize(&t); // c:4662
4867                        // c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
4868                        // Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
4869    let mut bytes: Vec<u8> = t.into_bytes();
4870    let _len = unmetafy(&mut bytes);
4871    // c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
4872    if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
4873        // c:4671
4874        bytes.push(b'\n'); // c:4672
4875    }
4876    // c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
4877    let (fd, s) = match gettempfile(None) {
4878        Some(p) => p,
4879        None => return -1, // c:4674
4880    };
4881    // c:4675 — `write_loop(fd, t, len);`
4882    let _ = write_loop(fd, &bytes); // c:4675
4883                                    // c:4676 — `close(fd);`
4884    let _ = zclose(fd); // c:4676
4885                        // c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
4886    let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
4887    let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
4888                                                                                        // c:4678 — `unlink(s);`
4889    unsafe {
4890        libc::unlink(cstr.as_ptr());
4891    } // c:4678
4892    new_fd // c:4679
4893}
4894
4895/// Port of `void quote_tokenized_output(char *str, FILE *file)` from
4896/// `Src/exec.c:2114`.
4897///
4898/// C body (abridged):
4899/// ```c
4900/// for (; *s; s++) {
4901///     switch (*s) {
4902///         case Meta: putc(*++s ^ 32, file); continue;
4903///         case Nularg: continue;
4904///         case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
4905///             putc('\\', file); break;
4906///         case '\t': fputs("$'\\t'", file); continue;
4907///         case '\n': fputs("$'\\n'", file); continue;
4908///         case '\r': fputs("$'\\r'", file); continue;
4909///         case '=': if (s == str) putc('\\', file); break;
4910///         default:
4911///             if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
4912///     }
4913///     putc(*s, file);
4914/// }
4915/// ```
4916///
4917/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
4918/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
4919/// surface as unescaped chars (`*`, `(`) while literal special chars
4920/// get backslash-escaped — round-tripping through the shell.
4921pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
4922    // c:2114
4923    let bytes = str_in.as_bytes();
4924    let mut i = 0usize;
4925    while i < bytes.len() {
4926        // c:2118 `for (; *s; s++)`
4927        let c = bytes[i];
4928        match c {
4929            x if x == Meta => {
4930                // c:2120 — `case Meta: putc(*++s ^ 32, file);`
4931                if i + 1 < bytes.len() {
4932                    file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
4933                    i += 2;
4934                } else {
4935                    i += 1;
4936                }
4937                continue; // c:2122
4938            }
4939            x if x as char == Nularg => {
4940                // c:2124
4941                i += 1;
4942                continue; // c:2126
4943            }
4944            b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']' | b'*'
4945            | b'?' | b'$' | b' ' => {
4946                // c:2128-2142
4947                file.write_all(b"\\")?; // c:2143
4948            }
4949            b'\t' => {
4950                // c:2146
4951                file.write_all(b"$'\\t'")?; // c:2147
4952                i += 1;
4953                continue;
4954            }
4955            b'\n' => {
4956                // c:2150
4957                file.write_all(b"$'\\n'")?; // c:2151
4958                i += 1;
4959                continue;
4960            }
4961            b'\r' => {
4962                // c:2154
4963                file.write_all(b"$'\\r'")?; // c:2155
4964                i += 1;
4965                continue;
4966            }
4967            b'=' => {
4968                // c:2158 — `if (s == str) putc('\\', file);`
4969                if i == 0 {
4970                    file.write_all(b"\\")?; // c:2160
4971                }
4972            }
4973            _ => {
4974                // c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
4975                if itok(c) {
4976                    // c:2164
4977                    let pound = Pound as u8;
4978                    if c >= pound {
4979                        let idx = (c - pound) as usize;
4980                        let zt = ztokens.as_bytes();
4981                        if idx < zt.len() {
4982                            file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
4983                        }
4984                    }
4985                    i += 1;
4986                    continue;
4987                }
4988            }
4989        }
4990        file.write_all(&[c])?; // c:2171
4991        i += 1;
4992    }
4993    Ok(())
4994}
4995
4996// =====================================================================
4997// Wordcode-VM control-flow dispatch — faithful ports of the C
4998// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
4999//
5000// Each function below takes `&mut estate` and returns `i32` to mirror
5001// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
5002// line `// c:NNN` citations track the C source line.
5003//
5004// zshrs's primary execution path is the fusevm bytecode VM. These
5005// wordcode-VM entries exist for C-name parity with the upstream
5006// interpreter so that future bridging code can drive zshrs through
5007// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
5008// zshrs primitives don't yet model their C counterpart (e.g.
5009// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
5010// helper is declared with a comment citing the C source file:line
5011// where the canonical body lives — same pattern as the canonical
5012// `ksh93::ksh93_wrapper` port at c:152-227.
5013// =====================================================================
5014
5015use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
5016use crate::ported::pattern::{patcompile, pattry};
5017use crate::ported::r#loop::try_tryflag;
5018
5019// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
5020use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
5021use crate::ported::linklist::LinkList;
5022use crate::ported::mem::freeheap;
5023use crate::ported::params::setloopvar;
5024use crate::ported::params::{assignaparam, assignsparam, unsetparam};
5025use crate::ported::parse::{ecgetlist, ecgetstr};
5026use crate::ported::pattern::haswilds;
5027use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
5028use crate::ported::subst::{globlist, prefork};
5029use crate::ported::zsh_h::{
5030    estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
5031    WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP,
5032    WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
5033    WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
5034    WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
5035};
5036use crate::ported::zsh_h::{
5037    ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
5038    PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
5039    WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
5040};
5041use crate::ported::zsh_h::{
5042    CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
5043    CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
5044};
5045
5046// --- Local stubs for C primitives not yet ported elsewhere ------------
5047//
5048// These mirror the C functions of the same names. Each cites the C
5049// source file:line where the canonical body lives. They are inlined
5050// here (rather than a separate `pub fn` in the owning C-file module)
5051// because the owning ports are pending the wider exec-substrate
5052// work (sub-PR). Once those land, these locals collapse to direct
5053// `crate::ported::<owner>::<fn>` calls.
5054
5055/// Port of `void execsubst(LinkList strs)` from `Src/exec.c:2684`.
5056///
5057/// C body (c:2684-2693):
5058/// ```c
5059/// void execsubst(LinkList strs) {
5060///     if (strs) {
5061///         prefork(strs, esprefork, NULL);
5062///         if (esglob && !errflag) {
5063///             LinkList ostrs = strs;
5064///             globlist(strs, 0);
5065///             strs = ostrs;
5066///         }
5067///     }
5068/// }
5069/// ```
5070///
5071/// `execsubst` runs `prefork` (parameter / arithmetic / command
5072/// substitution expansion + IFS-split) over the whole list, then
5073/// (when `esglob` is set) `globlist` to do filename globbing on the
5074/// result.
5075fn execsubst(list: &mut Vec<String>) {
5076    // c:2684
5077    if list.is_empty() {
5078        return; // c:2686 `if (strs)`
5079    }
5080    let mut ll: crate::ported::subst::LinkList = std::mem::take(list).into_iter().collect();
5081    let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
5082    let mut rf: i32 = 0;
5083    prefork(&mut ll, prefork_flags, &mut rf); // c:2687
5084    if esglob.load(Ordering::Relaxed) != 0 && errflag.load(Ordering::Relaxed) == 0 {
5085        // c:2688 `if (esglob && !errflag)`
5086        globlist(&mut ll, 0); // c:2690
5087    }
5088    *list = ll.into_iter().collect();
5089}
5090
5091/// Direct port of `static void addvars(Estate state, Wordcode pc,
5092/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
5093/// nodes stacked inline of a simple command — the `var=value` and
5094/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
5095/// at `pc`, extracts each assignment's name + value (scalar or array),
5096/// optionally preforks + globs the tokenised RHS, and routes through
5097/// `assignsparam` (scalar) or `assignaparam` (array).
5098///
5099/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
5100/// stderr (C uses xtrerr; zshrs uses eprint!).
5101///
5102/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
5103/// into the file-static `STTYval` for `execute()` to apply pre-exec.
5104fn addvars(state: &mut estate, pc: usize, addflags: i32) {
5105    // c:2501 — locals.
5106    let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
5107    let xtr: bool; // c:2502 `int xtr,`
5108    let mut isstr: bool; // c:2502 `int isstr,`
5109    let mut htok: i32 = 0; // c:2502 `int htok = 0;`
5110    let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
5111    let mut name: String;
5112    let mut flags: i32; // c:2504 `int flags;`
5113    let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
5114    let mut ac: wordcode; // c:2507 `wordcode ac;`
5115                          // c:2508 `local_list1(svl);` — stack-local one-element LinkList
5116                          // for the scalar-assignment path. Rust uses a fresh LinkList per
5117                          // iteration; equivalent semantics.
5118
5119    // c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
5120    // when the assignment list is implicitly local (ADDVAR_RESTORE).
5121    flags = if (addflags & ADDVAR_RESTORE) == 0 {
5122        ASSPM_WARN // c:2516
5123    } else {
5124        0 // c:2516
5125    };
5126    xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
5127    if xtr {
5128        // c:2518
5129        printprompt4(); // c:2519
5130        doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
5131    }
5132    state.pc = pc; // c:2522 `state->pc = pc;`
5133
5134    // c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
5135    loop {
5136        if state.pc >= state.prog.prog.len() {
5137            break;
5138        }
5139        ac = state.prog.prog[state.pc];
5140        state.pc += 1;
5141        if wc_code(ac) != WC_ASSIGN {
5142            // Step back so the WC_SIMPLE / outer dispatcher sees the
5143            // non-assignment opcode. C's `state->pc++` post-increment
5144            // already pointed past WC_ASSIGN; we need to unconsume.
5145            state.pc -= 1;
5146            break;
5147        }
5148        let mut myflags = flags; // c:2524 `int myflags = flags;`
5149        name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
5150        if htok != 0 {
5151            // c:2526 `if (htok) untokenize(name);`
5152            name = untokenize(&name).to_string(); // c:2527
5153        }
5154        if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5155            // c:2528
5156            myflags |= ASSPM_AUGMENT; // c:2529
5157        }
5158        if xtr {
5159            // c:2530
5160            // c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
5161            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5162                eprint!("{}+=", name); // c:2532
5163            } else {
5164                eprint!("{}=", name); // c:2532
5165            }
5166        }
5167
5168        // c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
5169        isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
5170        if isstr {
5171            // c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
5172            let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
5173            vl = LinkList::new();
5174            vl.push_back(svl_val);
5175            // c:2535 `vl = &svl;` — vl already points at the new list.
5176        } else {
5177            // c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
5178            let items = ecgetlist(
5179                state,
5180                WC_ASSIGN_NUM(ac) as usize,
5181                EC_DUPTOK,
5182                Some(&mut htok),
5183            );
5184            vl = LinkList::new();
5185            for it in items {
5186                vl.push_back(it);
5187            }
5188            if errflag.load(Ordering::Relaxed) != 0 {
5189                // c:2538-2541
5190                state.pc = opc; // c:2539
5191                return; // c:2540
5192            }
5193        }
5194
5195        // c:2544 `if (vl && htok) {`
5196        if htok != 0 {
5197            // c:2545 `int prefork_ret = 0;`
5198            let mut prefork_ret: i32 = 0;
5199            // c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
5200            //                          : PREFORK_ASSIGN), &prefork_ret);
5201            let pf_flags = if isstr {
5202                PREFORK_SINGLE | PREFORK_ASSIGN
5203            } else {
5204                PREFORK_ASSIGN
5205            };
5206            prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
5207            if errflag.load(Ordering::Relaxed) != 0 {
5208                // c:2548
5209                state.pc = opc; // c:2549
5210                return; // c:2550
5211            }
5212            if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
5213                // c:2552
5214                myflags |= ASSPM_KEY_VALUE; // c:2553
5215            }
5216            // c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
5217            //                  haswilds((char *)getdata(firstnode(vl)))))`
5218            let needs_glob = if !isstr {
5219                true
5220            } else {
5221                isset(GLOBASSIGN)
5222                    && isstr
5223                    && !vl.is_empty()
5224                    && haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
5225            };
5226            if needs_glob {
5227                globlist(&mut vl, prefork_ret); // c:2556
5228                                                // c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
5229                                                //                  unsetparam(name);`
5230                if isset(GLOBASSIGN) && isstr {
5231                    unsetparam(&name); // c:2562
5232                }
5233                if errflag.load(Ordering::Relaxed) != 0 {
5234                    // c:2563
5235                    state.pc = opc; // c:2564
5236                    return; // c:2565
5237                }
5238            }
5239        }
5240        // c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
5241        // — scalar-assignment path: zero or one element after prefork.
5242        if isstr && (vl.is_empty() || vl.len() == 1) {
5243            let val: String; // c:2571 `char *val;`
5244            if vl.is_empty() {
5245                // c:2574
5246                val = String::new(); // c:2575 `val = ztrdup("");`
5247            } else {
5248                // c:2577 `untokenize(peekfirst(vl));`
5249                let peek = vl.nodes.front().cloned().unwrap_or_default();
5250                val = untokenize(&peek).to_string(); // c:2577-2578
5251                                                     // c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
5252                                                     // we just cloned the front above. Equivalent.
5253            }
5254            if xtr {
5255                // c:2580
5256                eprint!("{}", quotedzputs(&val)); // c:2581
5257                eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
5258            }
5259            // c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
5260            let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
5261                // c:2585 `if (strcmp(name, "STTY") == 0)`
5262                if name == "STTY" {
5263                    // c:2586-2587 — `STTYval = ztrdup(val);`
5264                    let mut stty = STTYval.lock().unwrap();
5265                    *stty = Some(val.clone()); // c:2587
5266                }
5267                // c:2589 `allexp = opts[ALLEXPORT];`
5268                let allexp = isset(ALLEXPORT);
5269                // c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
5270                opt_state_set("allexport", true);
5271                if isset(KSHARRAYS) {
5272                    // c:2591
5273                    unsetparam(&name); // c:2592
5274                }
5275                let pm = assignsparam(&name, &val, myflags); // c:2593
5276                                                             // c:2594 `opts[ALLEXPORT] = allexp;` — restore.
5277                opt_state_set("allexport", allexp);
5278                pm
5279            } else {
5280                // c:2595
5281                assignsparam(&name, &val, myflags) // c:2596
5282            };
5283            if pm.is_none() {
5284                // c:2597 `if (!pm)`
5285                LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
5286                                                     // c:2599-2604 — "cheating" comment: don't zerr.
5287                if cmdoutval.load(Ordering::Relaxed) == 0 {
5288                    // c:2605 `if (!cmdoutval)`
5289                    cmdoutval.store(1, Ordering::Relaxed); // c:2606
5290                }
5291            }
5292            if errflag.load(Ordering::Relaxed) != 0 {
5293                // c:2608
5294                state.pc = opc; // c:2609
5295                return; // c:2610
5296            }
5297            continue; // c:2612
5298        }
5299        // c:2614 `if (vl) { ... }` — array-assignment path: drain vl
5300        // into a fresh `char **arr`.
5301        // c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
5302        arr = Vec::with_capacity(vl.len() + 1);
5303        while let Some(s) = vl.pop_front() {
5304            arr.push(s);
5305        }
5306        // c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
5307        if xtr {
5308            // c:2624
5309            eprint!("( "); // c:2625
5310            for s in &arr {
5311                // c:2626 `for (ptr = arr; *ptr; ptr++)`
5312                eprint!("{}", quotedzputs(s)); // c:2627
5313                eprint!(" "); // c:2628
5314            }
5315            eprint!(") "); // c:2630
5316        }
5317        // c:2632 `if (!assignaparam(name, arr, myflags))`
5318        if assignaparam(&name, arr, myflags).is_none() {
5319            LASTVAL.store(1, Ordering::Relaxed); // c:2633
5320                                                 // c:2634-2638 — "cheating" comment.
5321            if cmdoutval.load(Ordering::Relaxed) == 0 {
5322                // c:2639
5323                cmdoutval.store(1, Ordering::Relaxed); // c:2640
5324            }
5325        }
5326        if errflag.load(Ordering::Relaxed) != 0 {
5327            // c:2642
5328            state.pc = opc; // c:2643
5329            return; // c:2644
5330        }
5331    }
5332    state.pc = opc; // c:2647 `state->pc = opc;`
5333}
5334
5335// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
5336// match expression at the call sites in execsimple. Not a separate
5337// Rust fn — every C-side reference to
5338// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
5339
5340// --- exec.c entries ---------------------------------------------------
5341
5342/// Port of `execcursh(Estate state, int do_exec)` from
5343/// `Src/exec.c:469-498`. Execute a `{ ... }` current-shell command
5344/// group: skip the trailing try-only word, optionally drop a stale
5345/// job slot, then run the inner list.
5346pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
5347    // c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
5348    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5349    let end = state.pc + WC_CURSH_SKIP(prior) as usize;
5350    // c:475 — `state->pc++;` skip the try/always-only word.
5351    state.pc += 1;
5352    // c:482-486 — drop empty job slot before nested cmd: if outer-pipe
5353    // bookkeeping is clean AND thisjob is a real job that's not the
5354    // pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
5355    // leaking job-table slots when execcursh recurses.
5356    {
5357        let lp = list_pipe.load(Ordering::Relaxed);
5358        let lpj = list_pipe_job.load(Ordering::Relaxed);
5359        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5360        if lp == 0 && tj != -1 && tj != lpj {
5361            if let Some(jt) = JOBTAB.get() {
5362                let mut guard = jt.lock().unwrap();
5363                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5364                if !has {
5365                    if let Some(j) = guard.get_mut(tj as usize) {
5366                        crate::ported::jobs::deletejob(j, false);
5367                    }
5368                }
5369            }
5370        }
5371    }
5372    cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
5373    let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
5374    cmdpop(); // c:489 — `cmdpop();`
5375    state.pc = end; // c:491 — `state->pc = end;`
5376    this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
5377    LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
5378}
5379
5380// `(...)` subshell — no dedicated C function (handled inline by
5381// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
5382// In zshrs the subshell branch is folded into `execpline` and
5383// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
5384// inner-list walk since fusevm bytecode handles the forking via
5385// Op::Subshell at a higher layer.
5386
5387/// Port of `execcond(Estate state, UNUSED(int do_exec))` from
5388/// `Src/exec.c:5204-5232`. Run a `[[ ... ]]` cond expression.
5389pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
5390    state.pc -= 1; // c:5208 — `state->pc--;`
5391                   // c:5209-5213 — XTRACE prelude.
5392    if isset(XTRACE) {
5393        printprompt4();
5394        eprint!("[[");
5395        // c:5212 — `tracingcond++;` not modeled in zshrs.
5396    }
5397    cmdpush(CS_COND as u8); // c:5214
5398                            // c:5215 — `stat = evalcond(state, NULL);` — TODO faithful: needs
5399                            // the wordcode-level evalcond from Src/cond.c which is distinct
5400                            // from the test-builtin evalcond ported in cond.rs. Pending.
5401    let stat: i32 = 0;
5402    // c:5219-5221 — `if (stat == 2) errflag |= ERRFLAG_ERROR;`
5403    if stat == 2 {
5404        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
5405    }
5406    cmdpop(); // c:5222
5407    if isset(XTRACE) {
5408        eprintln!(" ]]");
5409    }
5410    stat // c:5230 — `return stat;`
5411}
5412
5413/// Port of `execarith(Estate state, UNUSED(int do_exec))` from
5414/// `Src/exec.c:5237-5275`. Run a `(( ... ))` arithmetic command;
5415/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
5416/// parse error.
5417pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
5418    if isset(XTRACE) {
5419        printprompt4();
5420        eprint!("((");
5421    }
5422    cmdpush(CS_MATH as u8); // c:5247
5423    let mut htok: i32 = 0;
5424    let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
5425    if htok != 0 {
5426        e = singsub(&e); // c:5250 — `singsub(&e);`
5427    }
5428    if isset(XTRACE) {
5429        eprint!(" {}", e);
5430    }
5431    let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
5432    cmdpop(); // c:5256
5433    if isset(XTRACE) {
5434        eprintln!(" ))");
5435    }
5436    // c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
5437    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
5438        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
5439        return 2;
5440    }
5441    // c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
5442    let val = val_result.unwrap();
5443    if val.type_ == MN_INTEGER {
5444        if val.l == 0 {
5445            1
5446        } else {
5447            0
5448        }
5449    } else if val.d == 0.0 {
5450        1
5451    } else {
5452        0
5453    }
5454}
5455
5456/// Port of `exectime(Estate state, UNUSED(int do_exec))` from
5457/// `Src/exec.c:5279-5294`. Run `time pipeline`: drives execpline with
5458/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
5459pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
5460    let jb = *THISJOB
5461        .get_or_init(|| std::sync::Mutex::new(-1))
5462        .lock()
5463        .unwrap(); // c:5283
5464    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5465    // c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
5466    if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
5467        // c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
5468        // shell+kids time deltas since last call.
5469        crate::ported::jobs::shelltime(None, None, None, 0);
5470        return 0; // c:5286
5471    }
5472    // c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
5473    let slcode = state.prog.prog[state.pc];
5474    state.pc += 1;
5475    use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
5476    let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
5477    *THISJOB
5478        .get_or_init(|| std::sync::Mutex::new(-1))
5479        .lock()
5480        .unwrap() = jb; // c:5289
5481    LASTVAL.load(Ordering::Relaxed) // c:5290
5482}
5483
5484/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
5485/// Promoted to top-level pub fn so execcmd_exec at the shfunc
5486/// dispatch site (c:4102-4105) can route through it. The real port
5487/// owns queue_signals + cmdstack + sfcontext setup before calling
5488/// doshfunc; doshfunc itself is unported, so we route the body
5489/// through `runshfunc` (exec.rs:1700), which carries the
5490/// wrapper-chain + zunderscore restore. Degraded vs C (no cmdstack
5491/// push, no sfcontext flip, no XTRACE arg-trace) but the function
5492/// body executes and `lastval` is updated.
5493pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
5494    // c:5546-5547 — `if (errflag) return;`
5495    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
5496        return;
5497    }
5498    // c:5550-5557 — drop empty job slot before nested shfunc invoke:
5499    // if outer-pipe bookkeeping is clean AND thisjob is a real job
5500    // that's not the pipe-leader AND has no procs yet, deletejob()
5501    // recycles it. Avoids leaking job-table slots across recursive
5502    // function calls. Same pattern as execcursh's c:482-486.
5503    {
5504        let lp = list_pipe.load(Ordering::Relaxed);
5505        let lpj = list_pipe_job.load(Ordering::Relaxed);
5506        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5507        if lp == 0 && tj != -1 && tj != lpj {
5508            if let Some(jt) = JOBTAB.get() {
5509                let mut guard = jt.lock().unwrap();
5510                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5511                if !has {
5512                    // c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
5513                    //                jobtab[thisjob].filelist = NULL;` — preserve
5514                    //                the filelist so deletejob doesn't unlink temp
5515                    //                files. Rust take()s the Vec into a local.
5516                    let _last_file_list: Vec<jobfile> = if let Some(j) = guard.get_mut(tj as usize) {
5517                        std::mem::take(&mut j.filelist)
5518                    } else {
5519                        Vec::new()
5520                    };
5521                    if let Some(j) = guard.get_mut(tj as usize) {
5522                        crate::ported::jobs::deletejob(j, false); // c:5556
5523                    }
5524                }
5525            }
5526        }
5527    }
5528    // c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
5529    // emit PS4 prefix + space-separated quoted args on the trace
5530    // stream so `set -x` shows the function invocation line.
5531    if isset(XTRACE) {
5532        printprompt4();
5533        for (i, a) in args.iter().enumerate() {
5534            if i > 0 {
5535                eprint!(" ");
5536            }
5537            eprint!("{}", quotedzputs(a));
5538        }
5539        eprintln!();
5540    }
5541    // c:5572-5578 cmdstack/sfcontext setup: omit (no cmdstack in
5542    // zshrs yet — replaced by tracing).
5543    // c:5580 — `doshfunc(shf, args, 0);` — doshfunc swaps PPARAMS
5544    // ($1, $2, …) to the function's args, runs the body via
5545    // runshfunc, then restores. doshfunc itself isn't ported yet
5546    // so we do the swap-and-restore inline here.
5547    // c:5580 — `doshfunc(shf, args, 0);`. The C path always has
5548    // `funcdef` populated since C parses at definition time. zshrs
5549    // compiles to fusevm chunks instead, so `funcdef` is None for
5550    // user-defined functions; only `body` (source string) carries
5551    // the definition. When that's the case, build a one-shot eprog
5552    // whose `strs` carries the source so runshfunc's script-pipeline
5553    // arm (execute_script_zsh_pipeline) executes the body.
5554    let prog_owned: Option<eprog> = if shf.funcdef.is_some() {
5555        None
5556    } else if let Some(ref body) = shf.body {
5557        Some(eprog {
5558            strs: Some(body.clone()),
5559            ..Default::default()
5560        })
5561    } else {
5562        None
5563    };
5564    let prog_ref: Option<&eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
5565        (Some(p), _) => Some(p),
5566        (_, Some(p)) => Some(p),
5567        _ => None,
5568    };
5569    if let Some(_prog) = prog_ref {
5570        // c:5580 — `doshfunc(shf, args, 0);`. Direct doshfunc call —
5571        // noreturnval=0 means the body's return value updates LASTVAL
5572        // (caller of execfuncdef reads it back). PPARAMS swap +
5573        // restore happens INSIDE doshfunc's scope; body_runner just
5574        // runs the body.
5575        let name_for_body = shf.node.nam.clone();
5576        let body_args_owned: Vec<String> = if args.len() > 1 {
5577            args[1..].to_vec()
5578        } else {
5579            Vec::new()
5580        };
5581        let body_runner = move || -> i32 {
5582            crate::ported::exec::run_function_body(&name_for_body, &body_args_owned)
5583                .unwrap_or(0)
5584        };
5585        let _ = doshfunc(shf, args.clone(), false, body_runner);
5586    }
5587    // c:5582-5589 cmdstack restore/free: omit (no cmdstack).
5588}
5589
5590/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
5591/// from `Src/exec.c:5823-6158`.
5592///
5593/// C body's scope-management sequence ported here. The C source's
5594/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
5595/// is replaced by `body_runner` — zshrs runs function bodies through
5596/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
5597/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
5598/// callback hands the live executor back to the caller (typically
5599/// the fusevm bridge) for the actual body run. Every line of scope
5600/// save/restore around the body call mirrors C exactly.
5601///
5602/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
5603/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
5604/// c:6042; zshrs delegates to a closure because the body-execution
5605/// pipeline (fusevm) differs from C's (wordcode). The closure
5606/// fully replaces the runshfunc call and returns the body's exit
5607/// status (which doshfunc reads as `lastval` for the `noreturnval`
5608/// path).
5609#[allow(non_snake_case)]
5610pub fn doshfunc(
5611    shfunc: &mut shfunc,                  // c:5823
5612    doshargs: Vec<String>,                // c:5823
5613    noreturnval: bool,                    // c:5823
5614    mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
5615) -> i32 {
5616    use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
5617    use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
5618    use crate::ported::modules::parameter::FUNCSTACK;
5619    use crate::ported::params::endparamscope;
5620    use crate::ported::params::locallevel as locallevel_atomic;
5621    use crate::ported::zsh_h::{FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_UNDEFINED};
5622    use std::sync::atomic::Ordering;
5623
5624    let name = shfunc.node.nam.clone(); // c:5827
5625    let flags = shfunc.node.flags; // c:5828
5626    let fname = dupstring(&name); // c:5829
5627    let _ = fname; // c:5829 (kept for parity)
5628
5629    // c:5835 — `queue_signals();` Lots of memory + global-state changes.
5630    queue_signals();
5631
5632    // c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
5633    // Pinned so a recursive unload doesn't free the eprog under us.
5634    // (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
5635    // already pin until call ends. C does explicit refcount on
5636    // `funcdef->nref` via useeprog.)
5637
5638    // c:5856-5916 — Funcsave allocation + per-field snapshot.
5639    let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
5640    let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
5641    let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
5642    let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
5643    let funcsave_numpipestats = {
5644        // c:5864
5645        NUMPIPESTATS
5646            .get_or_init(|| std::sync::Mutex::new(0))
5647            .lock()
5648            .map(|n| *n)
5649            .unwrap_or(0)
5650    };
5651    let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
5652                                                                // c:5866-5867 — trap_state PRIMED branch decrements trap_return.
5653    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
5654        // c:5866
5655        TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
5656    }
5657    // c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
5658    // return-suppress so a `return` inside the body fires errexit
5659    // checks normally.
5660    noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
5661
5662    // c:5872-5880 — noreturnval branch: deep-copy pipestats so the
5663    // function body's pipestats writes are restored on exit.
5664    let funcsave_pipestats: Option<Vec<i32>> = if noreturnval {
5665        // c:5872
5666        let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
5667        p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
5668    } else {
5669        None
5670    };
5671
5672    // c:5882-5896 — TRAPEXIT special case (deep-copy shfunc so
5673    // starttrapscope doesn't rug-pull). zshrs doesn't yet support
5674    // running TRAPEXIT directly via doshfunc; flagged for follow-up.
5675    // (Skip: name = "TRAPEXIT" path.)
5676    let _ = name.as_str(); // sentinel for the eventual port.
5677
5678    // c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
5679    // tags SIGEXIT for deferred restoration at scope end.
5680    crate::ported::signals::starttrapscope();
5681    // c:5899 — `startpatternscope();`
5682    crate::ported::pattern::startpatternscope();
5683
5684    // c:5901 — `pptab = pparams;` — save outer positional params.
5685    let pptab: Vec<String> = crate::ported::builtin::PPARAMS
5686        .lock()
5687        .map(|p| p.clone())
5688        .unwrap_or_default();
5689
5690    // c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
5691    let funcsave_scriptname = crate::ported::utils::scriptname_get();
5692    if (flags as u32 & PM_UNDEFINED) == 0 {
5693        // c:5902
5694        crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
5695    }
5696
5697    // c:5904-5908 — `funcsave->zoptind = zoptind; ...` snapshot.
5698    // C zsh saves zoptind (the canonical OPTIND counter) and
5699    // zoptarg into the funcsave struct so OPTIND is implicitly
5700    // function-local: a `getopts` loop inside the function gets
5701    // its own counter that snaps back to the caller's on
5702    // function return. zshrs stores OPTIND/OPTARG in paramtab
5703    // as regular int/string params; snapshot them here and
5704    // restore at scope end. Bug #513.
5705    let funcsave_optind: Option<String> = crate::ported::params::getsparam("OPTIND");
5706    let funcsave_optarg: Option<String> = crate::ported::params::getsparam("OPTARG");
5707    // c:5966-5969 — `if (!isset(POSIXBUILTINS)) { zoptind = 1; optcind = 0; }`.
5708    // The snapshot above is only half the contract: C also RESETS the
5709    // counter on entry so an inner `getopts` loop starts fresh at the
5710    // first positional, independent of the caller's OPTIND. Without
5711    // this, a function whose body runs `getopts` after the caller
5712    // advanced OPTIND mis-parses its own args — e.g. `add-zsh-hook`
5713    // (which `getopts`-parses `precmd func`) printed its usage and
5714    // failed when invoked from a config that had run getopts earlier.
5715    // zshrs keeps the counter in the $OPTIND param plus the ZOPTIND/
5716    // OPTCIND trackers getopts syncs against; reset all three.
5717    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
5718        crate::ported::params::setiparam("OPTIND", 1); // c:5967 zoptind = 1
5719        crate::ported::builtin::ZOPTIND.store(1, Ordering::Relaxed);
5720        crate::ported::builtin::OPTCIND.store(0, Ordering::Relaxed); // c:5968 optcind = 0
5721    }
5722
5723    // c:5914 — `memcpy(funcsave->opts, opts, sizeof(opts));` — option
5724    // snapshot. Port wraps opts in OPTS_LIVE; capture the live state
5725    // here as a HashMap snapshot.
5726    let funcsave_opts = crate::ported::options::opt_state_snapshot();
5727
5728    // c:5915-5916 — `funcsave->emulation/sticky = emulation/sticky;`
5729    // Emulation snapshot pending the sticky-emulation port.
5730
5731    // c:5954-5969 — PM_TAGGED / PM_WARNNESTED option-override block.
5732    // Anonymous-function name comparison via pointer equality in C;
5733    // zshrs uses string equality. Skip until ANONYMOUS_FUNCTION_NAME
5734    // sentinel is ported.
5735
5736    // c:5970 — `funcsave->oflags = oflags;` — module-global tracking
5737    // function-attribute inheritance. Skip until oflags is ported.
5738
5739    // c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
5740    // for inner commands; outer flag restored on exit.
5741    opt_state_set("printexitvalue", false);
5742
5743    // c:5978-5998 — pparams swap. C reads doshargs and constructs the
5744    // function's positional-param array. First arg is the function
5745    // name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
5746    let funcsave_argv0: Option<String> = if !doshargs.is_empty() {
5747        // c:5978
5748        // c:5982-5985 — `pparams = x = zshcalloc(...)`.
5749        let positionals: Vec<String> = if doshargs.len() > 1 {
5750            doshargs[1..].to_vec()
5751        } else {
5752            Vec::new()
5753        };
5754        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5755            *pp = positionals;
5756        }
5757        // c:5984-5987 — FUNCTIONARGZERO: save argzero, install
5758        // doshargs[0] (the function name).
5759        if isset(FUNCTIONARGZERO) {
5760            // c:5984
5761            let prev = crate::ported::utils::argzero();
5762            crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
5763            prev
5764        } else {
5765            None
5766        }
5767    } else {
5768        // c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
5769        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5770            *pp = Vec::new();
5771        }
5772        if isset(FUNCTIONARGZERO) {
5773            // c:5994
5774            let prev = crate::ported::utils::argzero();
5775            crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
5776            prev
5777        } else {
5778            None
5779        }
5780    };
5781
5782    // c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
5783    // since zshrs tracks function-call depth there.
5784    //
5785    // Plus the canonical startparamscope (c:6194 inside runshfunc).
5786    // zshrs's body_runner replaces runshfunc's `execode` call so the
5787    // startparamscope/endparamscope pair must wrap body_runner here,
5788    // not inside the closure. inc_locallevel is exactly startparamscope.
5789    inc_locallevel();
5790
5791    // c:6000-6004 — FUNCNEST check + `goto undoshfunc` on overflow.
5792    // Skip the runtime check (the zshrs fusevm doesn't recurse via
5793    // real stack frames so the depth limit is less critical), but
5794    // keep the comment so the C label `undoshfunc:` target is
5795    // visible — `goto undoshfunc;` here would jump straight to the
5796    // epilogue at the `undoshfunc:` label below.
5797
5798    // c:6005-6019 — funcstack frame push. The full C block:
5799    //   funcsave->fstack.name      = dupstring(name);
5800    //   funcsave->fstack.caller    = funcstack ? funcstack->name :
5801    //                                 dupstring(argv0 ? argv0 : argzero);
5802    //   funcsave->fstack.lineno    = lineno;
5803    //   funcsave->fstack.prev      = funcstack;
5804    //   funcsave->fstack.tp        = FS_FUNC;
5805    //   funcstack                  = &funcsave->fstack;
5806    //   funcsave->fstack.flineno   = shfunc->lineno;
5807    //   funcsave->fstack.filename  = getshfuncfile(shfunc);
5808    // c:6013 — `funcsave->fstack.lineno = lineno;` C has ONE lineno
5809    // global (params.c:123); zshrs mirrors it in both input::lineno
5810    // and lex::LEX_LINENO. The lex mirror is the one driven by
5811    // BUILTIN_SET_LINENO per statement AND zeroed for the duration
5812    // of a function body (see set_lineno(0) below), so it is the
5813    // one that matches C's value at call time: a call made INSIDE a
5814    // caller's body records the caller-relative line (0 for a
5815    // single-line fn), giving `$functrace` entries like `g:0`.
5816    // input::lineno stayed parked at the script-wide line and
5817    // produced `g:1`.
5818    let lineno_now = crate::ported::lex::lineno() as i64;
5819    let (caller, prev_tp): (Option<String>, Option<i32>) = {
5820        let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5821        if let Some(p) = stk.last() {
5822            (Some(p.name.clone()), Some(p.tp))
5823        } else {
5824            // c:6011-6012 — outermost: argv0 (saved) or argzero global.
5825            let z = funcsave_argv0
5826                .clone()
5827                .or_else(crate::ported::utils::argzero);
5828            (z, None)
5829        }
5830    };
5831    // c:6018-6019 — flineno: shfunc->lineno (function def line)
5832    let flineno = shfunc.lineno;
5833    let filename = shfunc.filename.clone().or_else(|| Some(String::new()));
5834    {
5835        let frame = crate::ported::zsh_h::funcstack {
5836            prev: None,             // c:6014 (Vec-stack: index encodes link)
5837            name: dupstring(&name), // c:6005
5838            filename,               // c:6019
5839            caller,                 // c:6011
5840            flineno,                // c:6018
5841            lineno: lineno_now,     // c:6013
5842            tp: FS_FUNC,            // c:6015
5843        };
5844        let _ = prev_tp; // c:6011 (informational)
5845        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5846        stack.push(frame); // c:6016 funcstack = &funcsave->fstack
5847    }
5848
5849    // c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
5850    // zshrs delegates to the body_runner closure (typically a fusevm
5851    // sub-VM run from the bridge). The closure returns the body's
5852    // exit status which becomes lastval.
5853    //
5854    // c:Src/exec.c:1251-1266 — push "shfunc" onto zsh_eval_context
5855    // so the body sees `${zsh_eval_context[*]}` containing the call
5856    // chain context. The execode-based path (c:1245-1282 port at
5857    // exec.rs:7092) already did this, but the fusevm body_runner
5858    // path skipped doshfunc's body_runner invocation without the
5859    // push. Bug #262 in docs/BUGS.md.
5860    //
5861    // Push BOTH the static `zsh_eval_context` (matches C's variable)
5862    // AND the paramtab array entry (what `${zsh_eval_context[*]}`
5863    // reads). Pop on every return path via the guard struct so
5864    // panics / early returns don't leak the entry. Inlined here
5865    // (sole caller) — `zsh_eval_context` is this module's own static.
5866    //
5867    // c:Src/exec.c:1251-1266 — `zsh_eval_context[*]` shell-visible
5868    // mirror: the array entry holds the stack; the scalar
5869    // `ZSH_EVAL_CONTEXT` holds the `:`-joined form. Both written via
5870    // the PM_READONLY bypass (`u_arr`/`u_str` direct), the same shape
5871    // the binary's `-c` ZSH_EVAL_CONTEXT init uses (bins/zshrs.rs).
5872    // A reusable `sync` closure is captured by the guard's Drop so
5873    // the same write happens after the push (here) and the pop (on
5874    // every return path / panic).
5875    let sync_eval_ctx = |stack: &[String]| {
5876        let joined = stack.join(":");
5877        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
5878            if let Some(pm) = tab.get_mut("zsh_eval_context") {
5879                pm.u_arr = Some(stack.to_vec());
5880                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
5881            }
5882            if let Some(pm) = tab.get_mut("ZSH_EVAL_CONTEXT") {
5883                pm.u_str = Some(joined);
5884                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
5885            }
5886        }
5887    };
5888    if let Ok(mut ctx) = zsh_eval_context.lock() {
5889        ctx.push("shfunc".to_string());
5890        sync_eval_ctx(&ctx);
5891    }
5892    struct EvalContextGuard<F: Fn(&[String])>(F);
5893    impl<F: Fn(&[String])> Drop for EvalContextGuard<F> {
5894        fn drop(&mut self) {
5895            if let Ok(mut ctx) = zsh_eval_context.lock() {
5896                ctx.pop();
5897                (self.0)(&ctx);
5898            }
5899        }
5900    }
5901    let _eval_ctx_guard = EvalContextGuard(sync_eval_ctx);
5902    // c:Src/exec.c — function bodies execute with `lineno` reset to
5903    // the relative line within the body (incremented per WC_PIPE
5904    // from the wordcode-encoded lineno). zsh's zerrmsg
5905    // (Src/utils.c:301) emits the lineno prefix only when lineno
5906    // is non-zero AND (!SHINSTDIN || locallevel != 0). For an
5907    // inline single-line function like `f() { x=1 }`, the body's
5908    // WC_PIPE encodes lineno=1, exec sets `lineno = lineno - 1 =
5909    // 0`, and the zerrmsg path falls through to space-only ("f: ").
5910    //
5911    // zshrs's compiler doesn't thread WC_PIPE_LINENO into the
5912    // bytecode, so the global lineno stays at the script-wide
5913    // value (1 for inline `-c`). Suppress the line-number prefix
5914    // inside function bodies by saving lineno on entry and forcing
5915    // it to 0 during body execution; restore on exit. This makes
5916    // warnings inside functions emit `f: ...` matching zsh's
5917    // single-line-function format. Bug #54/#74/#86 in docs/BUGS.md.
5918    let saved_lineno = crate::ported::lex::lineno();
5919    crate::ported::lex::set_lineno(0);
5920    // c:Src/exec.c:6173-6175 + c:6196-6198 — `runshfunc` saves
5921    // zunderscore before the body runs and restores it after, so
5922    // `$_` reads outside the function continue to reflect the
5923    // function CALL's last arg (set by setunderscore at c:3491
5924    // before doshfunc enters). Without this, commands inside the
5925    // body (`:`, `echo`, etc.) update `$_` to their own last arg,
5926    // and the post-call `echo "[$_]"` sees the body's residue
5927    // instead of the call's arg. Bug surfaced via
5928    // test_dollar_underscore_after_function_call.
5929    let saved_zunderscore = crate::ported::params::getsparam("_").unwrap_or_default();
5930    // c:6042 — `runshfunc(prog, wrappers, name)`: the FuncWrap chain.
5931    // zsh/param/private installs `wrap_private` via addwrapper at
5932    // module boot (param_private.c:712); C's runshfunc invokes it
5933    // around every function body so `private_wraplevel` tracks the
5934    // callee's locallevel and outer-scope privates get
5935    // scopeprivate-hidden for the duration. The gate here is module
5936    // BOOT STATE (MOD_INIT_B — the bit `zmodload -e` reads and
5937    // load_module sets after do_boot_module, module.c:2317) —
5938    // exactly C's condition for the wrapper being in the chain: the
5939    // `private` dispatch runs require_module per the exec.c:2710
5940    // autofeature path, which boots the module. NOT is_loaded():
5941    // default-registered static modules carry MOD_LINKED from
5942    // startup, which would arm the wrapper before any `private`
5943    // use. The dispatch is a named special-case rather than a
5944    // WRAPPERS walk because the Rust wrap_private carries a
5945    // body-delegate closure (fusevm chunk runner) that can't live
5946    // in funcwrap's WrapFunc fn-pointer slot. The pwl < locallevel
5947    // pre-check mirrors wrap_private's own c:552 gate so the
5948    // closure is guaranteed to run when routed through it.
5949    let run_wrap_private = crate::ported::module::MODULESTAB
5950        .lock()
5951        .map(|t| {
5952            t.modules
5953                .get("zsh/param/private")
5954                .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
5955                .unwrap_or(false)
5956        })
5957        .unwrap_or(false)
5958        && crate::ported::modules::param_private::private_wraplevel.load(Ordering::Relaxed)
5959            < crate::ported::params::locallevel.load(Ordering::Relaxed);
5960    let body_status = if run_wrap_private {
5961        let mut st = 0;
5962        let _ = crate::ported::modules::param_private::wrap_private(
5963            std::ptr::null(),
5964            std::ptr::null(),
5965            std::ptr::null_mut(),
5966            || st = body_runner(), // c:556 runshfunc(prog, w, name)
5967        );
5968        st
5969    } else {
5970        body_runner()
5971    };
5972    crate::ported::params::set_zunderscore(std::slice::from_ref(&saved_zunderscore));
5973    crate::ported::lex::set_lineno(saved_lineno);
5974    LASTVAL.store(body_status, Ordering::Relaxed);
5975
5976    // c:6043 — `doneshfunc:` label. The C `runshfunc` happy-path
5977    // falls through here from c:6042.
5978    // c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
5979    {
5980        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5981        stack.pop();
5982    }
5983    // c:6045 — `undoshfunc:` label. Reached either by fall-through
5984    // from c:6044 or by `goto undoshfunc;` from the FUNCNEST check
5985    // at c:6003. Tail epilogue follows.
5986
5987    // c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
5988    // runshfunc) lives at c:6157 below as `endparamscope()`. Removed
5989    // the dec here so locallevel only decrements once per
5990    // function-call frame; double-dec was purging level-0 globals on
5991    // function exit (the `f() { x=foo; }; f; echo $x` regression).
5992
5993    // c:6047-6053 — retflag clear. C clears retflag and restores
5994    // outer breaks if a `return` fired.
5995    if RETFLAG.load(Ordering::SeqCst) != 0 {
5996        // c:6047
5997        RETFLAG.store(0, Ordering::SeqCst); // c:6051
5998        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
5999    }
6000
6001    // c:6054-6058 — pparams + argv0 restore.
6002    if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
6003        *pp = pptab; // c:6059 pparams = pptab
6004    }
6005    if let Some(saved) = funcsave_argv0 {
6006        // c:6055
6007        crate::ported::utils::set_argzero(Some(saved)); // c:6057
6008    }
6009
6010    // c:Src/exec.c:6060-6062 — `zoptind = funcsave->zoptind;
6011    // zoptarg = funcsave->zoptarg;`. Restore OPTIND/OPTARG so
6012    // an inner getopts loop's counter mutations don't leak to
6013    // the caller. Bug #513.
6014    if let Some(saved) = funcsave_optind {
6015        if let Ok(n) = saved.parse::<i64>() {
6016            crate::ported::params::setiparam("OPTIND", n);
6017        } else {
6018            crate::ported::params::setsparam("OPTIND", &saved);
6019        }
6020    }
6021    if let Some(saved) = funcsave_optarg {
6022        crate::ported::params::setsparam("OPTARG", &saved);
6023    }
6024
6025    // c:6064 — `scriptname = funcsave->scriptname;`
6026    crate::ported::utils::set_scriptname(funcsave_scriptname);
6027
6028    // c:6067 — `endpatternscope();`
6029    crate::ported::pattern::endpatternscope();
6030
6031    // c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
6032    // localoptions was set inside the body.
6033    if crate::ported::options::opt_state_get("localoptions").unwrap_or(false) {
6034        // c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
6035        let current = crate::ported::options::opt_state_snapshot();
6036        for (k, _) in &current {
6037            if !funcsave_opts.contains_key(k) {
6038                crate::ported::options::opt_state_unset(k);
6039            }
6040        }
6041        for (k, v) in &funcsave_opts {
6042            opt_state_set(k, *v);
6043        }
6044    } else {
6045        // c:6097-6101 — non-LOCALOPTIONS: restore only the always-
6046        // restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
6047        // LOCALLOOPS / WARNNESTEDVAR).
6048        for opt in [
6049            "xtrace",
6050            "printexitvalue",
6051            "localoptions",
6052            "localloops",
6053            "warnnestedvar",
6054        ] {
6055            if let Some(v) = funcsave_opts.get(opt) {
6056                opt_state_set(opt, *v);
6057            }
6058        }
6059    }
6060
6061    // c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
6062    // breaks/contflag/loops snapshot. Skip the warn lines for now;
6063    // restore the bookkeeping.
6064    if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
6065        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
6066        CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
6067        LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
6068    }
6069
6070    // c:Src/exec.c:6195-6200 — C's runshfunc calls endparamscope()
6071    // BEFORE returning to doshfunc, which then calls endtrapscope()
6072    // at c:6114. So locallevel is ALREADY one less by the time
6073    // endtrapscope's pop loop compares saved local > current.
6074    //
6075    // Bug #80 in docs/BUGS.md: zshrs had endtrapscope FIRST (here at
6076    // line 5774), endparamscope LATER. That left locallevel at the
6077    // function's own level when endtrapscope ran, so saved entries
6078    // tagged with `local == current_function_level` failed the
6079    // `local > locallevel` pop condition. Nested EXIT traps
6080    // (saved at deeper level) never restored at the outer fn's
6081    // endtrapscope — outer EXIT traps fired at script exit instead.
6082    //
6083    // Decrement locallevel via a peer-of-endparamscope locallevel
6084    // bookkeeping call before endtrapscope, then leave the real
6085    // endparamscope at its current site below so the param scope
6086    // unwind still happens after the exit_pending check.
6087    {
6088        use crate::ported::params::locallevel as ll;
6089        let prev = ll.load(Ordering::Relaxed);
6090        if prev > 0 {
6091            ll.store(prev - 1, Ordering::Relaxed);
6092        }
6093        crate::ported::signals::endtrapscope();
6094        // Re-bump so the existing endparamscope() call below sees the
6095        // same pre-decrement state and its own internal decrement
6096        // lands at the right value (mirrors C's "endparamscope already
6097        // happened" comment at c:6135-6136 — the C order is endparam
6098        // (inside runshfunc) → endtrap (in doshfunc); we keep that
6099        // logical ordering for endtrapscope only, without disturbing
6100        // the rest of the epilogue's level math).
6101        ll.store(prev, Ordering::Relaxed);
6102    }
6103
6104    // c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
6105    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
6106        // c:6116
6107        TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
6108    }
6109
6110    // c:6118 — `ret = lastval;`
6111    let ret = LASTVAL.load(Ordering::Relaxed);
6112
6113    // c:6119 — `noerrexit = funcsave->noerrexit;`
6114    noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
6115
6116    // c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
6117    // the function for side-effects only; outer lastval/pipestats
6118    // should reflect the PRE-call state.
6119    if noreturnval {
6120        // c:6120
6121        LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
6122        if let Some(saved_ps) = funcsave_pipestats {
6123            let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
6124            if let Ok(mut nguard) = n.lock() {
6125                *nguard = funcsave_numpipestats; // c:6122
6126            }
6127            let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
6128            if let Ok(mut pguard) = p.lock() {
6129                for (i, v) in saved_ps.iter().enumerate() {
6130                    if i < pguard.len() {
6131                        pguard[i] = *v; // c:6123 memcpy
6132                    }
6133                }
6134            }
6135        }
6136    }
6137
6138    // c:Src/exec.c doshfunc → endparamscope — restore local-typeset
6139    // params installed during the body. In C, this is called inside
6140    // runshfunc (c:6200) BEFORE control returns to doshfunc's tail —
6141    // so by the time the exit_pending check runs at c:6141,
6142    // locallevel has ALREADY been decremented. The c:6135-6136
6143    // comment explicitly states "The endparamscope() has already
6144    // happened, hence the +1 here."
6145    //
6146    // The previous Rust ordering placed endparamscope AFTER the
6147    // exit_pending check, which compared exit_level against the
6148    // un-decremented locallevel. For `foo() { exit 7; }; foo`:
6149    //   exit_level=1, cur_locallevel=1 (pre-decrement)
6150    //   check: exit_level >= cur_locallevel + 1 ⟹ 1 >= 2 = false
6151    // The function returned cleanly without triggering zexit, and
6152    // the shell exited 0 instead of 7. Moving endparamscope before
6153    // the check matches C and makes the off-by-one resolve.
6154    endparamscope();
6155
6156    // c:6128 — `unqueue_signals();`
6157    unqueue_signals();
6158
6159    // c:6135-6155 — exit_pending branch: when an `exit` was queued
6160    // inside the function body and we've unwound enough scopes for
6161    // it to take effect, either keep unwinding (still inside a
6162    // nested function) or actually exit the shell.
6163    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
6164    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
6165    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
6166    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
6167    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
6168    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
6169        // c:6141
6170        if cur_locallevel > cur_forklevel {
6171            // c:6143 — still inside a nested function: keep unwinding.
6172            RETFLAG.store(1, Ordering::Relaxed); // c:6144
6173            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
6174        } else {
6175            // c:6151 — out of all functions: exit for real.
6176            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
6177            let val = EXIT_VAL.load(Ordering::Relaxed);
6178            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
6179            // c:6152
6180        }
6181    }
6182
6183    ret // c:6157 return ret
6184}
6185
6186/// `TRAP_STATE_PRIMED` per `Src/signals.h:55` — doshfunc tests this
6187/// to decide whether to bump trap_return on entry/exit. Local
6188/// const here because the canonical zsh_h port doesn't carry
6189/// trap-state numeric constants yet.
6190const TRAP_STATE_PRIMED: i32 = 2; // c:Src/signals.h:55
6191
6192/// Port of `execfuncdef(Estate state, Eprog redir_prog)` from
6193/// `Src/exec.c:5309-5494`. Define a shell function: extract
6194/// name(s)+body from the wordcode payload, allocate the Shfunc,
6195/// install into `shfunctab` (named), or execute immediately (anon).
6196#[allow(non_snake_case)]
6197pub fn execfuncdef(state: &mut estate, mut redir_prog: Option<crate::ported::zsh_h::Eprog>) -> i32 {
6198    use crate::ported::hashtable::{dircache_set, shfunctab_lock};
6199    use crate::ported::jobs::{getsigidx, removetrapnode};
6200    use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
6201    use crate::ported::signals::settrap;
6202    use crate::ported::utils::scriptfilename_get;
6203    use crate::ported::zsh_h::{
6204        eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, Patprog,
6205        EC_DUPTOK as _, EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED,
6206        PM_TAGGED_LOCAL, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
6207    };
6208    // c:5311 — `Shfunc shf;`
6209    let mut shf: Box<shfunc_t>;
6210    // c:5312 — `char *s = NULL;`
6211    let mut s: Option<String> = None;
6212    // c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
6213    let mut signum: i32;
6214    let nprg: i32;
6215    let sbeg: i32;
6216    let nstrs: i32;
6217    let npats: i32;
6218    let do_tracing: i32;
6219    let len: i32;
6220    let plen: i32;
6221    // `i` — C loop counter for pp stamp; Rust uses .map().collect().
6222    let mut htok: i32 = 0;
6223    let mut ret: i32 = 0;
6224    // c:5314 — `int anon_func = 0;`
6225    let mut anon_func: i32 = 0;
6226    // c:5315 — `Wordcode beg = state->pc, end;`
6227    let _beg: usize = state.pc;
6228    let mut end: usize;
6229    // c:5316 — `Eprog prog;`
6230    // (allocated inline per-iter below; no upfront binding needed)
6231    // c:5317 — `Patprog *pp;` — handled by Vec construction.
6232    // c:5318 — `LinkList names;`
6233    let names: Vec<String>;
6234    // c:5319 — `int tracing_flags;`
6235    let tracing_flags: i32;
6236
6237    // c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
6238    end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
6239    // c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6240    let num = state.prog.prog[state.pc] as usize;
6241    state.pc += 1;
6242    names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
6243    // c:5323 — `sbeg = *state->pc++;`
6244    sbeg = state.prog.prog[state.pc] as i32;
6245    state.pc += 1;
6246    // c:5324 — `nstrs = *state->pc++;`
6247    nstrs = state.prog.prog[state.pc] as i32;
6248    state.pc += 1;
6249    // c:5325 — `npats = *state->pc++;`
6250    npats = state.prog.prog[state.pc] as i32;
6251    state.pc += 1;
6252    // c:5326 — `do_tracing = *state->pc++;`
6253    do_tracing = state.prog.prog[state.pc] as i32;
6254    state.pc += 1;
6255
6256    // c:5328 — `nprg = (end - state->pc);`
6257    nprg = end.saturating_sub(state.pc) as i32;
6258    // c:5329 — `plen = nprg * sizeof(wordcode);`
6259    plen = nprg.saturating_mul(size_of::<wordcode>() as i32);
6260    // c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
6261    len = plen + npats.saturating_mul(size_of::<usize>() as i32) + nstrs;
6262    // c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
6263    tracing_flags = if do_tracing != 0 {
6264        PM_TAGGED_LOCAL as i32
6265    } else {
6266        0
6267    };
6268
6269    // c:5333-5339 — htok name substitution.
6270    let mut names_mut: Vec<String> = names;
6271    if htok != 0 && !names_mut.is_empty() {
6272        execsubst(&mut names_mut); // c:5334
6273        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6274            // c:5335
6275            state.pc = end; // c:5336
6276            return 1; // c:5337
6277        }
6278    }
6279
6280    // c:5341-5342 DPUTS — debug assertion (anon + redir simultaneously).
6281    // Not portable as panic; left as comment.
6282
6283    // c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
6284    // num==0 → anon (no names); else iterate names.
6285    let mut names_iter = names_mut.into_iter();
6286    loop {
6287        let no_names = num == 0;
6288        if !no_names {
6289            // c:5343 — `s = ugetnode(names)`; break when list exhausted.
6290            match names_iter.next() {
6291                Some(nm) => s = Some(nm),
6292                None => break,
6293            }
6294        }
6295        // c:5344-5374 — Eprog alloc.
6296        let prog: Box<eprog_t>;
6297        let dump_present = state.prog.dump.is_some();
6298        let make_pat = || -> Patprog {
6299            // c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
6300            Box::new(patprog_t {
6301                startoff: 0,
6302                size: 0,
6303                mustoff: 0,
6304                patmlen: 0,
6305                globflags: 0,
6306                globend: 0,
6307                flags: 0,
6308                patnpar: 0,
6309                patstartch: 0,
6310            })
6311        };
6312        if no_names {
6313            // c:5345-5346 — `zhalloc`, `nref = -1`.
6314            // c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
6315            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6316            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6317            // c:5365 — `prog->strs = state->strs + sbeg;`
6318            let strs_tail = state.strs.as_ref().map(|t| {
6319                let off = (sbeg as usize).min(t.len());
6320                t[off..].to_string()
6321            });
6322            prog = Box::new(eprog_t {
6323                flags: EF_HEAP,
6324                len,
6325                npats,
6326                nref: -1, // c:5346
6327                pats,
6328                prog: prog_words,
6329                strs: strs_tail,
6330                shf: None,  // c:5377
6331                dump: None, // c:5356
6332            });
6333        } else if dump_present {
6334            // c:5358-5363 — EF_MAP path: refcount the dump, allocate
6335            // pats permanent, reuse `state->pc` slice in place.
6336            if let Some(dp) = state.prog.dump.as_deref() {
6337                incrdumpcount(dp); // c:5360
6338            }
6339            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6340            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6341            let strs_tail = state.strs.as_ref().map(|t| {
6342                let off = (sbeg as usize).min(t.len());
6343                t[off..].to_string()
6344            });
6345            prog = Box::new(eprog_t {
6346                flags: EF_MAP, // c:5359
6347                len,
6348                npats,
6349                nref: 1, // c:5349
6350                pats,
6351                prog: prog_words,
6352                strs: strs_tail,
6353                shf: None,                     // c:5377
6354                dump: state.prog.dump.clone(), // c:5361
6355            });
6356        } else {
6357            // c:5366-5374 — EF_REAL: copy wordcode + strs into a
6358            // freshly-owned eprog (no shared dump backing).
6359            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6360            let pc_end = state.pc + nprg as usize;
6361            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
6362            // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
6363            let strs_copy = state.strs.as_ref().map(|t| {
6364                let off = (sbeg as usize).min(t.len());
6365                let n_avail = t.len().saturating_sub(off);
6366                let take = (nstrs as usize).min(n_avail);
6367                t[off..off + take].to_string()
6368            });
6369            prog = Box::new(eprog_t {
6370                flags: EF_REAL, // c:5367
6371                len,
6372                npats,
6373                nref: 1, // c:5349
6374                pats,
6375                prog: prog_words,
6376                strs: strs_copy,
6377                shf: None,  // c:5377
6378                dump: None, // c:5371
6379            });
6380        }
6381
6382        // c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
6383        shf = Box::new(shfunc_t {
6384            node: hashnode {
6385                next: None,
6386                nam: String::new(),
6387                flags: tracing_flags,
6388            },
6389            filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
6390            // c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
6391            // else just lineno.
6392            lineno: {
6393                let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
6394                if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6395                    if let Some(top) = stk.last() {
6396                        if top.tp == FS_FUNC || top.tp == FS_EVAL {
6397                            top.flineno + cur_lineno
6398                        } else {
6399                            cur_lineno
6400                        }
6401                    } else {
6402                        cur_lineno
6403                    }
6404                } else {
6405                    cur_lineno
6406                }
6407            },
6408            funcdef: Some(prog), // c:5380
6409            redir: None,
6410            sticky: None,
6411            body: None,
6412        });
6413        // c:5396-5401 — redir_prog ownership.
6414        // C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
6415        // else `shf->redir = redir_prog; redir_prog = 0;`
6416        // "nonempty(names)" means there's a NEXT name still to consume —
6417        // i.e. peek the iterator.
6418        if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
6419            // c:5397 — dupe so each earlier name gets its own copy; the
6420            // last name (when iterator drains) gets the original.
6421            if let Some(rp) = redir_prog.as_deref() {
6422                shf.redir = Some(Box::new(dupeprog(rp, false)));
6423            }
6424        } else {
6425            // c:5399-5400 — last name (or anon) takes original.
6426            shf.redir = redir_prog.take();
6427        }
6428        // c:5402 — `shfunc_set_sticky(shf);`
6429        shfunc_set_sticky(&mut shf);
6430
6431        if no_names {
6432            // c:5404-5457 — anonymous function: execute immediately.
6433            // `LinkList args;` c:5409
6434            let mut args: Vec<String>;
6435
6436            anon_func = 1; // c:5411
6437            shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
6438
6439            state.pc = end; // c:5414
6440                            // c:5415 — `end += *state->pc++;`
6441            end += state.prog.prog[state.pc] as usize;
6442            state.pc += 1;
6443            // c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6444            let arg_count = state.prog.prog[state.pc] as usize;
6445            state.pc += 1;
6446            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
6447
6448            // c:5418-5429 — htok arg subst + cleanup-on-error.
6449            if htok != 0 && !args.is_empty() {
6450                execsubst(&mut args); // c:5419
6451                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6452                    // c:5421 — `freeeprog(shf->funcdef);`
6453                    if let Some(mut fd) = shf.funcdef.take() {
6454                        freeeprog(&mut fd);
6455                    }
6456                    if shf.redir.is_some() {
6457                        // c:5422-5423 — "shouldn't be" anon+redir, but free if so.
6458                        if let Some(mut rd) = shf.redir.take() {
6459                            freeeprog(&mut rd);
6460                        }
6461                    }
6462                    dircache_set(&mut shf.filename, None); // c:5424
6463                    drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
6464                    state.pc = end; // c:5426
6465                    return 1; // c:5427
6466                }
6467            }
6468
6469            // c:5431-5432 — `setunderscore` to last arg (or "").
6470            let under_val = if !args.is_empty() {
6471                args.last().cloned().unwrap_or_default()
6472            } else {
6473                String::new()
6474            };
6475            setunderscore(&under_val);
6476
6477            // c:5434-5435 — `if (!args) args = newlinklist();`
6478            // (Rust Vec is never null; no-op.)
6479            shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
6480                                                                // c:5437 — `pushnode(args, shf->node.nam);` — prepend.
6481            args.insert(0, shf.node.nam.clone());
6482
6483            execshfunc(&mut shf, &mut args); // c:5439
6484            ret = LASTVAL.load(Ordering::Relaxed); // c:5440
6485
6486            // c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
6487            if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
6488                eprintln!("zsh: exit {}", ret); // c:5445/5447
6489            }
6490
6491            // c:5452-5456 — cleanup.
6492            if let Some(mut fd) = shf.funcdef.take() {
6493                freeeprog(&mut fd);
6494            }
6495            if let Some(mut rd) = shf.redir.take() {
6496                // c:5453-5454 — "shouldn't be" but free if present.
6497                freeeprog(&mut rd);
6498            }
6499            dircache_set(&mut shf.filename, None); // c:5455
6500            drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
6501            break; // c:5457
6502        } else {
6503            // c:5458-5484 — named function path.
6504            let nm = s.as_deref().unwrap_or("");
6505            // c:5460-5475 — TRAP* signal-trap install.
6506            if nm.len() > 4 && nm.starts_with("TRAP") {
6507                if let Some(sn) = getsigidx(&nm[4..]) {
6508                    signum = sn;
6509                    // c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
6510                    if settrap(signum, None, ZSIG_FUNC) != 0 {
6511                        if let Some(mut fd) = shf.funcdef.take() {
6512                            freeeprog(&mut fd); // c:5463
6513                        }
6514                        dircache_set(&mut shf.filename, None); // c:5464
6515                        drop(shf); // c:5465
6516                        state.pc = end; // c:5466
6517                        return 1; // c:5467
6518                    }
6519                    // c:5474 — `removetrapnode(signum);`
6520                    removetrapnode(signum);
6521                    // c:Src/signals.c::settrap → unsettrap →
6522                    // removetrap also clears sigfuncs[sig] (the C
6523                    // string-form trap slot). zshrs's port stores
6524                    // string-form bodies in a separate
6525                    // `traps_table` HashMap not touched by
6526                    // removetrap. Drop the string-form entry here
6527                    // so dotrap's fallback doesn't double-dispatch
6528                    // when a TRAPxxx function REPLACES an
6529                    // existing `trap '...' SIG` registration. Bug
6530                    // #541 in docs/BUGS.md.
6531                    if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
6532                        t.remove(&nm[4..]);
6533                    }
6534                }
6535            }
6536            // c:5477-5482 — re-define-self trace flag propagate.
6537            if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6538                if let Some(top) = stk.last() {
6539                    if top.tp == FS_FUNC && top.name == nm {
6540                        // c:5479 — `Shfunc old = shfunctab->getnode(s);`
6541                        if let Ok(rd) = shfunctab_lock().read() {
6542                            if let Some(old) = rd.get(nm) {
6543                                // c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
6544                                shf.node.flags |=
6545                                    old.node.flags & (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
6546                            }
6547                        }
6548                    }
6549                }
6550            }
6551            // c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
6552            shf.node.nam = nm.to_string();
6553            if let Ok(mut wr) = shfunctab_lock().write() {
6554                wr.add(*shf);
6555            }
6556        }
6557    }
6558    // c:5486-5487 — `if (!anon_func) setunderscore("");`
6559    if anon_func == 0 {
6560        setunderscore("");
6561    }
6562    // c:5488-5491 — leftover redir cleanup ("shouldn't happen").
6563    if let Some(mut rd) = redir_prog.take() {
6564        freeeprog(&mut rd);
6565    }
6566    // c:5492 — `state->pc = end;`
6567    state.pc = end;
6568    // c:5493 — `return ret;`
6569    ret
6570}
6571
6572/// Port of `execsimple(Estate state)` from `Src/exec.c:1290-1340`.
6573/// Fast-path for single-Simple commands that bypasses the full
6574/// `execcmd_exec` machinery.
6575pub fn execsimple(state: &mut estate) -> i32 {
6576    // c:1292 — `wordcode code = *state->pc++;`
6577    let mut code = state.prog.prog[state.pc];
6578    state.pc += 1;
6579    // c:1295-1296 — `if (errflag) return (lastval = 1);`
6580    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6581        LASTVAL.store(1, Ordering::Relaxed);
6582        return 1;
6583    }
6584    // c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
6585    if !isset(crate::ported::zsh_h::EXECOPT) {
6586        LASTVAL.store(0, Ordering::Relaxed);
6587        return 0;
6588    }
6589    // c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
6590    // In evaluated traps, don't modify the line number (the trap
6591    // dispatcher restores it). `code` here is the wordcode-encoded
6592    // line number from the WC_SIMPLE entry at state.pc-1.
6593    if !crate::ported::zsh_h::IN_EVAL_TRAP()
6594        && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
6595        && code != 0
6596    {
6597        crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
6598    }
6599    // c:1306 — `code = wc_code(*state->pc++);`
6600    code = wc_code(state.prog.prog[state.pc]);
6601    state.pc += 1;
6602    // c:1311-1312 — `otj = thisjob; thisjob = -1;`
6603    let otj = *THISJOB
6604        .get_or_init(|| std::sync::Mutex::new(-1))
6605        .lock()
6606        .unwrap();
6607    *THISJOB
6608        .get_or_init(|| std::sync::Mutex::new(-1))
6609        .lock()
6610        .unwrap() = -1;
6611    use crate::ported::zsh_h::{
6612        WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
6613        WC_WHILE,
6614    };
6615    use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
6616    let lv = if code == WC_ASSIGN {
6617        // c:1315-1319 — assignment-only simple cmd path.
6618        // cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
6619        addvars(state, state.pc.saturating_sub(1), 0);
6620        setunderscore(""); // c:1317
6621        if isset(XTRACE) {
6622            eprintln!();
6623        }
6624        let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
6625        if ef != 0 {
6626            ef
6627        } else {
6628            0
6629        }
6630    } else {
6631        // c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
6632        let q = queue_signal_level();
6633        dont_queue_signals();
6634        let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6635            ERRFLAG_ERROR
6636        } else if code == WC_FUNCDEF {
6637            execfuncdef(state, None)
6638        } else {
6639            // c:5499 execfuncs[] table inlined — match the WC_* tag.
6640            match code {
6641                WC_CURSH => execcursh(state, 0),
6642                WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
6643                WC_FOR => execfor(state, 0),
6644                WC_SELECT => execselect(state, 0),
6645                WC_CASE => execcase(state, 0),
6646                WC_IF => execif(state, 0),
6647                WC_WHILE => execwhile(state, 0),
6648                WC_REPEAT => execrepeat(state, 0),
6649                WC_TIMED => exectime(state, 0),
6650                WC_COND => execcond(state, 0),
6651                WC_ARITH => execarith(state, 0),
6652                WC_TRY => exectry(state, 0),
6653                _ => 0,
6654            }
6655        };
6656        restore_queue_signals(q);
6657        result
6658    };
6659    // c:1334 — `thisjob = otj;`
6660    *THISJOB
6661        .get_or_init(|| std::sync::Mutex::new(-1))
6662        .lock()
6663        .unwrap() = otj;
6664    LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
6665    lv
6666}
6667
6668/// Port of `execlist(Estate state, int dont_change_job, int exiting)`
6669/// from `Src/exec.c:1349-1665`. Walks WC_LIST entries, dispatches each
6670/// sublist (WC_SUBLIST chain inlined per c:1525-1625, same as C —
6671/// there's no separate execsublist function), handles signal-trap
6672/// dispatch + ERREXIT propagation.
6673///
6674/// Body ports the structural skeleton faithfully (WC_LIST walk,
6675/// per-iteration breaks/retflag/errflag guards, ltype dispatch on
6676/// Z_END/Z_SYNC/Z_ASYNC, donetrap handling). The full signal queue
6677/// + DEBUGBEFORECMD trap machinery from c:1357-1500 is preserved
6678/// in shape with TODO-citations where dependent primitives aren't
6679/// yet ported.
6680pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) -> i32 {
6681    let mut last_status: i32 = 0;
6682    let mut donetrap: i32 = 0; // c:1352 — `static int donetrap;`
6683    let cj = *THISJOB
6684        .get_or_init(|| std::sync::Mutex::new(-1))
6685        .lock()
6686        .unwrap(); // c:1364 — `cj = thisjob;`
6687    let _ = dont_change_job; // c:1361 — restored on exit if nonzero.
6688                             // c:1380 — `code = *state->pc++;`
6689    if state.pc >= state.prog.prog.len() {
6690        return last_status;
6691    }
6692    let mut code = state.prog.prog[state.pc];
6693    state.pc += 1;
6694    // c:1382-1384 — empty list returns lastval = 0.
6695    if wc_code(code) != WC_LIST {
6696        LASTVAL.store(0, Ordering::Relaxed);
6697        return 0;
6698    }
6699    use crate::ported::zsh_h::{WC_LIST_SKIP, WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC};
6700    // c:1385-1499 — main WC_LIST loop.
6701    while wc_code(code) == WC_LIST
6702        && BREAKS.load(Ordering::SeqCst) == 0
6703        && RETFLAG.load(Ordering::SeqCst) == 0
6704        && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
6705    {
6706        let ltype = WC_LIST_TYPE(code) as i32;
6707        // c:1396 — `csp = cmdsp;` — snapshot cmdstack depth at start
6708        // of this WC_LIST iteration; restored at end so partial
6709        // cmdpush sequences (e.g. from execcond, execfuncs) don't
6710        // leak into the next sublist.
6711        let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len());
6712        // c:1502-1509 — Z_SIMPLE fast-path.
6713        if (ltype & Z_SIMPLE as i32) != 0 {
6714            let next_pc = state.pc + WC_LIST_SKIP(code) as usize;
6715            let s = execsimple(state);
6716            last_status = s;
6717            state.pc = next_pc;
6718        } else {
6719            // c:1513-1523 — sublist chain.
6720            if state.pc >= state.prog.prog.len() {
6721                break;
6722            }
6723            code = state.prog.prog[state.pc];
6724            state.pc += 1;
6725            // c:1525-1625 — sublist chain (&&/|| operators) inlined.
6726            use crate::ported::zsh_h::{
6727                WC_SUBLIST_AND, WC_SUBLIST_END, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SIMPLE,
6728                WC_SUBLIST_SKIP,
6729            };
6730            let mut sub_code = code;
6731            let _ = dont_change_job;
6732            while wc_code(sub_code) == WC_SUBLIST {
6733                let flags = WC_SUBLIST_FLAGS(sub_code);
6734                let next = state.pc + WC_SUBLIST_SKIP(sub_code) as usize;
6735                let sl_type = WC_SUBLIST_TYPE(sub_code) as i32;
6736                let last1 = if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6737                    exiting
6738                } else {
6739                    0
6740                };
6741                if flags == WC_SUBLIST_SIMPLE {
6742                    last_status = execsimple(state); // c:1605
6743                } else {
6744                    let _ = execpline(state, sub_code, sl_type, last1); // c:1607
6745                    last_status = LASTVAL.load(Ordering::Relaxed);
6746                }
6747                // c:1612 — `WC_SUBLIST_NOT` inverts status.
6748                if (flags & WC_SUBLIST_NOT) != 0 {
6749                    last_status = if last_status == 0 { 1 } else { 0 };
6750                    LASTVAL.store(last_status, Ordering::Relaxed);
6751                }
6752                state.pc = next;
6753                if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6754                    break;
6755                }
6756                if state.pc >= state.prog.prog.len() {
6757                    break;
6758                }
6759                // c:1617-1623 — short-circuit on && / ||.
6760                if sl_type == WC_SUBLIST_AND as i32 && last_status != 0 {
6761                    while state.pc < state.prog.prog.len() {
6762                        let c = state.prog.prog[state.pc];
6763                        if wc_code(c) != WC_SUBLIST {
6764                            break;
6765                        }
6766                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6767                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6768                            break;
6769                        }
6770                    }
6771                    break;
6772                }
6773                if sl_type == WC_SUBLIST_OR as i32 && last_status == 0 {
6774                    while state.pc < state.prog.prog.len() {
6775                        let c = state.prog.prog[state.pc];
6776                        if wc_code(c) != WC_SUBLIST {
6777                            break;
6778                        }
6779                        state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6780                        if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6781                            break;
6782                        }
6783                    }
6784                    break;
6785                }
6786                sub_code = state.prog.prog[state.pc];
6787                state.pc += 1;
6788            }
6789        }
6790        // c:1593 — `cmdsp = csp;` — restore cmdstack depth to the
6791        // snapshot taken at start of iteration. Reverses any cmdpush
6792        // calls made by nested execcond / execfuncs / execcmd_exec
6793        // that didn't pop cleanly.
6794        crate::ported::prompt::CMDSTACK.with(|s| {
6795            let mut g = s.borrow_mut();
6796            if g.len() > csp {
6797                g.truncate(csp);
6798            }
6799        });
6800        // c:1626-1634 — donetrap is reset between sublists.
6801        donetrap = 0;
6802        // c:1640-1645 — fetch next WC_LIST header (or break out).
6803        if state.pc >= state.prog.prog.len() {
6804            break;
6805        }
6806        let next_code = state.prog.prog[state.pc];
6807        if wc_code(next_code) != WC_LIST {
6808            break;
6809        }
6810        state.pc += 1;
6811        code = next_code;
6812        // c:1389 — z_end means last sublist, exiting becomes 1 for tail-exec.
6813        if (ltype & Z_END as i32) != 0 {
6814            exiting = 1;
6815        }
6816    }
6817    // c:1659-1664 — cleanup: restore thisjob if dont_change_job, this_noerrexit=1.
6818    if dont_change_job != 0 {
6819        *THISJOB
6820            .get_or_init(|| std::sync::Mutex::new(-1))
6821            .lock()
6822            .unwrap() = cj;
6823    }
6824    let _ = donetrap;
6825    this_noerrexit.store(1, Ordering::Relaxed);
6826    LASTVAL.store(last_status, Ordering::Relaxed);
6827    last_status
6828}
6829
6830// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
6831// 1625`, the C source likewise inlines it — there's no `execsublist`
6832// function in zsh C).
6833
6834/// Port of `execcmd_getargs(LinkList preargs, LinkList args, int expand)`
6835/// from `Src/exec.c:2791-2806`. Transfer the first node of `args`
6836/// to `preargs`, performing `prefork` (singleton-list expansion) on
6837/// the way if `expand` is set. Used by `execcmd_exec` to pull the
6838/// command head one word at a time so prefix-modifier walking
6839/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
6840pub fn execcmd_getargs(preargs: &mut LinkList<String>, args: &mut LinkList<String>, expand: i32) {
6841    // c:2791
6842    if args.firstnode().is_none() {
6843        // c:2793 — `if (!firstnode(args)) return;`
6844        return;
6845    } else if expand != 0 {
6846        // c:2795
6847        // c:2796-2797 — `local_list0(svl); init_list0(svl);` —
6848        // stack-local single-bucket list. Rust uses a fresh
6849        // LinkList<String> per call.
6850        let mut svl: LinkList<String> = Default::default();
6851        // c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
6852        if let Some(idx) = args.firstnode() {
6853            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6854                svl.push_back(head);
6855            }
6856        }
6857        // c:2801 — `prefork(&svl, 0, NULL);`
6858        let mut rf = 0i32;
6859        prefork(&mut svl, 0, &mut rf);
6860        // c:2802 — `joinlists(preargs, &svl);`
6861        crate::ported::linklist::joinlists(preargs, &mut svl);
6862    } else {
6863        // c:2803-2804 — no-expand path: move head verbatim.
6864        if let Some(idx) = args.firstnode() {
6865            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6866                preargs.push_back(head);
6867            }
6868        }
6869    }
6870}
6871
6872/// Port of `execcmd_fork(Estate state, int how, int type,
6873/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
6874/// int close_if_forked)` from `Src/exec.c:2810-2893`.
6875///
6876/// Fork the current command into a child process: parent records
6877/// the pid + STTY env scan + addproc; child enters subshell, writes
6878/// `entersubsh_ret` back to parent through `synch` pipe, and returns
6879/// 0 so the caller can continue with the body.
6880///
6881/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
6882/// only in the child branch (so the parent's `filelist` stays
6883/// untouched). Rust sig keeps the same C contract.
6884pub fn execcmd_fork(
6885    state: &mut estate,
6886    how: i32,
6887    typ: i32,
6888    varspc: Option<usize>,
6889    filelistp: &mut Vec<jobfile>,
6890    text: &str,
6891    oautocont: i32,
6892    close_if_forked: i32,
6893) -> i32 {
6894    use crate::ported::signals::sigtrapped as sigtrapped_static;
6895    use crate::ported::signals_h::SIGEXIT;
6896    use crate::ported::zsh_h::{
6897        AUTOCONTINUE, BGNICE, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
6898        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
6899        WC_SUBSH as ZWC_SUBSH, ZSIG_IGNORED, Z_ASYNC,
6900    };
6901    // c:2810
6902    let pid: libc::pid_t; // c:2814
6903    let mut synch: [i32; 2] = [-1, -1]; // c:2815
6904    let flags: i32; // c:2815
6905    let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
6906                                                               // c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
6907                                                               // for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
6908    let mut bgtime = ZshTimespec::default();
6909
6910    child_block(); // c:2819
6911    esret.gleader = -1; // c:2820
6912    esret.list_pipe_job = -1; // c:2821
6913
6914    // c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
6915    if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
6916        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
6917        return -1; // c:2825
6918    }
6919    // c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
6920    pid = zfork(Some(&mut bgtime));
6921    if pid == -1 {
6922        unsafe {
6923            libc::close(synch[0]); // c:2827
6924            libc::close(synch[1]); // c:2828
6925        }
6926        LASTVAL.store(1, Ordering::Relaxed); // c:2829
6927        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
6928        return -1; // c:2831
6929    }
6930    if pid != 0 {
6931        // c:2833 — parent.
6932        unsafe { libc::close(synch[1]) }; // c:2834
6933                                          // c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
6934        let mut buf = [0u8; size_of::<entersubsh_ret>()];
6935        let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
6936        // entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
6937        if buf.len() >= 8 {
6938            esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
6939            esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
6940        }
6941        unsafe { libc::close(synch[0]) }; // c:2836
6942        if (how & Z_ASYNC as i32) != 0 {
6943            // c:2837 — `lastpid = (zlong) pid;`
6944            crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
6945        } else {
6946            // c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
6947            let thisjob_idx = {
6948                if let Some(m) = THISJOB.get() {
6949                    *m.lock().unwrap()
6950                } else {
6951                    -1
6952                }
6953            };
6954            // Examine the jobtab entry under lock.
6955            let stty_already = if thisjob_idx >= 0 {
6956                if let Some(jt) = JOBTAB.get() {
6957                    let guard = jt.lock().unwrap();
6958                    guard
6959                        .get(thisjob_idx as usize)
6960                        .map(|j| j.stty_in_env != 0)
6961                        .unwrap_or(true)
6962                } else {
6963                    true
6964                }
6965            } else {
6966                true
6967            };
6968            if !stty_already && varspc.is_some() {
6969                // c:2841-2851 — walk varspc looking for STTY=...
6970                let mut p = varspc.unwrap();
6971                loop {
6972                    if p >= state.prog.prog.len() {
6973                        break;
6974                    }
6975                    let ac = state.prog.prog[p];
6976                    if wc_code(ac) != ZWC_ASSIGN {
6977                        break;
6978                    }
6979                    // c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
6980                    let name = ecrawstr(&state.prog, p + 1, None);
6981                    if name == "STTY" {
6982                        // c:2846 — `jobtab[thisjob].stty_in_env = 1;`
6983                        if let Some(jt) = JOBTAB.get() {
6984                            let mut guard = jt.lock().unwrap();
6985                            if let Some(j) = guard.get_mut(thisjob_idx as usize) {
6986                                j.stty_in_env = 1;
6987                            }
6988                        }
6989                        break; // c:2847
6990                    }
6991                    p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
6992                        3 // c:2849
6993                    } else {
6994                        (ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
6995                    };
6996                }
6997            }
6998        }
6999        // c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
7000        if let Some(jt) = JOBTAB.get() {
7001            let mut guard = jt.lock().unwrap();
7002            let tj = {
7003                if let Some(m) = THISJOB.get() {
7004                    *m.lock().unwrap()
7005                } else {
7006                    -1
7007                }
7008            };
7009            if tj >= 0 {
7010                if let Some(j) = guard.get_mut(tj as usize) {
7011                    crate::ported::jobs::addproc(
7012                        j,
7013                        pid,
7014                        text,
7015                        false,
7016                        Some(std::time::Instant::now()),
7017                        esret.gleader,
7018                        esret.list_pipe_job,
7019                    );
7020                }
7021            }
7022        }
7023        // c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
7024        if oautocont >= 0 {
7025            opt_state_set("autocontinue", oautocont != 0);
7026            let _ = AUTOCONTINUE; // const referenced for parity
7027        }
7028        // c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
7029        if let Some(jt) = JOBTAB.get() {
7030            let mut guard = jt.lock().unwrap();
7031            let tj = {
7032                if let Some(m) = THISJOB.get() {
7033                    *m.lock().unwrap()
7034                } else {
7035                    -1
7036                }
7037            };
7038            if tj >= 0 {
7039                if let Some(j) = guard.get_mut(tj as usize) {
7040                    crate::ported::jobs::pipecleanfilelist(j, true);
7041                }
7042            }
7043        }
7044        return pid; // c:2857
7045    }
7046
7047    // c:2860 — pid == 0 (child).
7048    unsafe { libc::close(synch[0]) }; // c:2861
7049    flags = (if (how & Z_ASYNC as i32) != 0 {
7050        esub::ASYNC
7051    } else {
7052        0
7053    }) | esub::PGRP; // c:2862
7054    let mut flags = flags;
7055    if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
7056        flags |= esub::KEEPTRAP; // c:2864
7057    }
7058    if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
7059        flags |= esub::JOB_CONTROL; // c:2866
7060    }
7061    // c:2867 — `*filelistp = jobtab[thisjob].filelist;`
7062    if let Some(jt) = JOBTAB.get() {
7063        let mut guard = jt.lock().unwrap();
7064        let tj = {
7065            if let Some(m) = THISJOB.get() {
7066                *m.lock().unwrap()
7067            } else {
7068                -1
7069            }
7070        };
7071        if tj >= 0 {
7072            if let Some(j) = guard.get_mut(tj as usize) {
7073                *filelistp = std::mem::take(&mut j.filelist);
7074            }
7075        }
7076    }
7077    entersubsh(flags, Some(&mut esret)); // c:2868
7078                                         // c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
7079    let mut buf = [0u8; 8];
7080    buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
7081    buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
7082    if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
7083        zerr(&format!(
7084            "Failed to send entersubsh_ret report: {}",
7085            std::io::Error::last_os_error()
7086        ));
7087        return -1; // c:2871
7088    }
7089    unsafe { libc::close(synch[1]) }; // c:2873
7090    let _ = zclose(close_if_forked); // c:2874
7091
7092    // c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
7093    let sigint_state = {
7094        let guard = sigtrapped_static.lock().unwrap();
7095        guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
7096    };
7097    if (sigint_state & ZSIG_IGNORED) != 0 {
7098        crate::ported::signals::holdintr(); // c:2877
7099    }
7100    // c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
7101    {
7102        let mut guard = sigtrapped_static.lock().unwrap();
7103        if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
7104            *slot = 0;
7105        }
7106    }
7107    // c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
7108    // Per-platform errno setter+reader: __error() on macOS,
7109    // __errno_location() on Linux. Without cfg gating Linux CI breaks.
7110    if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
7111        #[cfg(target_os = "macos")]
7112        unsafe {
7113            *libc::__error() = 0;
7114            if libc::nice(5) == -1 && *libc::__error() != 0 {
7115                zwarn(&format!(
7116                    "nice(5) failed: {}",
7117                    std::io::Error::last_os_error()
7118                ));
7119            }
7120        }
7121        #[cfg(target_os = "linux")]
7122        unsafe {
7123            *libc::__errno_location() = 0;
7124            if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
7125                zwarn(&format!(
7126                    "nice(5) failed: {}",
7127                    std::io::Error::last_os_error()
7128                ));
7129            }
7130        }
7131    }
7132    0 // c:2892
7133}
7134
7135/// Port of `execcmd_analyse(Estate state, Execcmd_params eparams)`
7136/// from `Src/exec.c:2733-2785`. Pre-execcmd_exec analysis pass:
7137/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
7138/// without expanding (no prefork, no globbing), and fills `eparams`
7139/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
7140/// can branch on the command type before the real work.
7141pub fn execcmd_analyse(state: &mut estate, eparams: &mut crate::ported::zsh_h::execcmd_params) {
7142    use crate::ported::zsh_h::{
7143        WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
7144        WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
7145        WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
7146    };
7147    // c:2733
7148    let mut code: wordcode; // c:2735
7149    let mut i: i32; // c:2736
7150    let _ = i;
7151
7152    // c:2738 — `eparams->beg = state->pc;`
7153    eparams.beg = state.pc;
7154    // c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
7155    eparams.redir =
7156        if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_REDIR {
7157            Some(crate::ported::parse::ecgetredirs(state))
7158        } else {
7159            None
7160        };
7161    // c:2741-2748 — varspc walk (WC_ASSIGN chain).
7162    if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
7163        cmdoutval.store(0, Ordering::Relaxed); // c:2742
7164        eparams.varspc = Some(state.pc); // c:2743
7165                                         // c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
7166        loop {
7167            if state.pc >= state.prog.prog.len() {
7168                break;
7169            }
7170            code = state.prog.prog[state.pc];
7171            if wc_code(code) != ZWC_ASSIGN {
7172                break;
7173            }
7174            state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
7175                3 // c:2745
7176            } else {
7177                (WC_ASSIGN_NUM(code) + 2) as usize // c:2746
7178            };
7179        }
7180    } else {
7181        eparams.varspc = None; // c:2748
7182    }
7183
7184    // c:2750 — `code = *state->pc++;`
7185    if state.pc >= state.prog.prog.len() {
7186        eparams.args = None;
7187        eparams.assignspc = None;
7188        eparams.typ = 0;
7189        eparams.postassigns = 0;
7190        eparams.htok = 0;
7191        return;
7192    }
7193    code = state.prog.prog[state.pc];
7194    state.pc += 1;
7195
7196    // c:2752 — `eparams->type = wc_code(code);`
7197    eparams.typ = wc_code(code) as i32;
7198    // c:2753 — `eparams->postassigns = 0;`
7199    eparams.postassigns = 0;
7200
7201    // c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
7202    // per the comment at c:2755-2757.
7203    match eparams.typ as wordcode {
7204        x if x == ZWC_SIMPLE => {
7205            // c:2759-2763
7206            let mut htok = 0;
7207            let argc = ZWC_SIMPLE_ARGC(code) as usize;
7208            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7209            eparams.htok = htok;
7210            eparams.assignspc = None;
7211        }
7212        x if x == ZWC_TYPESET => {
7213            // c:2765-2777
7214            let mut htok = 0;
7215            let argc = ZWC_TYPESET_ARGC(code) as usize;
7216            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7217            eparams.htok = htok;
7218            // c:2768 — `eparams->postassigns = *state->pc++;`
7219            if state.pc < state.prog.prog.len() {
7220                eparams.postassigns = state.prog.prog[state.pc] as i32;
7221                state.pc += 1;
7222            }
7223            // c:2769 — `eparams->assignspc = state->pc;`
7224            eparams.assignspc = Some(state.pc);
7225            // c:2770-2776 — walk past the postassigns.
7226            let mut k = 0i32;
7227            while k < eparams.postassigns {
7228                if state.pc >= state.prog.prog.len() {
7229                    break;
7230                }
7231                code = state.prog.prog[state.pc];
7232                // c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
7233                state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
7234                    3 // c:2774
7235                } else {
7236                    (WC_ASSIGN_NUM(code) + 2) as usize // c:2775
7237                };
7238                k += 1;
7239            }
7240        }
7241        _ => {
7242            // c:2779-2783 default.
7243            eparams.args = None;
7244            eparams.assignspc = None;
7245            eparams.htok = 0;
7246        }
7247    }
7248}
7249
7250/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
7251/// Stack of `"context"` labels used by `eval`-style nested execution:
7252/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
7253/// ..., "context")` pushes its label and pops on return.
7254pub static zsh_eval_context: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
7255
7256/// Port of `static int donetrap;` from `Src/exec.c:1351`. Tracks
7257/// whether the ZERR trap has already fired for the current sublist.
7258/// C source resets to 0 at sublist start (c:1455) and sets to 1
7259/// after `dotrap(SIGZERR)` (c:1602). The check
7260/// `if (!this_noerrexit && !donetrap && !this_donetrap)` at c:1598
7261/// suppresses re-firing within the same sublist AND, crucially,
7262/// carries the "already fired" state across a function-call return
7263/// boundary so the outer caller's post-command check doesn't fire
7264/// ZERR a second time for the same logical error. Bug #303 in
7265/// docs/BUGS.md.
7266///
7267/// Reset at each top-level statement boundary via
7268/// `BUILTIN_DONETRAP_RESET` emitted by `compile_list`. Set after
7269/// `dotrap(SIGZERR)` fires inside `BUILTIN_ERREXIT_CHECK`.
7270pub static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
7271
7272/// Port of `save_params(Estate state, Wordcode pc, LinkList *restore_p,
7273/// LinkList *remove_p)` from `Src/exec.c:4410-4458`. Walk WC_ASSIGN
7274/// chain at `pc`, snapshot each existing param into `restore_p` (so
7275/// the builtin/shfunc can restore them on return) and enqueue every
7276/// touched name in `remove_p` (so we know what to unset).
7277pub fn save_params(
7278    state: &mut estate,
7279    pc: usize,
7280    restore_p: &mut Vec<crate::ported::zsh_h::param>,
7281    remove_p: &mut Vec<String>,
7282) {
7283    use crate::ported::zsh_h::{
7284        PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
7285        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
7286    };
7287    // c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
7288    // c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
7289    let mut p = pc;
7290    // c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
7291    loop {
7292        if p >= state.prog.prog.len() {
7293            break;
7294        }
7295        let ac = state.prog.prog[p];
7296        if wc_code(ac) != WC_ASSIGN {
7297            break;
7298        }
7299        // c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
7300        let s = ecrawstr(&state.prog, p + 1, None);
7301        // c:4421 — `pm = paramtab->getnode(paramtab, s)`
7302        let pm_clone: Option<crate::ported::zsh_h::param> = {
7303            let tab = paramtab().read().unwrap();
7304            tab.get(&s).map(|b| (**b).clone())
7305        };
7306        if let Some(pm) = pm_clone {
7307            // c:4423-4424 — `if (pm->env) delenv(pm);`
7308            if pm.env.is_some() {
7309                crate::ported::params::delenv(&s);
7310            }
7311            // c:4425-4448 — copy if not readonly-special.
7312            if (pm.node.flags & PM_SPECIAL as i32) == 0 {
7313                // c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
7314                let mut tpm = pm.clone();
7315                tpm.node.nam = s.clone();
7316                // copyparam with fakecopy=0 already done by the clone()
7317                // (Clone derives a deep copy of param fields).
7318                restore_p.push(tpm); // c:4451
7319            } else if (pm.node.flags & PM_READONLY as i32) == 0 {
7320                // c:4439-4448 — special-but-not-readonly: fakecopy=1.
7321                let mut tpm = pm.clone();
7322                tpm.node.nam = pm.node.nam.clone();
7323                restore_p.push(tpm); // c:4451
7324            }
7325            // c:4449 — `addlinknode(*remove_p, dupstring(s));`
7326            remove_p.push(s.clone());
7327        } else {
7328            // c:4453 — `addlinknode(*remove_p, dupstring(s));`
7329            remove_p.push(s.clone());
7330        }
7331        // c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
7332        p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
7333            3
7334        } else {
7335            (ZWC_ASSIGN_NUM(ac) + 2) as usize
7336        };
7337    }
7338}
7339
7340/// Port of `restore_params(LinkList restorelist, LinkList removelist)`
7341/// from `Src/exec.c:4464-4528`. After the builtin/shfunc returns,
7342/// unset every name in removelist, then for each saved param in
7343/// restorelist re-install its values (PM_SPECIAL go through gsu
7344/// setfn; regular params re-enter paramtab as-is).
7345pub fn restore_params(restorelist: Vec<crate::ported::zsh_h::param>, removelist: Vec<String>) {
7346    use crate::ported::zsh_h::{PM_READONLY, PM_SPECIAL};
7347    // c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
7348    for s in &removelist {
7349        // c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
7350        let flags = {
7351            let tab = paramtab().read().unwrap();
7352            tab.get(s).map(|p| p.node.flags)
7353        };
7354        if let Some(f) = flags {
7355            if (f & PM_SPECIAL as i32) == 0 {
7356                // c:4473 — `pm->node.flags &= ~PM_READONLY;`
7357                let mut tab = paramtab().write().unwrap();
7358                if let Some(pm_mut) = tab.get_mut(s) {
7359                    pm_mut.node.flags &= !(PM_READONLY as i32);
7360                }
7361                // Drop write guard before calling unsetparam_pm.
7362                drop(tab);
7363                let mut tab = paramtab().write().unwrap();
7364                if let Some(pm_mut) = tab.get_mut(s) {
7365                    let _ = crate::ported::params::unsetparam_pm(pm_mut, 0, 0); // c:4474
7366                }
7367            }
7368        }
7369    }
7370    // c:4478-4523 — restore saved params.
7371    for pm in restorelist {
7372        // c:4481-4520 — PM_SPECIAL: route through gsu setfn.
7373        // c:4521-4523 — non-special: re-install via paramtab.
7374        if (pm.node.flags & PM_SPECIAL as i32) != 0 {
7375            // PM_SPECIAL restore: full path requires PM_TYPE dispatch
7376            // on gsu_s/i/f/a/h setfn. Each setfn fires the param's
7377            // canonical write hook. Pragmatic port: overwrite in
7378            // paramtab; daily-driver path rarely saves specials (those
7379            // are reserved-name vars like PATH/FPATH/etc. which can't
7380            // appear as `VAR=val cmd` prefix anyway).
7381            let mut tab = paramtab().write().unwrap();
7382            tab.insert(pm.node.nam.clone(), Box::new(pm));
7383        } else {
7384            // c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
7385            let mut tab = paramtab().write().unwrap();
7386            tab.insert(pm.node.nam.clone(), Box::new(pm));
7387        }
7388    }
7389}
7390
7391/// Port of `void execode(Eprog p, int dont_change_job, int exiting,
7392/// char *context)` from `Src/exec.c:1245-1282`. Set up an `estate`
7393/// around the given Eprog and run `execlist`. Maintains the
7394/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
7395/// call chain.
7396///
7397/// NOTE: this is the WORDCODE form (drives the ported `execlist`
7398/// interpreter). zshrs's live execution pipeline is fusevm
7399/// (`compile_zsh` → VM), so the top-level REPL `loop()` and most call
7400/// sites run through [`execode`] (the ZshProgram/fusevm form below)
7401/// instead. This wordcode entry is retained for the internal
7402/// function-body callers (`doshfunc` / autoload) that already hold an
7403/// `Eprog`.
7404pub fn execode_wordcode(p: crate::ported::zsh_h::Eprog, dont_change_job: i32, exiting: i32, context: &str) {
7405    // c:1245
7406    let prog_ref = *p;
7407    // c:1247 — `struct estate s;`
7408    let mut s = estate {
7409        prog: Box::new(prog_ref.clone()),
7410        // c:1269 — `s.pc = p->prog;` — start at index 0.
7411        pc: 0,
7412        // c:1270 — `s.strs = p->strs;`
7413        strs: prog_ref.strs.clone(),
7414        strs_offset: 0,
7415    };
7416    // c:1251-1266 — push context onto zsh_eval_context.
7417    let pushed = {
7418        if let Ok(mut ctx) = zsh_eval_context.lock() {
7419            ctx.push(context.to_string());
7420            true
7421        } else {
7422            false
7423        }
7424    };
7425    // c:1271 — `useeprog(p);`
7426    crate::ported::parse::useeprog(&mut s.prog);
7427    // c:1273 — `execlist(&s, dont_change_job, exiting);`
7428    execlist(&mut s, dont_change_job, exiting);
7429    // c:1275 — `freeeprog(p);`
7430    crate::ported::parse::freeeprog(&mut s.prog);
7431    // c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
7432    if pushed {
7433        if let Ok(mut ctx) = zsh_eval_context.lock() {
7434            ctx.pop();
7435        }
7436    }
7437}
7438
7439thread_local! {
7440    /// The long-lived interactive executor for the top-level `loop()`
7441    /// REPL (the `zsh_main` path). Set once by the bin before
7442    /// `zsh_main`; [`execode`] runs each parsed program through it.
7443    /// Persists for the whole session so variables/functions survive
7444    /// across prompts.
7445    static SESSION_EXECUTOR: std::cell::Cell<Option<*mut crate::vm_helper::ShellExecutor>> =
7446        const { std::cell::Cell::new(None) };
7447}
7448
7449/// Register the persistent session executor used by [`execode`] for the
7450/// interactive `loop()` REPL. The pointer must outlive the session (the
7451/// bin keeps the executor alive until `zsh_main` exits the process).
7452pub fn install_session_executor(exec: &mut crate::vm_helper::ShellExecutor) {
7453    SESSION_EXECUTOR.with(|c| c.set(Some(exec as *mut crate::vm_helper::ShellExecutor)));
7454    // Mirror the pointer into the bridge so `with_session_context` can
7455    // establish a VM context for startup rc-sourcing (run_init_scripts,
7456    // c:1914) before the loop's first execode enters one.
7457    crate::fusevm_bridge::register_session_executor(exec);
7458}
7459
7460/// zshrs `execode` — run an already-parsed `ZshProgram` (Src/exec.c:220
7461/// `execode(prog, ...)`, called from `loop()`). This is the **exec.rs
7462/// exception** to the line-by-line port: rather than walk wordcode via
7463/// `execlist`, it drives zshrs's live engine — compile the program with
7464/// `compile_zsh` and run it on the session executor's fusevm VM. The
7465/// faithful `loop()` in init.rs calls this exactly as C calls execode.
7466/// Returns `$?` (0 when no session executor is installed).
7467pub fn execode(
7468    program: &crate::parse::ZshProgram,
7469    _dont_change_job: i32,
7470    _exiting: i32,
7471    _context: &str,
7472) -> i32 {
7473    SESSION_EXECUTOR.with(|c| match c.get() {
7474        // SAFETY: set by install_session_executor to an executor that
7475        // lives for the whole single-threaded interactive session;
7476        // loop() runs only after the bin installs it.
7477        Some(ptr) => unsafe { (*ptr).execute_program(program) },
7478        None => 0,
7479    })
7480}
7481
7482// =========================================================================
7483// Live-executor accessors (former `exec_hooks` OnceLock layer).
7484//
7485// These are the **exec.rs exception**: src/ported/ code reaches the
7486// live fusevm `ShellExecutor` (param store, function dispatch, nested
7487// script/cmdsubst execution) through these thin wrappers instead of the
7488// deleted `exec_hooks` fn-pointer registry. Each delegates to
7489// `fusevm_bridge::try_with_executor` — `Some` when a VM execution
7490// context is in scope, `None` in unit-test / compsys contexts with no
7491// bridge running — and reproduces the exact per-call fallback the old
7492// `exec_hooks` wrappers used when no hook was installed. Behavior is
7493// byte-for-byte identical to the OnceLock path; only the indirection is
7494// gone. See `feedback_no_shellexecutor_in_ported` /
7495// `feedback_no_exec_script_from_ported`: the bridge belongs in exec.rs
7496// (the sanctioned exception), not scattered through src/ported.
7497// =========================================================================
7498
7499/// Array param value via the live executor; falls back to the direct
7500/// param table (`params::getaparam`) when no executor is in scope, so
7501/// compsys / unit-test environments still observe shell-side arrays.
7502pub fn array(name: &str) -> Option<Vec<String>> {
7503    if let Some(Some(v)) = crate::fusevm_bridge::try_with_executor(|exec| exec.array(name)) {
7504        return Some(v);
7505    }
7506    crate::ported::params::getaparam(name)
7507}
7508
7509/// Associative-array param value via the live executor (`None` when no
7510/// executor / not set).
7511pub fn assoc(name: &str) -> Option<indexmap::IndexMap<String, String>> {
7512    crate::fusevm_bridge::try_with_executor(|exec| exec.assoc(name)).flatten()
7513}
7514
7515/// Store an array param into the live executor (no-op without one).
7516pub fn set_array(name: &str, val: Vec<String>) {
7517    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_array(name.to_string(), val));
7518}
7519
7520/// Store an associative-array param into the live executor (no-op
7521/// without one).
7522pub fn set_assoc(name: &str, val: indexmap::IndexMap<String, String>) {
7523    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_assoc(name.to_string(), val));
7524}
7525
7526/// Unset a scalar param in the live executor (no-op without one).
7527pub fn unset_scalar(name: &str) {
7528    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_scalar(name));
7529}
7530
7531/// Unset an array param in the live executor (no-op without one).
7532pub fn unset_array(name: &str) {
7533    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_array(name));
7534}
7535
7536/// Unset an associative-array param in the live executor (no-op
7537/// without one).
7538pub fn unset_assoc(name: &str) {
7539    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_assoc(name));
7540}
7541
7542/// Dispatch a shell-function call by name through the live executor
7543/// (full doshfunc scope wrap). `None` when no executor / not a
7544/// function.
7545pub fn dispatch_function_call(name: &str, args: &[String]) -> Option<i32> {
7546    if let Some(r) =
7547        crate::fusevm_bridge::try_with_executor(|exec| exec.dispatch_function_call(name, args))
7548    {
7549        return r;
7550    }
7551    // No active VM context: this is the loop()/zsh_main exit path where
7552    // `zexit` fires a `TRAPEXIT() { ... }` (via dotrap, Src/builtin.c:6043)
7553    // or the `zshexit` hook after `loop()` returned. Enter the installed
7554    // session executor so the handler runs in the shell. SAFETY per execode.
7555    SESSION_EXECUTOR.with(|c| match c.get() {
7556        Some(ptr) => {
7557            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
7558            unsafe { (*ptr).dispatch_function_call(name, args) }
7559        }
7560        None => None,
7561    })
7562}
7563
7564/// Body-only function dispatch (no doshfunc scope wrap) — call as the
7565/// `body_runner` of a direct `doshfunc(...)` invocation to avoid the
7566/// double-wrap of going back through [`dispatch_function_call`]. `None`
7567/// when no executor.
7568pub fn run_function_body(name: &str, args: &[String]) -> Option<i32> {
7569    if let Some(r) =
7570        crate::fusevm_bridge::try_with_executor(|exec| exec.run_function_body_only(name, args))
7571    {
7572        return r;
7573    }
7574    // Session-executor fallback for the no-VM-context exit path (e.g. the
7575    // `zshexit` hook fired by `zexit` from zsh_main). SAFETY per execode.
7576    SESSION_EXECUTOR.with(|c| match c.get() {
7577        Some(ptr) => {
7578            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
7579            unsafe { (*ptr).run_function_body_only(name, args) }
7580        }
7581        None => None,
7582    })
7583}
7584
7585/// Run a script source string on the live executor. `Ok(0)` when no
7586/// executor is in scope.
7587pub fn execute_script(src: &str) -> Result<i32, String> {
7588    if let Some(r) = crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script(src)) {
7589        return r;
7590    }
7591    // No active VM context: the loop()/zsh_main exit path where `zexit`
7592    // runs a `trap '...' EXIT` raw body (Src/builtin.c:6043) after `loop()`
7593    // returned. Enter the installed session executor so the trap body runs
7594    // in the shell instead of silently no-op'ing. SAFETY per execode.
7595    SESSION_EXECUTOR.with(|c| match c.get() {
7596        Some(ptr) => {
7597            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
7598            unsafe { (*ptr).execute_script(src) }
7599        }
7600        None => Ok(0),
7601    })
7602}
7603
7604/// Run a script source string through the live executor's zsh pipeline.
7605/// `Ok(0)` when no executor is in scope.
7606pub fn execute_script_zsh_pipeline(src: &str) -> Result<i32, String> {
7607    crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script_zsh_pipeline(src))
7608        .unwrap_or(Ok(0))
7609}
7610
7611/// Run a `$(...)` command substitution on the live executor, returning
7612/// captured stdout. Empty string when no executor is in scope.
7613pub fn run_command_substitution(cmd: &str) -> String {
7614    crate::fusevm_bridge::try_with_executor(|exec| exec.run_command_substitution(cmd))
7615        .unwrap_or_default()
7616}
7617
7618/// Positional parameters ($1..$N) from the live executor; empty without
7619/// one.
7620pub fn pparams() -> Vec<String> {
7621    crate::fusevm_bridge::try_with_executor(|exec| exec.pparams()).unwrap_or_default()
7622}
7623
7624/// Replace the positional parameters in the live executor (no-op
7625/// without one).
7626pub fn set_pparams(v: Vec<String>) {
7627    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_pparams(v));
7628}
7629
7630/// Drop a function from both the compiled-chunk and source maps in the
7631/// live executor. Returns true if either entry existed; false when no
7632/// executor.
7633pub fn unregister_function(name: &str) -> bool {
7634    crate::fusevm_bridge::try_with_executor(|exec| {
7635        let a = exec.functions_compiled.remove(name).is_some();
7636        let b = exec.function_source.remove(name).is_some();
7637        a || b
7638    })
7639    .unwrap_or(false)
7640}
7641
7642/// Saved outer stdout fd for an in-progress `$(...)` capture (top of
7643/// the bridge's CMDSUBST_OUTER_FDS stack), or `None` when not inside a
7644/// cmdsub. Used by the trap dispatcher to route a trap body's stdout to
7645/// the parent terminal instead of the cmdsub-bound pipe (Bug #56).
7646pub fn cmdsubst_outer_stdout() -> Option<i32> {
7647    crate::fusevm_bridge::cmdsubst_outer_stdout()
7648}
7649
7650/// Port of `execautofn_basic(Estate state, UNUSED(int do_exec))` from
7651/// `Src/exec.c:5608-5630`. Run a pre-loaded autoload function body
7652/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
7653/// the call so `%N` / `%x` reflect the autoload target during
7654/// execution.
7655pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
7656    // c:5608
7657    // c:5613 — `shf = state->prog->shf;`
7658    let shf = match state.prog.shf.as_deref() {
7659        Some(s) => s.clone(),
7660        None => return LASTVAL.load(Ordering::Relaxed),
7661    };
7662
7663    // c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
7664    // top-of-stack tracking is in modules::parameter::FUNCSTACK.
7665    {
7666        let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
7667        if let Some(top) = stk.last_mut() {
7668            if top.filename.is_none() {
7669                // c:5620 — `funcstack->filename = getshfuncfile(shf);`
7670                top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
7671            }
7672        }
7673    }
7674
7675    // c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
7676    let oldscriptname = crate::ported::utils::scriptname_get();
7677    let oldscriptfilename = crate::ported::utils::scriptfilename_get();
7678    // c:5624 — `scriptname = dupstring(shf->node.nam);`
7679    crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
7680    // c:5625 — `scriptfilename = getshfuncfile(shf);`
7681    crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
7682        &shf.node.nam,
7683    ));
7684    // c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
7685    if let Some(funcdef) = shf.funcdef.clone() {
7686        execode_wordcode(funcdef, 1, 0, "loadautofunc");
7687    }
7688    // c:5627-5628 — restore.
7689    crate::ported::utils::set_scriptname(oldscriptname);
7690    crate::ported::utils::set_scriptfilename(oldscriptfilename);
7691
7692    LASTVAL.load(Ordering::Relaxed) // c:5630
7693}
7694
7695/// Port of `static int execautofn(Estate state, UNUSED(int do_exec))`
7696/// from `Src/exec.c:5635-5644`. The autoload-aware dispatch entry
7697/// for `WC_AUTOFN`: fault the function body in via `loadautofn`,
7698/// then hand off to `execautofn_basic` to actually run it.
7699///
7700/// C body:
7701/// ```c
7702/// static int
7703/// execautofn(Estate state, UNUSED(int do_exec))
7704/// {
7705///     Shfunc shf;
7706///     if (!(shf = loadautofn(state->prog->shf, 1, 0, 0)))
7707///         return 1;
7708///     state->prog->shf = shf;
7709///     return execautofn_basic(state, 0);
7710/// }
7711/// ```
7712///
7713/// Rust port: `loadautofn` mutates the `shfunc` in place via a raw
7714/// pointer and returns 0/1 (success/failure), so the explicit
7715/// `state->prog->shf = shf` assignment in C is implicit here.
7716pub fn execautofn(state: &mut estate, _do_exec: i32) -> i32 {
7717    // c:5638-5640 — `if (!(shf = loadautofn(state->prog->shf, 1, 0, 0))) return 1;`
7718    let shf_ptr: *mut shfunc = match state.prog.shf.as_mut() {
7719        Some(b) => &mut **b as *mut shfunc,
7720        None => return 1,
7721    };
7722    if loadautofn(shf_ptr, 1, 0, 0) != 0 {
7723        return 1;
7724    }
7725    // c:5643 — `return execautofn_basic(state, 0);`
7726    execautofn_basic(state, 0)
7727}
7728
7729/// Port of `execpline2(Estate state, wordcode pcode, int how, int input,
7730/// int output, int last1)` from `Src/exec.c:1989-2040`. Recursive
7731/// multi-stage pipe walker: at each step, analyse the current
7732/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
7733/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
7734/// its input fd.
7735pub fn execpline2(
7736    state: &mut estate,
7737    pcode: wordcode,
7738    how: i32,
7739    input: i32,
7740    output: i32,
7741    last1: i32,
7742) {
7743    use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
7744    use crate::ported::zsh_h::{
7745        execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
7746        WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
7747    };
7748    // c:1991
7749    let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
7750
7751    // c:1996-1997 — `if (breaks || retflag) return;`
7752    if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
7753        return;
7754    }
7755
7756    // c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
7757    //                  lineno = WC_PIPE_LINENO(pcode) - 1;`
7758    if !crate::ported::zsh_h::IN_EVAL_TRAP()
7759        && INEVAL.load(Ordering::SeqCst) == 0
7760        && ZWC_PIPE_LINENO(pcode) != 0
7761    {
7762        let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
7763        crate::ported::input::lineno.with(|l| l.set(new_lineno));
7764    }
7765
7766    // c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
7767    if pline_level.load(Ordering::Relaxed) == 1 {
7768        // c:2003
7769        if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
7770            // c:2004 — `(how & Z_ASYNC) || !sfcontext`
7771            // c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
7772            //   state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
7773            let pc_for_text = state.pc
7774                + if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7775                    0
7776                } else {
7777                    1
7778                };
7779            let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
7780            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7781                *lpt = text;
7782            }
7783        } else {
7784            // c:2010 — `list_pipe_text[0] = '\0';`
7785            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7786                lpt.clear();
7787            }
7788        }
7789    }
7790
7791    if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7792        // c:2012-2014 — terminal stage: analyse + exec directly.
7793        execcmd_analyse(state, &mut eparams); // c:2013
7794        execcmd_exec(
7795            state,
7796            &mut eparams,
7797            input,
7798            output,
7799            how,
7800            if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
7801            -1,                             // c:2014 close_if_forked = -1
7802        );
7803    } else {
7804        // c:2015-2039 — non-terminal stage: pipe + fork + recurse.
7805        let mut pipes: [i32; 2] = [-1, -1]; // c:2016
7806        let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
7807                                                               // c:2018 — `Wordcode next = state->pc + (*state->pc);`
7808        let next = if state.pc < state.prog.prog.len() {
7809            state.pc + state.prog.prog[state.pc] as usize
7810        } else {
7811            state.pc
7812        };
7813        // c:2020 — `++state->pc;`
7814        if state.pc < state.prog.prog.len() {
7815            state.pc += 1;
7816        }
7817        execcmd_analyse(state, &mut eparams); // c:2021
7818
7819        if mpipe(&mut pipes) < 0 {
7820            // c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
7821        }
7822
7823        // c:2027 — `addfilelist(NULL, pipes[0]);`
7824        // C uses the current thisjob's filelist; Rust port wires through JOBTAB.
7825        if let Some(jt) = JOBTAB.get() {
7826            let mut guard = jt.lock().unwrap();
7827            let tj = {
7828                if let Some(m) = THISJOB.get() {
7829                    *m.lock().unwrap()
7830                } else {
7831                    -1
7832                }
7833            };
7834            if tj >= 0 {
7835                if let Some(j) = guard.get_mut(tj as usize) {
7836                    crate::ported::jobs::addfilelist(j, None, pipes[0]);
7837                }
7838            }
7839        }
7840
7841        // c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
7842        execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
7843        let _ = zclose(pipes[1]); // c:2029
7844        state.pc = next; // c:2030
7845
7846        // c:2034 — `cmdpush(CS_PIPE);`
7847        cmdpush(CS_PIPE as u8);
7848        // c:2035 — `list_pipe = 1;`
7849        list_pipe.store(1, Ordering::Relaxed);
7850        // c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
7851        let next_pcode = if state.pc < state.prog.prog.len() {
7852            state.prog.prog[state.pc]
7853        } else {
7854            0
7855        };
7856        if state.pc < state.prog.prog.len() {
7857            state.pc += 1;
7858        }
7859        execpline2(state, next_pcode, how, pipes[0], output, last1);
7860        // c:2037 — `list_pipe = old_list_pipe;`
7861        list_pipe.store(old_list_pipe, Ordering::Relaxed);
7862        // c:2038 — `cmdpop();`
7863        cmdpop();
7864    }
7865}
7866
7867/// Port of `execpline(Estate state, wordcode slcode, int how, int last1)`
7868/// from `Src/exec.c:1668-1942`. Walks the WC_PIPE chain, sets up
7869/// pipes/fork between stages, handles Z_TIMED / Z_ASYNC.
7870///
7871/// The full body needs: pipe(), fork(), execcmd_exec per-stage, job-
7872/// table installation, wait-status reaping. Until those primitives
7873/// land in faithfully-ported form, the structural shape is preserved
7874/// here: walk the WC_PIPE chain, exec each cmd inline (the inlined
7875/// match is the same dispatch C's exec.c:2901-3700 uses), propagate
7876/// LASTVAL through stages. Single-cmd pipelines work end-to-end;
7877/// multi-stage pipelines fall back to sequential execution (status
7878/// of last stage) until pipe + fork land.
7879pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
7880    use crate::ported::zsh_h::{WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, Z_TIMED};
7881    let slflags = WC_SUBLIST_FLAGS(slcode); // c:1673
7882                                            // c:1677-1680 — `if (wc_code(code) != WC_PIPE && !(how & Z_TIMED))
7883                                            //                  return lastval = (slflags & WC_SUBLIST_NOT) != 0;
7884                                            //                else if (slflags & WC_SUBLIST_NOT) last1 = 0;`
7885    if state.pc >= state.prog.prog.len() || wc_code(state.prog.prog[state.pc]) != WC_PIPE {
7886        if (how & Z_TIMED as i32) == 0 {
7887            let ret = if (slflags & WC_SUBLIST_NOT) != 0 {
7888                1
7889            } else {
7890                0
7891            };
7892            LASTVAL.store(ret, Ordering::Relaxed);
7893            return ret;
7894        }
7895    }
7896    let mut last1 = last1;
7897    if (slflags & WC_SUBLIST_NOT) != 0 {
7898        last1 = 0; // c:1680
7899    }
7900    let mut code = state.prog.prog[state.pc];
7901    state.pc += 1;
7902    let mut last_status: i32 = 0;
7903    use crate::ported::zsh_h::{WC_PIPE_END, WC_PIPE_TYPE};
7904    let _ = how;
7905    let _ = last1;
7906    // c:1700-1940 — main WC_PIPE loop. Each iter: exec one cmd, advance.
7907    loop {
7908        // c:2901-3700 — execcmd_exec dispatch tail inlined: match the
7909        // WC_* tag at state.pc and dispatch to the matching execX.
7910        // Same dispatch as `execfuncs[]` (exec.c:5499).
7911        use crate::ported::zsh_h::{
7912            WC_ARITH, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT, WC_SELECT,
7913            WC_SIMPLE, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
7914        };
7915        let s = if state.pc < state.prog.prog.len() {
7916            let inner = state.prog.prog[state.pc];
7917            match wc_code(inner) {
7918                WC_SIMPLE => execsimple(state),
7919                WC_SUBSH | WC_CURSH => execcursh(state, 0),
7920                WC_FOR => execfor(state, 0),
7921                WC_SELECT => execselect(state, 0),
7922                WC_CASE => execcase(state, 0),
7923                WC_IF => execif(state, 0),
7924                WC_WHILE => execwhile(state, 0),
7925                WC_REPEAT => execrepeat(state, 0),
7926                WC_FUNCDEF => execfuncdef(state, None),
7927                WC_TIMED => exectime(state, 0),
7928                WC_COND => execcond(state, 0),
7929                WC_ARITH => execarith(state, 0),
7930                WC_TRY => exectry(state, 0),
7931                _ => {
7932                    state.pc += 1;
7933                    0
7934                }
7935            }
7936        } else {
7937            0
7938        };
7939        last_status = s;
7940        // c:1885-1893 — last pipe stage check.
7941        if WC_PIPE_TYPE(code) == WC_PIPE_END {
7942            break;
7943        }
7944        // c:1897-1900 — fetch next WC_PIPE header for the next stage.
7945        if state.pc >= state.prog.prog.len() {
7946            break;
7947        }
7948        let next_code = state.prog.prog[state.pc];
7949        if wc_code(next_code) != WC_PIPE {
7950            break;
7951        }
7952        state.pc += 1;
7953        code = next_code;
7954        // Multi-stage pipe() + fork() per cmd is now ported via
7955        // `execpline2` (c:1991-2040). Callers wanting full pipeline
7956        // isolation route through that path; this inline dispatch
7957        // serves the single-process simple-command tree-walker used
7958        // by the fusevm bytecode shim, which does its own
7959        // pipe/fork via `OpPipeCreate`/`OpFork` ops.
7960    }
7961    LASTVAL.store(last_status, Ordering::Relaxed);
7962    last_status
7963}
7964
7965// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
7966// inlined at every call site (execsimple, execpline) as the match
7967// expression that selects the right execX function. There's no
7968// separate Rust fn for it because:
7969//   - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
7970//     occupies the canonical name (handling precommand modifiers).
7971//   - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
7972//     a table lookup at exec.c:5499 — not a separate function.
7973#[cfg(any())]
7974mod _execcmd_tail_doc_anchor {
7975    // c:2901-3700 — see inlined match in execpline + execsimple above.
7976    // c:5499 — execfuncs[] table inlined as the same match.
7977}
7978
7979// --- loop.c entries ---------------------------------------------------
7980
7981/// Port of `execfor(Estate state, int do_exec)` from `Src/loop.c:50-202`.
7982/// `for var in args; do body; done` and the C-style `for ((init;cond;adv))`
7983/// variant. WC_FOR_TYPE distinguishes PPARAM (use $@) / LIST (explicit
7984/// words) / COND (C-style).
7985pub fn execfor(state: &mut estate, do_exec: i32) -> i32 {
7986    use crate::ported::zsh_h::Z_END;
7987    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:54
7988    let iscond = WC_FOR_TYPE(code) == WC_FOR_COND; // c:55
7989    let mut last_iter = false; // c:57 — `int last = 0;`
7990    let mut val: i64 = 0; // c:59
7991    let mut vars: Vec<String> = Vec::new();
7992    let mut args: Vec<String> = Vec::new();
7993    let mut cond_expr: String = String::new();
7994    let mut advance_expr: String = String::new();
7995    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:62-63
7996    let end_pc = state.pc + WC_FOR_SKIP(code) as usize; // c:65
7997    let mut ctok = 0i32;
7998    let mut atok = 0i32;
7999    if iscond {
8000        // c:68-82 — C-style for: init expr at top, then cond/advance.
8001        let init = ecgetstr(state, EC_NODUP, None); // c:68
8002        let init_sub = singsub(&init); // c:69
8003        if isset(XTRACE) {
8004            // c:70-75
8005            let init_show = untokenize(&init_sub);
8006            printprompt4();
8007            eprintln!("{}", init_show);
8008        }
8009        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8010            let _ = wc_matheval(&init_sub); // c:77 — `matheval(str);`
8011        }
8012        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8013            // c:79-82
8014            state.pc = end_pc;
8015            simple_pline.store(old_simple_pline, Ordering::Relaxed);
8016            return 1;
8017        }
8018        cond_expr = ecgetstr(state, EC_NODUP, Some(&mut ctok)); // c:83
8019        advance_expr = ecgetstr(state, EC_NODUP, Some(&mut atok)); // c:84
8020    } else {
8021        // c:86 — `vars = ecgetlist(state, *state->pc++, EC_NODUP, NULL);`
8022        let count = state.prog.prog[state.pc] as usize;
8023        state.pc += 1;
8024        vars = ecgetlist(state, count, EC_NODUP, None);
8025        if WC_FOR_TYPE(code) == WC_FOR_LIST {
8026            // c:88-100 — explicit `for var in words`
8027            let mut htok = 0i32;
8028            let arg_count = state.prog.prog[state.pc] as usize;
8029            state.pc += 1;
8030            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
8031            if args.is_empty() {
8032                state.pc = end_pc;
8033                simple_pline.store(old_simple_pline, Ordering::Relaxed);
8034                return 0;
8035            }
8036            if htok != 0 {
8037                execsubst(&mut args); // c:96
8038                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8039                    state.pc = end_pc;
8040                    simple_pline.store(old_simple_pline, Ordering::Relaxed);
8041                    return 1;
8042                }
8043            }
8044        } else {
8045            // c:102-107 — implicit `for var` (no `in` clause) uses
8046            // the positional params $@ from PPARAMS (params.rs Mutex).
8047            args = crate::ported::builtin::PPARAMS
8048                .lock()
8049                .map(|p| p.clone())
8050                .unwrap_or_default();
8051        }
8052    }
8053    // c:111-112 — empty args ⇒ lastval = 0.
8054    if !iscond && args.is_empty() {
8055        LASTVAL.store(0, Ordering::Relaxed);
8056    }
8057    LOOPS.fetch_add(1, Ordering::SeqCst); // c:114 — `loops++;`
8058    pushheap(); // c:115
8059    cmdpush(CS_FOR as u8); // c:116
8060    let loop_pc = state.pc; // c:117
8061    let mut args_iter = args.into_iter();
8062    while !last_iter {
8063        if iscond {
8064            // c:119-138 — eval cond expression.
8065            let mut cs = cond_expr.clone();
8066            if ctok != 0 {
8067                cs = singsub(&cs);
8068            }
8069            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8070                let trimmed = cs.trim_start();
8071                if !trimmed.is_empty() {
8072                    if isset(XTRACE) {
8073                        printprompt4();
8074                        eprintln!("{}", trimmed);
8075                    }
8076                    val = wc_mathevali(trimmed).unwrap_or(0);
8077                } else {
8078                    val = 1;
8079                }
8080            }
8081            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8082                if BREAKS.load(Ordering::SeqCst) > 0 {
8083                    BREAKS.fetch_sub(1, Ordering::SeqCst);
8084                }
8085                LASTVAL.store(1, Ordering::Relaxed);
8086                break;
8087            }
8088            if val == 0 {
8089                break;
8090            }
8091        } else {
8092            // c:140-162 — for var binding from args.
8093            let mut count = 0;
8094            for name in &vars {
8095                let value = match args_iter.next() {
8096                    Some(v) => v,
8097                    None => {
8098                        if count != 0 {
8099                            last_iter = true;
8100                            String::new()
8101                        } else {
8102                            break;
8103                        }
8104                    }
8105                };
8106                if isset(XTRACE) {
8107                    printprompt4();
8108                    eprintln!("{}={}", name, value);
8109                }
8110                setloopvar(name, &value);
8111                count += 1;
8112            }
8113            if count == 0 {
8114                break;
8115            }
8116        }
8117        state.pc = loop_pc; // c:163
8118        let _do_exec_now = do_exec != 0 && !args_iter.clone().any(|_| true); // c:164 — `do_exec && args && empty(args)`
8119        let _ = execlist(state, 1, if _do_exec_now { 1 } else { 0 });
8120        // c:166-169 — breaks/continue handling.
8121        if BREAKS.load(Ordering::SeqCst) > 0 {
8122            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
8123            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
8124                break;
8125            }
8126            CONTFLAG.store(0, Ordering::SeqCst);
8127        }
8128        if RETFLAG.load(Ordering::SeqCst) != 0 {
8129            break;
8130        }
8131        // c:170-178 — C-style advance step.
8132        if iscond && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8133            let mut adv = advance_expr.clone();
8134            if atok != 0 {
8135                adv = singsub(&adv);
8136            }
8137            if isset(XTRACE) {
8138                printprompt4();
8139                eprintln!("{}", adv);
8140            }
8141            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
8142                let _ = wc_matheval(&adv);
8143            }
8144        }
8145        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8146            if BREAKS.load(Ordering::SeqCst) > 0 {
8147                BREAKS.fetch_sub(1, Ordering::SeqCst);
8148            }
8149            LASTVAL.store(1, Ordering::Relaxed);
8150            break;
8151        }
8152        freeheap(); // c:184
8153    }
8154    popheap(); // c:186
8155    cmdpop(); // c:187
8156    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:188
8157    simple_pline.store(old_simple_pline, Ordering::Relaxed);
8158    state.pc = end_pc;
8159    this_noerrexit.store(1, Ordering::Relaxed);
8160    let _ = Z_END;
8161    LASTVAL.load(Ordering::Relaxed)
8162}
8163
8164/// Port of `execselect(Estate state, UNUSED(int do_exec))` from
8165/// `Src/loop.c:217-410`. `select var in words; do body; done` REPL.
8166pub fn execselect(state: &mut estate, _do_exec: i32) -> i32 {
8167    // The full select body manages a REPL prompt, terminal columns,
8168    // selectlist redraw, etc. The `selectlist` helper at loop.rs:130
8169    // already ports c:347 (menu display). Structural execselect:
8170    // c:225-410 — read vars + words like execfor, then loop on stdin
8171    // input prompting via PROMPT3, set var=word, run body.
8172    let code = state.prog.prog[state.pc.wrapping_sub(1)];
8173    let end_pc = state.pc + WC_FOR_SKIP(code) as usize;
8174    // c:228-237 — read var name + words. Skip body and use existing
8175    // bridge handler at BUILTIN_RUN_SELECT for actual REPL until full
8176    // wordcode driver lands.
8177    state.pc = end_pc;
8178    this_noerrexit.store(1, Ordering::Relaxed);
8179    LASTVAL.load(Ordering::Relaxed)
8180}
8181
8182/// Port of `execwhile(Estate state, UNUSED(int do_exec))` from
8183/// `Src/loop.c:413-498`. `while/until cond; do body; done`.
8184pub fn execwhile(state: &mut estate, _do_exec: i32) -> i32 {
8185    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:417
8186    let isuntil = WC_WHILE_TYPE(code) == WC_WHILE_UNTIL; // c:419
8187    let end_pc = state.pc + WC_WHILE_SKIP(code) as usize; // c:422
8188    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:423
8189    let mut oldval: i32 = 0; // c:424
8190    pushheap(); // c:425
8191    cmdpush(if isuntil {
8192        CS_UNTIL as u8
8193    } else {
8194        CS_WHILE as u8
8195    }); // c:426
8196    LOOPS.fetch_add(1, Ordering::SeqCst); // c:427
8197    let loop_pc = state.pc; // c:428
8198    let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:419
8199                                                                 // c:430-456 — empty-loop fast path. If loop body is two WC_ENDs,
8200                                                                 // sit in a tight signal-wait loop until ^C breaks us.
8201    if state.prog.prog.get(loop_pc) == Some(&WC_END)
8202        && state.prog.prog.get(loop_pc + 1) == Some(&WC_END)
8203    {
8204        simple_pline.store(1, Ordering::Relaxed);
8205        // c:438-439 — spin until breaks.
8206        while BREAKS.load(Ordering::SeqCst) == 0 {
8207            std::thread::yield_now();
8208        }
8209        BREAKS.fetch_sub(1, Ordering::SeqCst);
8210        simple_pline.store(old_simple_pline, Ordering::Relaxed);
8211    } else {
8212        // c:441-485 — normal loop.
8213        loop {
8214            state.pc = loop_pc; // c:442
8215            noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:443
8216            simple_pline.store(1, Ordering::Relaxed); // c:446
8217            let _ = execlist(state, 1, 0); // c:448 — exec cond.
8218            simple_pline.store(old_simple_pline, Ordering::Relaxed);
8219            noerrexit.store(olderrexit, Ordering::Relaxed); // c:451
8220            let cond_status = LASTVAL.load(Ordering::Relaxed); // c:452
8221                                                               // c:453-460 — `if (!((lastval == 0) ^ isuntil)) break;`
8222            let cond_passed = (cond_status == 0) ^ isuntil;
8223            if !cond_passed {
8224                if BREAKS.load(Ordering::SeqCst) > 0 {
8225                    BREAKS.fetch_sub(1, Ordering::SeqCst);
8226                }
8227                if RETFLAG.load(Ordering::SeqCst) == 0 {
8228                    LASTVAL.store(oldval, Ordering::Relaxed);
8229                }
8230                break;
8231            }
8232            if RETFLAG.load(Ordering::SeqCst) != 0 {
8233                // c:461
8234                if BREAKS.load(Ordering::SeqCst) > 0 {
8235                    BREAKS.fetch_sub(1, Ordering::SeqCst);
8236                }
8237                break;
8238            }
8239            simple_pline.store(1, Ordering::Relaxed); // c:468
8240            let _ = execlist(state, 1, 0); // c:470 — exec body.
8241            simple_pline.store(old_simple_pline, Ordering::Relaxed);
8242            // c:472-477 — breaks/continue handling.
8243            if BREAKS.load(Ordering::SeqCst) > 0 {
8244                let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
8245                if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
8246                    break;
8247                }
8248                CONTFLAG.store(0, Ordering::SeqCst);
8249            }
8250            // c:478-481 — errflag bail.
8251            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8252                LASTVAL.store(1, Ordering::Relaxed);
8253                break;
8254            }
8255            // c:482-483 — retflag bail.
8256            if RETFLAG.load(Ordering::SeqCst) != 0 {
8257                break;
8258            }
8259            freeheap(); // c:484
8260            oldval = LASTVAL.load(Ordering::Relaxed); // c:485
8261        }
8262    }
8263    cmdpop(); // c:489
8264    popheap(); // c:490
8265    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:491
8266    state.pc = end_pc; // c:492
8267    this_noerrexit.store(1, Ordering::Relaxed); // c:493
8268    LASTVAL.load(Ordering::Relaxed)
8269}
8270
8271/// Port of `execrepeat(Estate state, UNUSED(int do_exec))` from
8272/// `Src/loop.c:499-551`. `repeat N; do body; done`.
8273pub fn execrepeat(state: &mut estate, _do_exec: i32) -> i32 {
8274    let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:503
8275    let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:507
8276    let end_pc = state.pc + WC_REPEAT_SKIP(code) as usize; // c:510
8277    let mut htok = 0i32;
8278    let mut tmp = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:512
8279    if htok != 0 {
8280        tmp = singsub(&tmp); // c:514
8281        tmp = untokenize(&tmp); // c:515
8282    }
8283    let count = wc_mathevali(&tmp).unwrap_or(0); // c:517
8284    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8285        simple_pline.store(old_simple_pline, Ordering::Relaxed);
8286        return 1;
8287    }
8288    LASTVAL.store(0, Ordering::Relaxed); // c:520
8289    pushheap(); // c:521
8290    cmdpush(CS_REPEAT as u8); // c:522
8291    LOOPS.fetch_add(1, Ordering::SeqCst); // c:523
8292    let loop_pc = state.pc; // c:524
8293    let mut remaining = count;
8294    while remaining > 0 {
8295        // c:525
8296        remaining -= 1;
8297        state.pc = loop_pc;
8298        let _ = execlist(state, 1, 0); // c:527
8299        freeheap(); // c:528
8300                    // c:529-534 — breaks/continue handling.
8301        if BREAKS.load(Ordering::SeqCst) > 0 {
8302            let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
8303            if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
8304                break;
8305            }
8306            CONTFLAG.store(0, Ordering::SeqCst);
8307        }
8308        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8309            // c:536-538
8310            LASTVAL.store(1, Ordering::Relaxed);
8311            break;
8312        }
8313        if RETFLAG.load(Ordering::SeqCst) != 0 {
8314            // c:540
8315            break;
8316        }
8317    }
8318    cmdpop(); // c:544
8319    popheap(); // c:545
8320    LOOPS.fetch_sub(1, Ordering::SeqCst); // c:546
8321    simple_pline.store(old_simple_pline, Ordering::Relaxed);
8322    state.pc = end_pc; // c:548
8323    this_noerrexit.store(1, Ordering::Relaxed); // c:549
8324    LASTVAL.load(Ordering::Relaxed)
8325}
8326
8327/// Port of `execif(Estate state, int do_exec)` from `Src/loop.c:553-598`.
8328/// `if cond; then body; elif ...; else ...; fi`.
8329pub fn execif(state: &mut estate, do_exec: i32) -> i32 {
8330    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:558
8331    let olderrexit = noerrexit.load(Ordering::Relaxed); // c:559
8332    let end_pc = state.pc + WC_IF_SKIP(code0) as usize; // c:560
8333    noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:562
8334    let mut s = 0i32; // c:557 — `s = 0`
8335    let mut run = 0i32; // c:557 — `run = 0`
8336    while state.pc < end_pc {
8337        // c:563
8338        let code = state.prog.prog[state.pc];
8339        state.pc += 1;
8340        // c:565-571 — non-IF, or IF_ELSE: break out.
8341        if wc_code(code) != WC_IF || WC_IF_TYPE(code) == WC_IF_ELSE {
8342            run = if wc_code(code) == WC_IF && WC_IF_TYPE(code) == WC_IF_ELSE {
8343                2
8344            } else {
8345                1
8346            };
8347            if run == 1 {
8348                state.pc -= 1; // back up onto the body header
8349            }
8350            break;
8351        }
8352        let next_pc = state.pc + WC_IF_SKIP(code) as usize; // c:572
8353        cmdpush(if s != 0 { CS_ELIF as u8 } else { CS_IF as u8 }); // c:573
8354        let _ = execlist(state, 1, 0); // c:574
8355        cmdpop(); // c:575
8356                  // c:576-579 — selected branch: lastval == 0.
8357        if LASTVAL.load(Ordering::Relaxed) == 0 {
8358            run = 1;
8359            break;
8360        }
8361        if RETFLAG.load(Ordering::SeqCst) != 0 {
8362            // c:580
8363            break;
8364        }
8365        s = 1;
8366        state.pc = next_pc;
8367    }
8368    noerrexit.store(olderrexit, Ordering::Relaxed); // c:584
8369                                                    // c:585-591 — run selected branch.
8370    if run != 0 {
8371        cmdpush(if run == 2 {
8372            CS_ELSE as u8
8373        } else if s != 0 {
8374            CS_ELIFTHEN as u8
8375        } else {
8376            CS_IFTHEN as u8
8377        });
8378        let _ = execlist(state, 1, do_exec);
8379        cmdpop();
8380    } else if RETFLAG.load(Ordering::SeqCst) == 0
8381        && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
8382    {
8383        LASTVAL.store(0, Ordering::Relaxed); // c:592
8384    }
8385    state.pc = end_pc; // c:594
8386    this_noerrexit.store(1, Ordering::Relaxed); // c:595
8387    LASTVAL.load(Ordering::Relaxed)
8388}
8389
8390/// Port of `execcase(Estate state, int do_exec)` from `Src/loop.c:600-733`.
8391/// `case word in pat) body ;; ... esac` with `;;`/`;&`/`;|` separators.
8392pub fn execcase(state: &mut estate, do_exec: i32) -> i32 {
8393    let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:603
8394    let end_pc = state.pc + WC_CASE_SKIP(code0) as usize; // c:607
8395                                                          // c:609-611 — read & expand the case-word.
8396    let raw_word = ecgetstr(state, EC_DUP, None);
8397    let word_sub = singsub(&raw_word);
8398    let word = untokenize(&word_sub);
8399    let mut anypatok = false; // c:613
8400    cmdpush(CS_CASE as u8); // c:615
8401    let mut code = 0u32;
8402    while state.pc < end_pc {
8403        // c:616
8404        code = state.prog.prog[state.pc];
8405        state.pc += 1;
8406        if wc_code(code) != WC_CASE {
8407            break;
8408        }
8409        let next_pc = state.pc + WC_CASE_SKIP(code) as usize; // c:621
8410        let nalts = state.prog.prog[state.pc] as i32; // c:622
8411        state.pc += 1;
8412        let mut patok = false;
8413        let mut nalts_remaining = nalts;
8414        while !patok && nalts_remaining > 0 {
8415            // c:629-672 — try each alternative pattern.
8416            // c:631-633 — `npat = state->pc[1]; spprog = state->prog->pats + npat;`
8417            // zshrs's pat-compile-on-demand path: extract raw pat text + try patcompile/pattry.
8418            queue_signals(); // c:636
8419            let mut htok = 0i32;
8420            let pat_raw = ecrawstr(&state.prog, state.pc, Some(&mut htok));
8421            let pat = if htok != 0 {
8422                singsub(&pat_raw)
8423            } else {
8424                pat_raw
8425            };
8426            if let Some(pprog) = patcompile(&{ let mut __pat_tok = (&pat).to_string(); crate::ported::glob::tokenize(&mut __pat_tok); __pat_tok }, PAT_STATIC, None) {
8427                // c:660 — `if (pprog && pattry(pprog, word)) patok = anypatok = 1;`
8428                if pattry(&pprog, &word) {
8429                    patok = true;
8430                    anypatok = true;
8431                }
8432            } else {
8433                zerr(&format!("bad pattern: {}", pat)); // c:657
8434            }
8435            state.pc += 2; // c:664 — `state->pc += 2;`
8436            nalts_remaining -= 1;
8437            unqueue_signals(); // c:666
8438        }
8439        state.pc += (2 * nalts_remaining) as usize; // c:668
8440        if patok {
8441            // c:672-684 — run selected arm body.
8442            let _ = execlist(
8443                state,
8444                1,
8445                ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
8446            );
8447            // c:675-682 — chain into ;& and ;| siblings.
8448            while RETFLAG.load(Ordering::SeqCst) == 0
8449                && wc_code(code) == WC_CASE
8450                && WC_CASE_TYPE(code) == WC_CASE_AND
8451                && state.pc < end_pc
8452            {
8453                state.pc = next_pc;
8454                code = state.prog.prog[state.pc];
8455                state.pc += 1;
8456                let inner_next = state.pc + WC_CASE_SKIP(code) as usize;
8457                let inner_nalts = state.prog.prog[state.pc] as usize;
8458                state.pc += 1 + 2 * inner_nalts;
8459                let _ = execlist(
8460                    state,
8461                    1,
8462                    ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
8463                );
8464                let _ = inner_next;
8465            }
8466            if WC_CASE_TYPE(code) != WC_CASE_TESTAND {
8467                break;
8468            }
8469        }
8470        state.pc = next_pc; // c:687
8471    }
8472    cmdpop(); // c:691
8473    state.pc = end_pc; // c:693
8474    if !anypatok {
8475        // c:695-696
8476        LASTVAL.store(0, Ordering::Relaxed);
8477    }
8478    this_noerrexit.store(1, Ordering::Relaxed); // c:697
8479    LASTVAL.load(Ordering::Relaxed)
8480}
8481
8482/// Port of `exectry(Estate state, int do_exec)` from `Src/loop.c:735-798`.
8483/// `{ try } always { finally }`: capture errflag/retflag/breaks/contflag
8484/// from the try-clause, reset them around the always-clause, then
8485/// restore if always-clause didn't override.
8486pub fn exectry(state: &mut estate, _do_exec: i32) -> i32 {
8487    let header = state.prog.prog[state.pc.wrapping_sub(1)]; // c:741
8488    let end_pc = state.pc + WC_TRY_SKIP(header) as usize; // c:742
8489    let try_inner = state.prog.prog[state.pc]; // c:743
8490    let always_pc = state.pc + 1 + WC_TRY_SKIP(try_inner) as usize; // c:743
8491    state.pc += 1; // c:744
8492    pushheap(); // c:745
8493    cmdpush(CS_CURSH as u8); // c:746
8494    try_tryflag.fetch_add(1, Ordering::SeqCst); // c:749
8495    let _ = execlist(state, 1, 0); // c:750
8496    try_tryflag.fetch_sub(1, Ordering::SeqCst); // c:751
8497    let try_status = LASTVAL.load(Ordering::Relaxed);
8498    let endval = if try_status != 0 {
8499        // c:754
8500        try_status
8501    } else {
8502        (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) as i32
8503    };
8504    freeheap(); // c:756
8505    cmdpop(); // c:758
8506    cmdpush(CS_ALWAYS as u8); // c:759
8507                              // c:762-763 — save try_errflag / try_interrupt.
8508    let saved_err = errflag.load(Ordering::Relaxed);
8509    let save_try_err = (saved_err & ERRFLAG_ERROR) != 0;
8510    let save_try_int = (saved_err & ERRFLAG_INT) != 0;
8511    // c:Src/loop.c:763-766 — save the canonical globals AND update
8512    // them to reflect the just-finished try body's exit state before
8513    // running the always-arm. `$TRY_BLOCK_ERROR` / `$TRY_BLOCK_INTERRUPT`
8514    // read these globals directly (lookup_special_var arms), so the
8515    // always body must see "errflag at try-end" not "-1 sentinel".
8516    let saved_try_errflag = crate::ported::r#loop::try_errflag
8517        .load(Ordering::Relaxed);
8518    let saved_try_interrupt = crate::ported::r#loop::try_interrupt
8519        .load(Ordering::Relaxed);
8520    crate::ported::r#loop::try_errflag.store(
8521        (saved_err & ERRFLAG_ERROR) as i64,
8522        Ordering::Relaxed,
8523    ); // c:765
8524    crate::ported::r#loop::try_interrupt.store(
8525        if (saved_err & ERRFLAG_INT) != 0 { 1 } else { 0 },
8526        Ordering::Relaxed,
8527    ); // c:766
8528    // c:768 — `errflag = 0;` (clear both bits).
8529    errflag.fetch_and(!(ERRFLAG_ERROR | ERRFLAG_INT), Ordering::Relaxed);
8530    // c:769-774 — save retflag/breaks/contflag.
8531    let save_retflag = RETFLAG.swap(0, Ordering::SeqCst);
8532    let save_breaks = BREAKS.swap(0, Ordering::SeqCst);
8533    let save_contflag = CONTFLAG.swap(0, Ordering::SeqCst);
8534    state.pc = always_pc; // c:776
8535    let _ = execlist(state, 1, 0); // c:777
8536                                   // c:779-786 — restore errflag bits.
8537    if save_try_err {
8538        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
8539    } else {
8540        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
8541    }
8542    if save_try_int {
8543        errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed);
8544    } else {
8545        errflag.fetch_and(!ERRFLAG_INT, Ordering::Relaxed);
8546    }
8547    // c:Src/loop.c:787-788 — restore the canonical globals.
8548    crate::ported::r#loop::try_errflag.store(saved_try_errflag, Ordering::Relaxed);
8549    crate::ported::r#loop::try_interrupt
8550        .store(saved_try_interrupt, Ordering::Relaxed);
8551    // c:789-794 — re-arm retflag/breaks/contflag only if always didn't override.
8552    if RETFLAG.load(Ordering::SeqCst) == 0 {
8553        RETFLAG.store(save_retflag, Ordering::SeqCst);
8554    }
8555    if BREAKS.load(Ordering::SeqCst) == 0 {
8556        BREAKS.store(save_breaks, Ordering::SeqCst);
8557    }
8558    if CONTFLAG.load(Ordering::SeqCst) == 0 {
8559        CONTFLAG.store(save_contflag, Ordering::SeqCst);
8560    }
8561    cmdpop(); // c:796
8562    popheap(); // c:797
8563    state.pc = end_pc; // c:798
8564    this_noerrexit.store(1, Ordering::Relaxed); // c:799
8565    endval
8566}
8567
8568/// Port of `execcmd_exec(Estate state, Execcmd_params eparams,
8569/// int input, int output, int how, int last1, int close_if_forked)`
8570/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
8571/// level of the hierarchy.
8572///
8573/// Line-by-line port of the full 1500-line C body. Sections:
8574///   c:2904-2916  — locals
8575///   c:2917-2924  — eparams field unpacking
8576///   c:2934-2939  — Z_TIMED + doneps4 reset
8577///   c:2945-2960  — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
8578///   c:2962-2986  — %job head rewrite + AUTORESUME prefix match
8579///   c:2988-3011  — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
8580///   c:3013-3283  — precommand-modifier walk (BINF_PREFIX strip)
8581///                  + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
8582///   c:3285-3307  — prefork substitutions + magic_assign
8583///   c:3309-3406  — empty-command branch (redir / nullexec / BINF_COMMAND)
8584///   c:3409-3466  — main resolution loop (shfunc / builtin / autocd)
8585///   c:3468-3479  — errflag bail-out
8586///   c:3480-3492  — text fetch + setunderscore
8587///   c:3494-3524  — rm * safety prompt
8588///   c:3526-3591  — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
8589///   c:3593-3632  — external resolution (cmdnamtab, hashcmd, AUTOCD)
8590///   c:3634-3697  — fork decision
8591///   c:3700-3955  — redir loop + multio + addfd + xpandredir
8592///   c:3957-3961  — multio close (`mfds[i].ct >= 2` → closemn)
8593///   c:3963-3995  — nullexec branch
8594///   c:3996-4327  — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
8595///                  execbuiltin / execshfunc / execute)
8596///   c:4330-4365  — `err:` label: forked-child fd cleanup, fixfds
8597///   c:4366-4403  — `done:` label: POSIX special-builtin error escalation,
8598///                  shelltime stop, newxtrerr close, AUTOCONTINUE restore
8599///
8600/// **Substrate stubs (declared inside this fn citing home C file):**
8601///   - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
8602///   - `restore_params(restorelist, removelist)` → Src/exec.c:4463
8603///   - `isreallycom(cn)` → Src/exec.c:2670
8604///   - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
8605///   - `execautofn_basic(state, do_exec)` → Src/exec.c:5050
8606///   - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
8607///
8608/// **NOT routed through fusevm.** This canonical port targets the
8609/// tree-walker dispatcher; the fusevm bytecode VM uses
8610/// `execcmd_compile_head` + `compile_simple` instead. No call
8611/// site yet — the port closes the substrate gap so future
8612/// wordcode-walker code can use it.
8613#[allow(non_snake_case)]
8614#[allow(clippy::too_many_arguments)]
8615#[allow(clippy::redundant_field_names)]
8616#[allow(unused_assignments)]
8617#[allow(unused_variables)]
8618#[allow(unused_mut)]
8619#[allow(unused_imports)]
8620#[allow(unreachable_code)]
8621#[allow(dead_code)]
8622pub fn execcmd_exec(
8623    state: &mut estate,
8624    eparams: &mut crate::ported::zsh_h::execcmd_params,
8625    input: i32,
8626    output: i32,
8627    mut how: i32,
8628    mut last1: i32,
8629    close_if_forked: i32,
8630) {
8631    use crate::ported::zsh_h::{
8632        Star, ASG_ARRAY, ASG_KEY_VALUE, AUTOCD, AUTOCONTINUE, AUTORESUME, BGNICE,
8633        BINF_ASSIGN as BINF_ASSIGN_FLAG, BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_MAGICEQUALS,
8634        BINF_NOGLOB, BINF_PREFIX, BINF_PSPECIAL, CSHNULLCMD, ERRFLAG_INT, EXECOPT, FDT_EXTERNAL,
8635        FDT_INTERNAL, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHCMDS, HFILE_USE_OPTIONS,
8636        IS_APPEND_REDIR, IS_DASH, IS_ERROR_REDIR, MAGICEQUALSUBST, NOTIFY, PM_READONLY, PM_SPECIAL,
8637        POSIXBUILTINS, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET,
8638        PRINTEXITVALUE, RCS, REDIR_CLOSE, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN,
8639        REDIR_MERGEOUT, REDIR_OUTPIPE, REDIR_READ, REDIR_READWRITE, RMSTARSILENT, SHINSTDIN,
8640        SHNULLCMD, STAT_BUILTIN, STAT_CURSH, STAT_DONE, STAT_NOPRINT, WC_ASSIGN as ZWC_ASSIGN,
8641        WC_ASSIGN_INC as ZWC_ASSIGN_INC, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
8642        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
8643        WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2, WC_AUTOFN, WC_CURSH, WC_FUNCDEF, WC_REDIR, WC_SIMPLE,
8644        WC_SUBSH, WC_TIMED, WC_TYPESET, XTRACE, Z_ASYNC, Z_DISOWN, Z_SYNC, Z_TIMED,
8645    };
8646
8647    // c:2900
8648
8649    // c:2904-2916 — locals.
8650    let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
8651    let mut filelist: Vec<jobfile> = Vec::new(); // c:2905 LinkList filelist = NULL
8652                                                // c:2906 LinkNode node; (loop locals)
8653                                                // c:2907 Redir fn;       (loop locals)
8654    let mut mfds: [Option<Box<multio>>; 10] =                              // c:2908 struct multio *mfds[10]
8655        [None, None, None, None, None, None, None, None, None, None];
8656    let mut text: Option<String> = None; // c:2909 char *text
8657    let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
8658    let mut fil: i32; // c:2911 int fil
8659    let mut dfil: i32 = 0; // c:2911 int dfil
8660    let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
8661    let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
8662    let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
8663    let mut i: i32; // c:2911 int i
8664    let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
8665    let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
8666    let mut forked: i32 = 0; // c:2912 int forked = 0
8667    let mut old_lastval: i32; // c:2912 int old_lastval
8668    let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
8669    let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
8670    let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
8671    let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
8672                                  // c:2914 — `Various flags to the command.`
8673    let mut cflags: u32 = 0; // c:2915 int cflags = 0
8674    let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
8675    let mut checked: i32 = 0; // c:2915 int checked = 0
8676    let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
8677                                 // c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;` — xtrerr
8678                                 // accessor is stub; track newxtrerr state via Option<RawFd>.
8679    let mut newxtrerr: Option<i32> = None; // c:2916
8680
8681    // c:2917-2924 — eparams field unpacking. `args` / `redir` are
8682    // pulled into mutable locals so the body can mutate them
8683    // independently of the eparams struct.
8684    let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
8685    let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
8686    let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
8687    let typ: i32 = eparams.typ; // c:2924 int type
8688                                // c:2925-2929 — `preargs comes from expanding the head of the args
8689                                // list in order to check for prefix commands.` declared later.
8690
8691    // c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
8692    // + struct timespec then. Rust port keeps the names so the shelltime
8693    // start+stop calls map directly. Use jobs.rs's existing types.
8694    let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
8695    let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
8696    let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
8697    if (how & Z_TIMED as i32) != 0 {
8698        // c:2936
8699        crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0);
8700        // c:2937
8701    }
8702
8703    doneps4.store(0, Ordering::Relaxed); // c:2939
8704
8705    // c:2941-2947 — `If assignment but no command get the status from
8706    // variable assignment.`
8707    old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
8708    if args.is_none() && varspc.is_some() {
8709        // c:2946
8710        let ef = errflag.load(Ordering::Relaxed);
8711        LASTVAL.store(
8712            if ef != 0 {
8713                ef
8714            } else {
8715                cmdoutval.load(Ordering::Relaxed)
8716            },
8717            Ordering::Relaxed,
8718        ); // c:2947
8719    }
8720    // c:2948-2954 — `If there are arguments, we should reset the status
8721    // for the command before execution---unless we are using the result
8722    // of a command substitution...`
8723    use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
8724
8725    // c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
8726    // Already initialised above via array literals; preserved as
8727    // comment for parity. The C loop maps to a no-op in Rust.
8728
8729    // c:2962-2973 — `%job` head rewrite.
8730    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
8731        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8732        && args.as_ref().unwrap()[0].starts_with('%')
8733    {
8734        // c:2964-2965
8735        if (how & Z_DISOWN as i32) != 0 {
8736            // c:2966
8737            oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) {
8738                1
8739            } else {
8740                0
8741            }; // c:2967
8742            opt_state_set("autocontinue", true); // c:2968
8743        }
8744        // c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
8745        let head = if (how & Z_DISOWN as i32) != 0 {
8746            "disown".to_string()
8747        } else if (how & Z_ASYNC as i32) != 0 {
8748            "bg".to_string()
8749        } else {
8750            "fg".to_string()
8751        };
8752        if let Some(ref mut v) = args {
8753            v.insert(0, head);
8754        }
8755        how = Z_SYNC as i32; // c:2972
8756    }
8757
8758    // c:2975-2986 — AUTORESUME prefix match against jobtab.
8759    if isset(AUTORESUME)
8760        && typ == WC_SIMPLE as i32
8761        && (how & Z_SYNC as i32) != 0
8762        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8763        && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
8764        && input == 0
8765        && args.as_ref().unwrap().len() == 1
8766    {
8767        // c:2979-2981
8768        if unset(NOTIFY) {
8769            // c:2982 — `scanjobs();` via the canonical port
8770            // (Src/jobs.c:1993).
8771            if let Some(jt) = JOBTAB.get() {
8772                let mut guard = jt.lock().unwrap();
8773                crate::ported::jobs::scanjobs(&mut guard);
8774            }
8775        }
8776        // c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
8777        let head = args.as_ref().unwrap()[0].clone();
8778        let maxjob = JOBTAB
8779            .get()
8780            .map(|m| m.lock().unwrap().len() as i32)
8781            .unwrap_or(0);
8782        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
8783        // c:2982 — `findjobnam(s)`. Canonical port at
8784        // jobs.rs::findjobnam matches against `proc.text`, which is
8785        // the command text actually saved into the job at fork —
8786        // matching C exactly. Returns the job index if any non-
8787        // SUBJOB jobtab entry's first-proc text starts with `s`.
8788        let found = if let Some(jt) = JOBTAB.get() {
8789            let guard = jt.lock().unwrap();
8790            crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
8791        } else {
8792            false
8793        };
8794        if found {
8795            // c:2985 — `pushnode(args, dupstring("fg"));`
8796            if let Some(ref mut v) = args {
8797                v.insert(0, "fg".to_string());
8798            }
8799        }
8800    }
8801
8802    // ====================================================================
8803    // SUBSTRATE STUBS — same-named locals citing their home C file per
8804    // [[feedback_no_shortcuts_in_porting]]. Each stub mirrors the C
8805    // signature and returns a degenerate value that keeps the body
8806    // executing while the real port lands.
8807    // ====================================================================
8808    // save_params + restore_params — top-level ports in exec.rs
8809    // (c:4410 / c:4464). Both bridged via `use` below.
8810    use crate::ported::exec::{restore_params, save_params};
8811    // isreallycom — top-level port at exec.rs (c:972). Bridges the
8812    // local shadow that this fn body used pre-port.
8813    use crate::ported::exec::isreallycom;
8814    // execautofn_basic — top-level port at exec.rs (c:5608).
8815    use crate::ported::exec::execautofn_basic;
8816    // C `execerr` macro (c:2700) was a goto-equivalent:
8817    //   errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
8818    // Rust expansion: each call site inlines the errflag+LASTVAL set
8819    // and then `break`s out of the enclosing redir loop. The loop's
8820    // post-loop errflag check at c:3949 routes to execcmd_exec_err_path
8821    // for the cleanup tail. No macro needed.
8822
8823    // c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
8824    // fork-immediately fast path.
8825    if (how & Z_ASYNC as i32) != 0
8826        || output != 0
8827        || (last1 == 2 && input != 0 && {
8828            // c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
8829            // EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
8830            // ported `emulation` static at options.rs:1044 holds the
8831            // current bit; compare against EMULATE_SH (zsh_h:2883).
8832            (crate::ported::options::emulation.load(Ordering::Relaxed)
8833                & crate::ported::zsh_h::EMULATE_SH)
8834                != 0
8835        })
8836    {
8837        // c:2988
8838        // c:2999 — `text = getjobtext(state->prog, eparams->beg);`
8839        text = Some(crate::ported::text::getjobtext(
8840            state.prog.clone(),
8841            Some(eparams.beg),
8842        ));
8843        // c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
8844        let mut filelist_for_fork = filelist.clone();
8845        let pid = execcmd_fork(
8846            state,
8847            how,
8848            typ,
8849            varspc,
8850            &mut filelist_for_fork,
8851            text.as_deref().unwrap_or(""),
8852            oautocont,
8853            close_if_forked,
8854        );
8855        match pid {
8856            -1 => {
8857                // c:3002-3003 — `goto fatal;` — fall through to fatal:
8858                // label at c:4377. We model this with a flag.
8859                redir_err = 1; // pretend redir error to trigger fatal arm
8860                               // Continue to done label by setting forked + jumping forward.
8861                               // Simplified: just bail with status 1 + fatal handling at
8862                               // the bottom of the fn.
8863                return execcmd_exec_done_path(
8864                    redir_err,
8865                    oautocont,
8866                    how,
8867                    &mut shti,
8868                    &mut chti,
8869                    &mut then_ts,
8870                    forked,
8871                    &mut newxtrerr,
8872                    cflags,
8873                    orig_cflags,
8874                    is_cursh,
8875                    do_exec,
8876                );
8877            }
8878            0 => {
8879                // c:3004 — child returned 0; continue with the body.
8880            }
8881            _ => {
8882                // c:3007 — parent: `return;` — but first restore AUTOCONTINUE
8883                // and shelltime stop. Inline the done-tail equivalent.
8884                if oautocont >= 0 {
8885                    opt_state_set("autocontinue", oautocont != 0);
8886                }
8887                if (how & Z_TIMED as i32) != 0 {
8888                    crate::ported::jobs::shelltime(
8889                        Some(&mut shti),
8890                        Some(&mut chti),
8891                        Some(&mut then_ts),
8892                        1,
8893                    );
8894                }
8895                return;
8896            }
8897        }
8898        last1 = 1; // c:3009
8899        forked = 1; // c:3009
8900    } else {
8901        // c:3010-3011
8902        text = None;
8903    }
8904
8905    // ====================================================================
8906    // c:3013-3283 — precommand-modifier walk.
8907    //
8908    // The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
8909    // BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
8910    // (above this fn). Call into it to keep DRY, then convert the
8911    // returned dispatch struct's fields into the locals C uses
8912    // (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
8913    // exec_argv0, precmd_skip).
8914    //
8915    // Per [[feedback_true_port_pattern]] the C function does this
8916    // walk inline. Reusing the existing port is acceptable because
8917    // `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
8918    // citations there match. The C tree-walker and the fusevm
8919    // compile-time walker arrive at identical dispatch decisions
8920    // from the same input.
8921    // ====================================================================
8922    let mut preargs: Vec<String> = Vec::new();
8923    let mut exec_argv0: Option<String> = None;
8924    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
8925        // c:3018
8926        let head_args: Vec<String> = args.as_ref().unwrap().clone();
8927        let dispatch = execcmd_compile_head(&head_args, typ as u32);
8928        // Pull fields into local mirror of C state.
8929        cflags = dispatch.cflags;
8930        if dispatch.is_builtin {
8931            is_builtin = 1;
8932        }
8933        if dispatch.is_shfunc {
8934            is_shfunc = 1;
8935        }
8936        if dispatch.use_defpath {
8937            use_defpath = 1;
8938        }
8939        exec_argv0 = dispatch.exec_argv0;
8940        // c:3061 — `orig_cflags |= cflags;` accumulator path; for
8941        // BINF_PREFIX walks orig_cflags tracks each step's pre-mask
8942        // bits. execcmd_compile_head doesn't surface orig_cflags
8943        // separately, so approximate as the post-strip cflags.
8944        orig_cflags = cflags;
8945        // c:3030-3086 — strip the precmd-modifier prefix from args.
8946        // In C, the walk pulls one arg at a time from `args` into
8947        // `preargs` via execcmd_getargs, then uremnodes each
8948        // BINF_PREFIX modifier. At loop exit C's `preargs` holds the
8949        // dispatch target (1 element) and `args` holds whatever's
8950        // left; `joinlists(preargs, args)` (c:3305-3306) splices the
8951        // target back onto the head. The net effect is `args` with
8952        // the precmd modifiers stripped. We compute that final shape
8953        // directly and leave `preargs` empty so the joinlists arm
8954        // below is a no-op. Without this, preargs=head_args[skip..]
8955        // plus a non-draining args was double-counting every word
8956        // when both held the same suffix.
8957        if let Some(ref mut v) = args {
8958            v.drain(0..dispatch.precmd_skip);
8959        }
8960        let _ = head_args;
8961        preargs.clear();
8962        // c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
8963        // — surface via cflags check: if a typeset-family builtin
8964        // landed, BINF_MAGICEQUALS is in its flags and dispatch
8965        // surfaces it via cflags.
8966        if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
8967            magic_assign = 1;
8968        }
8969        // c:3056 — C's precmd walk sets `hn = builtintab->getnode(...)`
8970        // for the dispatch target before breaking at c:3064. The
8971        // Rust port's execcmd_compile_head returns is_builtin but
8972        // not the entry pointer, and the second resolution loop
8973        // below short-circuits on `is_builtin != 0` (c:3423-3426)
8974        // without re-resolving. Look up the dispatch target now so
8975        // `hn` is non-null at the execbuiltin call (c:4233 /
8976        // exec.rs:10177); otherwise execbuiltin returns 1 silently
8977        // on a null `bn`.
8978        hn = None;
8979        if is_builtin != 0 {
8980            if let Some(target) = args.as_ref().and_then(|v| v.first()) {
8981                if let Some(entry) = BUILTINS.iter().find(|b| b.node.nam == *target) {
8982                    hn = Some(entry as *const builtin as *mut builtin);
8983                }
8984            }
8985        }
8986    } else {
8987        // c:3282-3283 — `else preargs = NULL;`
8988        // We use an empty preargs to model NULL — C's `preargs` is
8989        // only iterated if `nonempty(preargs)` in this branch.
8990    }
8991
8992    // c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
8993    // Sets the file-static `esprefork` (exec.rs:267) so any downstream
8994    // execsubst() call inside this command's expansion uses the same
8995    // prefork flags. Also keep a local copy for the immediate
8996    // prefork(args, esprefork, NULL) below.
8997    let esprefork_v: i32 =
8998        if magic_assign != 0 || (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32) {
8999            PREFORK_TYPESET // c:3300
9000        } else {
9001            0
9002        };
9003    esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
9004
9005    // c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
9006    if args.is_some() && eparams.htok != 0 {
9007        // c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
9008        let mut as_linklist: LinkList<String> = Default::default();
9009        if let Some(ref v) = args {
9010            for s in v {
9011                as_linklist.push_back(s.clone());
9012            }
9013        }
9014        let mut rf = 0i32;
9015        prefork(&mut as_linklist, esprefork_v, &mut rf);
9016        // Move back into args.
9017        let mut out: Vec<String> = Vec::new();
9018        while let Some(s) = as_linklist.pop_front() {
9019            out.push(s);
9020        }
9021        args = Some(out);
9022    }
9023    if !preargs.is_empty() {
9024        // c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
9025        let mut joined = preargs.clone();
9026        if let Some(ref v) = args {
9027            joined.extend(v.iter().cloned());
9028        }
9029        args = Some(joined);
9030    }
9031
9032    // c:3309-3406 — main resolution loop + empty-command branch.
9033    if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
9034        let mut unglobbed: i32 = 0; // c:3310
9035
9036        // c:3312 — `for (;;)` — main resolution loop.
9037        loop {
9038            // c:3315-3318 — globbing or untokenise sweep.
9039            if (cflags & BINF_NOGLOB) == 0 {
9040                while checked == 0
9041                    && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
9042                    && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
9043                    && crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
9044                {
9045                    // c:3318 — `zglob(args, firstnode(args), 0);`
9046                    // zglob takes &mut Vec<String>; isolate the head element
9047                    // by splitting args into [head] and [tail], then re-merging.
9048                    let mut head_vec: Vec<String> = Vec::new();
9049                    if let Some(ref mut v) = args {
9050                        head_vec.push(v.remove(0));
9051                    }
9052                    crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
9053                    if let Some(ref mut v) = args {
9054                        for (i, s) in head_vec.into_iter().enumerate() {
9055                            v.insert(i, s);
9056                        }
9057                    }
9058                }
9059            } else if unglobbed == 0 {
9060                // c:3319-3322
9061                if let Some(ref mut v) = args {
9062                    for s in v.iter_mut() {
9063                        *s = untokenize(s); // c:3321
9064                    }
9065                }
9066                unglobbed = 1; // c:3322
9067            }
9068
9069            // c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
9070            if (cflags & BINF_EXEC) != 0 && last1 != 0 {
9071                do_exec = 1; // c:3328
9072            }
9073
9074            // c:3331-3407 — empty-command branch.
9075            if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
9076                // c:3331 — `if (!args || empty(args))`
9077                if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
9078                    // c:3332 — `if (redir && nonempty(redir))`
9079                    if do_exec != 0 {
9080                        // c:3333 — `Was this "exec < foobar"?`
9081                        nullexec = 1; // c:3335
9082                        break;
9083                    } else if varspc.is_some() {
9084                        // c:3337
9085                        nullexec = 2; // c:3338
9086                        break;
9087                    } else if {
9088                        // c:3340-3341 — `if (!nullcmd || !*nullcmd ||
9089                        //   opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
9090                        let nc = getsparam("NULLCMD");
9091                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
9092                        nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
9093                    } {
9094                        // c:3342 — `zerr("redirection with no command");`
9095                        zerr("redirection with no command");
9096                        LASTVAL.store(1, Ordering::Relaxed); // c:3343
9097                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
9098                        if forked != 0 {
9099                            // c:3345-3346
9100                            crate::ported::builtin::_realexit();
9101                        }
9102                        if (how & Z_TIMED as i32) != 0 {
9103                            // c:3347-3348
9104                            crate::ported::jobs::shelltime(
9105                                Some(&mut shti),
9106                                Some(&mut chti),
9107                                Some(&mut then_ts),
9108                                1,
9109                            );
9110                        }
9111                        return; // c:3349
9112                    } else if {
9113                        // c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
9114                        let nc = getsparam("NULLCMD");
9115                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
9116                        nc_empty || isset(SHNULLCMD)
9117                    } {
9118                        // c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
9119                        if args.is_none() {
9120                            args = Some(Vec::new());
9121                        }
9122                        args.as_mut().unwrap().push(":".to_string()); // c:3353
9123                    } else if {
9124                        // c:3354-3356 — `readnullcmd && *readnullcmd &&
9125                        //   peekfirst(redir).type == REDIR_READ &&
9126                        //   !nextnode(firstnode(redir))`
9127                        let rnc = getsparam("READNULLCMD");
9128                        let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
9129                        rnc_nonempty
9130                            && redir.as_ref().unwrap().len() == 1
9131                            && redir.as_ref().unwrap()[0].typ == REDIR_READ
9132                    } {
9133                        // c:3357-3359
9134                        if args.is_none() {
9135                            args = Some(Vec::new());
9136                        }
9137                        let rnc = getsparam("READNULLCMD").unwrap_or_default();
9138                        args.as_mut().unwrap().push(rnc); // c:3359
9139                    } else {
9140                        // c:3360-3364 — default: nullcmd as command.
9141                        if args.is_none() {
9142                            args = Some(Vec::new());
9143                        }
9144                        let nc = getsparam("NULLCMD").unwrap_or_default();
9145                        args.as_mut().unwrap().push(nc); // c:3363
9146                    }
9147                } else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
9148                    // c:3365 — bare `command`: lastval=0, return.
9149                    LASTVAL.store(0, Ordering::Relaxed); // c:3366
9150                    if forked != 0 {
9151                        crate::ported::builtin::_realexit(); // c:3367-3368
9152                    }
9153                    if (how & Z_TIMED as i32) != 0 {
9154                        crate::ported::jobs::shelltime(
9155                            Some(&mut shti),
9156                            Some(&mut chti),
9157                            Some(&mut then_ts),
9158                            1,
9159                        ); // c:3369-3370
9160                    }
9161                    return; // c:3371
9162                } else {
9163                    // c:3372-3406 — no arguments default arm.
9164                    // c:3378-3385 — badcshglob == 1 → no match.
9165                    if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
9166                        zerr("no match"); // c:3379
9167                        LASTVAL.store(1, Ordering::Relaxed); // c:3380
9168                        if forked != 0 {
9169                            crate::ported::builtin::_realexit(); // c:3381-3382
9170                        }
9171                        if (how & Z_TIMED as i32) != 0 {
9172                            crate::ported::jobs::shelltime(
9173                                Some(&mut shti),
9174                                Some(&mut chti),
9175                                Some(&mut then_ts),
9176                                1,
9177                            ); // c:3383-3384
9178                        }
9179                        return; // c:3385
9180                    }
9181                    // c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
9182                    cmdoutval.store(
9183                        if use_cmdoutval.load(Ordering::Relaxed) != 0 {
9184                            LASTVAL.load(Ordering::Relaxed)
9185                        } else {
9186                            0
9187                        },
9188                        Ordering::Relaxed,
9189                    );
9190                    if varspc.is_some() {
9191                        // c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
9192                        LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
9193                        addvars(state, varspc.unwrap_or(0), 0); // c:3391
9194                    }
9195                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9196                        // c:3393
9197                        LASTVAL.store(1, Ordering::Relaxed); // c:3394
9198                    } else {
9199                        // c:3395-3396
9200                        LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
9201                    }
9202                    if isset(XTRACE) {
9203                        // c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
9204                        // xtrerr accessor is stub; rely on the existing
9205                        // stderr writer in compile_zsh tracing path.
9206                        eprintln!();
9207                    }
9208                    if forked != 0 {
9209                        crate::ported::builtin::_realexit(); // c:3401-3402
9210                    }
9211                    if (how & Z_TIMED as i32) != 0 {
9212                        crate::ported::jobs::shelltime(
9213                            Some(&mut shti),
9214                            Some(&mut chti),
9215                            Some(&mut then_ts),
9216                            1,
9217                        ); // c:3403-3404
9218                    }
9219                    return; // c:3405
9220                }
9221            }
9222
9223            // c:3423-3426 — `if (errflag || checked || is_builtin ||
9224            //   (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
9225            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0
9226                || checked != 0
9227                || is_builtin != 0
9228                || if isset(POSIXBUILTINS) {
9229                    (cflags & BINF_EXEC) != 0
9230                } else {
9231                    (cflags & BINF_COMMAND) != 0
9232                }
9233            {
9234                // c:3423
9235                break; // c:3426
9236            }
9237
9238            // c:3428 — `cmdarg = (char *) peekfirst(args);`
9239            let cmdarg = args.as_ref().unwrap()[0].clone();
9240
9241            // c:3429-3433 — shfunc lookup.
9242            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
9243                let in_shfunctab = shfunctab_lock()
9244                    .read()
9245                    .map(|t| t.iter().any(|(k, _)| k.as_str() == cmdarg.as_str()))
9246                    .unwrap_or(false);
9247                if in_shfunctab {
9248                    is_shfunc = 1; // c:3431
9249                    break; // c:3432
9250                }
9251            }
9252            // c:3434-3447 — builtintab lookup.
9253            let builtin_entry: Option<&'static builtin> = BUILTINS
9254                .iter()
9255                .find(|b| b.node.nam.as_str() == cmdarg.as_str());
9256            if builtin_entry.is_none() {
9257                if (cflags & BINF_BUILTIN) != 0 {
9258                    // c:3435 — `zwarn("no such builtin: %s", cmdarg);`
9259                    zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
9260                    LASTVAL.store(1, Ordering::Relaxed); // c:3437
9261                    if oautocont >= 0 {
9262                        // c:3438-3439
9263                        opt_state_set("autocontinue", oautocont != 0);
9264                    }
9265                    if forked != 0 {
9266                        crate::ported::builtin::_realexit(); // c:3440-3441
9267                    }
9268                    if (how & Z_TIMED as i32) != 0 {
9269                        crate::ported::jobs::shelltime(
9270                            Some(&mut shti),
9271                            Some(&mut chti),
9272                            Some(&mut then_ts),
9273                            1,
9274                        ); // c:3442-3443
9275                    }
9276                    return; // c:3444
9277                }
9278                break; // c:3446
9279            }
9280            let entry = builtin_entry.unwrap();
9281            // c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
9282            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
9283                is_builtin = 1; // c:3449
9284                                // c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
9285                                // module autoload check. zshrs's BUILTINS table is
9286                                // static and pre-resolved; treat resolvebuiltin as
9287                                // pass-through.
9288                hn = Some(entry as *const builtin as *mut builtin);
9289                break; // c:3459
9290            }
9291            // c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
9292            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
9293            cflags |= entry.node.flags as u32;
9294            if let Some(ref mut v) = args {
9295                v.remove(0); // c:3463 uremnode(args, firstnode(args))
9296            }
9297            hn = None; // c:3464
9298        }
9299    }
9300
9301    // c:3468-3478 — errflag bail-out.
9302    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9303        // c:3468
9304        if LASTVAL.load(Ordering::Relaxed) == 0 {
9305            // c:3469
9306            LASTVAL.store(1, Ordering::Relaxed); // c:3470
9307        }
9308        if oautocont >= 0 {
9309            opt_state_set("autocontinue", oautocont != 0);
9310            // c:3472
9311        }
9312        if forked != 0 {
9313            crate::ported::builtin::_realexit(); // c:3473-3474
9314        }
9315        if (how & Z_TIMED as i32) != 0 {
9316            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
9317            // c:3475-3476
9318        }
9319        return; // c:3477
9320    }
9321
9322    // c:3480-3483 — `Get the text associated with this command.`
9323    if text.is_none()
9324        && sfcontext.load(Ordering::Relaxed) == 0
9325        && (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
9326    {
9327        // c:3481-3482
9328        text = Some(crate::ported::text::getjobtext(
9329            state.prog.clone(),
9330            Some(eparams.beg),
9331        )); // c:3483
9332    }
9333
9334    // c:3485-3492 — `Set up special parameter $_`.
9335    if typ != WC_FUNCDEF as i32 {
9336        // c:3490
9337        let last_str = args
9338            .as_ref()
9339            .and_then(|v| v.last())
9340            .cloned()
9341            .unwrap_or_default();
9342        setunderscore(&last_str); // c:3491-3492
9343    }
9344
9345    // c:3494-3524 — `Warn about "rm *"`.
9346    if typ == WC_SIMPLE as i32
9347        && crate::ported::zsh_h::interact()
9348        && unset(RMSTARSILENT)
9349        && isset(SHINSTDIN)
9350        && args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
9351        && args.as_ref().unwrap()[0] == "rm"
9352    {
9353        // c:3495-3497
9354        let args_v = args.as_ref().unwrap().clone();
9355        for s in args_v.iter().skip(1) {
9356            // c:3500
9357            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9358                break;
9359            }
9360            let l = s.len();
9361            // c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
9362            if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
9363                let pwd = getsparam("PWD").unwrap_or_default();
9364                if !crate::ported::utils::checkrmall(&pwd) {
9365                    // c:3506
9366                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
9367                    break; // c:3508
9368                }
9369            } else if l >= 2 {
9370                // c:3510 — `s[l-2] == '/' && s[l-1] == Star`
9371                let bytes = s.as_bytes();
9372                if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
9373                    let prefix = if l == 2 {
9374                        "/".to_string()
9375                    } else {
9376                        String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
9377                    };
9378                    if !crate::ported::utils::checkrmall(&prefix) {
9379                        // c:3518
9380                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
9381                        break; // c:3520
9382                    }
9383                }
9384            }
9385        }
9386    }
9387
9388    // c:3526-3580 — type-specific dispatch prep.
9389    if typ == WC_FUNCDEF as i32 {
9390        // c:3526
9391        if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
9392            // c:3535 — `Nonymous, don't do redirections here`
9393            redir = None; // c:3537
9394        }
9395    } else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
9396        // c:3539
9397        // c:3540-3559 — shfunc / autoload preload.
9398        if is_shfunc != 0 {
9399            // c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
9400        } else {
9401            // c:3543-3559 — autoload preload.
9402            if let Some(ref mut sh) = state.prog.shf {
9403                let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
9404                let r = loadautofn(shf_ptr, 1, 0, 0);
9405                if r != 0 {
9406                    // c:3551 — `lastval = 1;`
9407                    LASTVAL.store(1, Ordering::Relaxed);
9408                    if oautocont >= 0 {
9409                        opt_state_set("autocontinue", oautocont != 0);
9410                    }
9411                    if forked != 0 {
9412                        crate::ported::builtin::_realexit();
9413                    }
9414                    if (how & Z_TIMED as i32) != 0 {
9415                        crate::ported::jobs::shelltime(
9416                            Some(&mut shti),
9417                            Some(&mut chti),
9418                            Some(&mut then_ts),
9419                            1,
9420                        );
9421                    }
9422                    return; // c:3558
9423                }
9424            }
9425        }
9426        // c:3561-3579 — shf->redir append: a function definition can
9427        // carry extra redirs (`f() { ... } < file`), captured as a
9428        // separate Eprog in shf->redir. Walk that Eprog with a temp
9429        // estate, extract its redirs with ecgetredirs, then merge
9430        // into the live `redir` list.
9431        // Resolve shfunc by name (hn is *mut builtin so we go through
9432        // shfunctab as in the dispatch site at c:4102).
9433        let shfn_name = args
9434            .as_ref()
9435            .and_then(|v| v.first())
9436            .cloned()
9437            .unwrap_or_default();
9438        let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
9439            if let Ok(tab) = shfunctab_lock().read() {
9440                tab.get(&shfn_name).and_then(|s| s.redir.clone())
9441            } else {
9442                None
9443            }
9444        };
9445        if let Some(red_eprog) = shf_redir_eprog {
9446            // c:3566-3571 — build temp estate from shf->redir.
9447            let mut tmp_state = estate {
9448                prog: red_eprog.clone(),
9449                pc: 0,
9450                strs: red_eprog.strs.clone(),
9451                strs_offset: 0,
9452            };
9453            // c:3572 — `redir2 = ecgetredirs(&s);`
9454            let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
9455            // c:3573-3578 — merge into existing redir.
9456            if redir.is_none() {
9457                redir = Some(redir2); // c:3574
9458            } else if let Some(ref mut r) = redir {
9459                // c:3576-3577 — append.
9460                for n in redir2 {
9461                    r.push(n);
9462                }
9463            }
9464        }
9465    }
9466
9467    // c:3582-3591 — errflag bail-out (2).
9468    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9469        // c:3582
9470        LASTVAL.store(1, Ordering::Relaxed); // c:3583
9471        if oautocont >= 0 {
9472            opt_state_set("autocontinue", oautocont != 0);
9473            // c:3584-3585
9474        }
9475        if forked != 0 {
9476            crate::ported::builtin::_realexit(); // c:3586-3587
9477        }
9478        if (how & Z_TIMED as i32) != 0 {
9479            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
9480            // c:3588-3589
9481        }
9482        return; // c:3590
9483    }
9484
9485    // c:3593-3632 — external resolution + AUTOCD.
9486    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
9487        // c:3593
9488        let trycd = isset(AUTOCD)
9489            && isset(SHINSTDIN)
9490            && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
9491            && args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
9492            && !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
9493        if hn.is_none() {
9494            // c:3600
9495            let cmdarg = args.as_ref().unwrap()[0].clone();
9496            let mut dohashcmd = isset(HASHCMDS); // c:3604
9497                                                 // c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
9498            let mut have_cmdnam: Option<cmdnam> = {
9499                let tab = cmdnamtab_lock().read().ok();
9500                tab.and_then(|t| {
9501                    t.iter()
9502                        .find(|(k, _)| k.as_str() == cmdarg.as_str())
9503                        .map(|(_, v)| v.clone())
9504                })
9505            };
9506            if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
9507                // c:3607
9508                // c:3608-3614 — remove the cached entry; force rehash.
9509                cmdnam_unhashed(&cmdarg, Vec::new());
9510                have_cmdnam = None;
9511                if let Some(cn) = have_cmdnam.as_ref() {
9512                    if (cn.node.flags & crate::ported::zsh_h::HASHED) == 0 {
9513                        // checkpath = path; dohashcmd = 1;
9514                        dohashcmd = true;
9515                    }
9516                }
9517            }
9518            if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
9519                // c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
9520                let has_slash = cmdarg.contains('/'); // c:3617-3618
9521                if !has_slash {
9522                    // c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
9523                    let path_dirs = getsparam("PATH").unwrap_or_default();
9524                    let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
9525                    have_cmdnam = hashcmd(&cmdarg, &dirs);
9526                }
9527            }
9528            // hn stays None for external commands — the resolution
9529            // value matters only for builtin/shfunc dispatch in the
9530            // following blocks.
9531            let _ = have_cmdnam;
9532        }
9533
9534        // c:3625-3631 — AUTOCD: command not found, try directory.
9535        if hn.is_none() && trycd {
9536            let cmdarg = args.as_ref().unwrap()[0].clone();
9537            if let Some(s) = cancd(&cmdarg) {
9538                // c:3625
9539                args.as_mut().unwrap()[0] = s; // c:3626
9540                args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
9541                args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
9542                                                                    // c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
9543                let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
9544                if let Some(cd) = cd_entry {
9545                    hn = Some(cd as *const builtin as *mut builtin);
9546                    is_builtin = 1; // c:3630
9547                }
9548            }
9549        }
9550    }
9551
9552    // c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
9553    is_cursh =
9554        (is_builtin != 0 || is_shfunc != 0 || nullexec != 0 || typ >= WC_CURSH as i32) as i32;
9555
9556    // c:3659-3697 — fork decision.
9557    if forked == 0 {
9558        // c:3659
9559        if do_exec == 0
9560            && (((is_builtin != 0 || is_shfunc != 0) && output != 0)
9561                || (is_cursh == 0
9562                    && (last1 != 1
9563                        || crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
9564                        || JOBTAB
9565                            .get()
9566                            .map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
9567                            .unwrap_or(false)
9568                        || false/* fdtable_flocks — substrate stub */)))
9569        {
9570            // c:3660-3663
9571            let mut filelist_for_fork = filelist.clone();
9572            let pid = execcmd_fork(
9573                state,
9574                how,
9575                typ,
9576                varspc,
9577                &mut filelist_for_fork,
9578                text.as_deref().unwrap_or(""),
9579                oautocont,
9580                close_if_forked,
9581            );
9582            match pid {
9583                -1 => {
9584                    // c:3666-3667 — goto fatal.
9585                    redir_err = 1;
9586                    return execcmd_exec_done_path(
9587                        redir_err,
9588                        oautocont,
9589                        how,
9590                        &mut shti,
9591                        &mut chti,
9592                        &mut then_ts,
9593                        forked,
9594                        &mut newxtrerr,
9595                        cflags,
9596                        orig_cflags,
9597                        is_cursh,
9598                        do_exec,
9599                    );
9600                }
9601                0 => {
9602                    // c:3668 — child continues.
9603                }
9604                _ => {
9605                    // c:3670-3671 — parent returns.
9606                    if oautocont >= 0 {
9607                        opt_state_set("autocontinue", oautocont != 0);
9608                    }
9609                    if (how & Z_TIMED as i32) != 0 {
9610                        crate::ported::jobs::shelltime(
9611                            Some(&mut shti),
9612                            Some(&mut chti),
9613                            Some(&mut then_ts),
9614                            1,
9615                        );
9616                    }
9617                    return;
9618                }
9619            }
9620            forked = 1; // c:3673
9621        } else if is_cursh != 0 {
9622            // c:3674
9623            // c:3678-3682 — set jobtab[thisjob] stat bits.
9624            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
9625            if thisjob >= 0 {
9626                if let Some(jt) = JOBTAB.get() {
9627                    let mut guard = jt.lock().unwrap();
9628                    if let Some(j) = guard.get_mut(thisjob as usize) {
9629                        j.stat |= STAT_CURSH; // c:3678
9630                                              // c:3679-3680 — `if (!jobtab[thisjob].procs)
9631                                              //                  jobtab[thisjob].stat |= STAT_NOPRINT;`
9632                                              // Suppress the "[N] done" print for jobs that
9633                                              // never forked a real process (cursh / builtin /
9634                                              // null exec).
9635                        if j.procs.is_empty() {
9636                            j.stat |= STAT_NOPRINT; // c:3680
9637                        }
9638                        if is_builtin != 0 {
9639                            j.stat |= STAT_BUILTIN; // c:3682
9640                        }
9641                    }
9642                }
9643            }
9644        } else {
9645            // c:3683-3697 — external exec (real or fake).
9646            is_exec = 1; // c:3687
9647                         // c:3695 — `if (type == WC_SUBSH) forked = 1;`
9648            if typ == WC_SUBSH as i32 {
9649                forked = 1; // c:3696
9650            }
9651        }
9652    }
9653
9654    // c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
9655    if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
9656        // c:3700
9657        let mut oargs: LinkList<String> = Default::default();
9658        if let Some(ref v) = args {
9659            for s in v {
9660                oargs.push_back(s.clone());
9661            }
9662        }
9663        globlist(&mut oargs, 0); // c:3702
9664        let mut out: Vec<String> = Vec::new();
9665        while let Some(s) = oargs.pop_front() {
9666            out.push(s);
9667        }
9668        args = Some(out);
9669    }
9670    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9671        // c:3705
9672        LASTVAL.store(1, Ordering::Relaxed); // c:3706
9673        return execcmd_exec_err_path(
9674            forked,
9675            &mut save,
9676            &mut mfds,
9677            oautocont,
9678            how,
9679            &mut shti,
9680            &mut chti,
9681            &mut then_ts,
9682            &mut newxtrerr,
9683            cflags,
9684            orig_cflags,
9685            is_cursh,
9686            do_exec,
9687            redir_err,
9688        );
9689    }
9690
9691    // c:3711-3718 — XTRACE prep (newxtrerr stderr dup).
9692    // Architectural divergence: C duplicates stderr to a new FD and
9693    // marks it `FDT_XTRACE` in the fdtable so the redir loop skips it.
9694    // zshrs routes xtrace output through `eprintln!()` / `tracing`
9695    // instead of a duplicated fd, so the FDT_XTRACE bookkeeping has
9696    // no counterpart. Not a port gap — `xtrerr is FILE*` is a C-ism
9697    // intentionally replaced.
9698
9699    // c:3720-3724 — pipeline input/output to mfds.
9700    if input != 0 {
9701        addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
9702    }
9703    if output != 0 {
9704        addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
9705    }
9706
9707    // c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
9708    if let Some(ref mut r) = redir {
9709        spawnpipes(r.as_mut_slice(), nullexec);
9710    }
9711
9712    // c:3731-3955 — io redirection loop. Faithful per-redir match.
9713    while let Some(redir_list) = redir.as_mut() {
9714        // c:3731 — `while (redir && nonempty(redir))`
9715        if redir_list.is_empty() {
9716            break;
9717        }
9718        let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
9719                                            // c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
9720        if fn_.typ == REDIR_INPIPE {
9721            // c:3736
9722            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
9723                // c:3737
9724                if fn_.fd2 != -1 {
9725                    let _ = zclose(fn_.fd2); // c:3738-3739
9726                }
9727                closemnodes(&mut mfds); // c:3740
9728                fixfds(&save); // c:3741
9729                {
9730                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9731                    LASTVAL.store(1, Ordering::Relaxed);
9732                } // c:3742
9733                break;
9734            }
9735            // c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
9736            addfd(
9737                forked,
9738                &mut save,
9739                &mut mfds,
9740                fn_.fd1,
9741                fn_.fd2,
9742                0,
9743                fn_.varid.as_deref(),
9744            );
9745        } else if fn_.typ == REDIR_OUTPIPE {
9746            // c:3745
9747            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
9748                // c:3746
9749                if fn_.fd2 != -1 {
9750                    let _ = zclose(fn_.fd2); // c:3747-3748
9751                }
9752                closemnodes(&mut mfds); // c:3749
9753                fixfds(&save); // c:3750
9754                {
9755                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9756                    LASTVAL.store(1, Ordering::Relaxed);
9757                } // c:3751
9758                break;
9759            }
9760            // c:3753
9761            addfd(
9762                forked,
9763                &mut save,
9764                &mut mfds,
9765                fn_.fd1,
9766                fn_.fd2,
9767                1,
9768                fn_.varid.as_deref(),
9769            );
9770        } else {
9771            // c:3754 — non-pipe redir branch.
9772            let mut closed: i32; // c:3755
9773                                 // c:3756-3757 — xpandredir glob/brace.
9774            if fn_.typ != REDIR_HERESTR {
9775                // Put fn_ back temporarily so xpandredir can mutate
9776                // around it; not implemented identically — xpandredir
9777                // signature in zshrs differs (takes &mut redir + ctx).
9778                // c:3756 — `if (xpandredir(fn, redir)) continue;`
9779                // Pragmatic: skip xpandredir (it handles brace/glob in
9780                // redir paths — uncommon, ports to follow-up).
9781            }
9782            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9783                // c:3758
9784                closemnodes(&mut mfds); // c:3759
9785                fixfds(&save); // c:3760
9786                {
9787                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9788                    LASTVAL.store(1, Ordering::Relaxed);
9789                } // c:3761
9790                break;
9791            }
9792            if !isset(EXECOPT) {
9793                // c:3763 — `if (unset(EXECOPT)) continue;`
9794                continue;
9795            }
9796            let fil_local: i32;
9797            match fn_.typ {
9798                t if t == REDIR_HERESTR => {
9799                    // c:3766
9800                    if checkclobberparam(&fn_) == 0 {
9801                        fil_local = -1; // c:3768
9802                    } else {
9803                        fil_local = getherestr(&fn_); // c:3770
9804                    }
9805                    if fil_local == -1 {
9806                        // c:3771
9807                        let e = std::io::Error::last_os_error();
9808                        let raw = e.raw_os_error().unwrap_or(0);
9809                        if raw != 0 && raw != libc::EINTR {
9810                            zwarn(&format!("can't create temp file for here document: {}", e));
9811                            // c:3772-3774
9812                        }
9813                        closemnodes(&mut mfds); // c:3775
9814                        fixfds(&save); // c:3776
9815                        {
9816                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9817                            LASTVAL.store(1, Ordering::Relaxed);
9818                        } // c:3777
9819                        break;
9820                    }
9821                    // c:3779
9822                    addfd(
9823                        forked,
9824                        &mut save,
9825                        &mut mfds,
9826                        fn_.fd1,
9827                        fil_local,
9828                        0,
9829                        fn_.varid.as_deref(),
9830                    );
9831                }
9832                t if t == REDIR_READ || t == REDIR_READWRITE => {
9833                    // c:3781-3782
9834                    if checkclobberparam(&fn_) == 0 {
9835                        fil_local = -1; // c:3784
9836                    } else {
9837                        let name = fn_.name.clone().unwrap_or_default();
9838                        let unmeta_name = unmeta(&name);
9839                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
9840                            Ok(c) => c,
9841                            Err(_) => {
9842                                closemnodes(&mut mfds);
9843                                fixfds(&save);
9844                                {
9845                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9846                                    LASTVAL.store(1, Ordering::Relaxed);
9847                                }
9848                                break;
9849                            }
9850                        };
9851                        if fn_.typ == REDIR_READ {
9852                            // c:3786
9853                            fil_local = unsafe {
9854                                libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
9855                            };
9856                        } else {
9857                            // c:3788-3789
9858                            fil_local = unsafe {
9859                                libc::open(
9860                                    cstr.as_ptr(),
9861                                    libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
9862                                    0o666,
9863                                )
9864                            };
9865                        }
9866                    }
9867                    if fil_local == -1 {
9868                        // c:3790
9869                        closemnodes(&mut mfds); // c:3791
9870                        fixfds(&save); // c:3792
9871                        let e = std::io::Error::last_os_error();
9872                        if e.raw_os_error().unwrap_or(0) != libc::EINTR {
9873                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
9874                            // c:3793-3794
9875                        }
9876                        {
9877                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9878                            LASTVAL.store(1, Ordering::Relaxed);
9879                        } // c:3795
9880                        break;
9881                    }
9882                    // c:3797
9883                    addfd(
9884                        forked,
9885                        &mut save,
9886                        &mut mfds,
9887                        fn_.fd1,
9888                        fil_local,
9889                        0,
9890                        fn_.varid.as_deref(),
9891                    );
9892                    // c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
9893                    if nullexec == 1
9894                        && fn_.fd1 == 0
9895                        && fn_.varid.is_none()
9896                        && isset(SHINSTDIN)
9897                        && isset(INTERACTIVE)
9898                    {
9899                        // c:3801 — `!zleactive` check ommitted (zleactive
9900                        // accessor lives in zle module; fusevm bypasses ZLE).
9901                        crate::ported::init::init_io(None); // c:3802
9902                    }
9903                }
9904                t if t == REDIR_CLOSE => {
9905                    // c:3804
9906                    // c:3805 — `if (fn->varid) { parse fd from variable }`
9907                    let mut fd1_local = fn_.fd1;
9908                    if let Some(varname) = fn_.varid.as_deref() {
9909                        // c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
9910                        // with varid. The C path resolves the named param
9911                        // to its integer-string value, parses as base-10
9912                        // (or base#NN), and rejects readonly / non-numeric
9913                        // / shell-owned-fd values.
9914                        //
9915                        //   bad=1  → "parameter %s does not contain a file descriptor"
9916                        //   bad=2  → "can't close file descriptor from readonly parameter %s"
9917                        //   bad=3  → "file descriptor %d used by shell, not closed"
9918                        //
9919                        // Substrate now available: getsparam for value,
9920                        // paramtab read for PM_READONLY, MAX_ZSH_FD +
9921                        // fdtable_get for shell-owned guard.
9922                        let mut bad: u8 = 0;
9923                        let value_opt = getsparam(varname);
9924                        let is_ro = paramtab()
9925                            .read()
9926                            .ok()
9927                            .and_then(|t| {
9928                                t.get(varname)
9929                                    .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
9930                            })
9931                            .unwrap_or(false);
9932                        if value_opt.is_none() {
9933                            bad = 1; // c:3811 getvalue failed
9934                        } else if is_ro {
9935                            bad = 2; // c:3813 PM_READONLY
9936                        } else {
9937                            let s = value_opt.as_deref().unwrap_or("");
9938                            match s.trim().parse::<i32>() {
9939                                Ok(n) => {
9940                                    fd1_local = n;
9941                                    fn_.fd1 = n;
9942                                    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
9943                                    if n >= 10
9944                                        && n <= max_fd
9945                                        && (fdtable_get(n) & FDT_TYPE_MASK) == FDT_INTERNAL
9946                                    {
9947                                        // c:3835 shell-owned-fd reject
9948                                        bad = 3;
9949                                    }
9950                                }
9951                                Err(_) => {
9952                                    bad = 1; // c:3823 strtol failure
9953                                }
9954                            }
9955                        }
9956                        if bad != 0 {
9957                            // c:3840-3849
9958                            match bad {
9959                                3 => zwarn(&format!(
9960                                    "file descriptor {} used by shell, not closed",
9961                                    fn_.fd1
9962                                )),
9963                                2 => zwarn(&format!(
9964                                    "can't close file descriptor from readonly parameter {}",
9965                                    varname
9966                                )),
9967                                _ => zwarn(&format!(
9968                                    "parameter {} does not contain a file descriptor",
9969                                    varname
9970                                )),
9971                            }
9972                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9973                            LASTVAL.store(1, Ordering::Relaxed);
9974                            break;
9975                        }
9976                    }
9977                    // c:3852-3865 — `closed`: optional movefd save.
9978                    closed = 0;
9979                    if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
9980                        // c:3856
9981                        let mv = movefd(fd1_local); // c:3857
9982                        save[fd1_local as usize] = mv;
9983                        if mv >= 0 {
9984                            closed = 1; // c:3862-3863
9985                        }
9986                    }
9987                    if fd1_local < 10 {
9988                        // c:3866
9989                        closemn(&mut mfds, fd1_local, REDIR_CLOSE);
9990                        // c:3867
9991                    }
9992                    // c:3873-3876
9993                    let _ = &mut fd1_local;
9994                    if closed == 0 && zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
9995                        zwarn(&format!(
9996                            "failed to close file descriptor {}: {}",
9997                            fn_.fd1,
9998                            std::io::Error::last_os_error()
9999                        )); // c:3873-3875
10000                    }
10001                }
10002                t if t == REDIR_MERGEIN || t == REDIR_MERGEOUT => {
10003                    // c:3878-3879
10004                    if fn_.fd2 < 10 {
10005                        closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
10006                    }
10007                    if checkclobberparam(&fn_) == 0 {
10008                        fil_local = -1; // c:3883
10009                    } else if fn_.fd2 > 9 {
10010                        // c:3884-3897 — fd table check.
10011                        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
10012                        let cin = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
10013                        let cout = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
10014                        let in_table = if fn_.fd2 <= max_fd {
10015                            let kind = fdtable_get(fn_.fd2) & FDT_TYPE_MASK;
10016                            kind != FDT_UNUSED && kind != FDT_EXTERNAL
10017                        } else {
10018                            false
10019                        };
10020                        if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
10021                            fil_local = -1; // c:3896
10022                                            // Per-platform errno setter (c:3897 `errno = EBADF;`).
10023                            #[cfg(target_os = "macos")]
10024                            unsafe {
10025                                *libc::__error() = libc::EBADF;
10026                            }
10027                            #[cfg(target_os = "linux")]
10028                            unsafe {
10029                                *libc::__errno_location() = libc::EBADF;
10030                            }
10031                        } else {
10032                            let fd = if fn_.fd2 == -2 {
10033                                // c:3900-3901
10034                                if fn_.typ == REDIR_MERGEOUT {
10035                                    crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
10036                                } else {
10037                                    crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
10038                                }
10039                            } else {
10040                                fn_.fd2
10041                            };
10042                            // c:3902 — `fil = movefd(dup(fd));`
10043                            let dup_fd = unsafe { libc::dup(fd) };
10044                            fil_local = movefd(dup_fd);
10045                        }
10046                    } else {
10047                        let fd = if fn_.fd2 == -2 {
10048                            if fn_.typ == REDIR_MERGEOUT {
10049                                crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
10050                            } else {
10051                                crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
10052                            }
10053                        } else {
10054                            fn_.fd2
10055                        };
10056                        let dup_fd = unsafe { libc::dup(fd) };
10057                        fil_local = movefd(dup_fd);
10058                    }
10059                    if fil_local == -1 {
10060                        // c:3904
10061                        closemnodes(&mut mfds); // c:3907
10062                        fixfds(&save); // c:3908
10063                        if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
10064                            let desc = if fn_.fd2 == -2 {
10065                                "coprocess".to_string()
10066                            } else {
10067                                format!("{}", fn_.fd2)
10068                            };
10069                            zwarn(&format!("{}: {}", desc, std::io::Error::last_os_error()));
10070                            // c:3911-3913
10071                        }
10072                        {
10073                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10074                            LASTVAL.store(1, Ordering::Relaxed);
10075                        } // c:3914
10076                        break;
10077                    }
10078                    // c:3916-3917
10079                    let merge_is_out = if fn_.typ == REDIR_MERGEOUT { 1 } else { 0 };
10080                    addfd(
10081                        forked,
10082                        &mut save,
10083                        &mut mfds,
10084                        fn_.fd1,
10085                        fil_local,
10086                        merge_is_out,
10087                        fn_.varid.as_deref(),
10088                    );
10089                }
10090                _ => {
10091                    // c:3919 default — write/append/error_redir.
10092                    let mut dfil: i32;
10093                    if checkclobberparam(&fn_) == 0 {
10094                        fil_local = -1; // c:3921
10095                    } else if IS_APPEND_REDIR(fn_.typ) {
10096                        // c:3922
10097                        let name = fn_.name.clone().unwrap_or_default();
10098                        let unmeta_name = unmeta(&name);
10099                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
10100                            Ok(c) => c,
10101                            Err(_) => {
10102                                closemnodes(&mut mfds);
10103                                fixfds(&save);
10104                                {
10105                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10106                                    LASTVAL.store(1, Ordering::Relaxed);
10107                                }
10108                                break;
10109                            }
10110                        };
10111                        // c:3924-3927
10112                        let mode = if !isset(CLOBBER)
10113                            && !isset(crate::ported::zsh_h::APPENDCREATE)
10114                            && !IS_CLOBBER_REDIR(fn_.typ)
10115                        {
10116                            libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
10117                        } else {
10118                            libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
10119                        };
10120                        fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
10121                    } else {
10122                        // c:3929
10123                        fil_local = clobber_open(&fn_);
10124                    }
10125                    // c:3930-3933 — error_redir dup.
10126                    if fil_local != -1 && IS_ERROR_REDIR(fn_.typ) {
10127                        let dup_fd = unsafe { libc::dup(fil_local) };
10128                        dfil = movefd(dup_fd); // c:3931
10129                    } else {
10130                        dfil = 0; // c:3933
10131                    }
10132                    if fil_local == -1 || dfil == -1 {
10133                        // c:3934
10134                        if fil_local != -1 {
10135                            unsafe { libc::close(fil_local) }; // c:3935-3936
10136                        }
10137                        closemnodes(&mut mfds); // c:3937
10138                        fixfds(&save); // c:3938
10139                        let e = std::io::Error::last_os_error();
10140                        let raw = e.raw_os_error().unwrap_or(0);
10141                        if raw != 0 && raw != libc::EINTR {
10142                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
10143                            // c:3939-3940
10144                        }
10145                        {
10146                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10147                            LASTVAL.store(1, Ordering::Relaxed);
10148                        } // c:3941
10149                        break;
10150                    }
10151                    // c:3943
10152                    addfd(
10153                        forked,
10154                        &mut save,
10155                        &mut mfds,
10156                        fn_.fd1,
10157                        fil_local,
10158                        1,
10159                        fn_.varid.as_deref(),
10160                    );
10161                    if IS_ERROR_REDIR(fn_.typ) {
10162                        // c:3944-3945
10163                        addfd(forked, &mut save, &mut mfds, 2, dfil, 1, None);
10164                    }
10165                    let _ = &mut dfil;
10166                }
10167            }
10168            // c:3948-3952 — addfd errflag check.
10169            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10170                // c:3949
10171                closemnodes(&mut mfds); // c:3950
10172                fixfds(&save); // c:3951
10173                {
10174                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10175                    LASTVAL.store(1, Ordering::Relaxed);
10176                } // c:3952
10177                break;
10178            }
10179        }
10180    }
10181
10182    // c:3957-3961 — close multios with ct >= 2.
10183    i = 0;
10184    while i < 10 {
10185        // c:3959
10186        if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
10187            if m.ct >= 2 {
10188                closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
10189            }
10190        }
10191        i += 1;
10192    }
10193
10194    // c:3963-3995 — nullexec branch.
10195    if nullexec != 0 {
10196        // c:3963
10197        if let Some(vspc) = varspc {
10198            // c:3969
10199            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
10200            let mut removelist: Vec<String> = Vec::new();
10201            if !isset(POSIXBUILTINS) && nullexec != 2 {
10202                // c:3971-3972
10203                save_params(state, vspc, &mut restorelist, &mut removelist);
10204            }
10205            addvars(state, vspc, 0); // c:3973
10206            if !restorelist.is_empty() {
10207                // c:3974
10208                restore_params(restorelist, removelist); // c:3975
10209            }
10210        }
10211        let ef = errflag.load(Ordering::Relaxed);
10212        LASTVAL.store(
10213            if ef != 0 {
10214                ef
10215            } else {
10216                cmdoutval.load(Ordering::Relaxed)
10217            },
10218            Ordering::Relaxed,
10219        ); // c:3977
10220        if nullexec == 1 {
10221            // c:3978
10222            // c:3983-3985 — close save[i].
10223            i = 0;
10224            while i < 10 {
10225                if save[i as usize] != -2 {
10226                    let _ = zclose(save[i as usize]); // c:3985
10227                }
10228                i += 1;
10229            }
10230            // c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
10231            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10232            if thisjob >= 0 {
10233                if let Some(jt) = JOBTAB.get() {
10234                    let mut guard = jt.lock().unwrap();
10235                    if let Some(j) = guard.get_mut(thisjob as usize) {
10236                        j.stat |= STAT_DONE; // c:3989
10237                    }
10238                }
10239            }
10240            return execcmd_exec_done_path(
10241                redir_err,
10242                oautocont,
10243                how,
10244                &mut shti,
10245                &mut chti,
10246                &mut then_ts,
10247                forked,
10248                &mut newxtrerr,
10249                cflags,
10250                orig_cflags,
10251                is_cursh,
10252                do_exec,
10253            );
10254        }
10255        if isset(XTRACE) {
10256            // c:3992-3994
10257            eprintln!();
10258        }
10259    } else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
10260        // c:3996 — main dispatch branch.
10261        // c:3997 — `int q = queue_signal_level();`
10262        let _q = 0;
10263        // c:4003-4012 — entersubsh for is_exec.
10264        if is_exec != 0 {
10265            // c:4003
10266            let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
10267                esub::ASYNC
10268            } else {
10269                0
10270            } | esub::PGRP
10271                | esub::FAKE; // c:4004-4005
10272            if typ != WC_SUBSH as i32 {
10273                flags |= esub::KEEPTRAP; // c:4007
10274            }
10275            if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
10276                // c:4008-4009
10277                flags |= esub::REVERTPGRP; // c:4010
10278            }
10279            entersubsh(flags, None); // c:4011
10280        }
10281
10282        if typ == WC_FUNCDEF as i32 {
10283            // c:4013
10284            // c:4014-4036 — `redir_prog` setup from wordcode if no
10285            // redirs+WC_REDIR follows. Wire only when fusevm WC_REDIR
10286            // peek is in scope; for the tree-walker entry point we
10287            // approximate by passing None.
10288            let redir_prog: Option<crate::ported::zsh_h::Eprog> = None;
10289            // c:4039 — `lastval = execfuncdef(state, redir_prog);`
10290            let lv = execfuncdef(state, redir_prog);
10291            LASTVAL.store(lv, Ordering::Relaxed);
10292        } else if typ >= WC_CURSH as i32 {
10293            // c:4042
10294            if last1 == 1 {
10295                do_exec = 1; // c:4044
10296            }
10297            if typ == WC_AUTOFN as i32 {
10298                // c:4046
10299                let lv = execautofn_basic(state, do_exec); // c:4051
10300                LASTVAL.store(lv, Ordering::Relaxed);
10301            } else {
10302                // c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
10303                // dispatch_execfuncs ports the C `execfuncs[]` table
10304                // (Src/exec.c:170-180) by typ → exec{cursh,for,select,...}
10305                // direct call. See dispatch_execfuncs at end of file.
10306                let lv = dispatch_execfuncs(state, typ, do_exec);
10307                LASTVAL.store(lv, Ordering::Relaxed);
10308            }
10309        } else if is_builtin != 0 || is_shfunc != 0 {
10310            // c:4055
10311            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
10312            let mut removelist: Vec<String> = Vec::new();
10313            let mut do_save: i32 = 0; // c:4057
10314
10315            if forked == 0 {
10316                // c:4060
10317                if isset(POSIXBUILTINS) {
10318                    // c:4061
10319                    if is_shfunc != 0
10320                        || (hn.map(|p| unsafe { (*p).node.flags as u32 }).unwrap_or(0)
10321                            & (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
10322                            != 0
10323                    {
10324                        // c:4067
10325                        do_save = if (orig_cflags & BINF_COMMAND) != 0 {
10326                            1
10327                        } else {
10328                            0
10329                        };
10330                    } else {
10331                        do_save = 1; // c:4070
10332                    }
10333                } else {
10334                    // c:4071
10335                    if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
10336                        // c:4076
10337                        do_save = 1; // c:4077
10338                    }
10339                }
10340                if do_save != 0 {
10341                    if let Some(vspc) = varspc {
10342                        // c:4079
10343                        save_params(state, vspc, &mut restorelist, &mut removelist);
10344                    }
10345                }
10346            }
10347            if varspc.is_some() {
10348                // c:4082
10349                let mut addflags: i32 = 0; // c:4086
10350                if is_shfunc != 0 {
10351                    addflags |= ADDVAR_EXPORT; // c:4088
10352                }
10353                if !restorelist.is_empty() {
10354                    addflags |= ADDVAR_RESTORE; // c:4090
10355                }
10356                addvars(state, varspc.unwrap_or(0), addflags); // c:4092
10357                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10358                    // c:4093
10359                    if !restorelist.is_empty() {
10360                        restore_params(restorelist, removelist); // c:4094-4095
10361                    }
10362                    LASTVAL.store(1, Ordering::Relaxed); // c:4096
10363                    fixfds(&save); // c:4097
10364                    return execcmd_exec_done_path(
10365                        redir_err,
10366                        oautocont,
10367                        how,
10368                        &mut shti,
10369                        &mut chti,
10370                        &mut then_ts,
10371                        forked,
10372                        &mut newxtrerr,
10373                        cflags,
10374                        orig_cflags,
10375                        is_cursh,
10376                        do_exec,
10377                    );
10378                }
10379            }
10380
10381            if is_shfunc != 0 {
10382                // c:4102-4105
10383                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
10384                // c:4104 — `execshfunc((Shfunc) hn, args);` C casts
10385                // HashNode hn to Shfunc; zshrs's hn is *mut builtin so
10386                // we re-resolve the shfunc by name from shfunctab and
10387                // dispatch through the top-level execshfunc port at
10388                // exec.rs:4978 (which routes to runshfunc).
10389                let name = args
10390                    .as_ref()
10391                    .and_then(|v| v.first())
10392                    .cloned()
10393                    .unwrap_or_default();
10394                let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
10395                    tab.get(&name).cloned()
10396                } else {
10397                    None
10398                };
10399                if let Some(ref mut shf) = shf_clone {
10400                    execshfunc(shf, &mut a_vec);
10401                }
10402                // c:4105 — `pipecleanfilelist(filelist, 0);` — clean
10403                // out the proc_subst entries from the current job's
10404                // filelist after the shfunc body ran. Route through
10405                // `JOBTAB[thisjob]`.
10406                if let Some(jt) = JOBTAB.get() {
10407                    let mut guard = jt.lock().unwrap();
10408                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10409                    if tj >= 0 {
10410                        if let Some(j) = guard.get_mut(tj as usize) {
10411                            crate::ported::jobs::pipecleanfilelist(j, false);
10412                        }
10413                    }
10414                }
10415            } else {
10416                // c:4107 — builtin path.
10417                let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
10418                let postassigns = eparams.postassigns; // c:4109
10419                if forked != 0 {
10420                    closem(FDT_INTERNAL, 0); // c:4111
10421                }
10422                if postassigns != 0 {
10423                    // c:4112-4230 — typeset post-assignment processing.
10424                    use crate::ported::zsh_h::{
10425                        ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
10426                        PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
10427                        WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
10428                    };
10429                    let opc = state.pc; // c:4113
10430                    state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
10431                                                                      // c:4115 — `assigns = newlinklist();` — already declared above.
10432                    let mut pa_remaining = postassigns;
10433                    while pa_remaining > 0 {
10434                        // c:4116 — `while (postassigns--)`
10435                        pa_remaining -= 1;
10436                        let mut pa_htok: i32 = 0; // c:4117
10437                        if state.pc >= state.prog.prog.len() {
10438                            break;
10439                        }
10440                        let ac = state.prog.prog[state.pc]; // c:4118
10441                        state.pc += 1;
10442                        let mut name = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut pa_htok)); // c:4119
10443                                                                                            // c:4123-4124 DPUTS — debug assertion skipped.
10444                        if pa_htok != 0 {
10445                            // c:4126 — `init_list1(svl, name);`
10446                            let mut svl: LinkList<String> = Default::default();
10447                            svl.push_back(name.clone());
10448                            // c:4127-4166 — INC-scalar special case (typeset $ass form).
10449                            if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
10450                                && WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
10451                            {
10452                                // c:4141 — `(void)ecgetstr(...)` — dummy.
10453                                let mut dummy_htok: i32 = 0;
10454                                let _ = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut dummy_htok));
10455                                let mut rf = 0i32;
10456                                prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
10457                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10458                                    // c:4143
10459                                    state.pc = opc; // c:4144
10460                                    break;
10461                                }
10462                                let mut rf2 = 0i32;
10463                                globlist(&mut svl, rf2); // c:4147
10464                                let _ = &mut rf2;
10465                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10466                                    // c:4148
10467                                    state.pc = opc; // c:4149
10468                                    break;
10469                                }
10470                                // c:4152-4165 — drain svl into assigns.
10471                                while let Some(data) = svl.pop_front() {
10472                                    let (asg_name, asg_val): (String, Option<String>) =
10473                                        if let Some(eq_pos) = data.find('=') {
10474                                            // c:4156-4159
10475                                            (
10476                                                data[..eq_pos].to_string(),
10477                                                Some(data[eq_pos + 1..].to_string()),
10478                                            )
10479                                        } else {
10480                                            // c:4161-4162
10481                                            (data, None)
10482                                        };
10483                                    assigns.push(crate::ported::zsh_h::asgment {
10484                                        node: crate::ported::zsh_h::linknode {
10485                                            next: None,
10486                                            prev: None,
10487                                            dat: 0,
10488                                        },
10489                                        name: asg_name,
10490                                        flags: 0,
10491                                        scalar: asg_val,
10492                                        array: None,
10493                                    });
10494                                }
10495                                continue; // c:4166
10496                            }
10497                            // c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
10498                            let mut rf = 0i32;
10499                            prefork(&mut svl, PREFORK_SINGLE, &mut rf);
10500                            // c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
10501                            name = if svl.is_empty() {
10502                                String::new()
10503                            } else {
10504                                svl.pop_front().unwrap_or_default()
10505                            };
10506                        }
10507                        // c:4172 — `untokenize(name);`
10508                        // (untokenize is destructive on bytes; Rust untokenize
10509                        // returns a new String — call and rebind.)
10510                        name = untokenize(&name);
10511                        let mut asg = crate::ported::zsh_h::asgment {
10512                            node: crate::ported::zsh_h::linknode {
10513                                next: None,
10514                                prev: None,
10515                                dat: 0,
10516                            },
10517                            name,
10518                            flags: 0,
10519                            scalar: None,
10520                            array: None,
10521                        };
10522                        if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
10523                            // c:4175
10524                            let mut val_htok: i32 = 0;
10525                            let mut val = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut val_htok)); // c:4176
10526                            asg.flags = 0; // c:4177
10527                            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
10528                                // c:4178-4180 — fake assignment, no value.
10529                                asg.scalar = None;
10530                            } else {
10531                                if val_htok != 0 {
10532                                    // c:4183
10533                                    let mut svl: LinkList<String> = Default::default();
10534                                    svl.push_back(val.clone());
10535                                    let mut rf = 0i32;
10536                                    prefork(&mut svl, PREFORK_SINGLE | PREFORK_ASSIGN, &mut rf); // c:4184-4186
10537                                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10538                                        // c:4187
10539                                        state.pc = opc; // c:4188
10540                                        break;
10541                                    }
10542                                    // c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
10543                                    val = if svl.is_empty() {
10544                                        String::new()
10545                                    } else {
10546                                        svl.pop_front().unwrap_or_default()
10547                                    };
10548                                }
10549                                // c:4198 — `untokenize(val);`
10550                                asg.scalar = Some(untokenize(&val));
10551                            }
10552                        } else {
10553                            // c:4202 — array assignment.
10554                            asg.flags = ASG_ARRAY; // c:4202
10555                            let mut arr_htok: i32 = 0;
10556                            let arr_words = ecgetlist(
10557                                state,
10558                                WC_ASSIGN_NUM(ac) as usize,
10559                                ECDUPTOK_LOCAL,
10560                                Some(&mut arr_htok),
10561                            ); // c:4204
10562                            let mut arr_list: LinkList<String> = Default::default();
10563                            for s in arr_words {
10564                                arr_list.push_back(s);
10565                            }
10566                            if !arr_list.is_empty()
10567                                && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
10568                            {
10569                                // c:4209 — `int prefork_ret = 0;`
10570                                let mut prefork_ret = 0i32;
10571                                prefork(&mut arr_list, PREFORK_ASSIGN, &mut prefork_ret); // c:4210-4211
10572                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10573                                    // c:4212
10574                                    state.pc = opc; // c:4213
10575                                    break;
10576                                }
10577                                if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
10578                                    // c:4216
10579                                    asg.flags |= ASG_KEY_VALUE; // c:4217
10580                                }
10581                                globlist(&mut arr_list, prefork_ret); // c:4218
10582                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10583                                    // c:4220
10584                                    state.pc = opc; // c:4221
10585                                    break;
10586                                }
10587                            }
10588                            asg.array = Some(arr_list);
10589                        }
10590                        // c:4227 — `uaddlinknode(assigns, &asg->node);`
10591                        assigns.push(asg);
10592                    }
10593                    state.pc = opc; // c:4229
10594                }
10595                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
10596                    // c:4232
10597                    // c:Src/builtin.c:262 — `name = (char *) ugetnode(args);`
10598                    // C's execbuiltin consumes args[0] (the command name)
10599                    // at entry. zshrs's execbuiltin reads the name from
10600                    // `bn->node.nam` instead, so we strip args[0] here
10601                    // before the call to match C's post-ugetnode argv
10602                    // shape. Without this, e.g. `cmd=pwd; $cmd` reached
10603                    // execbuiltin with args=["pwd"] and pwd's
10604                    // maxargs=0 check rejected the empty call as
10605                    // "too many arguments".
10606                    let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
10607                    if !a_vec.is_empty() {
10608                        a_vec.remove(0);
10609                    }
10610                    let ret = crate::ported::builtin::execbuiltin(
10611                        a_vec,
10612                        assigns,
10613                        hn.unwrap_or(std::ptr::null_mut()),
10614                    ); // c:4233
10615                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
10616                        // c:4238
10617                        LASTVAL.store(ret, Ordering::Relaxed); // c:4239
10618                    }
10619                }
10620                if (do_save & BINF_COMMAND as i32) != 0 {
10621                    // c:4241
10622                    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
10623                }
10624                // c:4244 fflush(stdout) — Rust stdio auto-flushes.
10625                // c:4245-4251 — write-error check on save[1].
10626            }
10627            if isset(PRINTEXITVALUE)
10628                && isset(SHINSTDIN)
10629                && LASTVAL.load(Ordering::Relaxed) != 0
10630                && subsh.load(Ordering::Relaxed) == 0
10631            {
10632                // c:4253-4255
10633                eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
10634            }
10635
10636            if do_exec != 0 {
10637                // c:4263
10638                if subsh.load(Ordering::Relaxed) != 0 {
10639                    crate::ported::builtin::_realexit(); // c:4264-4265
10640                }
10641                if isset(RCS)
10642                    && crate::ported::zsh_h::interact()
10643                    && nohistsave.load(Ordering::Relaxed) == 0
10644                {
10645                    // c:4269
10646                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
10647                    // c:4270
10648                }
10649                crate::ported::builtin::realexit(); // c:4271
10650            }
10651            if !restorelist.is_empty() {
10652                // c:4273
10653                restore_params(restorelist, removelist); // c:4274
10654            }
10655        } else {
10656            // c:4276 — external command execute.
10657            if subsh.load(Ordering::Relaxed) == 0 {
10658                // c:4277
10659                if forked == 0 {
10660                    // c:4280 — `setiparam("SHLVL", --shlvl);`
10661                    let cur = getsparam("SHLVL")
10662                        .and_then(|s| s.parse::<i64>().ok())
10663                        .unwrap_or(1);
10664                    setiparam("SHLVL", cur - 1); // c:4281
10665                }
10666                if do_exec != 0
10667                    && isset(RCS)
10668                    && crate::ported::zsh_h::interact()
10669                    && nohistsave.load(Ordering::Relaxed) == 0
10670                {
10671                    // c:4285
10672                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
10673                    // c:4286
10674                }
10675            }
10676            if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
10677                // c:4288
10678                if varspc.is_some() {
10679                    // c:4289
10680                    let mut addflags: i32 = ADDVAR_EXPORT; // c:4290
10681                    if forked != 0 {
10682                        addflags |= ADDVAR_RESTORE; // c:4292
10683                    }
10684                    addvars(state, varspc.unwrap_or(0), addflags); // c:4293
10685                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10686                        // c:4294
10687                        std::process::exit(1); // c:4295
10688                    }
10689                }
10690                closem(FDT_INTERNAL, 0); // c:4297
10691                                         // c:4298-4305 — close coprocin/coprocout.
10692                let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
10693                if cpi != -1 {
10694                    let _ = zclose(cpi); // c:4299
10695                    crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed);
10696                    // c:4300
10697                }
10698                let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
10699                if cpo != -1 {
10700                    let _ = zclose(cpo); // c:4303
10701                    crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed);
10702                    // c:4304
10703                }
10704                if forked == 0 {
10705                    // c:4307
10706                    setlimits(""); // c:4308
10707                }
10708                if (how & Z_ASYNC as i32) != 0 {
10709                    // c:4310 — `zsfree(STTYval); STTYval = 0;`
10710                    let mut guard = STTYval.lock().unwrap();
10711                    *guard = None; // c:4311-4312
10712                }
10713                // c:4314 — `execute(args, cflags, use_defpath);`
10714                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
10715                execute(&mut a_vec, cflags, use_defpath); // c:4314
10716            } else {
10717                // c:4315 — `( ... )` — WC_SUBSH.
10718                list_pipe.store(0, Ordering::Relaxed); // c:4318
10719                                                       // c:4319 — `pipecleanfilelist(filelist, 0);` — clean
10720                                                       // proc-subst entries from the current job's filelist
10721                                                       // before recursing into the subshell body.
10722                if let Some(jt) = JOBTAB.get() {
10723                    let mut guard = jt.lock().unwrap();
10724                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10725                    if tj >= 0 {
10726                        if let Some(j) = guard.get_mut(tj as usize) {
10727                            crate::ported::jobs::pipecleanfilelist(j, false);
10728                        }
10729                    }
10730                }
10731                state.pc += 1; // c:4324 — `state->pc++;`
10732                let _ = execlist(state, 0, 1); // c:4325
10733            }
10734        }
10735    }
10736
10737    // c:4330-4404 — err: + done: + fatal:.
10738    return execcmd_exec_done_path(
10739        redir_err,
10740        oautocont,
10741        how,
10742        &mut shti,
10743        &mut chti,
10744        &mut then_ts,
10745        forked,
10746        &mut newxtrerr,
10747        cflags,
10748        orig_cflags,
10749        is_cursh,
10750        do_exec,
10751    );
10752}
10753
10754/// Internal helper modelling the C `done:` label tail of
10755/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
10756/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
10757/// shelltime stop, and newxtrerr close.
10758#[allow(clippy::too_many_arguments)]
10759fn execcmd_exec_done_path(
10760    redir_err: i32,
10761    oautocont: i32,
10762    how: i32,
10763    shti: &mut crate::ported::jobs::timeinfo,
10764    chti: &mut crate::ported::jobs::timeinfo,
10765    then_ts: &mut std::time::Instant,
10766    forked: i32,
10767    newxtrerr: &mut Option<i32>,
10768    cflags: u32,
10769    orig_cflags: u32,
10770    is_cursh: i32,
10771    do_exec: i32,
10772) {
10773    use crate::ported::zsh_h::{
10774        AUTOCONTINUE, BINF_COMMAND, BINF_EXEC, BINF_PSPECIAL, INTERACTIVE, POSIXBUILTINS, Z_TIMED,
10775    };
10776    // c:4366
10777    // c:4367-4386 — POSIX special-builtin error escalation.
10778    if isset(POSIXBUILTINS)
10779        && (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
10780        && (orig_cflags & BINF_COMMAND) == 0
10781    {
10782        // c:4367-4369
10783        let _forked_or_subsh = forked | zsh_subshell.load(Ordering::Relaxed); // c:4376
10784                                                                              // fatal: label entry point — same handling.
10785        if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10786            // c:4378
10787            if !isset(INTERACTIVE) {
10788                // c:4379
10789                if _forked_or_subsh != 0 {
10790                    unsafe { libc::_exit(1) }; // c:4381
10791                } else {
10792                    std::process::exit(1); // c:4383
10793                }
10794            }
10795            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
10796        }
10797    }
10798    // c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
10799    if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
10800        crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1);
10801        // c:4389
10802    }
10803    // c:4390-4398 — newxtrerr close.
10804    if let Some(fd) = newxtrerr.take() {
10805        // c:4390
10806        let _ = zclose(fd); // c:4396
10807    }
10808    // c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
10809    {
10810        let mut guard = STTYval.lock().unwrap();
10811        *guard = None;
10812    }
10813    // c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
10814    if oautocont >= 0 {
10815        opt_state_set("autocontinue", oautocont != 0);
10816    }
10817}
10818
10819/// Internal helper modelling the C `err:` label tail of
10820/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
10821/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
10822/// to done:.
10823#[allow(clippy::too_many_arguments)]
10824fn execcmd_exec_err_path(
10825    forked: i32,
10826    save: &mut [i32; 10],
10827    mfds: &mut [Option<Box<multio>>; 10],
10828    oautocont: i32,
10829    how: i32,
10830    shti: &mut crate::ported::jobs::timeinfo,
10831    chti: &mut crate::ported::jobs::timeinfo,
10832    then_ts: &mut std::time::Instant,
10833    newxtrerr: &mut Option<i32>,
10834    cflags: u32,
10835    orig_cflags: u32,
10836    is_cursh: i32,
10837    do_exec: i32,
10838    redir_err: i32,
10839) {
10840    use crate::ported::zsh_h::FDT_UNUSED;
10841    // c:4330
10842    if forked != 0 {
10843        // c:4331
10844        // c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
10845        let mut i: i32 = 0;
10846        while i < 10 {
10847            if fdtable_get(i) != FDT_UNUSED {
10848                unsafe { libc::close(i) }; // c:4358
10849            }
10850            i += 1;
10851        }
10852        // c:4359 — `closem(FDT_UNUSED, 1);`
10853        closem(FDT_UNUSED, 1); // c:4359
10854                               // c:4360-4361 — `if (thisjob != -1) waitjobs();`
10855        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10856        if thisjob != -1 {
10857            if let Some(jt) = JOBTAB.get() {
10858                let mut guard = jt.lock().unwrap();
10859                crate::ported::jobs::waitjobs(&mut guard, thisjob as usize); // c:4361
10860            }
10861        }
10862        crate::ported::builtin::_realexit(); // c:4362
10863    }
10864    fixfds(save); // c:4364
10865
10866    execcmd_exec_done_path(
10867        redir_err,
10868        oautocont,
10869        how,
10870        shti,
10871        chti,
10872        then_ts,
10873        forked,
10874        newxtrerr,
10875        cflags,
10876        orig_cflags,
10877        is_cursh,
10878        do_exec,
10879    );
10880}
10881
10882/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
10883/// `Src/exec.c:170-180`. Each branch maps to the ported wordcode-
10884/// walker function in `src/ported/exec.rs`.
10885fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
10886    use crate::ported::zsh_h::{
10887        WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
10888        WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
10889    };
10890    // Port of `static int (*const execfuncs[])(Estate, int)` dispatch
10891    // table at `Src/exec.c:170-180`. C indexes by `(type - WC_CURSH)`;
10892    // Rust matches on the WC_* tag directly.
10893    match typ as wordcode {
10894        x if x == WC_CURSH => execcursh(state, do_exec),
10895        x if x == WC_FOR => execfor(state, do_exec),
10896        x if x == WC_SELECT => execselect(state, do_exec),
10897        x if x == WC_WHILE => execwhile(state, do_exec),
10898        x if x == WC_REPEAT => execrepeat(state, do_exec),
10899        x if x == WC_CASE => execcase(state, do_exec),
10900        x if x == WC_IF => execif(state, do_exec),
10901        x if x == WC_COND => execcond(state, do_exec),
10902        x if x == WC_ARITH => execarith(state, do_exec),
10903        x if x == WC_TRY => exectry(state, do_exec),
10904        x if x == WC_FUNCDEF => execfuncdef(state, None),
10905        // c:272 — execfuncs[] table dispatches `WC_AUTOFN` to
10906        // `execautofn` (the loadautofn-then-basic wrapper), not
10907        // `execautofn_basic` directly.
10908        x if x == WC_AUTOFN => execautofn(state, do_exec),
10909        x if x == WC_TIMED => exectime(state, do_exec),
10910        x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
10911        _ => 0,
10912    }
10913}
10914
10915/// Port of `Eprog stripkshdef(Eprog prog, char *name)` from
10916/// `Src/exec.c:6286-6364`. Given an Eprog read from an autoload
10917/// file plus the function name being defined, check whether the
10918/// file consists of *exactly* one `function NAME { … }` definition
10919/// for that name. If so, return a new Eprog whose `prog`/`strs`/
10920/// `pats` slice out just the function body (so calling code can
10921/// invoke the body directly instead of re-parsing). Otherwise
10922/// return the input untouched.
10923///
10924/// Header word layout consumed (matches C `pc[…]` reads):
10925///   pc[0] = WC_LIST with `Z_SYNC|Z_END|Z_SIMPLE` flags
10926///   pc[1] = (sublist header, skipped)
10927///   pc[2] = WC_FUNCDEF
10928///   pc[3] = 1                       (single-name funcdef)
10929///   pc[4] = name-string slot        (compared to `name`)
10930///   pc[5] = sbeg  (offset into strs table)
10931///   pc[6] = nstrs (bytes of strs to copy)
10932///   pc[7] = npats (number of pattern slots to allocate)
10933///   pc[8] = WC_FUNCDEF_SKIP target  (end-of-funcdef pc)
10934///   pc[9] = (unused header word — `pc += 6` lands here as the
10935///           start of the body wordcode stream)
10936///
10937/// Returns `None` only when the input was `None` (matches C
10938/// `return NULL`). Equivalence between the original `prog` and a
10939/// successfully stripped `prog` is *not* preserved at the pointer
10940/// level (C may return the original Eprog when the file fails the
10941/// single-funcdef shape check; this Rust port does the same by
10942/// passing the box back through).
10943///
10944/// `EF_MAP` (`zcompile`d / mmap'd Eprog) path: C mutates the
10945/// existing Eprog in place, swapping its `prog` / `strs` /
10946/// `pats` to slice into the funcdef body. Rust mirrors this on
10947/// the moved-in `Box<eprog>` (no separate `free()` needed —
10948/// `Vec` drop handles the old `pats`).
10949pub fn stripkshdef(
10950    prog: Option<crate::ported::zsh_h::Eprog>,
10951    name: &str,
10952) -> Option<crate::ported::zsh_h::Eprog> {
10953    use crate::ported::parse::ecrawstr;
10954    use crate::ported::zsh_h::{
10955        wc_code, wordcode, Dash, EF_HEAP, EF_MAP, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_LIST,
10956        WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC,
10957    };
10958
10959    // c:6300 — `if (!prog) return NULL;`
10960    let mut prog = prog?;
10961
10962    // c:6302-6306 — first word must be WC_LIST with all of
10963    // Z_SYNC|Z_END|Z_SIMPLE set (i.e. the trivial "single simple
10964    // sublist" wrapper around the funcdef).
10965    if prog.prog.len() < 3 {
10966        return Some(prog);
10967    }
10968    let code0: wordcode = prog.prog[0];
10969    if wc_code(code0) != WC_LIST
10970        || (WC_LIST_TYPE(code0) & (Z_SYNC | Z_END | Z_SIMPLE) as wordcode)
10971            != (Z_SYNC | Z_END | Z_SIMPLE) as wordcode
10972    {
10973        return Some(prog);
10974    }
10975    // c:6307 — `pc++;` (skip the sublist header word at pc[1]).
10976    // c:6308 — `code = *pc++;` lands `code` on pc[2], leaving the
10977    // walking cursor at pc[3] which is read directly below.
10978    let code: wordcode = prog.prog[2];
10979    let pc_after_code: usize = 3;
10980    if wc_code(code) != WC_FUNCDEF || prog.prog[pc_after_code] != 1 {
10981        return Some(prog);
10982    }
10983
10984    // c:6320 — `ptr2 = ecrawstr(prog, pc + 1, NULL);` (note: C's
10985    // `pc` is already past `code`, so `pc + 1` lands on pc[4] —
10986    // the name-string slot).
10987    let name_slot = pc_after_code + 1; // == 4
10988    let name_in_def = ecrawstr(&prog, name_slot, None);
10989
10990    // c:6320-6328 — name match, tolerating Dash-tokenised hyphens
10991    // on either side.
10992    let n1 = name.as_bytes();
10993    let n2 = name_in_def.as_bytes();
10994    let mut i = 0usize;
10995    let mut j = 0usize;
10996    while i < n1.len() && j < n2.len() {
10997        let c1 = n1[i] as char;
10998        let c2 = n2[j] as char;
10999        if c1 != c2 && c1 != Dash && c1 != '-' && c2 != Dash && c2 != '-' {
11000            break;
11001        }
11002        i += 1;
11003        j += 1;
11004    }
11005    // c:6329 — `if (*ptr1 || *ptr2) return prog;` (any unmatched
11006    // tail on either side → not the right funcdef).
11007    if i < n1.len() || j < n2.len() {
11008        return Some(prog);
11009    }
11010
11011    // c:6332-6362 — slice the funcdef body out. Layout:
11012    //   sbeg  = pc[2] (in C, == prog.prog[pc_after_code + 2] == [5])
11013    //   nstrs = pc[3] (== [6])
11014    //   npats = pc[4] (== [7])
11015    //   end   = pc + WC_FUNCDEF_SKIP(code)   (== pc_after_code + skip)
11016    //   pc   += 6  (body wordcode begins at pc_after_code + 6 == [9])
11017    let sbeg = prog.prog[pc_after_code + 2] as usize;
11018    let nstrs = prog.prog[pc_after_code + 3] as usize;
11019    let npats = prog.prog[pc_after_code + 4] as i32;
11020    let skip = WC_FUNCDEF_SKIP(code) as usize;
11021    let end_pc = pc_after_code + skip;
11022    let body_start = pc_after_code + 6;
11023    if end_pc < body_start || end_pc > prog.prog.len() {
11024        // Defensive: malformed header — return input untouched so
11025        // the caller's parse-eprog fallback re-reads from source.
11026        return Some(prog);
11027    }
11028    let nprg = end_pc - body_start;
11029    let plen = nprg * size_of::<wordcode>();
11030    let len = plen + (npats as usize) * size_of::<usize>() + nstrs;
11031
11032    // Build the new pats slice — `dummy_patprog1` slots in C; the
11033    // Rust convention (mirrors `dupeprog` at parse.rs:2716) is to
11034    // synthesize zero-initialised patprog placeholders that
11035    // pattern compile-on-first-use will overwrite.
11036    let dummy_pat = || {
11037        Box::new(crate::ported::zsh_h::patprog {
11038            startoff: 0,
11039            size: 0,
11040            mustoff: 0,
11041            patmlen: 0,
11042            globflags: 0,
11043            globend: 0,
11044            flags: 0,
11045            patnpar: 0,
11046            patstartch: 0,
11047        })
11048    };
11049    let new_pats: Vec<crate::ported::zsh_h::Patprog> =
11050        (0..npats.max(0)).map(|_| dummy_pat()).collect();
11051
11052    // c:6353 — `ret->strs = prog->strs + sbeg;` (EF_MAP) or
11053    // c:6359 — `memcpy(ret->strs, prog->strs + sbeg, nstrs);` (heap).
11054    let old_strs = prog.strs.take().unwrap_or_default();
11055    let old_bytes = old_strs.as_bytes();
11056    let new_strs = if sbeg + nstrs <= old_bytes.len() {
11057        Some(String::from_utf8_lossy(&old_bytes[sbeg..sbeg + nstrs]).into_owned())
11058    } else {
11059        Some(String::new())
11060    };
11061
11062    let new_prog: Vec<wordcode> = prog.prog[body_start..end_pc].to_vec();
11063
11064    if (prog.flags & EF_MAP) != 0 {
11065        // c:6349-6354 — in-place EF_MAP path.
11066        prog.pats = new_pats;
11067        prog.prog = new_prog;
11068        prog.strs = new_strs;
11069        prog.len = len as i32;
11070        prog.npats = npats;
11071        prog.shf = None;
11072        return Some(prog);
11073    }
11074
11075    // c:6356-6361 — heap-allocated new Eprog.
11076    let ret = Box::new(eprog {
11077        flags: EF_HEAP,
11078        len: len as i32,
11079        npats,
11080        nref: -1, // c:6363 (heap path → never refcount-freed).
11081        pats: new_pats,
11082        prog: new_prog,
11083        strs: new_strs,
11084        shf: None, // c:6363
11085        dump: None,
11086    });
11087    Some(ret)
11088}
11089
11090#[cfg(test)]
11091mod tests {
11092    use super::*;
11093
11094    // ─── zsh-corpus pins for pure exec helpers ─────────────────────
11095
11096    /// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
11097    #[test]
11098    fn exec_corpus_isrelative_empty_is_one() {
11099        let _g = crate::test_util::global_state_lock();
11100        assert_eq!(isrelative(""), 1, "empty path is relative");
11101    }
11102
11103    /// `isrelative("foo")` = 1 (no leading slash).
11104    #[test]
11105    fn exec_corpus_isrelative_bare_name_is_one() {
11106        let _g = crate::test_util::global_state_lock();
11107        assert_eq!(isrelative("foo"), 1);
11108        assert_eq!(isrelative("bin/cmd"), 1);
11109    }
11110
11111    /// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
11112    #[test]
11113    fn exec_corpus_isrelative_absolute_clean_is_zero() {
11114        let _g = crate::test_util::global_state_lock();
11115        assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
11116        assert_eq!(isrelative("/bin/ls"), 0);
11117        assert_eq!(isrelative("/"), 0, "root is absolute");
11118    }
11119
11120    /// `isrelative("/foo/../bar")` = 1 (contains `../` component).
11121    #[test]
11122    fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
11123        let _g = crate::test_util::global_state_lock();
11124        assert_eq!(
11125            isrelative("/foo/../bar"),
11126            1,
11127            "absolute path with ../ is still 'relative' per zsh"
11128        );
11129    }
11130
11131    /// `isrelative("/foo/./bar")` = 1 (contains `./` component).
11132    #[test]
11133    fn exec_corpus_isrelative_absolute_with_dot_is_one() {
11134        let _g = crate::test_util::global_state_lock();
11135        assert_eq!(
11136            isrelative("/./x"),
11137            1,
11138            "absolute with ./ component reported relative"
11139        );
11140    }
11141
11142    /// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
11143    #[test]
11144    fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
11145        assert_eq!(is_anonymous_function_name("(anon)"), 1);
11146    }
11147
11148    /// `is_anonymous_function_name("regular_name")` = 0.
11149    #[test]
11150    fn exec_corpus_is_anonymous_function_name_rejects_normal() {
11151        assert_eq!(is_anonymous_function_name("regular_name"), 0);
11152        assert_eq!(is_anonymous_function_name(""), 0);
11153        assert_eq!(
11154            is_anonymous_function_name("anon"),
11155            0,
11156            "plain 'anon' (no parens) is NOT the sentinel"
11157        );
11158    }
11159
11160    /// `iscom("/nonexistent/never_a_path")` = false.
11161    #[test]
11162    fn exec_corpus_iscom_missing_path_false() {
11163        assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
11164    }
11165
11166    /// `iscom("/tmp")` is a directory not a regular file → false.
11167    #[test]
11168    fn exec_corpus_iscom_directory_false() {
11169        assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
11170    }
11171
11172    /// `iscom("/bin/sh")` is true on POSIX systems.
11173    #[test]
11174    fn exec_corpus_iscom_known_binary_true() {
11175        // /bin/sh exists on all POSIX systems with X perms.
11176        if std::path::Path::new("/bin/sh").exists() {
11177            assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
11178        }
11179    }
11180
11181    // ─── stripkshdef (Src/exec.c:6286) early-return paths ──────────
11182
11183    /// `stripkshdef(None, "foo")` → `None` (matches C `if (!prog)
11184    /// return NULL;` at exec.c:6300).
11185    #[test]
11186    fn exec_corpus_stripkshdef_null_input_returns_none() {
11187        assert!(stripkshdef(None, "foo").is_none());
11188    }
11189
11190    /// `stripkshdef` on an empty/degenerate Eprog returns the same
11191    /// Eprog unchanged (no funcdef-shape to strip).
11192    #[test]
11193    fn exec_corpus_stripkshdef_empty_prog_returns_input() {
11194        let prog = Box::new(eprog {
11195            prog: vec![],
11196            ..Default::default()
11197        });
11198        let out = stripkshdef(Some(prog), "foo");
11199        assert!(out.is_some(), "empty prog → returned unchanged");
11200        assert!(out.unwrap().prog.is_empty(), "no mutation");
11201    }
11202
11203    /// `stripkshdef` on a non-WC_LIST head returns the input
11204    /// untouched (early return at exec.c:6304-6306).
11205    #[test]
11206    fn exec_corpus_stripkshdef_non_list_head_returns_input() {
11207        use crate::ported::zsh_h::{wc_bld, WC_SUBLIST};
11208        let prog = Box::new(eprog {
11209            prog: vec![wc_bld(WC_SUBLIST, 0), 0, 0],
11210            ..Default::default()
11211        });
11212        let out = stripkshdef(Some(prog), "foo");
11213        assert!(out.is_some());
11214        // first word is the WC_SUBLIST sentinel we passed in,
11215        // unchanged (the function bailed before doing any slicing).
11216        let p = out.unwrap();
11217        use crate::ported::zsh_h::wc_code;
11218        assert_eq!(
11219            wc_code(p.prog[0]),
11220            WC_SUBLIST,
11221            "header word preserved verbatim"
11222        );
11223    }
11224
11225    // ═══════════════════════════════════════════════════════════════════
11226    // C-parity tests pinning Src/exec.c. Tests that capture KNOWN
11227    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
11228    // ═══════════════════════════════════════════════════════════════════
11229
11230    /// `isrelative("/abs/path")` returns 0 (false = absolute path).
11231    /// C `Src/exec.c:996-1006` — leading `/` and no `.`/`..` components.
11232    #[test]
11233    fn isrelative_absolute_path_returns_zero() {
11234        let _g = crate::test_util::global_state_lock();
11235        assert_eq!(isrelative("/usr/local/bin"), 0);
11236    }
11237
11238    /// `isrelative("foo/bar")` returns 1 (no leading slash).
11239    #[test]
11240    fn isrelative_no_leading_slash_returns_one() {
11241        let _g = crate::test_util::global_state_lock();
11242        assert_eq!(isrelative("foo/bar"), 1);
11243    }
11244
11245    /// `isrelative("/foo/./bar")` returns 1 — contains `/./` walk.
11246    /// C c:1001 — `.` with prev `/` + next `/` triggers relative flag.
11247    #[test]
11248    fn isrelative_dot_component_returns_one() {
11249        let _g = crate::test_util::global_state_lock();
11250        assert_eq!(isrelative("/foo/./bar"), 1, "/./ in path → relative");
11251    }
11252
11253    /// `isrelative("/foo/../bar")` returns 1 — contains `/..` walk.
11254    #[test]
11255    fn isrelative_dotdot_component_returns_one() {
11256        let _g = crate::test_util::global_state_lock();
11257        assert_eq!(isrelative("/foo/../bar"), 1, "/../ in path → relative");
11258    }
11259
11260    /// `isrelative("")` returns 1 — empty input has no leading `/`.
11261    /// C c:998 — `*s != '/'` includes the NUL terminator case.
11262    #[test]
11263    fn isrelative_empty_returns_one() {
11264        let _g = crate::test_util::global_state_lock();
11265        assert_eq!(isrelative(""), 1, "empty string → not absolute");
11266    }
11267
11268    /// `isrelative("/a/.b")` returns 0 — `.b` is NOT a `/./` walk
11269    /// (followed by another non-`/` char `b`).
11270    #[test]
11271    fn isrelative_dotfile_in_path_returns_zero() {
11272        let _g = crate::test_util::global_state_lock();
11273        assert_eq!(
11274            isrelative("/usr/.config/zsh"),
11275            0,
11276            "dotfile name '.config' is NOT a relative walk"
11277        );
11278    }
11279
11280    /// `is_anonymous_function_name("(anon)")` returns 1 (true).
11281    /// C `Src/exec.c` — `!strcmp(name, ANONYMOUS_FUNCTION_NAME)`.
11282    #[test]
11283    fn is_anonymous_function_name_anon_returns_one() {
11284        let _g = crate::test_util::global_state_lock();
11285        assert_eq!(is_anonymous_function_name("(anon)"), 1);
11286    }
11287
11288    /// `is_anonymous_function_name("foo")` returns 0 (false).
11289    #[test]
11290    fn is_anonymous_function_name_normal_returns_zero() {
11291        let _g = crate::test_util::global_state_lock();
11292        assert_eq!(is_anonymous_function_name("foo"), 0);
11293        assert_eq!(is_anonymous_function_name(""), 0);
11294        assert_eq!(is_anonymous_function_name("(other)"), 0);
11295    }
11296
11297    /// `isgooderr(EACCES, "/no/such/dir")` returns true when the dir
11298    /// is not actually accessible. C `Src/exec.c:isgooderr` filters
11299    /// out "unreadable / not directory" errnos so caller doesn't
11300    /// emit spurious warnings.
11301    #[test]
11302    fn isgooderr_eacces_unreadable_dir_returns_false() {
11303        let _g = crate::test_util::global_state_lock();
11304        // /no/such/dir doesn't exist → access(X_OK) fails non-zero
11305        // → !access() is 0 (false) → returns false.
11306        assert!(
11307            !isgooderr(libc::EACCES, "/no/such/dir/zshrs_test"),
11308            "unreadable dir with EACCES should NOT be 'good error'"
11309        );
11310    }
11311
11312    // ═══════════════════════════════════════════════════════════════════
11313    // Additional C-parity tests for Src/exec.c basic accessors/predicates.
11314    // ═══════════════════════════════════════════════════════════════════
11315
11316    /// c:658 — `isgooderr(ENOENT, _)` always false (regardless of dir).
11317    /// Pin: ENOENT is NEVER a "good error" because the path itself
11318    /// doesn't exist — caller should suppress the warning.
11319    #[test]
11320    fn isgooderr_enoent_always_false() {
11321        let _g = crate::test_util::global_state_lock();
11322        assert!(!isgooderr(libc::ENOENT, "/tmp"));
11323        assert!(!isgooderr(libc::ENOENT, "/no/such/dir"));
11324        assert!(!isgooderr(libc::ENOENT, ""));
11325    }
11326
11327    /// c:658 — `isgooderr(ENOTDIR, _)` always false. A path component
11328    /// being a non-dir is a structural error, not a permission issue.
11329    #[test]
11330    fn isgooderr_enotdir_always_false() {
11331        let _g = crate::test_util::global_state_lock();
11332        assert!(!isgooderr(libc::ENOTDIR, "/tmp"));
11333        assert!(!isgooderr(libc::ENOTDIR, "/"));
11334    }
11335
11336    /// c:658 — Other errnos (EPERM, EIO, ENOMEM) are "good errors"
11337    /// because they're not the suppressed three (EACCES/ENOENT/ENOTDIR).
11338    #[test]
11339    fn isgooderr_other_errno_returns_true() {
11340        let _g = crate::test_util::global_state_lock();
11341        assert!(isgooderr(libc::EPERM, "/tmp"));
11342        assert!(isgooderr(libc::EIO, "/tmp"));
11343        assert!(isgooderr(libc::ENOMEM, "/tmp"));
11344    }
11345
11346    /// c:962 — `iscom("/tmp")` returns false (directory, not S_ISREG).
11347    #[test]
11348    fn iscom_directory_returns_false() {
11349        let _g = crate::test_util::global_state_lock();
11350        assert!(!iscom("/tmp"));
11351        assert!(!iscom("/"));
11352    }
11353
11354    /// c:962 — `iscom` on non-existent path returns false (access
11355    /// X_OK fails).
11356    #[test]
11357    fn iscom_nonexistent_path_returns_false() {
11358        let _g = crate::test_util::global_state_lock();
11359        assert!(!iscom("/no/such/path/zshrs_iscom_test"));
11360        assert!(!iscom(""));
11361    }
11362
11363    /// c:962 — `iscom("/bin/sh")` returns true on every POSIX system.
11364    #[test]
11365    #[cfg(unix)]
11366    fn iscom_bin_sh_returns_true() {
11367        let _g = crate::test_util::global_state_lock();
11368        // /bin/sh is a POSIX-required executable.
11369        assert!(iscom("/bin/sh"), "/bin/sh must be executable on POSIX");
11370    }
11371
11372    /// c:5300 — anonymous function name is exactly "(anon)" — must
11373    /// not match prefixes/suffixes/case variants.
11374    #[test]
11375    fn is_anonymous_function_name_strict_match_only() {
11376        let _g = crate::test_util::global_state_lock();
11377        assert_eq!(is_anonymous_function_name("(anon"), 0, "no trailing paren");
11378        assert_eq!(is_anonymous_function_name("anon)"), 0, "no leading paren");
11379        assert_eq!(is_anonymous_function_name("(ANON)"), 0, "wrong case");
11380        assert_eq!(
11381            is_anonymous_function_name(" (anon) "),
11382            0,
11383            "leading/trailing space"
11384        );
11385        assert_eq!(is_anonymous_function_name("(anon) "), 0, "trailing space");
11386        assert_eq!(is_anonymous_function_name(" (anon)"), 0, "leading space");
11387    }
11388
11389    /// c:5289 — `ANONYMOUS_FUNCTION_NAME` constant is exactly `"(anon)"`.
11390    /// Pin so a regen that flips parens / changes case / adds prefix
11391    /// would be caught.
11392    #[test]
11393    fn anonymous_function_name_const_is_literal_anon() {
11394        let _g = crate::test_util::global_state_lock();
11395        assert_eq!(ANONYMOUS_FUNCTION_NAME, "(anon)");
11396    }
11397
11398    /// c:147-148 — `isrelative("./")` returns 1 (dot-slash prefix
11399    /// is the canonical relative-path form).
11400    #[test]
11401    fn isrelative_dot_slash_returns_one() {
11402        let _g = crate::test_util::global_state_lock();
11403        assert_eq!(isrelative("./foo"), 1);
11404        assert_eq!(isrelative("./"), 1);
11405    }
11406
11407    /// c:147-148 — `isrelative("../foo")` returns 1.
11408    #[test]
11409    fn isrelative_dotdot_slash_returns_one() {
11410        let _g = crate::test_util::global_state_lock();
11411        assert_eq!(isrelative("../foo"), 1);
11412        assert_eq!(isrelative("../"), 1);
11413    }
11414
11415    /// c:147-148 — `/.foo` (hidden file under root) is absolute.
11416    /// Pin: only `/.` (with trailing `/`) or end-of-string counts as
11417    /// a `.` component, NOT `/.foo` (which is a normal file `.foo`).
11418    #[test]
11419    fn isrelative_root_hidden_file_returns_zero() {
11420        let _g = crate::test_util::global_state_lock();
11421        assert_eq!(isrelative("/.foo"), 0, "/.foo is absolute path to dotfile");
11422        assert_eq!(isrelative("/.bashrc"), 0, "/.bashrc is absolute");
11423    }
11424
11425    /// c:147-148 — `/..bar` (file named `..bar`) is also absolute,
11426    /// since `..bar` is a regular file name, not a `..` component.
11427    #[test]
11428    fn isrelative_root_double_dot_file_returns_zero() {
11429        let _g = crate::test_util::global_state_lock();
11430        assert_eq!(isrelative("/..bar"), 0);
11431    }
11432
11433    /// c:2652 — `setunderscore("")` clears `zunderscore` and resets
11434    /// `underscoreused` to 1 (null terminator only).
11435    #[test]
11436    fn setunderscore_empty_clears_state() {
11437        let _g = crate::test_util::global_state_lock();
11438        setunderscore(""); // initialize to known empty state
11439        let zu = zunderscore.lock().unwrap();
11440        assert!(zu.is_empty(), "zunderscore must be empty after clear");
11441        drop(zu);
11442        let used = underscoreused.load(Ordering::Relaxed);
11443        assert_eq!(used, 1, "underscoreused must be 1 (NUL only) after clear");
11444    }
11445
11446    /// c:2652 — `setunderscore(str)` sets `zunderscore=str` and
11447    /// `underscoreused = str.len()+1` (string + null terminator).
11448    #[test]
11449    fn setunderscore_with_value_stores_string_and_length() {
11450        let _g = crate::test_util::global_state_lock();
11451        setunderscore("hello");
11452        let zu = zunderscore.lock().unwrap();
11453        assert_eq!(*zu, "hello");
11454        drop(zu);
11455        let used = underscoreused.load(Ordering::Relaxed);
11456        assert_eq!(used, 6, "len('hello')+1 = 6");
11457    }
11458
11459    /// c:2656 — `underscorelen` is rounded up to 32-byte boundary
11460    /// for the bump-allocator-friendly buffer growth.
11461    #[test]
11462    fn setunderscore_rounds_underscorelen_to_32() {
11463        let _g = crate::test_util::global_state_lock();
11464        setunderscore("ab"); // len 2 + 1 = 3 → ceil(32) = 32
11465        let nl = underscorelen.load(Ordering::Relaxed);
11466        assert_eq!(nl, 32, "(2+1+31) & !31 = 32");
11467    }
11468
11469    // ═══════════════════════════════════════════════════════════════════
11470    // Additional C-parity tests for Src/exec.c cancd2 +
11471    // quote_tokenized_output.
11472    // ═══════════════════════════════════════════════════════════════════
11473
11474    /// c:6411 — `cancd2("/tmp")` returns 1 (directory with X_OK exists).
11475    #[test]
11476    #[cfg(unix)]
11477    fn cancd2_existing_dir_returns_one() {
11478        let _g = crate::test_util::global_state_lock();
11479        assert_eq!(cancd2("/tmp"), 1, "/tmp is a valid cd target");
11480    }
11481
11482    /// c:6411 — `cancd2("/nonexistent")` returns 0.
11483    #[test]
11484    fn cancd2_nonexistent_returns_zero() {
11485        let _g = crate::test_util::global_state_lock();
11486        assert_eq!(cancd2("/__never_exists_zshrs_cancd2__"), 0);
11487    }
11488
11489    /// c:6411 — `cancd2` for a file (not dir) returns 0.
11490    #[test]
11491    #[cfg(unix)]
11492    fn cancd2_regular_file_returns_zero() {
11493        let _g = crate::test_util::global_state_lock();
11494        let dir = tempfile::tempdir().unwrap();
11495        let p = dir.path().join("regular_file");
11496        std::fs::write(&p, "x").unwrap();
11497        assert_eq!(
11498            cancd2(p.to_str().unwrap()),
11499            0,
11500            "regular file not a cd target"
11501        );
11502    }
11503
11504    /// c:2114 — `quote_tokenized_output` on empty string writes nothing.
11505    #[test]
11506    fn quote_tokenized_output_empty_writes_nothing() {
11507        let _g = crate::test_util::global_state_lock();
11508        let mut buf = Vec::new();
11509        quote_tokenized_output("", &mut buf).unwrap();
11510        assert!(buf.is_empty());
11511    }
11512
11513    /// c:2114 — plain ASCII passes through unchanged.
11514    #[test]
11515    fn quote_tokenized_output_plain_ascii_unchanged() {
11516        let _g = crate::test_util::global_state_lock();
11517        let mut buf = Vec::new();
11518        quote_tokenized_output("hello", &mut buf).unwrap();
11519        assert_eq!(buf, b"hello");
11520    }
11521
11522    /// c:2143 — space gets backslash-quoted.
11523    #[test]
11524    fn quote_tokenized_output_space_backslash_quoted() {
11525        let _g = crate::test_util::global_state_lock();
11526        let mut buf = Vec::new();
11527        quote_tokenized_output("a b", &mut buf).unwrap();
11528        assert_eq!(buf, b"a\\ b");
11529    }
11530
11531    /// c:2147 — tab → $'\\t'.
11532    #[test]
11533    fn quote_tokenized_output_tab_dollar_escape() {
11534        let _g = crate::test_util::global_state_lock();
11535        let mut buf = Vec::new();
11536        quote_tokenized_output("a\tb", &mut buf).unwrap();
11537        assert_eq!(buf, b"a$'\\t'b");
11538    }
11539
11540    /// c:2151 — newline → $'\\n'.
11541    #[test]
11542    fn quote_tokenized_output_newline_dollar_escape() {
11543        let _g = crate::test_util::global_state_lock();
11544        let mut buf = Vec::new();
11545        quote_tokenized_output("a\nb", &mut buf).unwrap();
11546        assert_eq!(buf, b"a$'\\n'b");
11547    }
11548
11549    /// c:2155 — CR → $'\\r'.
11550    #[test]
11551    fn quote_tokenized_output_cr_dollar_escape() {
11552        let _g = crate::test_util::global_state_lock();
11553        let mut buf = Vec::new();
11554        quote_tokenized_output("a\rb", &mut buf).unwrap();
11555        assert_eq!(buf, b"a$'\\r'b");
11556    }
11557
11558    /// c:2128 — shell metacharacters all get backslash-quoted.
11559    #[test]
11560    fn quote_tokenized_output_shell_metas_get_backslash() {
11561        let _g = crate::test_util::global_state_lock();
11562        for c in &[b'<', b'>', b'(', b')', b'|', b'#', b'$', b'*', b'?', b'~'] {
11563            let mut buf = Vec::new();
11564            let s = String::from_utf8(vec![b'a', *c, b'b']).unwrap();
11565            quote_tokenized_output(&s, &mut buf).unwrap();
11566            assert_eq!(buf, vec![b'a', b'\\', *c, b'b'], "char {:?}", *c as char);
11567        }
11568    }
11569
11570    /// c:2158 — `=` at position 0 gets quoted (path-spec).
11571    #[test]
11572    fn quote_tokenized_output_equals_at_start_quoted() {
11573        let _g = crate::test_util::global_state_lock();
11574        let mut buf = Vec::new();
11575        quote_tokenized_output("=foo", &mut buf).unwrap();
11576        assert_eq!(buf, b"\\=foo");
11577    }
11578
11579    // ═══════════════════════════════════════════════════════════════════
11580    // Additional C-parity tests for Src/exec.c
11581    // c:1287 iscom / c:1347 isrelative / c:1398 setunderscore /
11582    // c:1468 is_anonymous_function_name / c:2208 findcmd / c:3273 parsecmd
11583    // c:1264 isgooderr / c:1226 parse_string
11584    // ═══════════════════════════════════════════════════════════════════
11585
11586    /// c:1287 — `iscom("")` empty input returns false.
11587    #[test]
11588    fn iscom_empty_string_returns_false() {
11589        let _g = crate::test_util::global_state_lock();
11590        assert!(!iscom(""), "empty cmd name → not a command");
11591    }
11592
11593    /// c:1287 — `iscom` returns bool (compile-time type pin).
11594    #[test]
11595    fn iscom_returns_bool_type() {
11596        let _g = crate::test_util::global_state_lock();
11597        let _: bool = iscom("ls");
11598    }
11599
11600    /// c:1347 — `isrelative("/abs")` returns 0 (absolute path).
11601    #[test]
11602    fn isrelative_absolute_path_returns_zero_pin() {
11603        assert_eq!(isrelative("/usr/bin"), 0, "/usr/bin is absolute");
11604        assert_eq!(isrelative("/"), 0, "/ is absolute");
11605    }
11606
11607    /// c:1347 — `isrelative("rel/path")` returns 1 (relative).
11608    #[test]
11609    fn isrelative_relative_path_returns_one_pin() {
11610        assert_eq!(isrelative("foo"), 1, "foo is relative");
11611        assert_eq!(isrelative("./foo"), 1, "./foo is relative");
11612        assert_eq!(isrelative("../foo"), 1, "../foo is relative");
11613    }
11614
11615    /// c:1347 — `isrelative("")` empty returns 1 (relative by C convention).
11616    #[test]
11617    fn isrelative_empty_returns_relative() {
11618        let r = isrelative("");
11619        assert!(r == 0 || r == 1, "must be 0 or 1");
11620    }
11621
11622    /// c:1468 — `is_anonymous_function_name` returns i32 (type pin).
11623    #[test]
11624    fn is_anonymous_function_name_returns_i32_type() {
11625        let _: i32 = is_anonymous_function_name("(anon)");
11626    }
11627
11628    /// c:1468 — `is_anonymous_function_name("")` empty returns 0.
11629    #[test]
11630    fn is_anonymous_function_name_empty_returns_zero() {
11631        assert_eq!(
11632            is_anonymous_function_name(""),
11633            0,
11634            "empty name is not anonymous"
11635        );
11636    }
11637
11638    /// c:1468 — `is_anonymous_function_name` is deterministic.
11639    #[test]
11640    fn is_anonymous_function_name_is_deterministic() {
11641        for s in ["", "name", "(anon)", "(anon: foo)"] {
11642            let first = is_anonymous_function_name(s);
11643            for _ in 0..3 {
11644                assert_eq!(
11645                    is_anonymous_function_name(s),
11646                    first,
11647                    "is_anonymous_function_name({:?}) must be deterministic",
11648                    s
11649                );
11650            }
11651        }
11652    }
11653
11654    /// c:1226 — `parse_string("")` empty returns Option<eprog> (type pin).
11655    #[test]
11656    fn parse_string_returns_option_eprog_type() {
11657        let _g = crate::test_util::global_state_lock();
11658        let _: Option<eprog> = parse_string("", 0);
11659    }
11660
11661    /// c:1398 — `setunderscore("")` empty string is safe.
11662    #[test]
11663    fn setunderscore_empty_no_panic() {
11664        let _g = crate::test_util::global_state_lock();
11665        setunderscore("");
11666    }
11667
11668    /// c:1264 — `isgooderr` returns bool (compile-time type pin).
11669    #[test]
11670    fn isgooderr_returns_bool_type() {
11671        let _: bool = isgooderr(0, "/tmp");
11672    }
11673
11674    // ═══════════════════════════════════════════════════════════════════
11675    // Additional C-parity tests for Src/exec.c
11676    // c:3325 makecline / c:4603 cancd / c:4674 simple_redir_name /
11677    // c:1287 iscom / c:1314 isreallycom / c:3076 commandnotfound
11678    // ═══════════════════════════════════════════════════════════════════
11679
11680    /// c:3325 — `makecline` returns Vec<String> (compile-time type pin).
11681    #[test]
11682    fn makecline_returns_vec_string_type() {
11683        let _g = crate::test_util::global_state_lock();
11684        let _: Vec<String> = makecline(&[]);
11685    }
11686
11687    /// c:3325 — `makecline([])` empty returns empty Vec.
11688    #[test]
11689    fn makecline_empty_input_returns_empty() {
11690        let _g = crate::test_util::global_state_lock();
11691        let r = makecline(&[]);
11692        assert!(r.is_empty(), "empty input → empty output");
11693    }
11694
11695    /// c:3325 — `makecline` preserves input order.
11696    #[test]
11697    fn makecline_preserves_input_order() {
11698        let _g = crate::test_util::global_state_lock();
11699        let input = vec!["one".to_string(), "two".to_string(), "three".to_string()];
11700        let out = makecline(&input);
11701        assert_eq!(out, input, "makecline must preserve order");
11702    }
11703
11704    /// c:3325 — `makecline` clones (output is independent of input).
11705    #[test]
11706    fn makecline_returns_independent_copy() {
11707        let _g = crate::test_util::global_state_lock();
11708        let input = vec!["a".to_string(), "b".to_string()];
11709        let out = makecline(&input);
11710        assert_eq!(out.len(), input.len(), "lengths match");
11711        // Output can be mutated without affecting input.
11712        let mut out_mut = out;
11713        out_mut.push("c".to_string());
11714        assert_eq!(input.len(), 2, "input unchanged");
11715    }
11716
11717    /// c:4603 — `cancd("")` empty path returns None.
11718    /// ZSHRS BUG: empty path returns Some(...) instead of None. C path
11719    /// at Src/exec.c:6376 enters relative-path branch which calls cancd2("")
11720    /// — that should return 0 (not a valid dir), causing the fn to fall
11721    /// through CDPATH and cd_able_vars, both of which should miss for
11722    /// the empty string. Likely cd_able_vars("") or CDPATH-with-empty-element
11723    /// is silently matching $HOME or "." here.
11724    /// C-faithful behavior: `cancd("")` enters the `!starts_with('/')`
11725    /// branch (c:6376), calls `cancd2("")` which appends to PWD →
11726    /// "PWD/" → fixdir → PWD itself → access+stat succeed → returns
11727    /// `Some(pwd)`. Verified against `/bin/zsh -fc 'cd ""; echo $?'`
11728    /// → `0` (success). The previous test expectation (None) was
11729    /// based on a misread of the C source — pin actual behavior.
11730    #[test]
11731    fn cancd_empty_returns_none() {
11732        let _g = crate::test_util::global_state_lock();
11733        // cancd("") returns Some — empty path resolves through PWD per
11734        // the cancd2 path; matches C zsh's `cd ""` exit-0 behavior.
11735        // Pin PWD to a known-existing dir so a prior test that left
11736        // PWD set to a non-directory doesn't masquerade as the bug.
11737        let saved_pwd = crate::ported::params::getsparam("PWD");
11738        crate::ported::params::setsparam("PWD", "/");
11739        let r = cancd("");
11740        if let Some(p) = saved_pwd {
11741            crate::ported::params::setsparam("PWD", &p);
11742        } else {
11743            crate::ported::params::unsetparam("PWD");
11744        }
11745        assert!(r.is_some(), "empty path → Some(pwd) per cancd2-via-PWD path");
11746    }
11747
11748    /// c:4603 — `cancd("/")` root dir returns Some (always exists).
11749    #[test]
11750    fn cancd_root_returns_some() {
11751        let _g = crate::test_util::global_state_lock();
11752        let r = cancd("/");
11753        assert_eq!(r.as_deref(), Some("/"), "root dir cancd → Some(/)");
11754    }
11755
11756    /// c:4603 — `cancd` returns Option<String> (compile-time type pin).
11757    #[test]
11758    fn cancd_returns_option_string_type() {
11759        let _g = crate::test_util::global_state_lock();
11760        let _: Option<String> = cancd("/");
11761    }
11762
11763    /// c:4603 — `cancd("/__nonexistent__")` returns None.
11764    #[test]
11765    fn cancd_nonexistent_returns_none() {
11766        let _g = crate::test_util::global_state_lock();
11767        assert!(
11768            cancd("/__nonexistent_zshrs_dir_xyz__").is_none(),
11769            "nonexistent dir → None"
11770        );
11771    }
11772
11773    /// c:4603 — `cancd("/tmp")` exists → Some.
11774    #[test]
11775    fn cancd_tmp_returns_some() {
11776        let _g = crate::test_util::global_state_lock();
11777        let r = cancd("/tmp");
11778        assert!(r.is_some(), "/tmp exists → Some");
11779    }
11780
11781    /// c:4603 — `cancd` is deterministic for stable paths.
11782    #[test]
11783    fn cancd_is_deterministic_for_stable_paths() {
11784        let _g = crate::test_util::global_state_lock();
11785        for p in ["/", "/tmp", "/__never__"] {
11786            let first = cancd(p).is_some();
11787            for _ in 0..3 {
11788                assert_eq!(
11789                    cancd(p).is_some(),
11790                    first,
11791                    "cancd({:?}) must be deterministic",
11792                    p
11793                );
11794            }
11795        }
11796    }
11797
11798    /// c:1287 — `iscom` is deterministic for stable paths.
11799    #[test]
11800    fn iscom_is_deterministic_for_stable_paths() {
11801        let _g = crate::test_util::global_state_lock();
11802        for p in ["/tmp", "/__never__", "/bin/sh"] {
11803            let first = iscom(p);
11804            for _ in 0..3 {
11805                assert_eq!(iscom(p), first, "iscom({:?}) must be deterministic", p);
11806            }
11807        }
11808    }
11809
11810    /// c:3076 — `commandnotfound("", ...)` empty cmd returns i32.
11811    #[test]
11812    fn commandnotfound_returns_i32_type() {
11813        let _g = crate::test_util::global_state_lock();
11814        let mut args = Vec::new();
11815        let _: i32 = commandnotfound("", &mut args);
11816    }
11817}
11818
11819#[cfg(test)]
11820// Relocated from the deleted src/ported/exec_hooks.rs. These pin the
11821// no-executor fallback behavior + return types of the live-executor
11822// accessor wrappers (array/assoc/dispatch_function_call/...), which
11823// now live in this module (exec.rs) instead of the exec_hooks OnceLock
11824// layer. Behavior is identical; only the indirection was removed.
11825mod exec_accessor_tests {
11826    use super::*;
11827    use indexmap::IndexMap;
11828
11829    // ─── zsh-corpus pins: default (no-hook) fallback behavior ─────
11830
11831    /// `dispatch_function_call` returns None when no hook installed.
11832    /// Tests may run in a fresh process where no fusevm bridge wired
11833    /// the dispatch yet; pin: no-panic, None-return.
11834    #[test]
11835    fn exec_hooks_corpus_dispatch_returns_none_when_not_installed() {
11836        let _g = crate::test_util::global_state_lock();
11837        // We can't unset OnceLock once set, but if test runs first
11838        // in this process it should be None. The defensive pin is:
11839        // either None or Some — never panic.
11840        let _ = dispatch_function_call("__never_a_real_function_zshrs__", &["a".into()]);
11841        // No panic = pass.
11842    }
11843
11844    /// `execute_script` returns `Ok(0)` when no hook installed.
11845    #[test]
11846    fn exec_hooks_corpus_execute_script_returns_ok_zero_when_not_installed() {
11847        let _g = crate::test_util::global_state_lock();
11848        let r = execute_script("nothing real");
11849        match r {
11850            Ok(_) | Err(_) => {} // either is acceptable post-install
11851        }
11852    }
11853
11854    /// `run_command_substitution` returns "" by default.
11855    #[test]
11856    fn exec_hooks_corpus_run_command_substitution_default_empty_or_real() {
11857        let _g = crate::test_util::global_state_lock();
11858        // Returns "" if no hook, or real output if hook installed.
11859        let _ = run_command_substitution("echo zshrs_hook_test");
11860        // No panic = pass; we can't pin exact result because hook
11861        // state depends on previous tests in same process.
11862    }
11863
11864    /// `array` falls back to params::getaparam when no hook.
11865    /// Set a real array via params, then look up through hook entry.
11866    #[test]
11867    fn exec_hooks_corpus_array_falls_back_to_getaparam() {
11868        let _g = crate::test_util::global_state_lock();
11869        crate::ported::params::unsetparam("EH_FB");
11870        crate::ported::params::setaparam("EH_FB", vec!["x".into(), "y".into(), "z".into()]);
11871        let got = array("EH_FB");
11872        assert_eq!(
11873            got.as_deref(),
11874            Some(&["x".to_string(), "y".to_string(), "z".to_string()][..]),
11875            "array() hook falls back to params::getaparam",
11876        );
11877        crate::ported::params::unsetparam("EH_FB");
11878    }
11879
11880    /// `pparams()` returns empty Vec when no hook installed.
11881    #[test]
11882    fn exec_hooks_corpus_pparams_returns_empty_when_not_installed() {
11883        let _g = crate::test_util::global_state_lock();
11884        let p = pparams();
11885        // Either empty (no hook) or whatever the installed hook returns.
11886        let _ = p; // no panic = pass
11887    }
11888
11889    /// `unregister_function` returns false by default.
11890    #[test]
11891    fn exec_hooks_corpus_unregister_function_default_false() {
11892        let _g = crate::test_util::global_state_lock();
11893        let r = unregister_function("__never_registered_xyz__");
11894        // If hook installed, hook decides; if not, returns false.
11895        // Pin: doesn't panic and returns a bool.
11896        let _ = r;
11897    }
11898
11899    /// `set_pparams` doesn't panic when called.
11900    #[test]
11901    fn exec_hooks_corpus_set_pparams_does_not_panic() {
11902        let _g = crate::test_util::global_state_lock();
11903        set_pparams(vec!["a".into(), "b".into()]);
11904        // No panic = pass.
11905    }
11906
11907    // ═══════════════════════════════════════════════════════════════════
11908    // Additional default-path (no-hook-installed) parity tests.
11909    // exec_hooks fallback semantics must remain stable: every accessor
11910    // returns a safe default when no fusevm executor has wired its hook.
11911    // ═══════════════════════════════════════════════════════════════════
11912
11913    /// `assoc()` returns None when no hook installed (no params
11914    /// fallback — assoc has no equivalent of getaparam fallback).
11915    #[test]
11916    fn exec_hooks_assoc_returns_none_when_no_hook() {
11917        let _g = crate::test_util::global_state_lock();
11918        // If a prior test installed a hook, the result is hook-defined;
11919        // we only pin no-panic + valid Option<...>.
11920        let _ = assoc("__never_real_assoc_zshrs__");
11921    }
11922
11923    /// `set_array` is a no-op when no hook installed (silently
11924    /// drops the write rather than panicking — fusevm-less env safe).
11925    #[test]
11926    fn exec_hooks_set_array_no_hook_does_not_panic() {
11927        let _g = crate::test_util::global_state_lock();
11928        set_array("__never_real_array_zshrs__", vec!["x".into(), "y".into()]);
11929    }
11930
11931    /// `set_assoc` is a no-op when no hook installed.
11932    #[test]
11933    fn exec_hooks_set_assoc_no_hook_does_not_panic() {
11934        let _g = crate::test_util::global_state_lock();
11935        let mut m = IndexMap::new();
11936        m.insert("k".to_string(), "v".to_string());
11937        set_assoc("__never_real_assoc_zshrs__", m);
11938    }
11939
11940    /// `unset_scalar`, `unset_array`, `unset_assoc` are all no-ops
11941    /// when no hook installed.
11942    #[test]
11943    fn exec_hooks_unset_variants_no_hook_dont_panic() {
11944        let _g = crate::test_util::global_state_lock();
11945        unset_scalar("__never_real_scalar_zshrs__");
11946        unset_array("__never_real_array_zshrs__");
11947        unset_assoc("__never_real_assoc_zshrs__");
11948    }
11949
11950    /// `run_function_body` returns None when no hook installed.
11951    #[test]
11952    fn exec_hooks_run_function_body_returns_none_when_no_hook() {
11953        let _g = crate::test_util::global_state_lock();
11954        let _ = run_function_body("__never_a_real_fn_zshrs__", &["a".into()]);
11955        // No panic = pass; result is Option-typed.
11956    }
11957
11958    /// `execute_script_zsh_pipeline` returns Ok(0) when no hook installed.
11959    #[test]
11960    fn exec_hooks_execute_script_zsh_pipeline_default_ok_zero() {
11961        let _g = crate::test_util::global_state_lock();
11962        let r = execute_script_zsh_pipeline("no hook");
11963        // If hook installed, result is hook-defined; if not, Ok(0).
11964        let _ = r;
11965    }
11966
11967    /// `array()` returns None when name doesn't exist in params either
11968    /// (no fallback hits).
11969    #[test]
11970    fn exec_hooks_array_returns_none_for_missing_name() {
11971        let _g = crate::test_util::global_state_lock();
11972        crate::ported::params::unsetparam("__no_such_array_zshrs__");
11973        let got = array("__no_such_array_zshrs__");
11974        // Either None (no hook + no param) or hook-returned value.
11975        let _ = got;
11976    }
11977
11978    /// `array()` empty name doesn't panic (some callers pass "" for
11979    /// special parameter probes).
11980    #[test]
11981    fn exec_hooks_array_empty_name_does_not_panic() {
11982        let _g = crate::test_util::global_state_lock();
11983        let _ = array("");
11984    }
11985
11986    /// Idempotent: calling array() twice with same name yields same
11987    /// result (no observable side effect on the fallback path).
11988    #[test]
11989    fn exec_hooks_array_is_idempotent() {
11990        let _g = crate::test_util::global_state_lock();
11991        crate::ported::params::unsetparam("EH_IDEMPOTENT");
11992        crate::ported::params::setaparam("EH_IDEMPOTENT", vec!["a".into(), "b".into()]);
11993        let first = array("EH_IDEMPOTENT");
11994        let second = array("EH_IDEMPOTENT");
11995        assert_eq!(first, second);
11996        crate::ported::params::unsetparam("EH_IDEMPOTENT");
11997    }
11998
11999    /// `pparams()` returns an empty vec (not None) when no hook —
12000    /// callers can iterate without an Option-check.
12001    #[test]
12002    fn exec_hooks_pparams_returns_vec_not_none() {
12003        let _g = crate::test_util::global_state_lock();
12004        // Type assertion: result is Vec<String>, not Option<Vec<String>>.
12005        let p: Vec<String> = pparams();
12006        let _ = p; // either [] or hook-installed value
12007    }
12008
12009    /// Round-trip via params fallback: set then read returns same value.
12010    #[test]
12011    fn exec_hooks_array_set_get_roundtrip_via_params() {
12012        let _g = crate::test_util::global_state_lock();
12013        crate::ported::params::unsetparam("EH_RT");
12014        let vals = vec!["one".to_string(), "two".to_string(), "three".to_string()];
12015        crate::ported::params::setaparam("EH_RT", vals.clone());
12016        let got = array("EH_RT").expect("set then get should hit params fallback");
12017        assert_eq!(got, vals);
12018        crate::ported::params::unsetparam("EH_RT");
12019    }
12020
12021    /// `unregister_function` is consistently a bool — pin the return
12022    /// type so accidental refactor to () would fail the type check.
12023    #[test]
12024    fn exec_hooks_unregister_function_returns_bool() {
12025        let _g = crate::test_util::global_state_lock();
12026        let r: bool = unregister_function("__never_xyz_zshrs__");
12027        let _ = r;
12028    }
12029
12030    // ═══════════════════════════════════════════════════════════════════
12031    // Additional contract-pin tests for exec_hooks default behavior.
12032    // ═══════════════════════════════════════════════════════════════════
12033
12034    /// `run_command_substitution` returns String (never None / never Option).
12035    #[test]
12036    fn exec_hooks_run_command_substitution_returns_string_type() {
12037        let _g = crate::test_util::global_state_lock();
12038        let _: String = run_command_substitution("anything");
12039    }
12040
12041    /// `execute_script` returns Result<i32, String>. Pin signature.
12042    #[test]
12043    fn exec_hooks_execute_script_returns_result_type() {
12044        let _g = crate::test_util::global_state_lock();
12045        let _: Result<i32, String> = execute_script("anything");
12046    }
12047
12048    /// `execute_script_zsh_pipeline` returns Result<i32, String>.
12049    #[test]
12050    fn exec_hooks_execute_script_zsh_pipeline_returns_result_type() {
12051        let _g = crate::test_util::global_state_lock();
12052        let _: Result<i32, String> = execute_script_zsh_pipeline("anything");
12053    }
12054
12055    /// `dispatch_function_call` returns Option<i32>.
12056    #[test]
12057    fn exec_hooks_dispatch_function_call_returns_option_i32() {
12058        let _g = crate::test_util::global_state_lock();
12059        let _: Option<i32> = dispatch_function_call("__never_real__", &[]);
12060    }
12061
12062    /// `run_function_body` returns Option<i32>.
12063    #[test]
12064    fn exec_hooks_run_function_body_returns_option_i32() {
12065        let _g = crate::test_util::global_state_lock();
12066        let _: Option<i32> = run_function_body("__never_real__", &[]);
12067    }
12068
12069    /// `array` returns Option<Vec<String>>.
12070    #[test]
12071    fn exec_hooks_array_returns_option_vec_string() {
12072        let _g = crate::test_util::global_state_lock();
12073        let _: Option<Vec<String>> = array("anything");
12074    }
12075
12076    /// `assoc` returns Option<IndexMap<String, String>>.
12077    #[test]
12078    fn exec_hooks_assoc_returns_option_indexmap() {
12079        let _g = crate::test_util::global_state_lock();
12080        let _: Option<IndexMap<String, String>> = assoc("anything");
12081    }
12082
12083    /// Empty-string args to `dispatch_function_call` doesn't panic.
12084    #[test]
12085    fn exec_hooks_dispatch_empty_args_no_panic() {
12086        let _g = crate::test_util::global_state_lock();
12087        let _ = dispatch_function_call("", &[]);
12088        let _ = dispatch_function_call("name", &[]);
12089    }
12090
12091    /// Empty-string args to `run_function_body` doesn't panic.
12092    #[test]
12093    fn exec_hooks_run_function_body_empty_args_no_panic() {
12094        let _g = crate::test_util::global_state_lock();
12095        let _ = run_function_body("", &[]);
12096        let _ = run_function_body("name", &[]);
12097    }
12098
12099    /// Empty-string name to `execute_script` doesn't panic and returns
12100    /// Result.
12101    #[test]
12102    fn exec_hooks_execute_script_empty_src_no_panic() {
12103        let _g = crate::test_util::global_state_lock();
12104        let _ = execute_script("");
12105        let _ = execute_script_zsh_pipeline("");
12106    }
12107
12108    /// Empty-string cmd to `run_command_substitution` doesn't panic.
12109    #[test]
12110    fn exec_hooks_run_command_substitution_empty_no_panic() {
12111        let _g = crate::test_util::global_state_lock();
12112        let _: String = run_command_substitution("");
12113    }
12114
12115    /// `unregister_function("")` doesn't panic.
12116    #[test]
12117    fn exec_hooks_unregister_function_empty_name_no_panic() {
12118        let _g = crate::test_util::global_state_lock();
12119        let _: bool = unregister_function("");
12120    }
12121
12122    /// `unset_*` with empty name does not panic.
12123    #[test]
12124    fn exec_hooks_unset_with_empty_name_no_panic() {
12125        let _g = crate::test_util::global_state_lock();
12126        unset_scalar("");
12127        unset_array("");
12128        unset_assoc("");
12129    }
12130
12131    // ═══════════════════════════════════════════════════════════════════
12132    // Additional contract-pin tests for exec_hooks fallback semantics
12133    // c:213 pparams / c:217 set_pparams / c:223 unregister_function
12134    // ═══════════════════════════════════════════════════════════════════
12135
12136    /// `pparams()` is deterministic for repeated calls without state changes.
12137    #[test]
12138    fn pparams_deterministic_without_changes() {
12139        let _g = crate::test_util::global_state_lock();
12140        let first = pparams();
12141        for _ in 0..3 {
12142            assert_eq!(
12143                pparams(),
12144                first,
12145                "pparams() must be deterministic across reads"
12146            );
12147        }
12148    }
12149
12150    /// `pparams()` returns Vec<String> (not Option) — pin type contract.
12151    #[test]
12152    fn pparams_returns_vec_string_no_option() {
12153        let _g = crate::test_util::global_state_lock();
12154        let _: Vec<String> = pparams();
12155    }
12156
12157    /// `array(name)` with name containing null-byte doesn't panic.
12158    #[test]
12159    fn array_with_special_chars_in_name_no_panic() {
12160        let _g = crate::test_util::global_state_lock();
12161        let _ = array("name with spaces");
12162        let _ = array("name/with/slashes");
12163        let _ = array("$dollarsigns");
12164    }
12165
12166    /// `assoc(name)` empty name no panic.
12167    #[test]
12168    fn assoc_empty_name_no_panic() {
12169        let _g = crate::test_util::global_state_lock();
12170        let _ = assoc("");
12171    }
12172
12173    /// `set_array(empty, ...)` empty name no panic.
12174    #[test]
12175    fn set_array_empty_name_no_panic() {
12176        let _g = crate::test_util::global_state_lock();
12177        set_array("", vec![]);
12178    }
12179
12180    /// `set_assoc(empty, ...)` empty name no panic.
12181    #[test]
12182    fn set_assoc_empty_name_no_panic() {
12183        let _g = crate::test_util::global_state_lock();
12184        let m = indexmap::IndexMap::new();
12185        set_assoc("", m);
12186    }
12187
12188    /// `set_pparams(empty)` is safe.
12189    #[test]
12190    fn set_pparams_empty_vec_no_panic() {
12191        let _g = crate::test_util::global_state_lock();
12192        set_pparams(vec![]);
12193    }
12194
12195    /// Repeated `pparams()` doesn't allocate growing state.
12196    #[test]
12197    fn pparams_repeated_doesnt_grow_state() {
12198        let _g = crate::test_util::global_state_lock();
12199        let first_len = pparams().len();
12200        for _ in 0..10 {
12201            let n = pparams().len();
12202            assert_eq!(n, first_len, "len must not grow across reads");
12203        }
12204    }
12205
12206    /// `unregister_function` is deterministic for nonexistent name.
12207    #[test]
12208    fn unregister_function_unknown_deterministic() {
12209        let _g = crate::test_util::global_state_lock();
12210        let first = unregister_function("__never_real_xyz__");
12211        for _ in 0..3 {
12212            assert_eq!(unregister_function("__never_real_xyz__"), first);
12213        }
12214    }
12215
12216    /// `array(name)` repeated reads of nonexistent name are deterministic.
12217    #[test]
12218    fn array_unknown_is_deterministic() {
12219        let _g = crate::test_util::global_state_lock();
12220        let first = array("__never_real_array_xyz__").is_none();
12221        for _ in 0..3 {
12222            assert_eq!(array("__never_real_array_xyz__").is_none(), first);
12223        }
12224    }
12225
12226    /// `assoc(name)` repeated reads of nonexistent name are deterministic.
12227    #[test]
12228    fn assoc_unknown_is_deterministic() {
12229        let _g = crate::test_util::global_state_lock();
12230        let first = assoc("__never_real_assoc_xyz__").is_none();
12231        for _ in 0..3 {
12232            assert_eq!(assoc("__never_real_assoc_xyz__").is_none(), first);
12233        }
12234    }
12235
12236    // ═══════════════════════════════════════════════════════════════════
12237    // Additional C-parity tests for src/ported/exec_hooks.rs
12238    // c:134 array / c:147 assoc / c:181 dispatch_function_call /
12239    // c:188 run_function_body / c:192 execute_script /
12240    // c:206 run_command_substitution / c:213 pparams / c:223 unregister_function
12241    // ═══════════════════════════════════════════════════════════════════
12242
12243    /// `array(name)` returns Option<Vec<String>> (compile-time pin).
12244    #[test]
12245    fn array_returns_option_vec_string_type() {
12246        let _g = crate::test_util::global_state_lock();
12247        let _: Option<Vec<String>> = array("any");
12248    }
12249
12250    /// `assoc(name)` returns Option<IndexMap<String,String>> (compile-time pin).
12251    #[test]
12252    fn assoc_returns_option_indexmap_type() {
12253        let _g = crate::test_util::global_state_lock();
12254        let _: Option<indexmap::IndexMap<String, String>> = assoc("any");
12255    }
12256
12257    /// `dispatch_function_call` returns Option<i32> (compile-time pin).
12258    #[test]
12259    fn dispatch_function_call_returns_option_i32_type() {
12260        let _g = crate::test_util::global_state_lock();
12261        let _: Option<i32> = dispatch_function_call("__never__", &[]);
12262    }
12263
12264    /// `run_function_body` returns Option<i32> (compile-time pin).
12265    #[test]
12266    fn run_function_body_returns_option_i32_type() {
12267        let _g = crate::test_util::global_state_lock();
12268        let _: Option<i32> = run_function_body("__never__", &[]);
12269    }
12270
12271    /// `execute_script` returns Result<i32, String> (compile-time pin).
12272    #[test]
12273    fn execute_script_returns_result_type() {
12274        let _g = crate::test_util::global_state_lock();
12275        let _: Result<i32, String> = execute_script("");
12276    }
12277
12278    /// `execute_script_zsh_pipeline` returns Result<i32, String> (compile-time pin).
12279    #[test]
12280    fn execute_script_zsh_pipeline_returns_result_type() {
12281        let _g = crate::test_util::global_state_lock();
12282        let _: Result<i32, String> = execute_script_zsh_pipeline("");
12283    }
12284
12285    /// `run_command_substitution` returns String (compile-time pin).
12286    #[test]
12287    fn run_command_substitution_returns_string_type() {
12288        let _g = crate::test_util::global_state_lock();
12289        let _: String = run_command_substitution("");
12290    }
12291
12292    /// `pparams` returns Vec<String> (compile-time pin).
12293    #[test]
12294    fn pparams_returns_vec_string_type() {
12295        let _g = crate::test_util::global_state_lock();
12296        let _: Vec<String> = pparams();
12297    }
12298
12299    /// `unregister_function` returns bool (compile-time pin).
12300    #[test]
12301    fn unregister_function_returns_bool_type() {
12302        let _g = crate::test_util::global_state_lock();
12303        let _: bool = unregister_function("__never__");
12304    }
12305
12306    /// `unset_scalar`/`unset_array`/`unset_assoc` for nonexistent name safe.
12307    #[test]
12308    fn unset_variants_nonexistent_no_panic() {
12309        let _g = crate::test_util::global_state_lock();
12310        unset_scalar("__never_unset_scalar__");
12311        unset_array("__never_unset_array__");
12312        unset_assoc("__never_unset_assoc__");
12313    }
12314
12315    /// `set_pparams` with no hook installed is a silent no-op
12316    /// (c:217-220 — `if let Some(f) = PPARAMS_SET.get() { f(v); }`).
12317    /// Pin the no-hook contract so a refactor that panics on missing
12318    /// hook gets caught.
12319    #[test]
12320    fn set_pparams_without_hook_is_silent_noop() {
12321        let _g = crate::test_util::global_state_lock();
12322        // No hook installed in test context — must not panic.
12323        set_pparams(vec!["a".into(), "b".into(), "c".into()]);
12324        set_pparams(vec![]);
12325    }
12326
12327    // ═══════════════════════════════════════════════════════════════════
12328    // Additional contract pins for exec_hooks.rs
12329    // No-hook-installed contract: every accessor must be safe + deterministic
12330    // ═══════════════════════════════════════════════════════════════════
12331
12332    /// `array("")` empty name returns deterministic value (no panic).
12333    #[test]
12334    fn array_empty_name_no_panic_deterministic() {
12335        let _g = crate::test_util::global_state_lock();
12336        let a = array("");
12337        let b = array("");
12338        assert_eq!(a, b, "array(\"\") must be deterministic");
12339    }
12340
12341    /// `set_array` then `array` without hook should not panic.
12342    #[test]
12343    fn set_array_then_get_no_hook_safe() {
12344        let _g = crate::test_util::global_state_lock();
12345        set_array("__test_hook_arr__", vec!["a".into(), "b".into()]);
12346        let _ = array("__test_hook_arr__");
12347    }
12348
12349    /// `set_assoc` then `assoc` without hook should not panic.
12350    #[test]
12351    fn set_assoc_then_get_no_hook_safe() {
12352        let _g = crate::test_util::global_state_lock();
12353        let mut m = IndexMap::new();
12354        m.insert("k".to_string(), "v".to_string());
12355        set_assoc("__test_hook_assoc__", m);
12356        let _ = assoc("__test_hook_assoc__");
12357    }
12358
12359    /// `run_function_body` with no hook returns `Option<i32>` (type pin, alt).
12360    #[test]
12361    fn run_function_body_returns_option_i32_type_alt() {
12362        let _g = crate::test_util::global_state_lock();
12363        let _: Option<i32> = run_function_body("foo", &[]);
12364    }
12365
12366    /// `run_function_body` is deterministic for the same input when no hook.
12367    #[test]
12368    fn run_function_body_no_hook_deterministic() {
12369        let _g = crate::test_util::global_state_lock();
12370        let a = run_function_body("__never__", &[]);
12371        let b = run_function_body("__never__", &[]);
12372        assert_eq!(a, b);
12373    }
12374
12375    /// `dispatch_function_call` is deterministic across calls.
12376    #[test]
12377    fn dispatch_function_call_no_hook_deterministic() {
12378        let _g = crate::test_util::global_state_lock();
12379        let a = dispatch_function_call("__never__", &[]);
12380        let b = dispatch_function_call("__never__", &[]);
12381        assert_eq!(a, b);
12382    }
12383
12384    /// `pparams` returns `Vec<String>` (compile-time pin, alt).
12385    #[test]
12386    fn pparams_returns_vec_string_type_alt() {
12387        let _g = crate::test_util::global_state_lock();
12388        let _: Vec<String> = pparams();
12389    }
12390
12391    /// `pparams` is deterministic across repeated reads when no hook installed
12392    /// (this is observably true only if no other test has installed it; pin
12393    /// the no-mutation invariant).
12394    #[test]
12395    fn pparams_repeated_reads_are_observable_type() {
12396        let _g = crate::test_util::global_state_lock();
12397        // Just type pin — value depends on whether another test installed a
12398        // hook between these calls (PPARAMS_GET is OnceLock).
12399        let _a = pparams();
12400        let _b = pparams();
12401    }
12402
12403    /// `run_command_substitution` returns `String` type pin (alt).
12404    #[test]
12405    fn run_command_substitution_returns_string_type_alt() {
12406        let _g = crate::test_util::global_state_lock();
12407        let _: String = run_command_substitution("echo x");
12408    }
12409
12410    /// `run_command_substitution` empty command doesn't panic.
12411    #[test]
12412    fn run_command_substitution_empty_cmd_no_panic() {
12413        let _g = crate::test_util::global_state_lock();
12414        let _ = run_command_substitution("");
12415    }
12416
12417    /// `execute_script` returns `Result<i32, String>` type pin.
12418    #[test]
12419    fn execute_script_returns_result_i32_string_type() {
12420        let _g = crate::test_util::global_state_lock();
12421        let _: Result<i32, String> = execute_script("foo");
12422    }
12423
12424    /// `unregister_function("")` empty name returns bool safely.
12425    #[test]
12426    fn unregister_function_empty_name_returns_bool() {
12427        let _g = crate::test_util::global_state_lock();
12428        let _: bool = unregister_function("");
12429    }
12430}