Skip to main content

zsh/ported/
exec.rs

1//! Faithful Rust ports of free functions and file-static globals from
2//! `Src/exec.c`. The wordcode-VM dispatch tree (`execlist` / `execpline`
3//! / `execcmd` / `execsimple` etc.) that drives execution in C zsh is
4//! NOT replicated here — zshrs runs the fusevm bytecode VM instead
5//! (see `src/vm_helper.rs` + `src/fusevm_bridge.rs`).
6//!
7//! What lives here are the parts of `Src/exec.c` that ARE faithful
8//! ports and don't depend on the C-side wordcode walker:
9//!
10//! - **`trap_state` / `trap_return` / `forklevel`** — file-static
11//!   integer globals from `Src/exec.c:134 / :155 / :1052`, exposed as
12//!   atomics shared between this module, `Src/signals.c`'s port at
13//!   `src/ported/signals.rs`, and `Src/params.c`'s port at
14//!   `src/ported/params.rs`.
15//! - **`gethere`** (`Src/exec.c:4573`) — turn a here-document into a
16//!   here-string. Called from the lexer port (`src/ported/lex.rs`).
17//! - **`getoutput`** (`Src/exec.c:4712`) — command-substitution body
18//!   runner. Called from the parameter-expansion port
19//!   (`src/ported/subst.rs`).
20//! - **`loadautofn`** + **`getfpfunc`** (`Src/exec.c:5682` / `:6219`)
21//!   — `$fpath` walker + autoload file installer. Called from
22//!   `bin_autoload` / `bin_functions -c` in `src/ported/builtin.rs`.
23//! - **`resolvebuiltin`** (`Src/exec.c:2703`) — module-autoload guard
24//!   used by the dispatch walk in `execcmd_exec`.
25//! - **`execcmd_compile_head`** — fusevm-bytecode-time head resolver
26//!   mirroring the head section (`c:2904-3275`) of C's `execcmd_exec`.
27//!   NOT a faithful port; the canonical 7-arg `execcmd_exec` port lives
28//!   alongside it.
29//! - **`execcmd_exec`** (`Src/exec.c:2900`) — canonical 7-arg port of
30//!   the C function (locals + dispatch walk through builtin/shfunc/external
31//!   invocation). Used by future tree-walker callers; the fusevm
32//!   bytecode flow goes through `execcmd_compile_head` instead.
33
34use std::os::unix::fs::PermissionsExt;
35use std::sync::atomic::Ordering;
36
37// `with_executor` import removed — all ShellExecutor reach-in calls
38// routed through `crate::ported::exec::*` fn-ptrs installed by
39// fusevm_bridge at startup. See memory feedback_no_exec_script_from_ported.
40use crate::ported::builtin::{cd_able_vars, fixdir, BUILTINS, DOPRINTDIR, EXIT_VAL, LASTVAL};
41use crate::ported::builtins::rlimits::setlimits;
42use crate::ported::builtins::sched::zleactive;
43use crate::ported::compat::zgettime_monotonic_if_available;
44use crate::ported::config_h::DEFAULT_PATH;
45use crate::ported::context::{zcontext_restore, zcontext_save};
46use crate::ported::hashtable::{
47    cmdnam_unhashed, cmdnamtab_lock, dircache_set, hashdir, pathchecked, shfunctab_lock,
48};
49use crate::ported::hist::{strinbeg, strinend};
50use crate::ported::init::{shout, underscorelen, underscoreused, zunderscore, SHTTY};
51use crate::ported::input::{inpop, inpush};
52use crate::ported::jobs::{expandjobtab, get_usage, release_pgrp, waitforpid, JOBTAB, THISJOB};
53use crate::ported::lex::{
54    hgetc, parsestr, tok, untokenize, ztokens, LEXERR, LEX_LEXSTOP, LEX_LINENO,
55};
56use crate::ported::mem::{dupstring, dyncat, popheap, pushheap};
57use crate::ported::modules::clone::mypgrp;
58use crate::ported::options::{dosetopt, opt_state_set, sticky};
59use crate::ported::params::{
60    endparamscope, getsparam, locallevel, paramtab, setiparam, zgetenv, zputenv,
61};
62use crate::ported::parse::{closedumps, ecrawstr, parse_list};
63use crate::ported::prompt::{cmdpop, cmdpush};
64use crate::ported::signals::{
65    intrap, queue_signals, settrap, signal_mask, signal_unblock, sigtrapped, trapisfunc,
66    traplocallevel, unqueue_signals, unsettrap,
67};
68use crate::ported::signals_h::{
69    child_block, child_unblock, dont_queue_signals, signal_default, signal_ignore, winch_unblock,
70    SIGCOUNT,
71};
72use crate::ported::subst::{quotesubst, singsub};
73use crate::ported::utils::{
74    errflag, fdtable_get, fdtable_set, gettempfile, gettempname, inc_locallevel, movefd, pathprog,
75    printprompt4, quotedzputs, redup, unmeta, unmetafy, write_loop, zclose, zerr, zwarn,
76    ERRFLAG_ERROR, MAX_ZSH_FD,
77};
78use crate::ported::r#loop::{execcase, execfor, execif, execrepeat, execselect, exectry, execwhile};
79use crate::ported::zsh_h::{
80    builtin, cmdnam, emulation_options, eprog, execstack, funcwrap, hashnode, isset, jobfile,
81    multio, redir, shfunc, unset, wc_code, Emulation_options, Inang, Inpar, Meta, Nularg, Outpar,
82    Pound, BINF_BUILTIN, BINF_CLEARENV, BINF_COMMAND, BINF_DASH, BINF_EXEC, BINF_PREFIX, CHASEDOTS,
83    CHASELINKS, CLOBBER, CLOBBEREMPTY, CS_CMDSUBST, ERRFLAG_INT, FDT_EXTERNAL, FDT_INTERNAL,
84    FDT_PROC_SUBST, FDT_SAVED_MASK, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHDIRS, INP_LINENO,
85    INTERACTIVE, IS_CLOBBER_REDIR, IS_DASH, JOBTEXTSIZE, MAX_PIPESTATS, MONITOR, MULTIOS,
86    MULTIOUNIT, PATHDIRS, PM_LOADDIR, PM_READONLY, PM_UNDEFINED, POSIXBUILTINS, POSIXJOBS,
87    POSIXTRAPS, REDIRF_FROM_HEREDOC, REDIR_CLOSE, REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE,
88    REDIR_OUTPIPE, USEZLE, VERBOSE, WC_LIST, WC_LIST_TYPE, WC_PIPE, WC_PIPE_END, WC_PIPE_TYPE,
89    WC_REDIR, WC_REDIR_TYPE, WC_REDIR_VARID, WC_SIMPLE, WC_SIMPLE_ARGC, WC_SUBLIST, WC_SUBLIST_END,
90    WC_SUBLIST_FLAGS, WC_SUBLIST_TYPE, WC_TYPESET, ZSIG_FUNC, ZSIG_IGNORED, Z_END,
91};
92use crate::ported::zsh_system_h::timespec as ZshTimespec;
93use crate::ported::ztype_h::{inull, itok};
94use crate::zsh_h::XTRACE;
95
96/// Port of the anonymous `enum { ... }` from `Src/exec.c:35-40`.
97/// Flag bits passed as the `addflags` argument to `addvars` /
98/// `addvarsfromargs`:
99///   - `ADDVAR_EXPORT`  (1<<0) — export each assignment for the
100///                                command `VAR=val cmd ...` form.
101///   - `ADDVAR_RESTORE` (1<<2) — the variable list is being restored
102///                                later (implicit local scope), so
103///                                suppress `ASSPM_WARN`.
104pub const ADDVAR_EXPORT: i32 = 1 << 0; // c:37 (Src/exec.c)
105/// `ADDVAR_RESTRICT` constant — prefix assignments of `VAR=val cmd` are
106/// checked against PM_RESTRICTED parameters under RESTRICTED.
107pub const ADDVAR_RESTRICT: i32 = 1 << 1; // c:38 (Src/exec.c)
108/// `ADDVAR_RESTORE` constant.
109pub const ADDVAR_RESTORE: i32 = 1 << 2; // c:39 (Src/exec.c)
110
111/// Port of `int trap_state;` from `Src/exec.c:134`. Tracks whether
112/// a trap handler is currently being processed and, paired with
113/// `TRAP_RETURN` below, whether a `return` inside the trap should
114/// promote to `TRAP_STATE_FORCE_RETURN` to unwind the trap caller.
115///
116/// Values: `TRAP_STATE_INACTIVE = 0`, `TRAP_STATE_PRIMED = 1`,
117/// `TRAP_STATE_FORCE_RETURN = 2` (see `Src/zsh.h`).
118pub static TRAP_STATE: std::sync::atomic::AtomicI32 = // c:134 (Src/exec.c)
119    std::sync::atomic::AtomicI32::new(0);
120
121/// Port of `int trap_return;` from `Src/exec.c:155`. Carries the
122/// pending exit status from inside a trap; sentinel `-2` means
123/// "running an EXIT/DEBUG-style trap at the current level"
124/// (signals.c:1166). Promoted to the user's `return N` value by
125/// `bin_return` when POSIX-trap semantics apply (builtin.c:5852).
126pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = // c:155 (Src/exec.c)
127    std::sync::atomic::AtomicI32::new(0);
128
129/// Port of `int forklevel;` from `Src/exec.c:1053`. Records the
130/// `locallevel` at the most recent fork point (set at c:1221:
131/// `forklevel = locallevel;` inside `entersubsh()`). Used by:
132///   - `signals.c:808` SIGPIPE handler — `!forklevel` distinguishes
133///     the top-level shell from a forked subshell.
134///   - `exec.c:6146` — `if (locallevel > forklevel)` decides whether
135///     a function-defined trap should fire on this subshell exit.
136///   - `params.c:3724` — WARNCREATEGLOBAL nest-depth check.
137///
138/// Initialised to 0 (no fork has occurred yet). Set to `locallevel`
139/// at every `entersubsh()` entry per c:1221.
140pub static FORKLEVEL: std::sync::atomic::AtomicI32 = // c:1052 (Src/exec.c)
141    std::sync::atomic::AtomicI32::new(0);
142
143// =============================================================================
144// File-static globals from Src/exec.c. Bucket choices per PORT_PLAN.md:
145//   - Per-evaluator transient state → thread_local Cell (bucket 1)
146//   - Shell-wide shared state       → AtomicI32 / Mutex (bucket 2)
147// All names match C exactly. Surrounding doc-comments cite the C
148// declaration line.
149// =============================================================================
150
151/// Port of `int noerrexit;` from `Src/exec.c:72`. Bit-flags that
152/// suppress ERREXIT triggering on the next command(s). Bits:
153/// `NOERREXIT_EXIT` (in `if`/`while`/`until` test contexts),
154/// `NOERREXIT_RETURN` (after `return`), `NOERREXIT_UNTIL_EXEC`
155/// (until next exec'd command). Bucket-1 — per-evaluator (each
156/// recursive eval has its own suppression frame).
157pub static noerrexit: std::sync::atomic::AtomicI32 = // c:72 (Src/exec.c)
158    std::sync::atomic::AtomicI32::new(0);
159
160/// Port of `int this_noerrexit;` from `Src/exec.c:109`. When set,
161/// suppress ERREXIT for THIS one command only (consumed + cleared
162/// before the next command starts). Set by `execcursh` and the
163/// `((expr))` arith path so a 0-result doesn't trigger errexit.
164pub static this_noerrexit: std::sync::atomic::AtomicI32 = // c:109 (Src/exec.c)
165    std::sync::atomic::AtomicI32::new(0);
166
167/// Port of `mod_export int noerrs;` from `Src/exec.c:117`. When
168/// non-zero, suppress `zerr()` output (lex error reporting during
169/// `parse_string`, `parseopts` etc.). Saved/restored by
170/// `execsave`/`execrestore`.
171/// Port of `static char list_pipe_text[JOBTEXTSIZE]` from
172/// `Src/exec.c:463`. Holds the textual rendering of the in-flight
173/// pipe list; saved across nested execlist invocations at
174/// exec.c:1372-1380 (zeroed on entry, restored from
175/// `old_list_pipe_text` at c:1634-1638) and round-tripped through
176/// execsave/execrestore (c:6448 / c:6484). zshrs models it as a
177/// length-bounded String guarded by a Mutex — the C `char[80]` cap
178/// is a buffer-overflow guard, but matching length matters for the
179/// `jobs` builtin's pipe-list rendering.
180pub static LIST_PIPE_TEXT: std::sync::Mutex<String> = std::sync::Mutex::new(String::new()); // c:463 (Src/exec.c)
181
182// c:117 (Src/exec.c) — `mod_export int noerrs;`
183//
184// C has exactly ONE `noerrs`. This port had TWO disjoint storages: an
185// `AtomicI32` here, and `utils::noerrs_lock()`'s `Mutex<i32>` (flagged in
186// utils.rs as "NOT IN UTILS.C"). The four diagnostic emitters — `zerr`,
187// `zwarn`, `zerrnam`, `zwarnnam` (utils.rs:221/244/262/279) — read the utils
188// one, while `docomplete`'s suppression window (`noerrs = 1` around
189// `doexpansion`, zle_tricky.c:825-828) wrote this one. So the suppression
190// never reached the printer and completion-time diagnostics leaked onto the
191// screen: `ls *(` printed `zsh: bad pattern: *(` where zsh prints nothing,
192// consuming a row and shifting the whole match grid.
193//
194// Unified onto the storage every emitter already consults; the accessor lives
195// at `crate::ported::utils::noerrs_lock()`. Do not reintroduce a second one.
196// The storage is `crate::ported::utils::noerrs_lock()`; read/write it directly.
197
198/// Port of `int nohistsave;` from `Src/exec.c:122`. When non-zero,
199/// the history file is not saved on `exec` (c:4324/c:4340), on shell exit
200/// (builtin.c:6014) or by `saveandpophiststack` (hist.c:3960). Set to 1 by
201/// `setupvals` (init.c:1332) and cleared once the startup files have run
202/// (init.c:1550).
203pub static nohistsave: std::sync::atomic::AtomicI32 = // c:122 (Src/exec.c)
204    std::sync::atomic::AtomicI32::new(0);
205
206/// Port of `int subsh;` from `Src/exec.c:160`. Subshell depth — bumped
207/// every time `entersubsh` forks a sub-shell, used by signal handling
208/// (different SIGINT semantics in subshells) and by `${$$}` (`$$`
209/// stays at the top-level pid).
210pub static subsh: std::sync::atomic::AtomicI32 = // c:160 (Src/exec.c)
211    std::sync::atomic::AtomicI32::new(0);
212
213/// Port of `int zsh_subshell;` from `Src/params.c:108`. Visible
214/// `$ZSH_SUBSHELL` parameter — incremented by `entersubsh()` each time
215/// the shell forks into a subshell (real or fake-exec). Distinct from
216/// `subsh` which records whether we ARE a subshell; `zsh_subshell` is
217/// the visible depth count.
218pub static zsh_subshell: std::sync::atomic::AtomicI32 = // c:108 (Src/params.c)
219    std::sync::atomic::AtomicI32::new(0);
220
221/// Port of `mod_export volatile int retflag;` from `Src/exec.c:165`.
222/// Set by `bin_return` to unwind the function-call stack. Cleared
223/// by `runshfunc` on entry, checked by `execlist`'s main loop.
224///
225/// Re-export alias of the canonical [`crate::ported::builtin::RETFLAG`] — C
226/// has ONE `retflag` (exec.c:165). All return-flow logic (execlist, the
227/// `return` builtin, signals) uses `RETFLAG`; this lowercase twin was never
228/// stored, so `zpty`/`system` module read-loops that check `exec::retflag`
229/// saw a permanent 0 and never aborted on a pending `return`.
230pub use crate::ported::builtin::RETFLAG as retflag; // c:165 (Src/exec.c)
231
232/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
233/// recent `$(cmd)` command-substitution child. Used by exit-status
234/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
235/// the pid `waitpid`-d for it.
236pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
237    std::sync::atomic::AtomicI32::new(0);
238
239/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
240/// Pid of the most recent process-substitution child (`<(cmd)` /
241/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
242/// jobs aren't wait-collected by the parent until the fd is closed.
243pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
244    std::sync::atomic::AtomicI32::new(0);
245
246/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
247/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
248/// runs alongside a substitution.
249pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
250    std::sync::atomic::AtomicI32::new(0);
251
252/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
253/// `lastval` is updated from `cmdoutval` after the command
254/// (i.e. the command had substitutions whose exit status matters).
255pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
256    std::sync::atomic::AtomicI32::new(0);
257
258/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
259/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
260/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
261/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
262/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
263/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
264pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
265    std::sync::atomic::AtomicI32::new(0);
266
267/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
268/// currently-executing pipeline is the long-running pipe-into-loop
269/// shape (`cat foo | while read a; do ... done`) — drives the
270/// super/sub-job tracking documented in the famous `Allen Edeln…`
271/// comment block above this declaration in C.
272pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
273    std::sync::atomic::AtomicI32::new(0);
274
275/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
276/// dispatch of a "simple" pipeline (single-stage / no shell-construct
277/// tail) so the `list_pipe` machinery short-circuits.
278pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
279    std::sync::atomic::AtomicI32::new(0);
280
281/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
282/// PID of the sub-shell created to host the loop-after-pipe pattern;
283/// passed up the recursive `execlist` stack so the cat-job's super-
284/// job entry can record it.
285pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
286    std::sync::atomic::AtomicI32::new(0);
287
288/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
289/// `execpline` doesn't wait for the pipeline; used during the
290/// list_pipe sub-shell fork bookkeeping.
291pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
292    std::sync::atomic::AtomicI32::new(0);
293
294/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
295/// pipeline depth (counts nested pipelines within the current
296/// `execlist` call chain).
297pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
298    std::sync::atomic::AtomicI32::new(0);
299
300/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
301/// Set in the child after the list_pipe fork so the child knows to
302/// continue executing the loop body (vs the parent which records
303/// the pid + returns).
304pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
305    std::sync::atomic::AtomicI32::new(0);
306
307/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
308/// table index of the pipeline's first-stage job (the `cat` in
309/// `cat foo | while ...`).
310pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
311    std::sync::atomic::AtomicI32::new(0);
312
313/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
314/// `printprompt4` has emitted the `$PS4` prefix for the current
315/// xtrace command — prevents double-printing when an inner sub-eval
316/// also wants to xtrace.
317pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
318    std::sync::atomic::AtomicI32::new(0);
319
320/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
321///
322/// File-static "execsubst parameters" — callers (execcmd_exec at
323/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
324/// uses them as the `flags` arg to prefork() and the gate on
325/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
326/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
327/// when the dispatched builtin has `BINF_NOGLOB`.
328pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
329    std::sync::atomic::AtomicI32::new(0);
330pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
331    std::sync::atomic::AtomicI32::new(1);
332
333/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
334/// of the linked exec-context save stack — `execsave` pushes a frame
335/// before signal-handler / trap dispatch; `execrestore` pops it
336/// afterwards so the interrupted command resumes with its state intact.
337pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
338    std::sync::Mutex::new(None);
339
340/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
341/// `stty` argument string captured by `addvars` when the command's
342/// inline env contains `STTY=...`. Applied by `execute` before fork
343/// + exec so the spawned program sees its tty configured. Reset to
344/// `None` after consumption to avoid infinite recursion.
345pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
346    std::sync::Mutex::new(None);
347
348/// Convert a here-document into a here-string. Line-by-line port of
349/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
350/// input stream via `hgetc()` until the terminator line is matched,
351/// returning the collected body as a string. `strp` is in/out: on
352/// entry the raw terminator (possibly with token markers + leading
353/// tabs); on return the munged terminator (after `quotesubst` +
354/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
355///
356/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
357/// Rust's `String` auto-grows so the OOM branch is effectively
358/// unreachable, but the return type stays `Option<String>` to mirror
359/// the C signature which can return NULL.
360///
361/// Port of `gethere()` from `Src/exec.c:4570` — C decl `gethere(char **strp, int typ)`.
362pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
363    // c:4573 (Src/exec.c)
364    let mut buf: String; // c:4575 char *buf
365    let mut bsiz: usize; // c:4576 int bsiz
366    let mut qt: i32 = 0; // c:4576 int qt = 0
367    let mut strip: i32 = 0; // c:4576 int strip = 0
368                            // c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
369                            // `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
370                            // C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
371                            // `s` (the loop iterator for the inull-scan) stays local to its
372                            // for-loop. `c` mirrors the C `char c`.
373    let mut t: usize; // c:4577 char *t
374    let mut c: Option<char>; // c:4577 char c
375    let mut str: String = strp.clone(); // c:4578 char *str = *strp
376
377    // c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
378    for s in str.chars() {
379        if crate::token_char::token_byte(s).is_some_and(inull) {
380            // c:4581
381            qt = 1; // c:4582
382            break; // c:4583
383        }
384    }
385    str = quotesubst(&str); // c:4585
386    str = untokenize(&str); // c:4586
387    if typ == REDIR_HEREDOCDASH {
388        // c:4587
389        strip = 1; // c:4588
390                   // c:4589-4590 — while (*str == '\t') str++;
391        while str.starts_with('\t') {
392            str.remove(0);
393        }
394    }
395    *strp = str.clone(); // c:4592 *strp = str
396
397    // c:4593 — bptr = buf = zalloc(bsiz = 256);
398    bsiz = 256;
399    buf = String::with_capacity(bsiz);
400    let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
401
402    // c:4594 — for (;;)
403    loop {
404        t = buf.len(); // c:4595 t = bptr
405
406        // c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
407        loop {
408            c = hgetc();
409            if !(c == Some('\t') && strip != 0) {
410                break;
411            }
412        }
413
414        // c:4599 — for (;;) — inner body-read loop
415        loop {
416            // c:4600-4613 — buffer-growth realloc dance. Rust's
417            // String auto-grows; nothing to do.
418            // c:4614 — if (lexstop || c == '\n') break;
419            if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
420                break;
421            }
422            // c:4616 — if (!qt && c == '\\')
423            if qt == 0 && c == Some('\\') {
424                buf.push('\\'); // c:4617 *bptr++ = c
425                c = hgetc(); // c:4618
426                if c == Some('\n') {
427                    // c:4619
428                    buf.pop(); // c:4620 bptr--
429                    c = hgetc(); // c:4621
430                    continue; // c:4622
431                }
432            }
433            if let Some(ch) = c {
434                // c:4625 *bptr++ = c
435                buf.push(ch);
436            }
437            c = hgetc(); // c:4626
438        }
439        // c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
440
441        // c:4629-4630 — if (!strcmp(t, str)) break;
442        if &buf[t..] == str.as_str() {
443            break;
444        }
445        // c:4631-4634 — if (lexstop) { t = bptr; break; }
446        // C's ingetc sets `lexstop = 1` when input is exhausted
447        // (Src/input.c:289-292), so the flag check alone suffices
448        // there. zshrs's hgetc signals the same exhaustion by
449        // returning None WITHOUT setting LEX_LEXSTOP — treat both as
450        // the c:4631 condition, else an unterminated heredoc loops
451        // here forever appending '\n'. Gap #3 2026-06-12 (A04
452        // "Here-documents don't perform shell expansion" wedge).
453        if LEX_LEXSTOP.with(|f| f.get()) || c.is_none() {
454            t = buf.len();
455            break;
456        }
457        // c:4635 — *bptr++ = '\n';
458        buf.push('\n');
459    }
460    // c:4637 — *t = '\0';
461    buf.truncate(t);
462
463    // c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
464    // The C dance frees the realloc'd block and re-allocates via the
465    // string-heap allocator. Rust drops the old String when reassigned.
466    buf = dupstring(&buf);
467
468    if qt == 0 {
469        // c:4641
470        // c:4642 — int ef = errflag;
471        let ef = errflag.load(Ordering::Relaxed);
472        // c:4644 — parsestr(&buf);
473        if let Ok(parsed) = parsestr(&buf) {
474            buf = parsed;
475        }
476        // c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
477        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
478            let cur = errflag.load(Ordering::Relaxed);
479            errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
480        }
481    }
482    Some(buf) // c:4651 return buf
483}
484
485/// Port of `LinkList getoutput(char *cmd, int qt)` from
486/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
487/// active executor, then routes the captured stdout through
488/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
489///
490/// C return shape: `LinkList` of `char*`. Rust port returns
491/// `Vec<String>` (same shape, owned).
492///
493/// `qt` matches C exactly:
494///   - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
495///     entire output as a single-element vec. C c:4858-4862: if
496///     output empty, returns a single Nularg sentinel so callers
497///     see "empty value" rather than "no value".
498///   - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
499///     `spacesplit(buf, allownull=false)` per c:4865-4871, and
500///     `shtokenize` each word when `GLOBSUBST` is set (c:4868-4869)
501///     so the substituted words glob downstream.
502///
503/// Both arms — `$(< file)` (c:4746) and `$( cmd )` (c:4772) — go through
504/// the same c:4858-4872 tail (inlined twice — see the cross-reference
505/// notes at :619 and :3829), mirroring C where both go through `readoutput`.
506///
507/// Uses `with_executor` (panics on missing VM context), not
508/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
509/// `execpline` directly — there's no "no shell" code path. The
510/// silent-no-op pattern (return empty string when no executor) would
511/// mask catastrophic state corruption as "command produced no output",
512/// which is the failure mode the `subst.rs:496` warning block flags.
513/* $(...) */
514/// Port of `getoutput()` from `Src/exec.c:4713` — C decl `getoutput(char *cmd, int qt)`.
515// c:4709
516/// `getoutput` — see implementation.
517pub fn getoutput(cmd: &str, qt: i32) -> Option<Vec<String>> {
518    // c:4713
519    // c:4715 — `Eprog prog;`
520    let prog: Option<eprog>;
521    // c:4716 — `int pipes[2];`  (collapsed: in-process executor; no fork)
522    // c:4717 — `pid_t pid;`     (collapsed)
523    let mut s: String; // c:4718
524                       // c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
525                       //                !sourcelevel && unset(INTERACTIVECOMMENTS));
526                       //                prog = parse_string(cmd, 0); nocomments = onc;`
527    let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
528    let new_nc = crate::ported::zsh_h::interact()
529        && crate::ported::init::sourcelevel.load(Ordering::Relaxed) == 0
530        && !isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
531    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
532    prog = parse_string(cmd, 0);
533    crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
534
535    if prog.is_none() {
536        // c:4725
537        return None; // c:4726 return NULL
538    }
539    let prog = prog.unwrap();
540
541    if !isset(crate::ported::zsh_h::EXECOPT) {
542        // c:4728
543        return Some(Vec::new()); // c:4729 newlinklist()
544    }
545
546    // c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
547    if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
548        /* $(< word) */
549        // c:4732
550        s = red_name;
551        s = singsub(&s); // c:4737
552        if errflag.load(Ordering::Relaxed) != 0 {
553            return None; // c:4739
554        }
555        let s = untokenize(&s); // c:4740
556        let path_meta = unmeta(&s); // c:4741 unmeta(s)
557        let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
558            Ok(c) => c,
559            Err(_) => return Some(Vec::new()),
560        };
561        let stream = unsafe {
562            libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
563        };
564        if stream == -1 {
565            // c:4798 — `zwarn("%e: %s", errno, s);`
566            //
567            // `zwarn`, NOT `zerr`: `zerr` raises ERRFLAG_ERROR, which
568            // abandons the rest of the line, so `print -r -- "[$(<f)]"`
569            // against a missing `f` printed nothing and exited 1. zsh
570            // warns, substitutes empty, and runs the command — the
571            // observed output is `[]` with status 0.
572            //
573            // `%e` is zsh's errno formatter (`zsh_errno_msg`, utils.c:355),
574            // which lowercases strerror's first letter: "no such file or
575            // directory". Rust's `io::Error` Display renders "No such file
576            // or directory (os error 2)" instead.
577            let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
578            zwarn(&format!(
579                "{}: {}",
580                crate::ported::utils::zsh_errno_msg(errno),
581                s
582            ));
583            LASTVAL.store(1, Ordering::Relaxed);
584            cmdoutval.store(1, Ordering::Relaxed);
585            return Some(Vec::new()); // c:4800
586        }
587        // c:4746 — `retval = readoutput(stream, qt, &readerror);`
588        let mut readerror: i32 = 0;
589        let retval = readoutput(stream, qt, &mut readerror); // c:4746
590        if readerror != 0 {
591            // c:4803-4805 — `zwarn("error when reading %s: %e", s, readerror);`
592            // Non-fatal for the same reason as the open failure above.
593            zwarn(&format!(
594                "error when reading {}: {}", // c:4804
595                s,
596                crate::ported::utils::zsh_errno_msg(readerror)
597            ));
598            LASTVAL.store(1, Ordering::Relaxed);
599            cmdoutval.store(1, Ordering::Relaxed);
600        }
601        return Some(retval); // c:4751
602    }
603
604    // c:4753-4790 — Full fork path: mpipe + zfork + parent
605    // readoutput / waitforpid / child execode + _realexit. fusevm runs
606    // command substitution in-process, so the fork shape collapses to a
607    // synchronous executor call. C control points preserved as cites:
608    //   c:4753 mpipe       — handled by ShellExecutor pipe wiring
609    //   c:4758 child_block — no-op (no fork)
610    //   c:4760 zfork       — replaced by in-process exec
611    //   c:4768-4776 parent — equivalent to executor return
612    //   c:4778-4789 child  — entersubsh+execode+_realexit collapse.
613    //     The `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` at c:4781 is NOT
614    //     dropped by that collapse: `run_command_substitution` below enters
615    //     `SubshStateGuard`, which applies the deltas that are correct
616    //     without a fork (opts[MONITOR], shout, opts[USEZLE], zleactive) and
617    //     restores them on return. See SubshStateGuard for what is skipped
618    //     and why. `_realexit()` has no in-process counterpart — the
619    //     executor simply returns the captured buffer.
620    cmdoutval.store(0, Ordering::Relaxed); // c:4759
621    let buf = crate::ported::exec::run_command_substitution(cmd);
622    LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed); // c:4775
623
624    // ── c:4772 `retval = readoutput(pipes[0], qt, NULL);` ─────────────
625    // The fd died with the fork, but readoutput's POST-READ half (c:4858-
626    // 4872) still applies verbatim, so it is inlined here.
627    //
628    // !!! THIS BLOCK IS DUPLICATED at `readoutput` (:3829). See the note
629    // there for why build.rs's no-new-functions gate rules out a shared
630    // helper. ANY EDIT HERE MUST BE MIRRORED AT :3829 AND VICE VERSA.
631    //
632    // What was missing before: the c:4868-4869 `if (isset(GLOBSUBST))
633    // shtokenize(*words);` step. `readoutput` has it, this copy did not, so
634    // `setopt globsubst; echo $(echo '*')` printed `*` where zsh expands the
635    // glob — while `setopt globsubst; echo $(< star.txt)`, which reaches
636    // `readoutput` through the c:4746 arm, globbed correctly.
637    //
638    // `buf` arrives as a plain (un-metafied) String from
639    // `run_command_substitution`; `readoutput` now hands its own tail the
640    // same representation (see the deviation note at :3792).
641    //
642    // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
643    let s = buf.trim_end_matches('\n');
644    // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
645    if qt != 0 {
646        // c:4861
647        if s.is_empty() {
648            return Some(vec![String::from(Nularg)]); // c:4862
649        }
650        return Some(vec![s.to_string()]); // c:4864
651    }
652    // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
653    let mut words = crate::ported::utils::spacesplit(s, false, false); // c:4867
654    if isset(crate::ported::zsh_h::GLOBSUBST) {
655        // c:4870
656        for w in words.iter_mut() {
657            crate::ported::glob::shtokenize(w); // c:4870
658        }
659    }
660    Some(words)
661}
662
663/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
664/// int ignore_loaddir)` from `Src/exec.c:5682`. Walks `$fpath` for a
665/// file named `shf->node.nam`, reads it, installs the text body on
666/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
667///
668/// C body (abridged):
669///   1. `name = shf->node.nam`
670///   2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
671///   3. If !test_only && file found: parse → store eprog on
672///      `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
673///   4. Returns shf on success, NULL on failure.
674///
675/// Rust port: returns 0 = success, 1 = failure (matches the
676/// existing call-site convention in `bin_functions -c`). Stores
677/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
678/// `hashtable.rs:362`); the parser pass that converts text →
679/// Eprog runs lazily at first call site.
680/// Port of `loadautofn()` from `Src/exec.c:5682` — C decl `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)`.
681pub fn loadautofn(
682    shf: *mut shfunc, // c:5682 (Src/exec.c)
683    _ks: i32,
684    autol: i32,
685    current_fpath: i32, // c:5735 — 4th C param is `current_fpath`, NOT an
686                        // "ignore loaddir" flag; `eval_autoload` passes
687                        // `OPT_ISSET(ops,'d')` here (c:3182).
688) -> i32 {
689    if shf.is_null() {
690        return 1;
691    }
692    // c:5054 — `name = shf->node.nam`.
693    let name = unsafe { (*shf).node.nam.clone() };
694    // c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
695    let mut dir_path: Option<String> = None;
696    let mut dump_hit: Option<(eprog, i32)> = None;
697    // A function autoloaded by ABSOLUTE PATH (`autoload -Uz /dir/name`) — or
698    // one already resolved via `autoload -r` — caches its directory in
699    // `shf.filename` with PM_LOADDIR set. Load-on-call must search THAT dir
700    // (`shf.filename/name`), not walk `$fpath` (which won't contain it).
701    // Without this, `autoload -Uz $fdir/.hist.*` (zsh-hist) named the
702    // functions correctly but every call died with "definition file not
703    // found". Mirrors C loadautofn's PM_LOADDIR spec-path branch.
704    //
705    // c:5747-5757 —
706    //     if (shf->filename && shf->filename[0] == '/' &&
707    //         (shf->node.flags & PM_LOADDIR))
708    //     {
709    //         char *spec_path[2];
710    //         spec_path[0] = dupstring(shf->filename);
711    //         spec_path[1] = NULL;
712    //         prog = getfpfunc(shf->node.nam, &ksh, &fdir, spec_path, 0);
713    //         if (prog == &dummy_eprog &&
714    //             (current_fpath || (shf->node.flags & PM_CUR_FPATH)))
715    //             prog = getfpfunc(shf->node.nam, &ksh, &fdir, NULL, 0);
716    //     }
717    //     else
718    //         prog = getfpfunc(shf->node.nam, &ksh, &fdir, NULL, 0);
719    let fn_flags = unsafe { (*shf).node.flags } as u32;
720    let loaddir_spec: Option<Vec<String>> = {
721        let s = unsafe { &*shf };
722        // c:5747 — the spec-path arm needs an ABSOLUTE filename, not just
723        // PM_LOADDIR.
724        if s.filename.as_deref().is_some_and(|f| f.starts_with('/')) && (fn_flags & PM_LOADDIR) != 0
725        {
726            s.filename.clone().map(|d| vec![d])
727        } else {
728            None
729        }
730    };
731    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
732    // C runs getfpfunc (c:5753/c:5759) — a walk of `$fpath`, then a read and
733    // parse of the file — on the first call of every autoload in every
734    // process. autoloads.rkyv records what that search chose and read
735    // (`autoload_cache::ResolvedLoad`), so a plain `$fpath` autoload whose
736    // entry is still current installs the stored body without touching
737    // `$fpath` at all. With an explicit load directory (the c:5747 arm) the
738    // entry must have come from that directory.
739    let shard_hit = {
740        let dirs = match loaddir_spec.as_deref() {
741            Some(spec) => spec.to_vec(),
742            None => crate::ported::params::getaparam("fpath").unwrap_or_default(),
743        };
744        crate::autoload_cache::try_resolve(&name, &dirs)
745    };
746    let (path, dump_ksh, from_wordcode, body) = if let Some((dir, resolved)) = shard_hit {
747        dir_path = Some(dir.clone());
748        let hit = (
749            format!("{dir}/{name}"),
750            (resolved.dump_ksh >= 0).then_some(resolved.dump_ksh),
751            resolved.from_wordcode,
752            resolved.body.clone(),
753        );
754        crate::autoload_cache::stage_hit(&name, resolved);
755        hit
756    } else {
757        let mut looked_up = getfpfunc(
758            &name,
759            &mut dir_path,
760            loaddir_spec.as_deref(),
761            0,
762            &mut dump_hit,
763        ); // c:5753 / c:5759
764           // c:5754-5756 — the explicit load directory missed; `-d` (PM_CUR_FPATH,
765           // set by `autoload -d`, c:3383) or an explicit `current_fpath` argument
766           // means "also try $fpath". The Rust port never retried, so
767           // `autoload -dUz $PWD/extra/def; def` and
768           // `def() { autoload -dXUz $PWD/extra; }; def` both reported
769           // "function definition file not found" where zsh loads ./def
770           // (C04funcdef:33,40).
771        if looked_up.is_none()
772            && loaddir_spec.is_some()
773            && (current_fpath != 0 || (fn_flags & crate::ported::zsh_h::PM_CUR_FPATH) != 0)
774        {
775            dir_path = None;
776            dump_hit = None;
777            looked_up = getfpfunc(&name, &mut dir_path, None, 0, &mut dump_hit); // c:5756
778        }
779        let path = match looked_up {
780            Some(p) => p,
781            None => {
782                // !!! WARNING: RUST-ONLY BRANCH — NO DIRECT C COUNTERPART !!!
783                // compsys ships as native Rust functions (src/compsys/router.rs),
784                // so names like `_main_complete` have no definition file in
785                // $fpath. C zsh always loads them from files; zshrs must let
786                // `autoload +X -Uz _main_complete` (e.g. fzf-tab via zinit's
787                // :zinit-tmp-subst-autoload, zinit.zsh:356) succeed without one.
788                // Mark the stub loaded and return success — call-time dispatch
789                // short-circuits to the native fn (vm_helper.rs:2276), so no
790                // funcdef/body is needed.
791                if crate::compsys::router::is_intercepted(&name) {
792                    unsafe {
793                        (*shf).node.flags &= !(PM_UNDEFINED as i32);
794                    }
795                    if let Ok(mut tab) = shfunctab_lock().write() {
796                        if let Some(existing) = tab.get_mut(&name) {
797                            existing.node.flags &= !(PM_UNDEFINED as i32);
798                        }
799                    }
800                    return 0;
801                }
802                // c:Src/exec.c:5713-5719 — file not found path. C:
803                //   `if (prog == &dummy_eprog) {
804                //        locallevel--;
805                //        zwarn("%s: function definition file not found",
806                //              shf->node.nam);
807                //        locallevel++;
808                //        popheap();
809                //        return NULL;
810                //    }`
811                // C's getfpfunc returns &dummy_eprog as the "not found"
812                // sentinel when test_only==0; loadautofn detects it and
813                // emits the diagnostic before returning NULL. Rust's
814                // getfpfunc returns Option::None for the same condition,
815                // so we emit the same diagnostic here. The locallevel
816                // dance is preserved as a comment because the Rust
817                // port's zwarn doesn't reference locallevel in the
818                // format string itself (the dance in C is only to keep
819                // the prefix line counter consistent with the function-
820                // body context). Bug #107 in docs/BUGS.md.
821                crate::ported::utils::zwarn(&format!("{}: function definition file not found", name));
822                return 1; // c:5719 NULL
823            }
824        };
825        let _ = autol;
826        // Previously the Rust port treated this parameter as
827        // "test_only" and early-returned when set, so the `+X`
828        // call from `eval_autoload` (`loadautofn(shf, mode, 1, d)`)
829        // never actually loaded the file. C's parameter is `autol`
830        // (autoload mode), NOT a test-only flag — the C body
831        // unconditionally loads/parses regardless of autol. autol=1
832        // controls the EF_RUN / map-flag dance for the wordcode prog
833        // (c:5725-5749), but the loaded-body / PM_UNDEFINED-clear
834        // path runs in all cases. Removing the early-return so
835        // `autoload -U +X funcname` actually loads the body and
836        // `type funcname` reports `function from /path/file` instead
837        // of `autoload shell function`. Bug #160 in docs/BUGS.md.
838        // c:5100-5140 — read the file. C uses zopen + read + parse_string +
839        // execsave; Rust port stores raw text on the ShFunc and defers
840        // parse-to-Eprog until the first call.
841        //
842        // c:Src/exec.c:6238 / parse.c:3833 — when getfpfunc resolved the
843        // function out of a compiled `.zwc` dump, there is no source file
844        // to read; the wordcode Eprog came back through `dump_hit`. C
845        // executes that wordcode directly (`shf->funcdef = stripkshdef(
846        // prog, ...)`, exec.c:5753-5755). zshrs executes function bodies
847        // through the fusevm bytecode pipeline which consumes source
848        // text, so bridge wordcode → text with the canonical text.c
849        // renderer (`getpermtext`, ported at text.rs:189) — the same
850        // walker `functions NAME` printing uses for wordcode-backed
851        // funcdefs (C hashtable.c:954). The downstream
852        // `autoload_register_source` step (vm_helper.rs:3162) performs
853        // the `stripkshdef` shape decision, matching c:5725-5760.
854        // c:5706-5710 — the ksh-mode precedence chain:
855        //   `if (ksh == 1) { ksh = fksh; if (ksh == 1)
856        //        ksh = PM_KSHSTORED ? 2 : PM_ZSHSTORED ? 0 : 1; }`
857        // The dump header flag (FDHF_KSHLOAD/FDHF_ZSHLOAD via `*ksh`
858        // from try_dump_file) outranks the stub's PM_*STORED bits, which
859        // are only consulted when the dump says 1 (no explicit style).
860        // zshrs's load/register split (vm_helper's
861        // `autoload_register_source` makes the c:5725 ksh-vs-zsh
862        // decision later, from the tab entry's flags + KSHAUTOLOAD) —
863        // fold a decisive dump flag into the PM bits so the downstream
864        // decision sees the same precedence.
865        let dump_ksh = dump_hit.as_ref().map(|(_, k)| *k);
866        // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
867        // C installs the dump's wordcode itself (c:5753-5755) and never lexes it
868        // again; zshrs installs a `getpermtext` deparse whose real compile happens
869        // at the call that defines the function, so the provenance has to be
870        // recorded for that later compile. See vm_helper::autoload_note_wordcode_body.
871        let from_wordcode = dump_hit.is_some();
872        let body = match dump_hit {
873            // The wordcode C executes carries a line number on every pipe
874            // (`WCB_PIPE(type, toklineno + 1)`, c:Src/parse.c:911/935/944, read
875            // back by `lineno = WC_PIPE_LINENO(pcode) - 1` at c:Src/exec.c:2057
876            // and `lineno = code - 1` at c:Src/exec.c:1356), so a dump-loaded
877            // function reports exactly the `$LINENO` of its ORIGINAL source.
878            //
879            // `getpermtext` cannot reproduce that: `gettext2` is a pretty-printer
880            // (`Src/text.c`), so it drops comments and blank lines and re-breaks
881            // compounds onto its own lines — `if X; then` becomes `if X` NEWLINE
882            // `then`. Re-parsing that text yields a DIFFERENT line for every
883            // statement, and `$LINENO` / error prefixes / `funcfiletrace` inside
884            // any `.zwc`-loaded function drift from zsh (measured: 29 vs 32 for
885            // `_parameters` loaded out of a `comp_utils.zwc` digest).
886            //
887            // !!! WARNING: RUST-ONLY BRANCH — NO DIRECT C COUNTERPART !!!
888            // C has no choice to make here: `shf->funcdef = stripkshdef(prog, …)`
889            // (c:Src/exec.c:5753-5755) runs the DUMP's wordcode and never looks at
890            // the source file again. zshrs executes function bodies as TEXT, so it
891            // has to render the wordcode back — and that render is what loses the
892            // line numbers. The source file is preferred ONLY when it still renders
893            // to the same program as the dump; then it is provably the text the
894            // wordcode was compiled from and keeps the original line numbering.
895            //
896            // The previous version skipped that test and took the source file
897            // whenever it existed, on the theory that try_dump_file's mtime gate
898            // (parse.rs, c:Src/parse.c:3762-3784) already proved they agree. It
899            // does not: the gate is `stc.st_mtime >= stn.st_mtime` at SECOND
900            // granularity, so a source rewritten within the same second as the
901            // dump — or back-dated — passes it while holding completely different
902            // code. `zcompile f; print 'print FRESH' > f; touch f; autoload f; f`
903            // ran FRESH where zsh runs the compiled body.
904            Some((prog, _ksh)) => {
905                let dump_text = crate::ported::text::getpermtext(Box::new(prog), None, 0); // c:5753
906                // The equality test below LEXES the source file, and it is asking
907                // whether that file is the text the dump's wordcode was compiled
908                // from. `zcompile` resolved the dump with the lexer state C uses
909                // for a compile, so the comparison has to be made in that same
910                // state or it answers a different question: under RCQUOTES an
911                // adjacent quote pair re-lexes as one literal quote
912                // (c:Src/lex.c:1328) and a live alias rewrites words from inside
913                // the lexer (c:Src/lex.c:1909), so a source that IS the dump's
914                // original compared unequal and the deparse was taken instead —
915                // losing the original line numbering for no reason. Same pin the
916                // `source` leg uses (vm_helper::execute_zwc_program).
917                let _relex = crate::vm_helper::ZwcRelexGuard::enter();
918                // Raw-byte read (c:Src/exec.c:5745 `getfpfunc` → `metafy`):
919                // an autoload file is not required to be UTF-8.
920                match crate::script_bytes::read_script_file(&path) {
921                    // Both sides go through the SAME renderer, so a `.zwc` written
922                    // by C zsh (metafied string pool) still compares equal to a
923                    // locally parsed source.
924                    Ok(t)
925                        if crate::ported::exec::parse_string(&t, 1).is_some_and(|p| {
926                            crate::ported::text::getpermtext(Box::new(p), None, 0) == dump_text
927                        }) =>
928                    {
929                        t
930                    }
931                    _ => dump_text,
932                }
933            }
934            // c:Src/exec.c:5745 `getfpfunc` reads the function file with
935            // `read()` and hands the bytes to `metafy` — there is no encoding
936            // requirement. `read_to_string` rejected the file on the first
937            // non-UTF-8 byte and this arm returned 1, so an $fpath completer
938            // carrying one legacy byte autoloaded to NOTHING, silently.
939            None => match crate::script_bytes::read_script_file(&path) {
940                Ok(t) => t,
941                Err(_) => return 1,
942            },
943        };
944        if let Some(dir) = dir_path.as_deref() {
945            crate::autoload_cache::stage_search(&name, dir, &body, dump_ksh, from_wordcode);
946        }
947        (path, dump_ksh, from_wordcode, body)
948    };
949    // c:Src/exec.c:5735/5757 — `loadautofnsetfile(shf, fdir)`. The
950    // helper stamps PM_LOADDIR alongside the filename when fdir is
951    // present, so `whence -v NAME` later concatenates the directory
952    // with `/NAME` (PM_LOADDIR branch at hashtable.rs:1350). zshrs's
953    // prior `shf->filename = dir_path` assignment skipped the flag
954    // → `type colors` printed `from /path/to/functions` instead of
955    // `from /path/to/functions/colors`. Mirror C exactly.
956    unsafe {
957        loadautofnsetfile(&mut *shf, dir_path.as_deref().or(Some(&path)));
958    }
959    // c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
960    unsafe {
961        (*shf).node.flags &= !(PM_UNDEFINED as i32);
962    }
963    // c:5706-5710 fold (see comment above): decisive dump style wins
964    // over the stub's stored-style bits.
965    let (ksh_on, ksh_off): (i32, i32) = match dump_ksh {
966        Some(2) => (
967            crate::ported::zsh_h::PM_KSHSTORED as i32,
968            crate::ported::zsh_h::PM_ZSHSTORED as i32,
969        ),
970        Some(0) => (
971            crate::ported::zsh_h::PM_ZSHSTORED as i32,
972            crate::ported::zsh_h::PM_KSHSTORED as i32,
973        ),
974        _ => (0, 0),
975    };
976    unsafe {
977        (*shf).node.flags = ((*shf).node.flags | ksh_on) & !ksh_off;
978    }
979    // c:5753-5755 — C's `shf->funcdef = stripkshdef(prog, …)` installs the
980    // dump's own wordcode; zshrs installs TEXT and compiles it later, so mark
981    // (or, on a plain-file load, un-mark) the name so the compile that finally
982    // lexes this text knows it is a deparse and pins the lexer accordingly.
983    crate::vm_helper::autoload_note_wordcode_body(
984        &name,
985        if from_wordcode { Some(&body[..]) } else { None },
986    );
987    // Sync the body string into the Rust-side ShFunc table so the
988    // lazy-parse path can find it later.
989    if let Ok(mut tab) = shfunctab_lock().write() {
990        if let Some(existing) = tab.get_mut(&name) {
991            existing.body = Some(body);
992            existing.node.flags = (existing.node.flags | ksh_on) & !ksh_off;
993            // c:5657 loadautofnsetfile — store the fpath DIRECTORY absolutized
994            // with PM_LOADDIR (not the raw relative `./fns`), so `whence -v`
995            // prints the absolute source. The `+X` (eval-autoload) path relies
996            // on this since it does not re-register through vm_helper.
997            loadautofnsetfile(existing, dir_path.as_deref());
998        } else {
999            let mut shf = shfunc {
1000                node: hashnode {
1001                    next: None,
1002                    nam: name.clone(),
1003                    flags: ksh_on, // c:5706-5710 dump-style fold
1004                },
1005                filename: None,
1006                lineno: 0,
1007                funcdef: None,
1008                redir: None,
1009                sticky: None,
1010                body: Some(body),
1011                redir_text: None,
1012            };
1013            loadautofnsetfile(&mut shf, dir_path.as_deref()); // c:5657
1014            tab.add(shf);
1015        }
1016    }
1017    0
1018}
1019
1020/// Port of `getfpfunc()` from `Src/exec.c:6219` — C decl `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)`.
1021/// supplied `spec_path` slice) for a file named `name` and writes the
1022/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
1023/// Returns `Some(file_path)` on success, `None` when not found.
1024///
1025/// Per dir, the compiled-dump lookup runs FIRST (c:6238
1026/// `try_dump_file(*pp, s, buf, ksh, test_only)`) — a directory
1027/// digest `<dir>.zwc` or per-function `<dir>/<name>.zwc` wins over
1028/// the plain file when newer (mtime logic inside try_dump_file,
1029/// c:parse.c:3762-3784). On a dump hit the loaded program + ksh
1030/// mode (C's `*ksh` out-param) are written through `dump_out` and
1031/// the nominal `<dir>/<name>` path is returned; the caller must
1032/// check `dump_out` before reading the returned path as a plain
1033/// file.
1034pub fn getfpfunc(
1035    name: &str,
1036    dir_path_out: &mut Option<String>, // c:6219 (Src/exec.c)
1037    spec_path: Option<&[String]>,
1038    test_only: i32,                      // c:6219 `int test_only`
1039    dump_out: &mut Option<(eprog, i32)>, // c:6219 `int *ksh` + dump Eprog return
1040) -> Option<String> {
1041    // C reads $fpath via `getaparam("fpath")` (the param-table array form
1042    // tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
1043    // misses any in-script modification like `fpath=(/some/dir $fpath)`
1044    // because that mutates the internal param table, not the inherited
1045    // process env. Fall back to env only when the param table is empty
1046    // (cold start before any param-table init).
1047    let dirs: Vec<String> = match spec_path {
1048        Some(s) => s.to_vec(),
1049        None => crate::ported::params::getaparam("fpath")
1050            .filter(|v| !v.is_empty())
1051            .or_else(|| getsparam("FPATH").map(|v| v.split(':').map(String::from).collect()))
1052            .or_else(|| {
1053                std::env::var("FPATH")
1054                    .ok()
1055                    .map(|v| v.split(':').map(String::from).collect())
1056            })
1057            .unwrap_or_default(),
1058    };
1059    for dir in &dirs {
1060        if dir.is_empty() {
1061            continue;
1062        }
1063        let path = format!("{}/{}", dir, name); // c:6230 snprintf(buf, ..., "%s/%s", *pp, s)
1064                                                // c:6238 — `if ((r = try_dump_file(*pp, s, buf, ksh, test_only)))`
1065                                                // — the .zwc digest / per-function dump is tried BEFORE the
1066                                                // plain file in each directory.
1067        if let Some(hit) = crate::ported::parse::try_dump_file(dir, name, &path, test_only != 0) {
1068            *dump_out = Some(hit);
1069            *dir_path_out = Some(dir.clone()); // c:6240 `*fdir = *pp;`
1070            return Some(path); // c:6241
1071        }
1072        if std::path::Path::new(&path).exists() {
1073            // c:6245 access(buf, R_OK)
1074            *dir_path_out = Some(dir.clone());
1075            return Some(path);
1076        }
1077    }
1078    None
1079}
1080
1081/// Port of `resolvebuiltin()` from `Src/exec.c:2703`. — C decl `resolvebuiltin(const char *cmdarg, HashNode hn)`.
1082/// the autoloaded-builtin stub firing.
1083///
1084/// C body:
1085/// ```c
1086/// if (!((Builtin) hn)->handlerfunc) {
1087///     char *modname = dupstring(((Builtin) hn)->optstr);
1088///     (void)ensurefeature(modname, "b:",
1089///                         (hn->flags & BINF_AUTOALL) ? NULL : hn->nam);
1090///     hn = builtintab->getnode(builtintab, cmdarg);
1091///     if (!hn) {
1092///         lastval = 1;
1093///         zerr("autoloading module %s failed to define builtin: %s",
1094///              modname, cmdarg);
1095///         return NULL;
1096///     }
1097/// }
1098/// return hn;
1099/// ```
1100///
1101/// zshrs split: the C autoload stub (builtintab node with NULL
1102/// handlerfunc, installed by `add_autobin` c:426) lives in the
1103/// `autoload_builtins` ledger (name → module). This fn is the
1104/// dispatch-time consult:
1105///   - `None` — name has no autoload stub; caller continues its
1106///     normal lookup chain.
1107///   - `Some(0)` — module loaded; caller re-dispatches the (now
1108///     registered) builtin.
1109///   - `Some(1)` — load failed or the loaded module didn't define
1110///     the feature; diagnostics already printed (load_module's
1111///     `failed to load module` zwarn, or the c:2718 zerr here).
1112///     Caller returns status 1.
1113///
1114/// Ledger upkeep: the entry is removed in every fired path —
1115///   - success: C's addbuiltin (module.c:411-415) replaces the stub
1116///     with the real node, so the stub is gone;
1117///   - load failure: C's execbuiltin head (Src/builtin.c:264-267)
1118///     hits the still-NULL handlerfunc and `deletebuiltin`s the
1119///     stub — a second call reports `command not found` / 127
1120///     (probed: zsh 5.9 `zmodload -ab zsh/bogus mybltn; mybltn;
1121///     mybltn` → rc1=1, rc2=127).
1122///
1123/// AUTOALL note: the ledger doesn't carry BINF_AUTOALL, so the
1124/// ensurefeature arg is always `Some(name)` (the non-AUTOALL form).
1125/// The AUTOALL path is unreachable for builtins via `zmodload -a MOD`
1126/// (both shells error "`/' is invalid in a builtin"); revisit if the
1127/// ledger grows flags.
1128pub fn resolvebuiltin(name: &str) -> Option<i32> {
1129    // c:2700
1130    let mut tab = crate::ported::module::MODULESTAB.lock().ok()?;
1131    // c:2705 — `if (!((Builtin) hn)->handlerfunc)`: ledger hit IS the
1132    // "no handlerfunc" stub in zshrs.
1133    let module = tab.autoload_builtins.get(name)?.clone();
1134    // c:2706 — `modname = dupstring(hn->optstr)` (done: `module`).
1135    // Stub fires exactly once per registration (see ledger upkeep
1136    // in the doc above).
1137    tab.autoload_builtins.remove(name);
1138    // c:2711-2713 — ensurefeature(modname, "b:", hn->nam).
1139    let _ = crate::ported::module::ensurefeature(&mut tab, &module, "b:", Some(name));
1140    // c:2714 — `hn = builtintab->getnode(builtintab, cmdarg);`
1141    // zshrs analog: the module booted (is_loaded) AND the name is in
1142    // the static builtintab (createbuiltintable pre-registers every
1143    // module bintab entry).
1144    let defined =
1145        tab.is_loaded(&module) && crate::ported::builtin::createbuiltintable().contains_key(name);
1146    if defined {
1147        return Some(0); // c:2723 `return hn;`
1148    }
1149    if tab.is_loaded(&module) {
1150        // c:2716-2720 — module loaded but feature missing.
1151        crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed); // c:2717 lastval = 1
1152        crate::ported::utils::zerr(&format!(
1153            "autoloading module {} failed to define builtin: {}",
1154            module, name
1155        )); // c:2718
1156    }
1157    // Load failure: load_module already printed `failed to load
1158    // module \`...'`; C's execbuiltin head returns 1 silently
1159    // (Src/builtin.c:264-267).
1160    Some(1)
1161}
1162
1163/// Port of `static struct builtin commandbn` from `Src/exec.c:276`.
1164///
1165/// c:275 — /* structure for command builtin for when it is used with -v or -V */
1166///
1167/// `BUILTIN("command", 0, bin_whence, 0, -1, BIN_COMMAND, "pvV", NULL)`.
1168/// This is a SEPARATE descriptor from the `BIN_PREFIX("command", …)` row in
1169/// `builtintab` (which carries no handler at all): c:3209 swaps `hn` to this
1170/// one when `command -v` / `command -V` is seen, so the dispatch lands in
1171/// `bin_whence` with funcid `BIN_COMMAND`.
1172pub static commandbn: std::sync::LazyLock<crate::ported::zsh_h::builtin> =
1173    std::sync::LazyLock::new(|| {
1174        crate::ported::builtin::BUILTIN(
1175            "command",
1176            0,
1177            Some(crate::ported::builtin::bin_whence),
1178            0,
1179            -1,
1180            crate::ported::hashtable_h::BIN_COMMAND,
1181            Some("pvV"),
1182            None,
1183        ) // c:282
1184    });
1185
1186/// Dispatch decision returned by `execcmd_compile_head` — the
1187/// fusevm-bytecode-time head resolver that mirrors the local-variable
1188/// state the C `execcmd_exec` function carries through `c:2913-2916`
1189/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
1190/// precmd-modifier strip count. The fusevm bytecode compiler reads
1191/// this to emit the correct dispatch opcode in
1192/// `src/extensions/compile_zsh.rs::compile_simple`.
1193///
1194/// Not a C struct — invented to bridge the divergence between the
1195/// C wordcode-walker (which mutates locals + falls through to
1196/// invocation) and zshrs's split parse → compile → VM pipeline.
1197#[allow(non_camel_case_types)]
1198#[derive(Debug, Default, Clone)]
1199pub struct execcmd_dispatch {
1200    /// Number of `BINF_PREFIX` words to strip from the head of args.
1201    /// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
1202    pub precmd_skip: usize,
1203    /// Set when the head (after strip) is a real builtin
1204    /// (`Src/exec.c:3065 is_builtin = 1`).
1205    pub is_builtin: bool,
1206    /// Set when the head (after strip) is a shell function
1207    /// (`Src/exec.c:3053 is_shfunc = 1`).
1208    pub is_shfunc: bool,
1209    /// `cflags` accumulator from `Src/exec.c:2915` — gathers
1210    /// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
1211    /// BINF_NOGLOB` bits encountered during the precommand-modifier
1212    /// walk (c:3062 `cflags |= hn->flags`).
1213    pub cflags: u32,
1214    /// `orig_cflags` accumulator from `Src/exec.c:2915` — the `cflags` in
1215    /// force before each `cflags |= hn->flags` step (c:3115), i.e. every
1216    /// modifier flag the walk consumed, `BINF_COMMAND` included (c:4123,
1217    /// c:4424 test it).
1218    pub orig_cflags: u32,
1219    /// `command -p` requested: use the default `$PATH` for lookup
1220    /// (`Src/exec.c:3160 use_defpath = 1`). Consumed by the fusevm
1221    /// compiler (`fusevm_bridge.rs`) when it emits the exec call.
1222    pub use_defpath: bool,
1223    /// `command -v` / `command -V` requested: the dispatch target
1224    /// flips to `bin_whence` per `Src/exec.c:3149-3157`
1225    /// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
1226    /// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
1227    /// instead of resolving the post-strip head.
1228    pub has_command_vv: bool,
1229    /// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
1230    /// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
1231    pub exec_argv0: Option<String>,
1232    /// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
1233    /// — the `else` arm of `if (redir && nonempty(redir))`). Covers
1234    /// bare `exec` / `noglob` / `command`. Caller emits
1235    /// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
1236    /// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
1237    /// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
1238    /// returns 0 without complaining about missing redirs).
1239    pub is_empty_command: bool,
1240    /// `preargs` as the walk left it (the c:3086 / c:3165 / c:3177 /
1241    /// c:3209 / c:3239 `uremnode`s): the command words are
1242    /// `preargs[precmd_skip..]`, with `command -p` / `--` and the `exec`
1243    /// option words already removed.
1244    pub preargs: Vec<String>,
1245}
1246
1247/// !!! WARNING: RUST-ONLY HELPER !!!
1248///
1249/// No function of this name exists in `Src/exec.c`. This helper stands in for
1250/// the head section of `execcmd_exec` (c:2901; locals + the BINF_PREFIX
1251/// precommand-modifier walk at c:2966-3096), called at fusevm
1252/// bytecode-compile time. It does NOT perform dispatch the way the C function
1253/// does.
1254/// !!! NOT A PORT OF C `execcmd_exec` !!!
1255///
1256/// This is a fusevm-bytecode-time head resolver invoked by
1257/// `src/extensions/compile_zsh.rs::compile_simple` and the
1258/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
1259/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
1260/// file under the C-faithful name `execcmd_exec`.
1261///
1262/// This helper mirrors the head section (`c:2904-3275`) of the C
1263/// function — local initialisation, the precommand-modifier walk
1264/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
1265/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
1266/// sub-option parsers — and returns the resulting dispatch
1267/// decision via `execcmd_dispatch`. The fusevm compiler reads
1268/// that struct to decide which `Op::CallBuiltin` /
1269/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
1270/// correct post-strip `argc`.
1271///
1272/// =================== WARNING — DIVERGENCE ====================
1273///
1274/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
1275/// `multio` redirections, evaluates `varspc` assignments, then calls
1276/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
1277/// stops after the precmd-modifier walk and only returns the head
1278/// decision; runtime dispatch is driven by the bytecode the fusevm
1279/// compiler emits.
1280///
1281/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
1282/// wordcode iterator state — zshrs doesn't traverse wordcode here,
1283/// so the args list arrives already-expanded as a `&[String]`
1284/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
1285/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
1286///
1287/// =============================================================
1288pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
1289    // c:2900 (Src/exec.c)
1290
1291    // c:2904-2916 — locals.
1292    let mut hn: Option<&'static builtin> = None; // c:2904
1293    let mut is_shfunc = false; // c:2913
1294    let mut is_builtin = false; // c:2913
1295    let mut use_defpath = false; // c:2913
1296    let mut cflags: u32 = 0; // c:2915
1297    let mut orig_cflags: u32 = 0; // c:2915
1298    // c:3263 — `char *exec_argv0 = NULL;` (declared inside the
1299    // BINF_EXEC arm; hoisted here so the dispatch struct can carry it
1300    // out after the loop terminates).
1301    let mut exec_argv0: Option<String> = None;
1302    // c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
1303    // (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
1304    // compiler can emit `bin_whence` instead of resolving the head.
1305    let mut has_command_vv = false;
1306
1307    // c:2962-2973 (`%job` head rewrite) and c:2975-2986 (AUTORESUME
1308    // prefix match) need `how`, `redir` and `input`, which this head
1309    // classifier does not receive; execcmd_exec performs both rewrites
1310    // before calling it.
1311
1312    // c:3013-3091 — precommand-modifier walk.
1313    let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
1314    let mut precmd_skip: usize = 0;
1315
1316    // c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
1317    if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
1318        // c:3018
1319        // c:3029 — `while (nonempty(preargs))`.
1320        while precmd_skip < preargs.len() {
1321            // c:3029
1322            // c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
1323            let cmdarg = untokenize(&preargs[precmd_skip]);
1324            // c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
1325            // already performed prefork expansion on `preargs`, so
1326            // `has_token` is effectively false here; the C `break` on
1327            // unexpanded tokens is unreachable in this entry point.
1328
1329            // c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
1330            // even disabled builtins so the reserved-word form
1331            // (`integer x`, `local foo`) still dispatches to the
1332            // typeset family. The static `BUILTINS` array doesn't
1333            // expose a separate disabled-bit lookup; one path covers
1334            // both. Effect is identical for the precmd-modifier walk.
1335
1336            // c:3098-3103 — `} else if (isset(POSIXBUILTINS) && (cflags &
1337            // BINF_EXEC)) { /* POSIX doesn't allow "exec" to operate on
1338            // builtins or shell functions. */ break; }`
1339            let typeset_hit = type_ == WC_TYPESET && BUILTINS.iter().any(|b| b.node.nam == cmdarg); // c:3088-3089
1340            if !typeset_hit && isset(POSIXBUILTINS) && (cflags & BINF_EXEC) != 0 {
1341                break; // c:3103
1342            }
1343
1344            // c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
1345            // BINF_COMMAND)) && shfunctab->getnode(...))` — shell
1346            // function takes precedence unless a `builtin`/`command`
1347            // modifier preceded it.
1348            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
1349                // c:3106 — `(hn = shfunctab->getnode(shfunctab, cmdarg))`.
1350                // `shfunctab->getnode` is `gethashnode`
1351                // (Src/hashtable.c:821 in `createshfunctable`), i.e. an
1352                // O(1) hash probe that also skips DISABLED nodes. The
1353                // previous Rust code walked the whole table with
1354                // `iter().any(...)`, which is O(#functions) per command
1355                // head AND ignored the DISABLED bit `gethashnode`
1356                // honours. With a real `.zcompdump` loaded the table
1357                // holds ~50k autoload stubs, so every compiled command
1358                // head paid a 50k-entry String scan under the read lock.
1359                if shfunctab_lock()
1360                    .read()
1361                    .map(|t| !t.getnode(&cmdarg).is_null())
1362                    .unwrap_or(false)
1363                {
1364                    is_shfunc = true; // c:3107
1365                    break; // c:3108
1366                }
1367            }
1368            // c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
1369            let entry = BUILTINS.iter().find(|b| b.node.nam == cmdarg);
1370            let Some(entry) = entry else {
1371                // c:3056-3058
1372                break;
1373            };
1374            hn = Some(entry);
1375            // c:3061-3063 — accumulate cflags.
1376            orig_cflags |= cflags;
1377            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
1378            cflags |= entry.node.flags as u32;
1379            // c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
1380            // builtin, stop.
1381            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
1382                // c:3064
1383                // WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
1384                // to autoload the builtin's module if its
1385                // `handlerfunc` is NULL. In zshrs, builtins live in
1386                // two places: the static `BUILTINS` table (which
1387                // mirrors C `handlerfunc`, often `None` for ports
1388                // dispatched through fusevm) AND fusevm's
1389                // `register_builtins` map (the actual runtime
1390                // dispatcher). A null `handlerfunc` in the static
1391                // table is NOT an autoload failure for us — it
1392                // means dispatch routes through fusevm. So we
1393                // skip the resolvebuiltin call here; the faithful
1394                // port remains available for future callers that
1395                // genuinely need module-autoload semantics.
1396                is_builtin = true; // c:3065
1397                break; // c:3077
1398            }
1399            // c:3086 — `uremnode(preargs, firstnode(preargs))`.
1400            precmd_skip += 1;
1401            // c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
1402            //   (...); if (!firstnode(preargs)) break; }`. zshrs has
1403            // no `execcmd_getargs` (args arrive pre-expanded); the
1404            // bounds-check at the top of `while precmd_skip <
1405            // preargs.len()` handles the empty case identically.
1406
1407            // c:3092-3177 — BINF_COMMAND sub-option parsing
1408            // (`command -p / -v / -V`).
1409            if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
1410                // c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
1411                //                int has_p = 0, has_vV = 0, has_other = 0;`
1412                let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
1413                let mut pnode: Option<usize> = None; // c:3102
1414                let mut has_p = false; // c:3104
1415                let mut has_vv = false; // c:3104
1416                let mut has_other = false; // c:3104
1417                                           // c:3107 — `while (IS_DASH(*argdata))`
1418                while argnode < preargs.len()
1419                    && IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
1420                {
1421                    let argdata = preargs[argnode].clone(); // c:3106
1422                    let bytes = argdata.as_bytes();
1423                    // c:3108-3111 — stop on bare `-` or `--`.
1424                    if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
1425                        // c:3109
1426                        break; // c:3111
1427                    }
1428                    // c:3112-3133 — scan flag chars.
1429                    for &c in &bytes[1..] {
1430                        // c:3112
1431                        match c as char {
1432                            'p' => {
1433                                // c:3114
1434                                has_p = true; // c:3122
1435                                pnode = Some(argnode); // c:3123
1436                            }
1437                            'v' | 'V' => {
1438                                // c:3125-3126
1439                                has_vv = true; // c:3127
1440                            }
1441                            _ => {
1442                                // c:3129
1443                                has_other = true; // c:3130
1444                            }
1445                        }
1446                    }
1447                    // c:3134-3138 — unknown flag → don't try, leave alone.
1448                    if has_other {
1449                        // c:3134
1450                        has_p = false; // c:3136
1451                        has_vv = false; // c:3136
1452                        break; // c:3137
1453                    }
1454                    // c:3140-3147 — advance to next arg.
1455                    argnode += 1; // c:3141 nextnode(argnode)
1456                    if argnode >= preargs.len() {
1457                        // c:3142 — execcmd_getargs (skipped: pre-expanded)
1458                        break; // c:3145
1459                    }
1460                }
1461                // c:3149-3157 — `-v`/`-V` → dispatch to whence.
1462                if has_vv {
1463                    // c:3149
1464                    // c:3154 `pushnode(preargs, "command")` — C re-inserts
1465                    // "command" so bin_whence sees it as argv[0]. zshrs
1466                    // surfaces this via `has_command_vv`; the fusevm
1467                    // compiler emits the equivalent whence call.
1468                    has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
1469                    is_builtin = true;
1470                    break; // c:3157
1471                } else if has_p {
1472                    // c:3158
1473                    use_defpath = true; // c:3160
1474                    if let Some(pn) = pnode {
1475                        // c:3165 — `uremnode(preargs, pnode)`. zshrs:
1476                        // remove the `-p`-bearing arg from preargs.
1477                        if pn < preargs.len() {
1478                            preargs.remove(pn);
1479                            // precmd_skip already accounts for the
1480                            // stripped `command` prefix; we just removed
1481                            // the `-p` flag which sat at preargs[pn].
1482                            // No precmd_skip change needed — the head
1483                            // remains where it was.
1484                        }
1485                    }
1486                }
1487                // c:3176-3177 — `--` trailing end-of-options strip.
1488                if argnode < preargs.len() {
1489                    let argdata = &preargs[argnode];
1490                    let b = argdata.as_bytes();
1491                    if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
1492                        // c:3176
1493                        preargs.remove(argnode); // c:3177
1494                    }
1495                }
1496            } else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
1497                // c:3178-3275 — BINF_EXEC sub-option parsing
1498                // (`exec -a NAME -l -c`).
1499                let mut argnode: usize = precmd_skip; // c:3185
1500                let mut error_done = false;
1501                // c:3196 — `while (argdata && IS_DASH(*argdata) &&
1502                //                  strlen(argdata) >= 2)`
1503                while argnode < preargs.len() {
1504                    let argdata = preargs[argnode].clone();
1505                    let bytes = argdata.as_bytes();
1506                    if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
1507                        break; // c:3196 loop guard
1508                    }
1509                    let oldnode = argnode; // c:3197
1510                    argnode += 1; // c:3198 nextnode(oldnode)
1511                                  // c:3203-3208 — empty next → error.
1512                    if argnode >= preargs.len() {
1513                        // c:3203
1514                        zerr(
1515                            // c:3204
1516                            "exec requires a command to execute",
1517                        );
1518                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
1519                        error_done = true;
1520                        break; // c:3207 goto done
1521                    }
1522                    // c:3209 — `uremnode(preargs, oldnode)`.
1523                    preargs.remove(oldnode);
1524                    argnode -= 1; // re-anchor — `argnode` was the post-removed slot
1525                                  // c:3210-3211 — `--` stops option scan.
1526                    if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
1527                        // c:3210
1528                        break; // c:3211
1529                    }
1530                    // c:3212-3258 — scan flag chars after the leading `-`.
1531                    let mut k = 1usize;
1532                    while k < bytes.len() && !error_done {
1533                        let cmdopt = bytes[k] as char; // c:3212
1534                        match cmdopt {
1535                            'a' => {
1536                                // c:3214 — `-a` ARGV0 override.
1537                                if k + 1 < bytes.len() {
1538                                    // c:3216 — `-aNAME` inline form.
1539                                    exec_argv0 =
1540                                        Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
1541                                    k = bytes.len(); // c:3219 position past end
1542                                } else {
1543                                    // c:3220 — `-a NAME` separate form.
1544                                    if argnode >= preargs.len() {
1545                                        // c:3274-3279 — `if (!argnode)`.
1546                                        zerr("exec requires a command to execute");
1547                                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
1548                                        error_done = true;
1549                                        break;
1550                                    }
1551                                    // c:3280-3283 — `if (!nextnode(argnode))`:
1552                                    // the NAME must still leave a command.
1553                                    if argnode + 1 >= preargs.len() {
1554                                        // c:3230
1555                                        zerr(
1556                                            // c:3231
1557                                            "exec flag -a requires a parameter",
1558                                        );
1559                                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
1560                                        error_done = true;
1561                                        break; // c:3234 goto done
1562                                    }
1563                                    exec_argv0 = Some(preargs[argnode].clone()); // c:3236
1564                                    preargs.remove(argnode); // c:3239
1565                                }
1566                            }
1567                            'c' => {
1568                                // c:3242
1569                                cflags |= BINF_CLEARENV; // c:3243
1570                            }
1571                            'l' => {
1572                                // c:3245
1573                                cflags |= BINF_DASH; // c:3246
1574                            }
1575                            _ => {
1576                                // c:3248
1577                                zerr(
1578                                    // c:3249
1579                                    &format!("unknown exec flag -{}", cmdopt),
1580                                );
1581                                errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
1582                                error_done = true;
1583                                break; // c:3256
1584                            }
1585                        }
1586                        k += 1;
1587                    }
1588                    if error_done {
1589                        break;
1590                    }
1591                }
1592                // c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
1593                // the actual `setenv` to the fusevm compiler / external
1594                // exec path; we surface `exec_argv0` via the dispatch
1595                // struct so the caller can apply it before fork+exec.
1596                if let Some(ref a0) = exec_argv0 {
1597                    // c:3263 — `remnulargs + untokenize` then setenv.
1598                    let cleaned = untokenize(a0); // c:3266-3267
1599                    exec_argv0 = Some(cleaned);
1600                }
1601                if error_done {
1602                    return execcmd_dispatch {
1603                        precmd_skip,
1604                        is_builtin,
1605                        is_shfunc,
1606                        cflags,
1607                        orig_cflags,
1608                        use_defpath,
1609                        has_command_vv,
1610                        exec_argv0,
1611                        is_empty_command: false,
1612                        preargs,
1613                    };
1614                }
1615            }
1616            // c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
1617            // unset(POSIXBUILTINS)) break;`. After processing a
1618            // `command` precmd modifier (and its -p/-v/-V flags), the
1619            // C loop exits with hn cleared so the dispatch falls
1620            // through to external lookup. Without this, the next
1621            // iteration would find `command print` → print's builtin
1622            // and dispatch to it; zsh's intentional behaviour is to
1623            // skip builtins under `command` (unless POSIXBUILTINS is
1624            // set, where the loop continues normally).
1625            if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
1626                hn = None; // c:3275 hn = NULL
1627                break; // c:3277
1628            }
1629        }
1630    }
1631
1632    // c:3309-3406 — "Empty command" branch. When the precmd-modifier
1633    // walk above strips every word with nothing left to dispatch
1634    // (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
1635    // C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
1636    //
1637    // - redir-present + do_exec       → nullexec=1 (continue to run)
1638    // - redir-present + varspc        → nullexec=2 (continue)
1639    // - redir-present + no nullcmd    → `zerr("redirection with no command")`
1640    //                                   lastval=1, return
1641    // - redir-present + SHNULLCMD     → args=[":"]
1642    // - redir-present + readnullcmd   → args=[readnullcmd]
1643    // - redir-present + default       → args=[nullcmd]
1644    // - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
1645    // - NO redir + default            → lastval=cmdoutval, return (c:3372-3406)
1646    //
1647    // zshrs's `execcmd_compile_head` doesn't receive `redir` (it
1648    // takes `args` only). The cases that DEPEND on redirs are handled by
1649    // `compile_zsh.rs::compile_redir` before this dispatch fires; the
1650    // remaining cases collapse into the single `is_empty_command`
1651    // flag below. Both NO-redir sub-cases produce the same observable
1652    // outcome (lastval=0, return without invoking anything), so a
1653    // single flag suffices.
1654    let is_empty_command = precmd_skip >= preargs.len();
1655
1656    // =================== WARNING — DIVERGENCE ====================
1657    // c:3285+: prefork-substitution, magic_assign decision, multio
1658    // setup, varspc evaluation, and the actual execbuiltin /
1659    // runshfunc / execute call. ~1300 lines of interpreter-only
1660    // code, entirely replaced by fusevm bytecode dispatch in
1661    // `src/extensions/compile_zsh.rs::compile_simple` and the
1662    // opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
1663    // The return value below feeds those compile-time decisions.
1664    // =============================================================
1665
1666    let _ = hn;
1667    execcmd_dispatch {
1668        precmd_skip,
1669        is_builtin,
1670        is_shfunc,
1671        cflags,
1672        orig_cflags,
1673        use_defpath,
1674        has_command_vv,
1675        exec_argv0,
1676        is_empty_command,
1677        preargs,
1678    }
1679}
1680
1681// =============================================================================
1682// Leaf-function ports — c:283 (parse_string) and below. The wordcode walker
1683// (execlist / execpline / execcmd_exec) is ported below; the fusevm bytecode
1684// VM is the primary execution path (see the WARNING block in execcmd_exec).
1685// =============================================================================
1686
1687/// Port of `parse_string()` from `Src/exec.c:283` — C decl `parse_string(char *s, int reset_lineno)`.
1688///
1689/// C body:
1690/// ```c
1691/// Eprog p; zlong oldlineno;
1692/// zcontext_save();
1693/// inpush(s, INP_LINENO, NULL);
1694/// strinbeg(0);
1695/// oldlineno = lineno;
1696/// if (reset_lineno) lineno = 1;
1697/// p = parse_list();
1698/// lineno = oldlineno;
1699/// if (tok == LEXERR && !lastval) lastval = 1;
1700/// strinend();
1701/// inpop();
1702/// zcontext_restore();
1703/// return p;
1704/// ```
1705///
1706/// Parses an arbitrary string as a zsh command list, returning the
1707/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
1708/// `bin_eval` for `eval`, and the autoload path.
1709pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
1710    use crate::ported::lex::{LEX_FILE_WINDOW_STRIN, LEX_INPUT, LEX_POS, LEX_UNGET_BUF};
1711
1712    // c:285-286
1713    let p: Option<eprog>;
1714    let oldlineno: i64;
1715
1716    zcontext_save(); // c:288
1717
1718    // zshrs-only: park the `LEX_INPUT` window. C has ONE reader — the
1719    // input stack — so `inpush` at c:289 is all the isolation `parse_list`
1720    // needs. zshrs lexes from a second source as well, the `LEX_INPUT` /
1721    // `LEX_POS` char window (lex.rs:5181), which `inpush` does not touch,
1722    // so a nested parse ran on whatever text the OUTER lexer still had
1723    // parked there. That was invisible while every caller's window was
1724    // already drained at execution time, and became live when `bin_dot`
1725    // started running a plain sourced file per command
1726    // (`execute_script_per_command`, vm_helper.rs:3321): the file is now
1727    // the lexer's own window WHILE its commands execute, so
1728    // `zstyle -e ':x' lc 'reply=(1)'` (zutil.rs:856 → here) parsed the
1729    // REST OF THE FILE as its eval body and everything after that line
1730    // silently vanished. Draining the window makes `hgetc`'s two-input
1731    // bridge read only the `inpush`ed frame — the same parking
1732    // `parsestrnoerr` (lex.rs:3402-3444) and `parse_isolated`
1733    // (vm_helper.rs:843-877) already do around their own nested parses.
1734    let saved_input = LEX_INPUT.with_borrow(|w| w.clone());
1735    let saved_pos = LEX_POS.get();
1736    let saved_unget = LEX_UNGET_BUF.with_borrow(|b| b.clone());
1737    let saved_file_window = LEX_FILE_WINDOW_STRIN.get();
1738    let saved_in_lexstop = crate::ported::input::lexstop.with(|c| c.get());
1739    LEX_INPUT.with_borrow_mut(|w| w.clear());
1740    LEX_POS.set(0);
1741    LEX_UNGET_BUF.with_borrow_mut(|b| b.clear());
1742    // The pushed frame is a STRING unit, whatever the outer window was.
1743    LEX_FILE_WINDOW_STRIN.set(0);
1744
1745    inpush(s, INP_LINENO, None); // c:289
1746    strinbeg(0); // c:290
1747    oldlineno = LEX_LINENO.get() as i64; // c:291
1748    if reset_lineno != 0 {
1749        // c:292
1750        LEX_LINENO.set(1); // c:293
1751    }
1752    p = parse_list(); // c:294
1753    LEX_LINENO.set(oldlineno as u64); // c:295
1754                                      // c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
1755    if tok() == LEXERR && LASTVAL.load(Ordering::Relaxed) == 0 {
1756        LASTVAL.store(1, Ordering::Relaxed);
1757    }
1758    strinend(); // c:298
1759    inpop(); // c:299
1760
1761    // Put the outer window back where the nested parse found it. Draining
1762    // it set the input-side `lexstop`, whose lex.rs half is the only one
1763    // `zcontext` covers (parse_isolated, vm_helper.rs:857), so the outer
1764    // reader would otherwise resume at EOF.
1765    LEX_INPUT.with_borrow_mut(|w| *w = saved_input);
1766    LEX_POS.set(saved_pos);
1767    LEX_UNGET_BUF.with_borrow_mut(|b| *b = saved_unget);
1768    LEX_FILE_WINDOW_STRIN.set(saved_file_window);
1769    crate::ported::input::lexstop.with(|c| c.set(saved_in_lexstop));
1770
1771    zcontext_restore(); // c:300
1772    p // c:301
1773}
1774
1775/// Port of `isgooderr()` from `Src/exec.c:652` — C decl `isgooderr(int e, char *dir)`.
1776///
1777/// C body:
1778/// ```c
1779/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
1780/// return ((e != EACCES || !access(dir, X_OK)) &&
1781///         e != ENOENT && e != ENOTDIR);
1782/// ```
1783///
1784/// errno classifier for `execve` failures during PATH search: if the
1785/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
1786/// it's "expected" (try next PATH entry); otherwise it's a real
1787/// failure worth surfacing.
1788pub fn isgooderr(e: i32, dir: &str) -> bool {
1789    // c:652
1790    // c:Src/exec.c:658-659 — `(e != EACCES || !access(dir, X_OK)) &&
1791    //   e != ENOENT && e != ENOTDIR`. C's `access(dir, X_OK)` returns
1792    //   0 on success / -1 on failure. The previous Rust port used
1793    //   `metadata().permissions().mode() & 0o111` which reports the
1794    //   X bit even when the path doesn't exist as the EFFECTIVE caller
1795    //   (root, ACLs, capabilities all flip access() vs raw mode).
1796    //   `/no/such/dir` metadata() fails → returned false for
1797    //   dir_x_ok, then `!false` = true, giving "good error" for a
1798    //   nonexistent path. Use libc::access directly to match C exactly.
1799    let unmeta_dir = unmeta(dir);
1800    let cstr = std::ffi::CString::new(unmeta_dir.as_bytes()).unwrap_or_default();
1801    let access_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1802    (e != libc::EACCES || access_ok) && e != libc::ENOENT && e != libc::ENOTDIR
1803}
1804
1805/// Port of `iscom()` from `Src/exec.c:962` — C decl `iscom(char *s)`.
1806///
1807/// C body:
1808/// ```c
1809/// struct stat statbuf;
1810/// char *us = unmeta(s);
1811/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
1812///         S_ISREG(statbuf.st_mode));
1813/// ```
1814///
1815/// True iff `s` names an executable regular file (X-perm + S_IFREG).
1816/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
1817/// validate candidate paths before exec.
1818pub fn iscom(s: &str) -> bool {
1819    // c:962
1820    let us = unmeta(s); // c:965
1821                        // c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
1822    let cstr = match std::ffi::CString::new(us.as_str()) {
1823        Ok(c) => c,
1824        Err(_) => return false,
1825    };
1826    let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1827    if !x_ok {
1828        return false;
1829    }
1830    let meta = match std::fs::metadata(&us) {
1831        Ok(m) => m,
1832        Err(_) => return false,
1833    };
1834    meta.file_type().is_file()
1835}
1836
1837/// Port of `isreallycom()` from `Src/exec.c:973` — C decl `isreallycom(Cmdnam cn)`.
1838///
1839/// Verify that a hashed/cached cmdnamtab entry still names a real
1840/// external command (X-perm + regular file). For HASHED entries
1841/// (`cn->u.cmd` carries the absolute path), test the path directly;
1842/// otherwise concatenate `name[0] + "/" + nam` and test that.
1843/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
1844/// turn into a failed `execve` syscall.
1845pub fn isreallycom(cn: &cmdnam) -> bool {
1846    // c:972
1847    let fullnam: String;
1848    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
1849        // c:977-978 — `strcpy(fullnam, cn->u.cmd);`
1850        fullnam = cn.cmd.clone().unwrap_or_default();
1851    } else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
1852        // c:979-980 — `if (!cn->u.name) return 0;`
1853        return false;
1854    } else {
1855        // c:982-984 — `strcpy + strcat("/") + strcat(nam)`
1856        let path0 = &cn.name.as_ref().unwrap()[0];
1857        fullnam = format!("{}/{}", path0, cn.node.nam);
1858    }
1859    iscom(&fullnam) // c:986
1860}
1861
1862/// Port of `isrelative()` from `Src/exec.c:996` — C decl `isrelative(char *s)`.
1863///
1864/// C body:
1865/// ```c
1866/// if (*s != '/') return 1;
1867/// for (; *s; s++)
1868///     if (*s == '.' && s[-1] == '/' &&
1869///         (s[1] == '/' || s[1] == '\0' ||
1870///          (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
1871///         return 1;
1872/// return 0;
1873/// ```
1874///
1875/// True iff `s` either doesn't start with `/` OR contains a `./` or
1876/// `../` component anywhere. Used by `cd` resolution and PATH-cache
1877/// invalidation to detect non-canonical paths.
1878pub fn isrelative(s: &str) -> i32 {
1879    // c:996
1880    let bytes = s.as_bytes();
1881    if bytes.is_empty() || bytes[0] != b'/' {
1882        // c:998
1883        return 1; // c:999
1884    }
1885    // c:1000-1004 — walk for `./` or `../` components.
1886    for i in 1..bytes.len() {
1887        let c = bytes[i];
1888        let prev = bytes[i - 1];
1889        if c == b'.' && prev == b'/' {
1890            let next = bytes.get(i + 1).copied().unwrap_or(0);
1891            if next == b'/' || next == 0 {
1892                // c:1002
1893                return 1;
1894            }
1895            if next == b'.' {
1896                let next2 = bytes.get(i + 2).copied().unwrap_or(0);
1897                if next2 == b'/' || next2 == 0 {
1898                    // c:1003
1899                    return 1;
1900                }
1901            }
1902        }
1903    }
1904    0 // c:1005
1905}
1906
1907/// Port of `setunderscore()` from `Src/exec.c:2652` — C decl `setunderscore(char *str)`.
1908///
1909/// C body:
1910/// ```c
1911/// queue_signals();
1912/// if (str && *str) {
1913///     size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
1914///     if (nl > underscorelen || (underscorelen - nl) > 64) {
1915///         zfree(zunderscore, underscorelen);
1916///         zunderscore = (char *) zalloc(underscorelen = nl);
1917///     }
1918///     strcpy(zunderscore, str);
1919///     underscoreused = l;
1920/// } else {
1921///     ... reset zunderscore = "" ...
1922/// }
1923/// unqueue_signals();
1924/// ```
1925///
1926/// Sets the `$_` global to the last argument of the most recent
1927/// command. Called from `execcmd_exec` (c:3936) per `last_status`
1928/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
1929pub fn setunderscore(str: &str) {
1930    // c:2652
1931    queue_signals(); // c:2654
1932    if !str.is_empty() {
1933        // c:2655 `if (str && *str)`
1934        // c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
1935        let mut zu = zunderscore.lock().unwrap();
1936        *zu = str.to_string();
1937        let nl = (str.len() + 1 + 31) & !31; // c:2656
1938        underscorelen.store(nl, Ordering::Relaxed); // c:2660
1939        underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
1940    // c:2663
1941    } else {
1942        // c:2664
1943        let mut zu = zunderscore.lock().unwrap();
1944        zu.clear(); // c:2669 `*zunderscore = '\0';`
1945        underscoreused.store(1, Ordering::Relaxed); // c:2670
1946    }
1947    unqueue_signals(); // c:2672
1948}
1949
1950/// Port of `mpipe()` from `Src/exec.c:5160` — C decl `mpipe(int *pp)`.
1951///
1952/// C body:
1953/// ```c
1954/// if (pipe(pp) < 0) {
1955///     zerr("pipe failed: %e", errno);
1956///     return -1;
1957/// }
1958/// pp[0] = movefd(pp[0]);
1959/// pp[1] = movefd(pp[1]);
1960/// return 0;
1961/// ```
1962///
1963/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
1964/// fd range via `movefd`. Used by `getpipe` / `getproc` /
1965/// `spawnpipes` for process substitution and pipeline wiring.
1966pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
1967    // c:5160
1968    let mut fds: [libc::c_int; 2] = [-1; 2];
1969    if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
1970        // c:5162
1971        zerr(&format!(
1972            // c:5163
1973            "pipe failed: {}",
1974            std::io::Error::last_os_error()
1975        ));
1976        return -1; // c:5164
1977    }
1978    pp[0] = movefd(fds[0]); // c:5166
1979    pp[1] = movefd(fds[1]); // c:5167
1980    0 // c:5168
1981}
1982
1983/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
1984/// from `Src/exec.c:5289`. Anonymous-function name marker used by
1985/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
1986/// `() { ... }` anonymous function dispatch.
1987pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
1988
1989/// Port of `is_anonymous_function_name()` from `Src/exec.c:5300` — C decl `is_anonymous_function_name(const char *name)`.
1990///
1991/// C body:
1992/// ```c
1993/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
1994/// ```
1995///
1996/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
1997/// reporting and `whence -v` to skip / annotate anonymous functions.
1998pub fn is_anonymous_function_name(name: &str) -> i32 {
1999    // c:5300
2000    if name == ANONYMOUS_FUNCTION_NAME {
2001        // c:5302
2002        1
2003    } else {
2004        0
2005    }
2006}
2007
2008/// Port of `execsave()` from `Src/exec.c:6438` — C decl `execsave(void)`.
2009///
2010/// C body:
2011/// ```c
2012/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
2013/// es->list_pipe_pid = list_pipe_pid;
2014/// es->nowait = nowait;
2015/// es->pline_level = pline_level;
2016/// es->list_pipe_child = list_pipe_child;
2017/// es->list_pipe_job = list_pipe_job;
2018/// strcpy(es->list_pipe_text, list_pipe_text);
2019/// es->lastval = lastval;
2020/// es->noeval = noeval;
2021/// es->badcshglob = badcshglob;
2022/// es->cmdoutpid = cmdoutpid;
2023/// es->cmdoutval = cmdoutval;
2024/// es->use_cmdoutval = use_cmdoutval;
2025/// es->procsubstpid = procsubstpid;
2026/// es->trap_return = trap_return;
2027/// es->trap_state = trap_state;
2028/// es->trapisfunc = trapisfunc;
2029/// es->traplocallevel = traplocallevel;
2030/// es->noerrs = noerrs;
2031/// es->this_noerrexit = this_noerrexit;
2032/// es->underscore = ztrdup(zunderscore);
2033/// es->next = exstack;
2034/// exstack = es;
2035/// noerrs = cmdoutpid = 0;
2036/// ```
2037///
2038/// Snapshot every transient exec-context global onto the `exstack`
2039/// linked list so a signal-handler / trap-firing nested eval can
2040/// scribble freely; `execrestore` pops the frame back. Called by
2041/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
2042pub fn execsave() {
2043    // c:6438
2044    // c:6442 — `es = zalloc(sizeof(execstack));`
2045    let mut es = Box::new(execstack {
2046        // c:6442
2047        next: None,
2048        list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
2049        nowait: nowait.load(Ordering::Relaxed),               // c:6444
2050        pline_level: pline_level.load(Ordering::Relaxed),     // c:6445
2051        list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
2052        list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
2053        list_pipe_text: {
2054            // c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
2055            let mut buf = [0u8; JOBTEXTSIZE];
2056            if let Ok(s) = LIST_PIPE_TEXT.lock() {
2057                let bytes = s.as_bytes();
2058                let n = bytes.len().min(JOBTEXTSIZE - 1);
2059                buf[..n].copy_from_slice(&bytes[..n]);
2060            }
2061            buf
2062        },
2063        lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
2064        // c:6450 — `es->noeval = noeval;`. Snapshot math.c's
2065        // `int noeval` (the parse-only side-effect-skip counter)
2066        // via math.rs's pub accessor.
2067        noeval: crate::ported::math::m_noeval(),
2068        // c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
2069        // csh-glob diagnostic counter (glob.c:103 / glob.rs
2070        // BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
2071        // the outer command's per-line accounting.
2072        badcshglob: crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed), // c:6451
2073        cmdoutpid: cmdoutpid.load(Ordering::Relaxed),                        // c:6452
2074        cmdoutval: cmdoutval.load(Ordering::Relaxed),                        // c:6453
2075        use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed),                // c:6454
2076        procsubstpid: procsubstpid.load(Ordering::Relaxed),                  // c:6455
2077        trap_return: TRAP_RETURN.load(Ordering::Relaxed),                    // c:6456
2078        trap_state: TRAP_STATE.load(Ordering::Relaxed),                      // c:6457
2079        trapisfunc: trapisfunc.load(Ordering::Relaxed),                      // c:6458
2080        traplocallevel: traplocallevel.load(Ordering::Relaxed),              // c:6459
2081        noerrs: *crate::ported::utils::noerrs_lock().lock().unwrap(),        // c:6460
2082        this_noerrexit: this_noerrexit.load(Ordering::Relaxed),              // c:6461
2083        // c:6462 — `es->underscore = ztrdup(zunderscore);`
2084        underscore: Some(zunderscore.lock().unwrap().clone()),
2085    });
2086    // c:6463-6464 — `es->next = exstack; exstack = es;`
2087    let mut head = exstack.lock().unwrap();
2088    es.next = head.take();
2089    *head = Some(es);
2090    // c:6465 — `noerrs = cmdoutpid = 0;`
2091    *crate::ported::utils::noerrs_lock().lock().unwrap() = 0;
2092    cmdoutpid.store(0, Ordering::Relaxed);
2093}
2094
2095/// Port of `execrestore()` from `Src/exec.c:6470` — C decl `execrestore(void)`.
2096///
2097/// C body:
2098/// ```c
2099/// struct execstack *en = exstack;
2100/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
2101/// queue_signals();
2102/// exstack = exstack->next;
2103/// list_pipe_pid = en->list_pipe_pid;
2104/// nowait = en->nowait;
2105/// pline_level = en->pline_level;
2106/// list_pipe_child = en->list_pipe_child;
2107/// list_pipe_job = en->list_pipe_job;
2108/// strcpy(list_pipe_text, en->list_pipe_text);
2109/// lastval = en->lastval;
2110/// noeval = en->noeval;
2111/// badcshglob = en->badcshglob;
2112/// cmdoutpid = en->cmdoutpid;
2113/// cmdoutval = en->cmdoutval;
2114/// use_cmdoutval = en->use_cmdoutval;
2115/// procsubstpid = en->procsubstpid;
2116/// trap_return = en->trap_return;
2117/// trap_state = en->trap_state;
2118/// trapisfunc = en->trapisfunc;
2119/// traplocallevel = en->traplocallevel;
2120/// noerrs = en->noerrs;
2121/// this_noerrexit = en->this_noerrexit;
2122/// setunderscore(en->underscore);
2123/// zsfree(en->underscore);
2124/// free(en);
2125/// unqueue_signals();
2126/// ```
2127///
2128/// Pop the top `execstack` frame and restore every transient
2129/// exec-context global. Inverse of `execsave`.
2130pub fn execrestore() {
2131    // c:6470
2132    let mut head = exstack.lock().unwrap();
2133    let en = match head.take() {
2134        // c:6472 + c:6477
2135        Some(en) => en,
2136        None => {
2137            // c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
2138            crate::DPUTS!(true, "BUG: execrestore() without execsave()");
2139            return;
2140        }
2141    };
2142    queue_signals(); // c:6476
2143    *head = en.next; // c:6477
2144    drop(head); // release lock before scalar restores
2145
2146    list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
2147    nowait.store(en.nowait, Ordering::Relaxed); // c:6480
2148    pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
2149    list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
2150    list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
2151                                                              // c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
2152    if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
2153        let nul = en
2154            .list_pipe_text
2155            .iter()
2156            .position(|&b| b == 0)
2157            .unwrap_or(JOBTEXTSIZE);
2158        *s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
2159    }
2160    LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
2161                                                  // c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
2162                                                  // counter from the saved frame.
2163    crate::ported::math::m_noeval_set(en.noeval);
2164    // c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
2165    // diagnostic counter saved on entry.
2166    crate::ported::glob::BADCSHGLOB.store(en.badcshglob, Ordering::Relaxed);
2167    cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
2168    cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
2169    use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
2170    procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
2171    TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
2172    TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
2173    trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
2174    traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
2175    *crate::ported::utils::noerrs_lock().lock().unwrap() = en.noerrs; // c:6496
2176    this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
2177                                                                // c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
2178    if let Some(ref u) = en.underscore {
2179        setunderscore(u); // c:6498
2180    }
2181    // c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
2182    unqueue_signals(); // c:6502
2183}
2184
2185/// Port of `execstring()` from `Src/exec.c:1228` — C decl `execstring(char *s, int dont_change_job, int exiting, char *context)`.
2186///
2187/// C body:
2188/// ```c
2189/// Eprog prog;
2190/// pushheap();
2191/// if (isset(VERBOSE)) {
2192///     zputs(s, stderr);
2193///     fputc('\n', stderr);
2194///     fflush(stderr);
2195/// }
2196/// if ((prog = parse_string(s, 0)))
2197///     execode(prog, dont_change_job, exiting, context);
2198/// popheap();
2199/// ```
2200///
2201/// Public entry — execute an arbitrary string as a zsh command list.
2202/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
2203/// body executors, command substitution body runners.
2204///
2205/// =================== WARNING — DIVERGENCE ====================
2206/// The C path is `parse_string` → `execode` → `execlist` (wordcode
2207/// walker). zshrs replaces `execode/execlist` with the fusevm
2208/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
2209/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
2210/// parse+execute chain delegates to the fusevm entry. When `execlist`
2211/// lands as a strict 1:1 port, swap the delegate for the canonical
2212/// chain.
2213/// =============================================================
2214pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
2215    // c:1228
2216    pushheap(); // c:1232
2217                // c:1233-1237 — VERBOSE banner.
2218    if isset(VERBOSE) {
2219        // c:1233
2220        let mut stderr = std::io::stderr().lock();
2221        use std::io::Write;
2222        let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
2223        let _ = stderr.write_all(b"\n"); // c:1235
2224        let _ = stderr.flush(); // c:1236
2225    }
2226    // c:1238-1239 — parse + execode. zshrs delegates the parse+VM
2227    // chain to the fusevm pipeline via the exec_hooks fn-ptr
2228    // installed by fusevm_bridge at startup. Direct
2229    // `with_executor` / ShellExecutor reach-in from src/ported/ is
2230    // forbidden — see memory feedback_no_exec_script_from_ported.
2231    // c:1277 — `parse_string(s, 0)`: reset_lineno is 0, so the string is
2232    // parsed from the line being executed, not from 1. A `${| … }` body
2233    // re-run on the glob path left `lineno` at 1 and the NOMATCH error
2234    // for its own word said `zsh:1:`.
2235    let running = crate::ported::lex::lineno();
2236    if crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_at_lineno(s, running))
2237        .is_none()
2238    {
2239        let _ = crate::ported::exec::execute_script_zsh_pipeline(s);
2240    }
2241    popheap(); // c:1240
2242}
2243
2244// =====================================================================
2245// Function-wrapper chain — `Src/module.c:570` `FuncWrap wrappers`, as
2246// walked by `runshfunc` (`Src/exec.c:6166`).
2247// =====================================================================
2248
2249/// Which module-registered wrapper a [`BodyWrap`] node stands for.
2250///
2251/// C reaches the handler through a fn pointer stored in the node
2252/// (`WrapFunc handler` — `Src/zsh.h:1365`); this port dispatches on the
2253/// discriminant instead, because the two handlers take a body DELEGATE
2254/// that a `WrapFunc` pointer cannot carry (see [`BodyWrap`]).
2255#[derive(Clone, Copy)]
2256enum WrapKind {
2257    /// `WRAPDEF(comp_wrapper)` — `Src/Zle/complete.c:1694-1695`.
2258    Comp,
2259    /// `WRAPDEF(wrap_private)` — `Src/Modules/param_private.c:541-542`.
2260    Private,
2261    /// `WRAPDEF(zprof_wrapper)` — `Src/Modules/zprof.c:318-320`.
2262    Zprof,
2263    /// `WRAPDEF(ksh93_wrapper)` — `Src/Modules/ksh93.c:230-232`.
2264    Ksh93,
2265}
2266
2267/// One node of the `wrappers` linked list C keeps at `Src/module.c:570`,
2268/// tail-appended by `addwrapper` (`Src/module.c:592-597`) from a module's
2269/// `boot_`.
2270///
2271/// Why the live chain is this table and not `module::WRAPPERS`: C's
2272/// `struct funcwrap` (`Src/zsh.h:1362-1367`) stores an
2273/// `int (*handler)(Eprog prog, FuncWrap w, char *name)`, and each handler
2274/// re-enters the chain on its own with `runshfunc(prog, w->next, name)`
2275/// (`Src/Zle/complete.c:1591`, `Src/Modules/param_private.c:556`) because
2276/// in C the function body is an `Eprog` anyone can walk. A zshrs function
2277/// body is not — it is `doshfunc`'s caller-supplied `body_runner` closure
2278/// (fusevm chunk runner, Rust compsys port, or plugin override), so the
2279/// port's handlers take the continuation as an explicit delegate that a
2280/// plain fn pointer has nowhere to hold. `zsh_h::funcwrap` and
2281/// `module::WRAPPERS` keep C's pointer shape so the `addwrapper` /
2282/// `deletewrapper` ports stay 1:1; this table is the same list expressed
2283/// in the shape `runshfunc` can actually call.
2284struct BodyWrap {
2285    /// The module whose `boot_` calls `addwrapper(m, wrapper)`. C reads it
2286    /// back through `wrap->module` for the refcount at c:6178/6180 and the
2287    /// deferred `unload_module` at c:6184.
2288    module: &'static str,
2289    /// Which handler this node holds.
2290    kind: WrapKind,
2291}
2292
2293/// The chain in `addwrapper` tail-append order — module boot order, so
2294/// the wrapper of the module booted first ends up OUTERMOST.
2295static BODY_WRAPPERS: &[BodyWrap] = &[
2296    // `Src/Modules/ksh93.c:230-232` — `static struct funcwrap wrapper[] =
2297    // { WRAPDEF(ksh93_wrapper) };` — installed by c:260
2298    // `return addwrapper(m, wrapper);` in `zsh/ksh93`'s `boot_`, removed
2299    // by c:269 `deletewrapper(m, wrapper);` in `cleanup_`. zsh/ksh93 is a
2300    // `load=yes` module (config.modules) that boots ahead of
2301    // zsh/param/private and zsh/zprof, so its node comes first. The
2302    // handler never calls the body — it creates the `.sh.*` locals and
2303    // returns 1 (c:227), letting the walk continue.
2304    BodyWrap {
2305        module: "zsh/ksh93",
2306        kind: WrapKind::Ksh93,
2307    },
2308    // `Src/Zle/complete.c:1694-1695` — `static struct funcwrap wrapper[] =
2309    // { WRAPDEF(comp_wrapper) };` — installed by c:1767
2310    // `return addwrapper(m, wrapper);` in `zsh/complete`'s `boot_`.
2311    BodyWrap {
2312        module: "zsh/complete",
2313        kind: WrapKind::Comp,
2314    },
2315    // `Src/Modules/param_private.c:541-542` — `static struct funcwrap
2316    // wrapper[] = { WRAPDEF(wrap_private) };` — installed by c:712
2317    // `return addwrapper(m, wrapper);` in `zsh/param/private`'s `boot_`.
2318    BodyWrap {
2319        module: "zsh/param/private",
2320        kind: WrapKind::Private,
2321    },
2322    // `Src/Modules/zprof.c:318-320` — `static struct funcwrap wrapper[] =
2323    // { WRAPDEF(zprof_wrapper) };` — installed by c:362
2324    // `return addwrapper(m, wrapper);` in `zsh/zprof`'s `boot_`, removed
2325    // by c:371 `deletewrapper(m, wrapper);` in `cleanup_`. `zsh/zprof` is
2326    // never linked in at startup, so it always boots after the two nodes
2327    // above and `addwrapper`'s tail-append (`Src/module.c:591-595`) puts
2328    // it LAST — innermost, closest to the body, which is what makes its
2329    // measured window the function itself.
2330    BodyWrap {
2331        module: "zsh/zprof",
2332        kind: WrapKind::Zprof,
2333    },
2334];
2335
2336/// Port of `runshfunc()` from `Src/exec.c:6166` — C decl `runshfunc(Eprog prog, FuncWrap wrap, char *name)`.
2337/// the wrapper-chain walk that stands between
2338/// `doshfunc` and a function body. C:
2339///
2340/// ```c
2341/// queue_signals();
2342/// ou = zalloc(ouu = underscoreused);
2343/// if (ou) memcpy(ou, zunderscore, underscoreused);
2344/// while (wrap) {                       // wrapper chain
2345///     wrap->module->wrapper++;
2346///     cont = wrap->handler(prog, wrap->next, name);
2347///     wrap->module->wrapper--;
2348///     if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
2349///         unload_module(wrap->module);
2350///     if (!cont) {                     // wrapper handled it
2351///         if (ou) zfree(ou, ouu);
2352///         unqueue_signals();
2353///         return;
2354///     }
2355///     wrap = wrap->next;
2356/// }
2357/// startparamscope();
2358/// execode(prog, 1, 0, "shfunc");
2359/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
2360/// endparamscope();
2361/// unqueue_signals();
2362/// ```
2363///
2364/// **RUST-ONLY ADAPTATION — what this port does and does NOT own.** When
2365/// zshrs replaced C's wordcode walk with a body delegate, everything in
2366/// the listing above except the chain walk moved into `doshfunc`, which is
2367/// the only caller:
2368///
2369/// | C line | Where it lives in zshrs |
2370/// |---|---|
2371/// | c:6171 / c:6202 `queue_signals` / `unqueue_signals` | `doshfunc`'s own outer pair (c:5835 / c:6128) already brackets this window; C's inner pair is a nested refcount bump inside it |
2372/// | c:6173-6175 / c:6196-6198 `$_` save / `setunderscore` | `doshfunc`, around this call (`saved_zunderscore`) |
2373/// | c:6194 `startparamscope()` | `doshfunc` — `inc_locallevel()` before the funcstack push |
2374/// | c:6200 `endparamscope()` | `doshfunc`'s epilogue |
2375/// | c:6195 `execode(prog, 1, 0, "shfunc")` | the `body` delegate |
2376///
2377/// Duplicating any of them here would double-apply it, so the body below
2378/// is c:6177-6193 plus the c:6195 body call, and nothing else.
2379///
2380/// WARNING: param names don't match C — Rust=(_prog, wrap, name, body) vs
2381/// C=(prog, wrap, name). `wrap` is an INDEX into [`BODY_WRAPPERS`] rather
2382/// than a `FuncWrap` pointer (a `Vec`-shaped chain has no `->next` to
2383/// hand out); `wrap = 0` is C's `wrappers` head and `wrap = i + 1` is the
2384/// `wrap->next` a handler re-enters with. `_prog` is unused because the
2385/// body arrives as `body`, and the return value is the body's exit status
2386/// (C returns void and communicates through `lastval`).
2387pub fn runshfunc(
2388    _prog: Option<&eprog>, // c:6166
2389    wrap: usize,           // c:6166 (FuncWrap wrap — see WARNING)
2390    name: &str,            // c:6166
2391    body: &mut dyn FnMut() -> i32,
2392) -> i32 {
2393    // c:6166
2394    let mut w_idx = wrap;
2395    while w_idx < BODY_WRAPPERS.len() {
2396        // c:6177 — `while (wrap)`
2397        let w = &BODY_WRAPPERS[w_idx];
2398
2399        // Chain membership. C asks nothing here because a wrapper is in
2400        // the list only while its module has it registered; this port
2401        // evaluates the equivalent condition per node.
2402        let armed = match w.kind {
2403            // zshrs statically links the completion system — `compadd` /
2404            // `compset` are always present and `zsh/complete` is never
2405            // dlopen'd — so there is no boot event to key on. The
2406            // wrapper's own first statement is the gate that matters and
2407            // it is exact: `if (incompfunc != 1) return 1;`
2408            // (`Src/Zle/complete.c:1558-1559`). `incompfunc` is 1 only
2409            // between `Src/Zle/compcore.c:815` and c:841, i.e. strictly
2410            // inside `callcompfunc`, which is the only thing that can run
2411            // a completion function at all. Hoisting the test here keeps
2412            // an ordinary function call at one relaxed load with no chain
2413            // node entered.
2414            WrapKind::Comp => crate::ported::zle::complete::INCOMPFUNC.load(Ordering::Relaxed) == 1,
2415            // Module BOOT STATE (`MOD_INIT_B` — the bit `zmodload -e`
2416            // reads and `load_module` sets after `do_boot_module`,
2417            // `Src/module.c:2317`), which is exactly C's condition for
2418            // `wrap_private` being in the list: the `private` dispatch
2419            // runs `require_module` via the `Src/exec.c:2710` autofeature
2420            // path, which boots the module. Deliberately NOT
2421            // `is_loaded()` — default-registered static modules carry
2422            // `MOD_LINKED` from startup, which would arm the wrapper
2423            // before any `private` was ever used.
2424            WrapKind::Private => crate::ported::module::MODULESTAB
2425                .lock()
2426                .map(|t| {
2427                    t.modules
2428                        .get(w.module)
2429                        .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
2430                        .unwrap_or(false)
2431                })
2432                .unwrap_or(false),
2433            // Genuine `addwrapper` membership: `zsh/zprof`'s `boot_`
2434            // calls `addwrapper(m, wrapper)` (`Src/Modules/zprof.c:362`)
2435            // and its `cleanup_` calls `deletewrapper` (c:371), and both
2436            // mirror the list into `module::WRAPPERS_ADDED`. So this is
2437            // C's `while (wrap)` test verbatim — "is this node in the
2438            // `wrappers` list" — costing one relaxed load, with no lock
2439            // and no allocation when `zsh/zprof` was never loaded.
2440            WrapKind::Zprof => {
2441                (crate::ported::module::WRAPPERS_ADDED.load(Ordering::Relaxed)
2442                    & crate::ported::module::WRAPPER_BIT_ZPROF)
2443                    != 0
2444            }
2445            // Same `addwrapper` membership test as zprof: zsh/ksh93's boot_
2446            // (c:260) and cleanup_ (c:269) mirror it into WRAPPERS_ADDED.
2447            WrapKind::Ksh93 => {
2448                (crate::ported::module::WRAPPERS_ADDED.load(Ordering::Relaxed)
2449                    & crate::ported::module::WRAPPER_BIT_KSH93)
2450                    != 0
2451            }
2452        };
2453        if !armed {
2454            // Not in C's `wrappers` list right now — no node to step over.
2455            w_idx += 1;
2456            continue;
2457        }
2458
2459        // c:6178 — `wrap->module->wrapper++;` — hold the module open so a
2460        // recursive unload during the handler defers until we return.
2461        if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2462            if let Some(m) = tab.modules.get_mut(w.module) {
2463                m.wrapper += 1;
2464            }
2465        }
2466
2467        // c:6179 — `cont = wrap->handler(prog, wrap->next, name);`. The
2468        // closure IS `wrap->next` made callable: the rest of the chain,
2469        // then the body — which is what each handler's own
2470        // `runshfunc(prog, w, name)` reaches in C.
2471        let mut status = 0;
2472        let cont = {
2473            let mut run_next = || runshfunc(_prog, w_idx + 1, name, &mut *body);
2474            match w.kind {
2475                WrapKind::Comp => crate::ported::zle::complete::comp_wrapper(
2476                    std::ptr::null(),
2477                    std::ptr::null(),
2478                    name,
2479                    || status = run_next(), // complete.c:1591
2480                ),
2481                WrapKind::Private => crate::ported::modules::param_private::wrap_private(
2482                    std::ptr::null(),
2483                    std::ptr::null(),
2484                    std::ptr::null_mut(),
2485                    || status = run_next(), // param_private.c:556
2486                ),
2487                WrapKind::Zprof => crate::ported::modules::zprof::zprof_wrapper(
2488                    std::ptr::null(),
2489                    std::ptr::null(),
2490                    name,
2491                    || status = run_next(), // zprof.c:285
2492                ),
2493                // ksh93.c:143-227 never re-enters the chain: it sets up the
2494                // `.sh.*` locals and returns 1, so `run_next` stays unused
2495                // and the walk continues at c:6192.
2496                WrapKind::Ksh93 => {
2497                    let cname = std::ffi::CString::new(name).unwrap_or_default();
2498                    crate::ported::modules::ksh93::ksh93_wrapper(
2499                        std::ptr::null(),
2500                        std::ptr::null(),
2501                        cname.as_ptr() as *mut libc::c_char,
2502                    )
2503                }
2504            }
2505        };
2506
2507        // c:6180 — `wrap->module->wrapper--;`
2508        // c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD))
2509        //                    unload_module(wrap->module);`
2510        let should_unload = {
2511            if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2512                if let Some(m) = tab.modules.get_mut(w.module) {
2513                    m.wrapper -= 1;
2514                    m.wrapper == 0 && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
2515                } else {
2516                    false
2517                }
2518            } else {
2519                false
2520            }
2521        };
2522        if should_unload {
2523            if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2524                let _ = tab.unload_module(w.module); // c:6184
2525            }
2526        }
2527
2528        if cont == 0 {
2529            // c:6186 — the wrapper claimed the call and has already run
2530            // the body through the delegate.
2531            return status; // c:6190
2532        }
2533        w_idx += 1; // c:6192
2534    }
2535    // c:6195 — `execode(prog, 1, 0, "shfunc");`. execode runs execlist,
2536    // whose loop is `while (wc_code(code) == WC_LIST && !breaks && !retflag
2537    // && !errflag)` (c:1390), and doshfunc saved `breaks` without clearing
2538    // it (c:5859): a function entered while a `break` is still unwinding —
2539    // `repeat 3 do f ${ for …; do break 2; done }; done` — runs no list and
2540    // leaves lastval alone (D10nofork.ztst "break N propagates").
2541    if crate::ported::builtin::BREAKS.load(Ordering::Relaxed) != 0 {
2542        return crate::ported::builtin::LASTVAL.load(Ordering::Relaxed);
2543    }
2544    body()
2545}
2546
2547/// Port of `sticky_emulation_dup()` from `Src/exec.c:5501` — C decl `sticky_emulation_dup(Emulation_options src, int useheap)`.
2548///
2549/// C body (`useheap` selects between heap-arena and permanent zalloc;
2550/// Rust collapses both into owned `Box` clones):
2551/// ```c
2552/// Emulation_options newsticky = useheap ?
2553///     hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
2554/// newsticky->emulation = src->emulation;
2555/// if (src->n_on_opts) {
2556///     size_t sz = src->n_on_opts * sizeof(*src->on_opts);
2557///     newsticky->n_on_opts = src->n_on_opts;
2558///     newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
2559///     memcpy(newsticky->on_opts, src->on_opts, sz);
2560/// }
2561/// if (src->n_off_opts) {
2562///     size_t sz = src->n_off_opts * sizeof(*src->off_opts);
2563///     newsticky->n_off_opts = src->n_off_opts;
2564///     newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
2565///     memcpy(newsticky->off_opts, src->off_opts, sz);
2566/// }
2567/// return newsticky;
2568/// ```
2569///
2570/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
2571/// at function-def time to snapshot the pending `sticky` global so
2572/// the function carries its own immutable copy.
2573pub fn sticky_emulation_dup(src: &emulation_options, _useheap: i32) -> Emulation_options {
2574    // c:5501
2575    // c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
2576    let mut newsticky = Box::new(emulation_options {
2577        emulation: src.emulation, // c:5505
2578        n_on_opts: 0,
2579        n_off_opts: 0,
2580        on_opts: Vec::new(),
2581        off_opts: Vec::new(),
2582    });
2583    // c:5506-5511 — copy on_opts.
2584    if src.n_on_opts != 0 {
2585        // c:5506
2586        newsticky.n_on_opts = src.n_on_opts; // c:5508
2587        newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
2588    }
2589    // c:5512-5517 — copy off_opts.
2590    if src.n_off_opts != 0 {
2591        // c:5512
2592        newsticky.n_off_opts = src.n_off_opts; // c:5514
2593        newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
2594    }
2595    newsticky // c:5519
2596}
2597
2598/// Port of `sticky_emulation_differs()` from `Src/exec.c:5770` — C decl `int sticky_emulation_differs(Emulation_options sticky2)`.
2599///
2600/// C body:
2601/// ```c
2602/// /* If no new sticky emulation, not a different emulation */
2603/// if (!sticky2)
2604///     return 0;
2605/// /* If no current sticky emulation, different */
2606/// if (!sticky)
2607///     return 1;
2608/// /* If basic emulation different, different */
2609/// if (sticky->emulation != sticky2->emulation)
2610///     return 1;
2611/// /* If differing numbers of options, different */
2612/// if (sticky->n_on_opts != sticky2->n_on_opts ||
2613///     sticky->n_off_opts != sticky2->n_off_opts)
2614///     return 1;
2615/// /* If different options turned on, different */
2616/// if (sticky->n_on_opts &&
2617///     memcmp(sticky->on_opts, sticky2->on_opts,
2618///            sticky->n_on_opts * sizeof(*sticky->on_opts)) != 0)
2619///     return 1;
2620/// /* If different options turned on, different */
2621/// if (sticky->n_off_opts &&
2622///     memcmp(sticky->off_opts, sticky2->off_opts,
2623///            sticky->n_off_opts * sizeof(*sticky->off_opts)) != 0)
2624///     return 1;
2625/// return 0;
2626/// ```
2627pub fn sticky_emulation_differs(sticky2: Option<&emulation_options>) -> i32 {
2628    // c:5829
2629    let Some(sticky2) = sticky2 else {
2630        return 0; // c:5832-5833
2631    };
2632    let guard = sticky.lock().unwrap_or_else(|e| e.into_inner());
2633    let Some(ref cur) = *guard else {
2634        return 1; // c:5835-5836
2635    };
2636    if cur.emulation != sticky2.emulation {
2637        return 1; // c:5838-5839
2638    }
2639    if cur.n_on_opts != sticky2.n_on_opts || cur.n_off_opts != sticky2.n_off_opts {
2640        return 1; // c:5841-5843
2641    }
2642    if cur.n_on_opts != 0 && cur.on_opts != sticky2.on_opts {
2643        return 1; // c:5850-5853
2644    }
2645    if cur.n_off_opts != 0 && cur.off_opts != sticky2.off_opts {
2646        return 1; // c:5855-5858
2647    }
2648    0 // c:5859
2649}
2650
2651/// Port of `shfunc_set_sticky()` from `Src/exec.c:5527` — C decl `shfunc_set_sticky(Shfunc shf)`.
2652///
2653/// C body:
2654/// ```c
2655/// if (sticky)
2656///     shf->sticky = sticky_emulation_dup(sticky, 0);
2657/// else
2658///     shf->sticky = NULL;
2659/// ```
2660///
2661/// Stamp the function with the current pending sticky-emulation
2662/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
2663pub fn shfunc_set_sticky(shf: &mut shfunc) {
2664    // c:5527
2665    let sticky_guard = sticky.lock().unwrap();
2666    if let Some(ref s) = *sticky_guard {
2667        // c:5529
2668        shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
2669    } else {
2670        // c:5531
2671        shf.sticky = None; // c:5532
2672    }
2673}
2674
2675/// Port of `search_defpath()` from `Src/exec.c:691` — C decl `search_defpath(char *cmd, char *pbuf, int plen)`.
2676///
2677/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
2678/// Used by `command -p` to bypass the user's `$PATH` and search the
2679/// system default (`/bin:/usr/bin:...`).
2680pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
2681    // c:691
2682    // c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
2683    for ps in DEFAULT_PATH.split(':') {
2684        // c:695
2685        // c:697 — `if (*ps == '/')`.
2686        if !ps.starts_with('/') {
2687            continue;
2688        }
2689        // c:700-707 — PATH_MAX bounds check on `<dir>` segment.
2690        if ps.len() >= plen {
2691            // c:700 / c:704
2692            continue; // c:701 / c:705
2693        }
2694        // c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
2695        let full_len = ps.len() + 1 + cmd.len();
2696        if full_len >= plen {
2697            // c:709
2698            continue; // c:710
2699        }
2700        let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
2701                                             // c:712 — `if (iscom(pbuf)) return pbuf;`
2702        if iscom(&buf) {
2703            // c:712
2704            return Some(buf); // c:713
2705        }
2706    }
2707    None // c:716
2708}
2709
2710/// Port of `checkclobberparam()` from `Src/exec.c:2178` — C decl `checkclobberparam(struct redir *f)`.
2711///
2712/// C body:
2713/// ```c
2714/// struct value vbuf; Value v;
2715/// char *s = f->varid; int fd;
2716/// if (!s) return 1;
2717/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
2718/// if (v->pm->node.flags & PM_READONLY) {
2719///     zwarn("can't allocate file descriptor to readonly parameter %s",
2720///           f->varid);
2721///     errno = 0;
2722///     return 0;
2723/// }
2724/// /* We can't clobber the value in the parameter if it's
2725///  * already an opened file descriptor */
2726/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
2727///     (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
2728///     fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
2729///     zwarn("can't clobber parameter %s containing file descriptor %d",
2730///          f->varid, fd);
2731///     errno = 0;
2732///     return 0;
2733/// }
2734/// return 1;
2735/// ```
2736///
2737/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
2738/// name) is writable and (under NOCLOBBER) doesn't currently hold an
2739/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
2740/// already emitted).
2741///
2742/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
2743/// NOCLOBBER is set and the param's value is the fd-number of an
2744/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
2745/// the existing fd doesn't get clobbered by the upcoming open(2).
2746pub fn checkclobberparam(f: &redir) -> i32 {
2747    // c:2178
2748    // c:2182 — `char *s = f->varid;`
2749    let s = match &f.varid {
2750        Some(v) => v.clone(),
2751        None => return 1, // c:2185-2186 — `if (!s) return 1;`
2752    };
2753    // c:2186 — `if (!(v = getvalue(&vbuf, &s, 0))) return 1;`
2754    let mut vbuf = crate::ported::zsh_h::value {
2755        pm: None,
2756        arr: Vec::new(),
2757        scanflags: 0,
2758        valflags: 0,
2759        start: 0,
2760        end: 0,
2761    };
2762    let mut cursor: &str = s.as_str();
2763    let v_opt = crate::ported::params::getvalue(Some(&mut vbuf), &mut cursor, 0);
2764    if v_opt.is_none() {
2765        return 1; // c:2187
2766    }
2767    // c:2188-2197 — readonly refusal via v->pm->node.flags.
2768    let readonly = vbuf
2769        .pm
2770        .as_ref()
2771        .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
2772        .unwrap_or(false);
2773    if readonly {
2774        // c:2191
2775        zwarn(&format!(
2776            // c:2192
2777            "can't allocate file descriptor to readonly parameter {}",
2778            s
2779        ));
2780        // c:2195 — `errno = 0;` not flagged as a system error.
2781        return 0; // c:2196
2782    }
2783    // c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
2784    // AND the param holds a valid fd that's already in our fdtable as
2785    // FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
2786    // open so we don't clobber it.
2787    if !isset(CLOBBER) {
2788        // c:2201 — `getstrvalue(v)` — read the param's string form.
2789        let val_str = crate::ported::params::getstrvalue(Some(&mut vbuf));
2790        if let Ok(fd) = val_str.trim().parse::<i32>() {
2791            // c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
2792            let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2793            if fd >= 0 && fd <= max_fd {
2794                let kind = fdtable_get(fd);
2795                if kind == FDT_EXTERNAL {
2796                    zwarn(&format!("{}: file descriptor {} already open", s, fd)); // c:2206-2210
2797                    return 0; // c:2211
2798                }
2799            }
2800        }
2801    }
2802    1 // c:2214
2803}
2804
2805/// Port of `clobber_open()` from `Src/exec.c:2221` — C decl `clobber_open(struct redir *f)`.
2806///
2807/// C body:
2808/// ```c
2809/// struct stat buf;
2810/// int fd, oerrno;
2811/// char *ufname = unmeta(f->name);
2812/// /* If clobbering, just open. */
2813/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
2814///     return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
2815/// /* If not clobbering, attempt to create file exclusively. */
2816/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
2817///     return fd;
2818/// /* If that fails, we are still allowed to open non-regular files. */
2819/// oerrno = errno;
2820/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
2821///     if (!fstat(fd, &buf)) {
2822///         if (!S_ISREG(buf.st_mode)) return fd;
2823///         /* CLOBBER_EMPTY allows re-use of empty regular files. */
2824///         if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
2825///     }
2826///     close(fd);
2827/// }
2828/// errno = oerrno;
2829/// return -1;
2830/// ```
2831///
2832/// Open the redir target for write with the NOCLOBBER rules:
2833/// - CLOBBER set or `>|` form → just open with O_TRUNC
2834/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
2835///   files (FIFOs, devices, sockets) OR empty regular files under
2836///   CLOBBEREMPTY.
2837pub fn clobber_open(f: &redir) -> i32 {
2838    // c:2221
2839    let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
2840    let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
2841        Ok(c) => c,
2842        Err(_) => return -1,
2843    };
2844    // c:2228-2230 — clobber path: just open + truncate.
2845    if isset(CLOBBER) || IS_CLOBBER_REDIR(f.typ) {
2846        // c:2228
2847        // c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
2848        let fd = unsafe {
2849            libc::open(
2850                ufname.as_ptr(),
2851                libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
2852                0o666 as libc::c_uint,
2853            )
2854        };
2855        return fd; // c:2230
2856    }
2857    // c:2233-2235 — try O_EXCL create first.
2858    let fd = unsafe {
2859        // c:2233
2860        libc::open(
2861            ufname.as_ptr(),
2862            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
2863            0o666 as libc::c_uint,
2864        )
2865    };
2866    if fd >= 0 {
2867        return fd; // c:2235
2868    }
2869    // c:2240 — `oerrno = errno;` — save for restoration on the recover path.
2870    let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2871    // c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
2872    // OR (CLOBBEREMPTY && size == 0).
2873    let fd = unsafe {
2874        // c:2241
2875        libc::open(
2876            ufname.as_ptr(),
2877            libc::O_WRONLY | libc::O_NOCTTY,
2878            0o666 as libc::c_uint,
2879        )
2880    };
2881    if fd != -1 {
2882        let mut buf: libc::stat = unsafe { std::mem::zeroed() };
2883        if unsafe { libc::fstat(fd, &mut buf) } == 0 {
2884            // c:2242
2885            // c:2243-2244 — non-regular file: accept.
2886            if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
2887                // c:2243
2888                return fd; // c:2244
2889            }
2890            // c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
2891            if isset(CLOBBEREMPTY) && buf.st_size == 0 {
2892                // c:2256
2893                return fd; // c:2257
2894            }
2895        }
2896        unsafe {
2897            libc::close(fd);
2898        } // c:2259
2899    }
2900    // c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
2901    // "file exists" not the noisier "couldn't reopen" trailing errno.
2902    // Per-platform errno setter: __error() on macOS, __errno_location()
2903    // on Linux. Without cfg gating the build breaks on Linux (CI).
2904    #[cfg(target_os = "macos")]
2905    unsafe {
2906        *libc::__error() = oerrno;
2907    }
2908    #[cfg(target_os = "linux")]
2909    unsafe {
2910        *libc::__errno_location() = oerrno;
2911    }
2912    -1 // c:2263
2913}
2914
2915/// Port of `findcmd()` from `Src/exec.c:897` — C decl `findcmd(char *arg0, int docopy, int default_path)`.
2916/// Walk `$PATH` (or DEFAULT_PATH under
2917/// `default_path=1`) for `arg0`, returning the matching path on
2918/// success. `_docopy` is the C source's "duplicate the result"
2919/// flag — Rust ownership covers it without an explicit copy step.
2920/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
2921/// `command -p`).
2922pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
2923    // c:897
2924    // c:903-908 — if (default_path) → search_defpath; return.
2925    if default_path != 0 {
2926        return search_defpath(arg0, libc::PATH_MAX as usize);
2927    }
2928    // c:936-938 — the table comes FIRST, and a miss populates it:
2929    //     cn = (Cmdnam) cmdnamtab->getnode(cmdnamtab, arg0);
2930    //     if (!cn && isset(HASHCMDS))
2931    //         cn = hashcmd(arg0, path);
2932    // This port went straight to the `$PATH` walk below, so `whence -p foo`
2933    // after `hash foo=/usr/bin/awk` reported nothing (a HASHED entry names a
2934    // path that no `$PATH` directory contains), and no lookup through
2935    // `findcmd` — `whence`, `type`, `command -v`, compctl — ever hashed
2936    // anything, where C hashes on every one of them.
2937    let cn: Option<cmdnam> = {
2938        // c:936
2939        let hit = cmdnamtab_lock()
2940            .read()
2941            .ok()
2942            .and_then(|t| t.get_including_disabled(arg0).cloned());
2943        match hit {
2944            Some(c) => Some(c),
2945            // c:937-938
2946            None if isset(crate::ported::zsh_h::HASHCMDS) => {
2947                let dirs: Vec<String> = getsparam("PATH")
2948                    .unwrap_or_default()
2949                    .split(':')
2950                    .map(String::from)
2951                    .collect();
2952                hashcmd(arg0, &dirs)
2953            }
2954            None => None,
2955        }
2956    };
2957    // c:939-940 — `if ((int) ztrlen(arg0) >= PATH_MAX) return NULL;`.
2958    if arg0.len() > libc::PATH_MAX as usize {
2959        return None;
2960    }
2961    // c:Src/exec.c:914-920 — `/`-bearing arg path resolution.
2962    //   if ((s = strchr(arg0, '/'))) {
2963    //       RET_IF_COM(arg0);   // ← unconditional accept on iscom hit
2964    //       if (arg0 == s || unset(PATHDIRS) ||
2965    //           !strncmp(arg0, "./", 2) ||
2966    //           !strncmp(arg0, "../", 3))
2967    //           return NULL;
2968    //   }
2969    // The Rust port had the iscom check gated on `starts_with('/')`,
2970    // so `type ./target/debug/zshrs` returned None even when the
2971    // file was executable. Bug #496 family.
2972    if arg0.contains('/') {
2973        if iscom(arg0) {
2974            return Some(arg0.to_string()); // c:915 RET_IF_COM
2975        }
2976        // c:916-919 — absolute OR PATHDIRS-off OR `./` / `../` →
2977        // give up here (no $PATH walk for these). Relative without
2978        // those prefixes falls through to the $PATH scan below for
2979        // the PATHDIRS=set case.
2980        if arg0.starts_with('/')
2981            || !isset(PATHDIRS)
2982            || arg0.starts_with("./")
2983            || arg0.starts_with("../")
2984        {
2985            return None;
2986        }
2987        // else fall through to PATH walk.
2988    }
2989    // c:948-977 — resolve through the node the table gave us.
2990    //     if (cn) {
2991    //         if (cn->node.flags & HASHED) nn = cn->u.cmd;
2992    //         else {
2993    //             for (pp = path; pp < cn->u.name; pp++)
2994    //                 if (**pp != '/') { buf = "<*pp>/<arg0>"; RET_IF_COM(buf); }
2995    //             nn = "<*cn->u.name>/<arg0>";
2996    //         }
2997    //         RET_IF_COM(nn);
2998    //     }
2999    let path = getsparam("PATH").unwrap_or_default();
3000    let dirs: Vec<&str> = path.split(':').collect();
3001    if let Some(cn) = cn.as_ref() {
3002        // c:948
3003        let nn = if (cn.node.flags & (crate::ported::zsh_h::HASHED as i32)) != 0 {
3004            // c:951-954
3005            cn.cmd.clone()
3006        } else {
3007            // c:956-968 — the `$path` entries BEFORE the matched one still
3008            // get a look-in, because `hashcmd` only ever matches an ABSOLUTE
3009            // entry (c:1058 `if (**pp == '/')`) and a relative one earlier in
3010            // `$path` takes precedence. `cn.name` is C's `cn->u.name` tail
3011            // slice, so its length recovers the index C compares against.
3012            let matched_at = dirs.len().saturating_sub(cn.name.as_ref().map_or(0, |v| v.len()));
3013            for d in dirs.iter().take(matched_at) {
3014                // c:956
3015                if !d.starts_with('/') {
3016                    // c:957
3017                    let buf = if d.is_empty() {
3018                        arg0.to_string() // c:959 — an empty entry means `.`
3019                    } else {
3020                        format!("{}/{}", d, arg0) // c:960-965
3021                    };
3022                    if iscom(&buf) {
3023                        return Some(buf); // c:966 RET_IF_COM
3024                    }
3025                }
3026            }
3027            // c:969-974 — `"%s/%s", *(cn->u.name), cn->node.nam`.
3028            cn.name
3029                .as_ref()
3030                .and_then(|v| v.first())
3031                .map(|d| format!("{}/{}", d, arg0))
3032        };
3033        if let Some(nn) = nn {
3034            if iscom(&nn) {
3035                return Some(nn); // c:976 RET_IF_COM
3036            }
3037        }
3038    }
3039    // c:978-988 — walk `path[]` (the shell `$path` array). Read $PATH
3040    // from paramtab so shell-private edits via `path=(...)` take
3041    // effect (not OS env only).
3042    for dir in dirs {
3043        if dir.is_empty() {
3044            continue;
3045        }
3046        let candidate = format!("{}/{}", dir, arg0);
3047        if iscom(&candidate) {
3048            return Some(candidate);
3049        }
3050    }
3051    None // c:989
3052}
3053
3054/// Port of `addfd()` from `Src/exec.c:2397` — C decl `addfd(int forked, int *save, struct multio **mfds, int fd1, int fd2, int rflag, char *varid)`.
3055///                             int fd1, int fd2, int rflag, char *varid)`
3056/// from `Src/exec.c:2397`.
3057///
3058/// C body (~100 lines, three branches):
3059/// ```c
3060/// if (varid) {
3061///     /* {varid}>file form — move fd above 10 and bind $varid to it */
3062/// } else if (!mfds[fd1] || unset(MULTIOS)) {
3063///     /* new multio OR MULTIOS off — first redir on this fd */
3064/// } else {
3065///     /* additional redir on a fd that's already a multio (split or extend) */
3066/// }
3067/// ```
3068///
3069/// Register `fd2` (already-open) as a redirection target for `fd1`.
3070/// Three branches: `varid` writes the moved fd to `$varid` and bumps
3071/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
3072/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
3073/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
3074/// via `mpipe`, or appends another fd to an already-split stream
3075/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
3076///
3077/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
3078/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
3079/// longer a hard cap (still 8 for the initial allocation, grown
3080/// on demand thereafter).
3081///
3082/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
3083/// stores the int value but doesn't expose a bitwise-OR setter; we
3084/// re-read + OR + re-store as two atomic-feeling steps.
3085pub fn addfd(
3086    forked: i32,
3087    save: &mut [i32; 10],
3088    mfds: &mut [Option<Box<multio>>; 10],
3089    fd1: i32,
3090    fd2: i32,
3091    rflag: i32,
3092    varid: Option<&str>,
3093) {
3094    // c:2397
3095    let mut pipes: [i32; 2] = [-1; 2]; // c:2400
3096
3097    // c:2402-2417 — `if (varid)` branch — {varid}>file shape.
3098    if let Some(vid) = varid {
3099        // c:2402
3100        let fd_moved = movefd(fd2); // c:2404
3101        if fd_moved == -1 {
3102            // c:2405
3103            zerr(&format!(
3104                // c:2406
3105                "cannot move fd {}: {}",
3106                fd2,
3107                std::io::Error::last_os_error()
3108            ));
3109            return; // c:2407
3110        }
3111        // c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
3112        fdtable_set(fd_moved, FDT_EXTERNAL);
3113        // c:2410 — `setiparam(varid, (zlong)fd1);`
3114        setiparam(vid, fd_moved as i64);
3115        // c:2415-2416 — `if (errflag) zclose(fd1);`
3116        // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
3117        // the C line cited above tests the WHOLE errflag, so masking here let an
3118        // interrupted shell keep going where zsh stops.
3119        if errflag.load(Ordering::Relaxed) != 0 {
3120            // c:2415
3121            let _ = zclose(fd_moved); // c:2416
3122        }
3123        return;
3124    }
3125    // c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
3126    let fd1u = fd1 as usize;
3127    if fd1u >= mfds.len() {
3128        return;
3129    }
3130    if mfds[fd1u].is_none() || unset(MULTIOS) {
3131        // c:2418
3132        if mfds[fd1u].is_none() {
3133            // c:2419 — `starting a new multio`
3134            // c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
3135            mfds[fd1u] = Some(Box::new(multio {
3136                ct: 0,
3137                rflag: 0,
3138                pipe: -1,
3139                // c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
3140                // grow on demand via push() below. Pre-fill MULTIOUNIT
3141                // slots with -1 so existing indexed writes (fds[0], fds[1])
3142                // still work without explicit resize().
3143                fds: vec![-1; MULTIOUNIT],
3144            }));
3145            // c:2421 — `if (!forked && save[fd1] == -2)`
3146            if forked == 0 && save[fd1u] == -2 {
3147                if fd1 == fd2 {
3148                    // c:2422
3149                    save[fd1u] = -1; // c:2423
3150                } else {
3151                    // c:2424
3152                    let fd_n = movefd(fd1); // c:2425
3153                    if fd_n < 0 {
3154                        // c:2430
3155                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3156                        if e != libc::EBADF {
3157                            // c:2431
3158                            zerr(&format!(
3159                                // c:2432
3160                                "cannot duplicate fd {}: {}",
3161                                fd1,
3162                                std::io::Error::from_raw_os_error(e)
3163                            ));
3164                            mfds[fd1u] = None; // c:2433
3165                            closemnodes(mfds); // c:2434
3166                            return; // c:2435
3167                        }
3168                    } else {
3169                        // c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
3170                        crate::DPUTS!(
3171                            fdtable_get(fd_n) != FDT_INTERNAL,
3172                            "Saved file descriptor not marked as internal"
3173                        );
3174                        // c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
3175                        let cur = fdtable_get(fd_n);
3176                        fdtable_set(fd_n, cur | FDT_SAVED_MASK);
3177                    }
3178                    save[fd1u] = fd_n; // c:2442
3179                }
3180            }
3181        }
3182        // c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
3183        // handled above; this is the non-varid branch.)
3184        let _ = redup(fd2, fd1);
3185        // c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
3186        if let Some(mn) = mfds[fd1u].as_mut() {
3187            mn.ct = 1; // c:2448
3188            mn.fds[0] = fd1; // c:2449
3189            mn.rflag = rflag; // c:2450
3190        }
3191    } else {
3192        // c:2451 — extend existing multio.
3193        // c:2452-2456 — rflag mismatch check.
3194        let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
3195        if cur_rflag != rflag {
3196            // c:2452
3197            zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
3198            closemnodes(mfds); // c:2454
3199            return; // c:2455
3200        }
3201        let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
3202        if cur_ct == 1 {
3203            // c:2457 — split the stream.
3204            // c:2458 — `int fdN = movefd(fd1);`
3205            let fd_n = movefd(fd1);
3206            if fd_n < 0 {
3207                // c:2459
3208                zerr(&format!(
3209                    // c:2460
3210                    "multio failed for fd {}: {}",
3211                    fd1,
3212                    std::io::Error::last_os_error()
3213                ));
3214                closemnodes(mfds); // c:2461
3215                return; // c:2462
3216            }
3217            if let Some(mn) = mfds[fd1u].as_mut() {
3218                mn.fds[0] = fd_n; // c:2464
3219            }
3220            // c:2465 — `fdN = movefd(fd2);`
3221            let fd_n2 = movefd(fd2);
3222            if fd_n2 < 0 {
3223                // c:2466
3224                zerr(&format!(
3225                    // c:2467
3226                    "multio failed for fd {}: {}",
3227                    fd2,
3228                    std::io::Error::last_os_error()
3229                ));
3230                closemnodes(mfds); // c:2468
3231                return; // c:2469
3232            }
3233            if let Some(mn) = mfds[fd1u].as_mut() {
3234                mn.fds[1] = fd_n2; // c:2471
3235            }
3236            // c:2472 — `mpipe(pipes)`
3237            if mpipe(&mut pipes) < 0 {
3238                // c:2472
3239                zerr(&format!(
3240                    // c:2473
3241                    "multio failed for fd {}: {}",
3242                    fd2,
3243                    std::io::Error::last_os_error()
3244                ));
3245                closemnodes(mfds); // c:2474
3246                return; // c:2475
3247            }
3248            // c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
3249            if let Some(mn) = mfds[fd1u].as_mut() {
3250                mn.pipe = pipes[(1 - rflag) as usize];
3251            }
3252            // c:2478 — `redup(pipes[rflag], fd1);`
3253            let _ = redup(pipes[rflag as usize], fd1);
3254            // c:2479 — `mfds[fd1]->ct = 2;`
3255            if let Some(mn) = mfds[fd1u].as_mut() {
3256                mn.ct = 2;
3257            }
3258        } else {
3259            // c:2480 — extend already-split stream.
3260            // c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
3261            //                              sizeof + ct*sizeof(int));`
3262            // Rust's `Vec<i32>` grows on demand; ensure capacity for the
3263            // new slot before the indexed write below.
3264            if let Some(mn) = mfds[fd1u].as_mut() {
3265                while mn.fds.len() <= cur_ct as usize {
3266                    mn.fds.push(-1);
3267                }
3268            }
3269            // c:2487 — `if ((fdN = movefd(fd2)) < 0)`
3270            let fd_n = movefd(fd2);
3271            if fd_n < 0 {
3272                zerr(&format!(
3273                    // c:2488
3274                    "multio failed for fd {}: {}",
3275                    fd2,
3276                    std::io::Error::last_os_error()
3277                ));
3278                closemnodes(mfds); // c:2489
3279                return; // c:2490
3280            }
3281            // c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
3282            if let Some(mn) = mfds[fd1u].as_mut() {
3283                let slot = mn.ct as usize;
3284                if slot < mn.fds.len() {
3285                    mn.fds[slot] = fd_n;
3286                    mn.ct += 1;
3287                }
3288            }
3289        }
3290    }
3291}
3292
3293/// Port of `closemn()` from `Src/exec.c:2273` — C decl `closemn(struct multio **mfds, int fd, int type)`.
3294///
3295/// C body (abridged — the meat is the fork-into-tee-or-cat child):
3296/// ```c
3297/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
3298///     struct multio *mn = mfds[fd];
3299///     char buf[TCBUFSIZE]; int len, i;
3300///     pid_t pid; struct timespec bgtime;
3301///     child_block();
3302///     if ((pid = zfork(&bgtime))) {
3303///         for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
3304///         zclose(mn->pipe);
3305///         if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
3306///         mn->ct = 1; mn->fds[0] = fd;
3307///         addproc(pid, NULL, 1, &bgtime, -1, -1);
3308///         child_unblock(); return;
3309///     }
3310///     /* pid == 0 (child) */
3311///     opts[INTERACTIVE] = 0;
3312///     dont_queue_signals();
3313///     child_unblock();
3314///     closeallelse(mn);
3315///     if (mn->rflag) {
3316///         /* tee process: read mn->pipe, write each mn->fds[i] */
3317///     } else {
3318///         /* cat process: read each mn->fds[i], write mn->pipe */
3319///     }
3320///     _exit(0);
3321/// } else if (fd >= 0 && type == REDIR_CLOSE)
3322///     mfds[fd] = NULL;
3323/// ```
3324///
3325/// Success-path close of a multio. For ct>=2 (multiple-output
3326/// redirection), forks a tee/cat child that proxies bytes between
3327/// the original fd and the per-output fds. Single-output multios
3328/// (ct=1) skip the fork entirely and just clear the slot.
3329///
3330/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
3331/// tee/cat child in the current job's auxprocs.
3332pub fn closemn(mfds: &mut [Option<Box<multio>>; 10], fd: i32, type_: i32) {
3333    // c:2273
3334    // c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
3335    let needs_tee = fd >= 0
3336        && (fd as usize) < mfds.len()
3337        && mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
3338    if needs_tee {
3339        // c:2275
3340        // Take the multio out of the slot so we can move pieces into
3341        // the child without aliasing the slot.
3342        let mn = mfds[fd as usize].take().unwrap();
3343        let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
3344                                   // c:2287 — `child_block();` block SIGCHLD before fork race.
3345        child_block();
3346        // c:2288 — `pid = zfork(&bgtime);`
3347        let mut bgtime = ZshTimespec {
3348            tv_sec: 0,
3349            tv_nsec: 0,
3350        };
3351        let pid = zfork(Some(&mut bgtime));
3352        if pid != 0 {
3353            // c:2288 parent branch
3354            // c:2289-2290 — close all per-output fds.
3355            for i in 0..mn.ct as usize {
3356                if i < mn.fds.len() {
3357                    let _ = zclose(mn.fds[i]); // c:2290
3358                }
3359            }
3360            let _ = zclose(mn.pipe); // c:2291
3361            if pid == -1 {
3362                // c:2292
3363                // c:2293 — `mfds[fd] = NULL;` already done via .take()
3364                child_unblock(); // c:2294
3365                return; // c:2295
3366            }
3367            // c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
3368            let mut mn_back = mn;
3369            mn_back.ct = 1; // c:2297
3370            mn_back.fds[0] = fd; // c:2298
3371            mfds[fd as usize] = Some(mn_back);
3372            // c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
3373            // the tee/cat child in the current job's auxprocs (aux=true).
3374            if let Some(jt) = JOBTAB.get() {
3375                let mut guard = jt.lock().unwrap();
3376                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
3377                if tj >= 0 {
3378                    if let Some(j) = guard.get_mut(tj as usize) {
3379                        crate::ported::jobs::addproc(
3380                            j,
3381                            pid,
3382                            "",
3383                            true,
3384                            Some(std::time::Instant::now()),
3385                            -1,
3386                            -1,
3387                        );
3388                    }
3389                }
3390            }
3391            let _ = bgtime;
3392            child_unblock(); // c:2300
3393            return; // c:2301
3394        }
3395        // c:2303 — child branch (pid == 0).
3396        opt_state_set("interactive", false); // c:2304
3397        dont_queue_signals(); // c:2305
3398        child_unblock(); // c:2306
3399        closeallelse(&mn); // c:2307
3400                           // c:2308-2333 — tee or cat loop.
3401        if mn.rflag != 0 {
3402            // c:2308 — `mn->rflag` set → tee process
3403            // c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
3404            loop {
3405                let len = unsafe {
3406                    libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
3407                };
3408                if len == 0 {
3409                    break;
3410                }
3411                if len < 0 {
3412                    // c:2311
3413                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3414                    if e == libc::EINTR {
3415                        // c:2312
3416                        continue;
3417                    } else {
3418                        break; // c:2315
3419                    }
3420                }
3421                // c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
3422                for i in 0..mn.ct as usize {
3423                    if i >= mn.fds.len() {
3424                        break;
3425                    }
3426                    if write_loop(mn.fds[i], &buf[..len as usize]).is_err() {
3427                        break; // c:2319
3428                    }
3429                }
3430            }
3431        } else {
3432            // c:2321 — cat process
3433            for i in 0..mn.ct as usize {
3434                if i >= mn.fds.len() {
3435                    break;
3436                }
3437                // c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
3438                loop {
3439                    let len = unsafe {
3440                        libc::read(mn.fds[i], buf.as_mut_ptr() as *mut libc::c_void, buf.len())
3441                    };
3442                    if len == 0 {
3443                        break;
3444                    }
3445                    if len < 0 {
3446                        let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3447                        // c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
3448                        if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
3449                            continue;
3450                        } else {
3451                            break; // c:2329
3452                        }
3453                    }
3454                    // c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
3455                    if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
3456                        break; // c:2332
3457                    }
3458                }
3459            }
3460        }
3461        // c:2335 — `_exit(0);`
3462        unsafe {
3463            libc::_exit(0);
3464        }
3465    } else if fd >= 0 && type_ == REDIR_CLOSE {
3466        // c:2336
3467        // c:2337 — `mfds[fd] = NULL;`
3468        if (fd as usize) < mfds.len() {
3469            mfds[fd as usize] = None;
3470        }
3471    }
3472}
3473
3474/// Port of `closemnodes()` from `Src/exec.c:2344` — C decl `closemnodes(struct multio **mfds)`.
3475///
3476/// C body:
3477/// ```c
3478/// int i, j;
3479/// for (i = 0; i < 10; i++)
3480///     if (mfds[i]) {
3481///         for (j = 0; j < mfds[i]->ct; j++)
3482///             zclose(mfds[i]->fds[j]);
3483///         mfds[i] = NULL;
3484///     }
3485/// ```
3486///
3487/// Failure-path cleanup: close every fd stashed in any of the 10
3488/// multio slots and null the slot. Called from `execcmd_exec` when
3489/// a redirect setup fails partway through and we need to roll back.
3490pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
3491    // c:2344
3492    for i in 0..10 {
3493        // c:2348
3494        if let Some(mn) = mfds[i].take() {
3495            // c:2349
3496            for j in 0..mn.ct as usize {
3497                // c:2350
3498                if j < mn.fds.len() {
3499                    let _ = zclose(mn.fds[j]); // c:2351
3500                }
3501            }
3502            // c:2352 — `mfds[i] = NULL;` — handled by .take() above.
3503        }
3504    }
3505}
3506
3507/// Port of `closeallelse()` from `Src/exec.c:2358` — C decl `closeallelse(struct multio *mn)`.
3508///
3509/// C body:
3510/// ```c
3511/// int i, j;
3512/// long openmax;
3513/// openmax = fdtable_size;
3514/// for (i = 0; i < openmax; i++)
3515///     if (mn->pipe != i) {
3516///         for (j = 0; j < mn->ct; j++)
3517///             if (mn->fds[j] == i) break;
3518///         if (j == mn->ct)
3519///             zclose(i);
3520///     }
3521/// ```
3522///
3523/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
3524/// stashed in `mn->fds`. Called inside the multio tee/cat child
3525/// process to release every fd the parent had open — only the pipe
3526/// + per-output fds stay alive for the read/write loop.
3527pub fn closeallelse(mn: &multio) {
3528    // c:2358
3529    // c:2363 — `openmax = fdtable_size;`. fdtable_size starts at
3530    // `zopenmax()` (c:Src/init.c:1931) and only grows (check_fd_table,
3531    // c:Src/utils.c:1974-1981) to cover max_zsh_fd, so it spans every
3532    // descriptor the process may hold — not just the ones the table
3533    // records. Stopping at max_zsh_fd left the tee holding a descriptor
3534    // the table never saw (a user `exec 3>&1 3>&2` target, a Rust-side
3535    // pipe), and a tee that holds its own pipe's write end never exits.
3536    let openmax = std::cmp::max(
3537        MAX_ZSH_FD.load(Ordering::Relaxed) + 1,
3538        crate::ported::compat::zopenmax() as i32,
3539    ); // c:2363
3540    for i in 0..openmax {
3541        // c:2365
3542        if mn.pipe == i {
3543            // c:2366
3544            continue;
3545        }
3546        // c:2367-2369 — scan mn->fds[] for i; skip-close if found.
3547        let mut found = false;
3548        for j in 0..mn.ct as usize {
3549            // c:2367
3550            if j < mn.fds.len() && mn.fds[j] == i {
3551                // c:2368
3552                found = true;
3553                break; // c:2369
3554            }
3555        }
3556        // c:2370-2371 — `if (j == mn->ct) zclose(i);`
3557        if !found {
3558            // c:2371 zclose → c:Src/utils.c:2145 `close(fd)`, blind. zclose's
3559            // skip-unowned guard exists for sibling threads recycling fds;
3560            // this runs in the forked child, which has only one thread.
3561            unsafe { libc::close(i) }; // c:2371
3562        }
3563    }
3564}
3565
3566/// Port of `fixfds()` from `Src/exec.c:4523` — C decl `fixfds(int *save)`.
3567///
3568/// C body:
3569/// ```c
3570/// int old_errno = errno;
3571/// int i;
3572/// for (i = 0; i != 10; i++)
3573///     if (save[i] != -2)
3574///         redup(save[i], i);
3575/// errno = old_errno;
3576/// ```
3577///
3578/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
3579/// means "no save was made for this fd"; any other value is the
3580/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
3581/// caller's errno across the loop so a downstream caller diagnoses
3582/// the original failure, not a noisy dup2 errno.
3583pub fn fixfds(save: &[i32; 10]) {
3584    // c:4523
3585    let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
3586    for i in 0..10i32 {
3587        // c:4528 — `for (i = 0; i != 10; i++)`
3588        if save[i as usize] != -2 {
3589            // c:4529
3590            redup(save[i as usize], i); // c:4530
3591        }
3592    }
3593    // c:4531 — `errno = old_errno;`
3594    #[cfg(target_os = "macos")]
3595    unsafe {
3596        *libc::__error() = old_errno;
3597    }
3598    #[cfg(target_os = "linux")]
3599    unsafe {
3600        *libc::__errno_location() = old_errno;
3601    }
3602}
3603
3604/// Port of `closem()` from `Src/exec.c:4546` — C decl `closem(int how, int all)`.
3605///
3606/// C body:
3607/// ```c
3608/// int i;
3609/// for (i = 10; i <= max_zsh_fd; i++)
3610///     if (fdtable[i] != FDT_UNUSED &&
3611///         (all || (fdtable[i] != FDT_PROC_SUBST &&
3612///                  fdtable[i] != FDT_EXTERNAL)) &&
3613///         (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
3614///         if (i == SHTTY) SHTTY = -1;
3615///         zclose(i);
3616///     }
3617/// ```
3618///
3619/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
3620/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
3621/// type filter); otherwise only fds whose low-nibble type equals
3622/// `how` are closed. `all == 0` preserves user-visible fds
3623/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
3624/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
3625/// reference if we just closed the controlling tty.
3626pub fn closem(how: i32, all: i32) {
3627    // c:4546
3628    let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
3629    for i in 10i32..=max {
3630        // c:4550
3631        let kind = fdtable_get(i); // c:4551 fdtable[i]
3632        if kind == FDT_UNUSED {
3633            // c:4551
3634            continue;
3635        }
3636        // c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
3637        if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
3638            continue;
3639        }
3640        // c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
3641        if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
3642            continue;
3643        }
3644        // c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
3645        if i == SHTTY.load(Ordering::Relaxed) {
3646            // c:4560
3647            SHTTY.store(-1, Ordering::Relaxed); // c:4561
3648        }
3649        // c:4562 — `zclose(i);`
3650        let _ = zclose(i);
3651    }
3652}
3653
3654/// Port of `hashcmd()` from `Src/exec.c:1010` — C decl `hashcmd(char *arg0, char **pp)`.
3655///
3656/// C body:
3657/// ```c
3658/// Cmdnam cn;
3659/// char *s, buf[PATH_MAX+1];
3660/// char **pq;
3661/// if (*arg0 == '/') return NULL;
3662/// for (; *pp; pp++)
3663///     if (**pp == '/') {
3664///         s = buf;
3665///         struncpy(&s, *pp, PATH_MAX);
3666///         *s++ = '/';
3667///         if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
3668///         strcpy(s, arg0);
3669///         if (iscom(buf)) break;
3670///     }
3671/// if (!*pp) return NULL;
3672/// cn = (Cmdnam) zshcalloc(sizeof *cn);
3673/// cn->node.flags = 0;
3674/// cn->u.name = pp;
3675/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
3676/// if (isset(HASHDIRS)) {
3677///     for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
3678///     pathchecked = pp + 1;
3679/// }
3680/// return cn;
3681/// ```
3682///
3683/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
3684/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
3685/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
3686/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
3687/// so subsequent commands hit the cache.
3688///
3689/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
3690/// `None` if `arg0` is absolute or no PATH entry contains it.
3691pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
3692    // c:1010
3693    // c:1055 — `if ((*arg0 == '/') || !strncmp(arg0, "./", 2) ||
3694    //            !strncmp(arg0, "../", 3)) return NULL;`
3695    // The doc-comment above quotes an older zsh where only the leading `/`
3696    // was rejected; current C rejects the two explicitly-relative spellings
3697    // too, and this port had kept the old single test.
3698    if arg0.starts_with('/') || arg0.starts_with("./") || arg0.starts_with("../") {
3699        return None; // c:1056
3700    }
3701    // c:1018-1028 — walk pp[] for first matching absolute entry.
3702    let mut found_idx: Option<usize> = None;
3703    for (i, dir) in pp.iter().enumerate() {
3704        // c:1018
3705        if !dir.starts_with('/') {
3706            // c:1019
3707            continue;
3708        }
3709        // c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
3710        if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
3711            // c:1023
3712            continue; // c:1024
3713        }
3714        let buf = format!("{}/{}", dir, arg0); // c:1025
3715        if iscom(&buf) {
3716            // c:1026
3717            found_idx = Some(i);
3718            break; // c:1027
3719        }
3720    }
3721    // c:1030-1031 — `if (!*pp) return NULL;`
3722    let pp_idx = match found_idx {
3723        Some(i) => i,
3724        None => return None, // c:1031
3725    };
3726    // c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
3727    let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
3728    let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
3729                                                // c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
3730    if let Ok(mut tab) = cmdnamtab_lock().write() {
3731        tab.add(cn.clone());
3732    }
3733    // c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
3734    // including the matching one, then bump pathchecked past it.
3735    if isset(HASHDIRS) {
3736        // c:1038
3737        let start = pathchecked.load(Ordering::Relaxed); // c:1039
3738        for pq in start..=pp_idx {
3739            // c:1039
3740            if pq < pp.len() {
3741                hashdir(&pp[pq], pq); // c:1040
3742            }
3743        }
3744        pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
3745    }
3746    Some(cn) // c:1044
3747}
3748
3749/// Port of `zfork()` from `Src/exec.c:349` — C decl `zfork(struct timespec *ts)`.
3750///
3751/// C body:
3752/// ```c
3753/// pid_t pid;
3754/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
3755///     zerr("job table full");
3756///     return -1;
3757/// }
3758/// if (ts) zgettime_monotonic_if_available(ts);
3759/// queue_signals();
3760/// pid = fork();
3761/// unqueue_signals();
3762/// if (pid == -1) {
3763///     zerr("fork failed: %e", errno);
3764///     return -1;
3765/// }
3766/// #ifdef HAVE_GETRLIMIT
3767/// if (!pid) setlimits(NULL);
3768/// #endif
3769/// return pid;
3770/// ```
3771///
3772/// fork(2) wrapper with jobtab capacity check + child rlimit
3773/// re-application. Used by every subshell-spawning path: pipelines,
3774/// process substitution, async commands, command substitution.
3775pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
3776    // c:349
3777    let pid: libc::pid_t;
3778
3779    // c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
3780    let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
3781    let thisjob = *thisjob_lock.lock().unwrap();
3782    if thisjob != -1 {
3783        // c:356
3784        let needed = (thisjob + 1) as usize;
3785        let needs_expand = JOBTAB
3786            .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3787            .lock()
3788            .map(|t| needed >= t.len().saturating_sub(1))
3789            .unwrap_or(false);
3790        if needs_expand {
3791            let mut tab = JOBTAB.get().unwrap().lock().unwrap();
3792            if !expandjobtab(&mut tab, needed) {
3793                // c:357
3794                zerr("job table full"); // c:357
3795                return -1; // c:358
3796            }
3797        }
3798    }
3799    // c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
3800    if let Some(ts) = ts {
3801        zgettime_monotonic_if_available(ts);
3802    }
3803    // c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
3804    queue_signals(); // c:368
3805    pid = unsafe { libc::fork() }; // c:369
3806    unqueue_signals(); // c:370
3807                       // c:371-374 — fork failure.
3808    if pid == -1 {
3809        // c:371
3810        zerr(&format!(
3811            // c:372
3812            "fork failed: {}",
3813            std::io::Error::last_os_error()
3814        ));
3815        return -1; // c:373
3816    }
3817    // c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
3818    #[cfg(unix)]
3819    if pid == 0 {
3820        // c:376
3821        let _ = setlimits(""); // c:378
3822    }
3823    pid // c:380
3824}
3825
3826/// Port of `loadautofnsetfile()` from `Src/exec.c:5657` — C decl `loadautofnsetfile(Shfunc shf, char *fdir)`.
3827///
3828/// C body:
3829/// ```c
3830/// if (!(shf->node.flags & PM_LOADDIR) ||
3831///     strcmp(shf->filename, fdir) != 0) {
3832///     dircache_set(&shf->filename, NULL);
3833///     if (fdir) {
3834///         shf->node.flags |= PM_LOADDIR;
3835///         dircache_set(&shf->filename, fdir);
3836///     } else {
3837///         shf->node.flags &= ~PM_LOADDIR;
3838///         shf->filename = ztrdup(shf->node.nam);
3839///     }
3840/// }
3841/// ```
3842///
3843/// Update `shf->filename` to the autoload directory `fdir`. Routes
3844/// through the refcounted `dircache_set` so identical directory
3845/// strings are shared across shfunc table entries.
3846pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
3847    // c:5657
3848    // c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
3849    let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
3850    let same = match (&shf.filename, fdir) {
3851        (Some(a), Some(b)) => a == b,
3852        _ => false,
3853    };
3854    if !loaddir || !same {
3855        // c:5664
3856        // c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
3857        dircache_set(&mut shf.filename, None);
3858        if let Some(fdir) = fdir {
3859            // c:5668
3860            shf.node.flags |= PM_LOADDIR as i32; // c:5670
3861            dircache_set(&mut shf.filename, Some(fdir)); // c:5671
3862        } else {
3863            // c:5672
3864            shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
3865            shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
3866        }
3867    }
3868}
3869
3870/// Port of `commandnotfound()` from `Src/exec.c:669` — C decl `commandnotfound(char *arg0, LinkList args)`.
3871///
3872/// C body:
3873/// ```c
3874/// Shfunc shf = (Shfunc)
3875///     shfunctab->getnode(shfunctab, "command_not_found_handler");
3876/// if (!shf) {
3877///     lastval = 127;
3878///     return 1;
3879/// }
3880/// pushnode(args, arg0);
3881/// lastval = doshfunc(shf, args, 1);
3882/// return 0;
3883/// ```
3884///
3885/// Look up the user-defined `command_not_found_handler` shfunc and
3886/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
3887/// 1 if no handler (so caller emits the standard "command not found"
3888/// error). Sets `$?` to 127 in the no-handler path.
3889pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
3890    // c:669
3891    // c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
3892    let has_handler = shfunctab_lock()
3893        .read()
3894        .map(|t| t.get("command_not_found_handler").is_some())
3895        .unwrap_or(false);
3896    if !has_handler {
3897        // c:674
3898        LASTVAL.store(127, Ordering::Relaxed); // c:675
3899        return 1; // c:676
3900    }
3901    // c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
3902    // is the first positional arg per C convention).
3903    args.insert(0, arg0.to_string());
3904    args.insert(0, "command_not_found_handler".to_string());
3905    // c:680 — `lastval = doshfunc(shf, args, 1);`. Direct doshfunc
3906    // call mirrors C — body_runner routes through the host body-only
3907    // entry so the function body runs once inside doshfunc's scope.
3908    let shf_clone: Option<shfunc> = shfunctab_lock()
3909        .read()
3910        .ok()
3911        .and_then(|t| t.get("command_not_found_handler").cloned());
3912    if let Some(mut shf) = shf_clone {
3913        let body_args = args.clone();
3914        let body_runner = move || -> i32 {
3915            crate::ported::exec::run_function_body("command_not_found_handler", &body_args[1..])
3916                .unwrap_or(0)
3917        };
3918        let lv = doshfunc(&mut shf, args.clone(), true, body_runner);
3919        LASTVAL.store(lv, Ordering::Relaxed);
3920    }
3921    0 // c:681
3922}
3923
3924/// Port of `namedpipe()` from `Src/exec.c:5001` — C decl `namedpipe(void)`.
3925///
3926/// C body (#ifdef HAVE_FIFOS branch):
3927/// ```c
3928/// char *tnam = gettempname(NULL, 1);
3929/// if (!tnam) {
3930///     zerr("failed to create named pipe: %e", errno);
3931///     return NULL;
3932/// }
3933/// if (mkfifo(tnam, 0600) < 0) {
3934///     zerr("failed to create named pipe: %s, %e", tnam, errno);
3935///     return NULL;
3936/// }
3937/// return tnam;
3938/// ```
3939///
3940/// Create a FIFO with a unique name for process substitution. Used by
3941/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
3942pub fn namedpipe() -> Option<String> {
3943    // c:5001
3944    let tnam = gettempname(None, true); // c:5003
3945    let tnam = match tnam {
3946        Some(t) => t,
3947        None => {
3948            // c:5005
3949            zerr(&format!(
3950                // c:5006
3951                "failed to create named pipe: {}",
3952                std::io::Error::last_os_error()
3953            ));
3954            return None; // c:5007
3955        }
3956    };
3957    // c:5010 — `mkfifo(tnam, 0600)`.
3958    let cstr = match std::ffi::CString::new(tnam.as_str()) {
3959        Ok(c) => c,
3960        Err(_) => return None,
3961    };
3962    if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
3963        // c:5010
3964        zerr(&format!(
3965            // c:5014
3966            "failed to create named pipe: {}, {}",
3967            tnam,
3968            std::io::Error::last_os_error()
3969        ));
3970        return None; // c:5015
3971    }
3972    Some(tnam) // c:5017
3973}
3974
3975/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
3976///
3977/// C body:
3978/// ```c
3979/// char *str;
3980/// Eprog prog;
3981/// for (str = cmd + 2; *str && *str != Outpar; str++);
3982/// if (!*str || cmd[1] != Inpar) {
3983///     char *errstr = dupstrpfx(cmd, 2);
3984///     untokenize(errstr);
3985///     zerr("unterminated `%s...)'", errstr);
3986///     return NULL;
3987/// }
3988/// *str = '\0';
3989/// if (eptr) *eptr = str+1;
3990/// if (!(prog = parse_string(cmd + 2, 0))) {
3991///     zerr("parse error in process substitution");
3992///     return NULL;
3993/// }
3994/// return prog;
3995/// ```
3996///
3997/// Port of `readoutput()` from `Src/exec.c:4805` — C decl `readoutput(int in, int qt, int *readerror)`.
3998/// Drain a command-substitution pipe fd and
3999/// return the captured output split per `qt`.
4000///
4001/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
4002/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
4003/// per c:4861).
4004/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
4005/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
4006///
4007/// `readerror` is set to the errno on read failure, 0 on clean EOF.
4008pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
4009    // c:4805
4010    let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
4011    let mut readret: isize = 0; // c:4818 readret tracks last read return
4012                                // c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
4013                                // dance keeps SIGCHLD live so the foreground process can be reaped
4014                                // while we drain. zshrs's in-process command-sub runs without the
4015                                // queue (no fork), but the C call surface is preserved for parity.
4016    dont_queue_signals(); // c:4824
4017    child_unblock(); // c:4825
4018    let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
4019    loop {
4020        // c:4826
4021        // c:4828 — `readret = read(in, inbuf, 64);`
4022        let r = unsafe { libc::read(in_fd, inbuf.as_mut_ptr() as *mut libc::c_void, inbuf.len()) };
4023        readret = r as isize;
4024        if readret <= 0 {
4025            // c:4829
4026            if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
4027                // c:4830 — `if (readret < 0 && errno == EINTR) continue;`
4028                continue;
4029            }
4030            break; // c:4832
4031        }
4032        // c:4835-4849 — `for (bufptr = inbuf; ...) { c = *bufptr; if
4033        // (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; } ... *ptr++ = c; }`
4034        //
4035        // !!! DELIBERATE DEVIATION — the byte-level metafication is DROPPED.
4036        //
4037        // C's buffer is a `char *` that may hold arbitrary bytes, so it
4038        // escapes every `imeta` byte (NUL, and Meta 0x83 .. Marker 0xa2 —
4039        // `Src/utils.c:4195-4201`) to keep raw input bytes from colliding
4040        // with the token bytes the lexer/glob layers use.
4041        //
4042        // zshrs's buffer is a Rust `String`. A lone 0x83 is not valid UTF-8,
4043        // so `String::from_utf8_lossy` below replaced every emitted `Meta`
4044        // with U+FFFD — the metafied form could never survive the conversion,
4045        // and the "escaped" payload byte was left stranded next to a
4046        // replacement char. `utils::spacesplit`'s Meta decoder
4047        // (`utils.rs:5307`, `bytes[i] == Meta`) can never see a real Meta in a
4048        // `String` for the same reason, so nothing downstream un-did it
4049        // either. The loop was pure corruption for any input containing a
4050        // byte in {0x00} ∪ [0x83,0xa2] — which includes the UTF-8 tails of
4051        // common characters (U+2003 EM SPACE is `e2 80 83`, U+2022 BULLET is
4052        // `e2 80 a2`).
4053        //
4054        // The in-process `$( cmd )` arm in `getoutput` (:619) never had a
4055        // metafication step at all — it receives a plain `String` from
4056        // `run_command_substitution`. Dropping it here is what makes the two
4057        // arms hand this tail the SAME representation, which is the
4058        // property C gets for free by routing both arms through `readoutput`.
4059        buf.extend_from_slice(&inbuf[..readret as usize]); // c:4848
4060    }
4061    child_block(); // c:4854
4062                   // c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
4063    *readerror = if readret < 0 {
4064        std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
4065    } else {
4066        0
4067    };
4068    // c:4857 — `close(in);`
4069    unsafe {
4070        libc::close(in_fd);
4071    }
4072    // ── c:4858-4872 tail ──────────────────────────────────────────────
4073    // !!! THIS BLOCK IS DUPLICATED at `getoutput` (:619), which is the
4074    // `$( cmd )` arm. C needs no duplicate: both of getoutput's arms
4075    // (c:4746 `$(< file)` and c:4772 the pipe) call `readoutput`, so they
4076    // share this tail by construction. zshrs collapses the fork, so its
4077    // `$( cmd )` arm holds an in-memory String and has no fd to hand
4078    // `readoutput`. Factoring the tail into a helper is what build.rs's
4079    // no-new-functions gate on src/ported/ forbids (there is no C function
4080    // to name it after), and its own remedy #1 is "inline the body at every
4081    // call site" — so the two copies must be kept in lockstep BY HAND.
4082    // ANY EDIT HERE MUST BE MIRRORED AT :619 AND VICE VERSA. The copies
4083    // already drifted once: the getoutput copy (:619) had silently lost the
4084    // c:4868-4869 `isset(GLOBSUBST)` → `shtokenize` step, so `setopt
4085    // globsubst; echo $(echo '*')` did not glob while `$(< f)` did.
4086    // c:4835-4849 — this is where C's `metafy` lands the read bytes. The
4087    // byte-level loop stays dropped (see the deviation note at :3799), but
4088    // the CONVERSION must still be lossless: `String::from_utf8_lossy` here
4089    // turned every byte that is not valid UTF-8 into U+FFFD, so `$(< f)` on
4090    // a latin-1 file printed `caf\xef\xbf\xbd` where zsh prints `caf\xe9`.
4091    // `decode_script_bytes` keeps valid UTF-8 as `char`s and Meta-encodes
4092    // only the bytes that cannot be, which `utils::unmetafy_str` turns back
4093    // into the original byte at the output boundary.
4094    let s = crate::script_bytes::decode_script_bytes(&buf);
4095    // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
4096    let s = s.trim_end_matches('\n');
4097    // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
4098    if qt != 0 {
4099        // c:4861
4100        if s.is_empty() {
4101            return vec![String::from(Nularg)]; // c:4862
4102        }
4103        return vec![s.to_string()]; // c:4864
4104    }
4105    // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
4106    let mut words = crate::ported::utils::spacesplit(s, false, false); // c:4867
4107    if isset(crate::ported::zsh_h::GLOBSUBST) {
4108        // c:4870
4109        for w in words.iter_mut() {
4110            crate::ported::glob::shtokenize(w); // c:4870
4111        }
4112    }
4113    words
4114}
4115
4116/// Port of `parsecmd()` from `Src/exec.c:4934` — C decl `parsecmd(char *cmd, char **eptr)`.
4117/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
4118/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
4119/// remainder is the command up to the matching `Outpar`. Returns the
4120/// parsed Eprog (and writes the post-`)` cursor through `eptr`, as a
4121/// BYTE offset into `cmd`). The tokens are multi-byte in a Rust `String`,
4122/// so the scan walks chars: comparing single bytes against `Inpar` /
4123/// `Outpar` (as the port did) could never match and every call failed.
4124pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
4125    // c:4934
4126    let mut it = cmd.char_indices();
4127    let _marker = it.next(); // cmd[0]
4128    let second = it.next().map(|(_, c)| c); // cmd[1]
4129    let body_start = it.next().map_or(cmd.len(), |(i, _)| i); // cmd + 2
4130    // c:4939 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
4131    let close = cmd[body_start..].find(Outpar).map(|i| body_start + i);
4132    // c:4940 — `if (!*str || cmd[1] != Inpar)`.
4133    let close = match close {
4134        Some(c) if second == Some(Inpar) => c,
4135        _ => {
4136            // c:4941-4946 — `This can happen if the expression is being parsed
4137            // inside another construct, e.g. as a value within ${..:..} etc.
4138            // So print a proper error message instead of the not very
4139            // useful but traditional "oops".`
4140            let pfx_end = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4141            let errstr = untokenize(&cmd[..pfx_end]); // c:4947-4948
4142            zerr(&format!("unterminated `{}...)'", errstr)); // c:4949
4143            return None; // c:4950
4144        }
4145    };
4146    // c:4952 — `*str = '\0';` — cmd[close] becomes the terminator.
4147    // c:4953-4954 — `if (eptr) *eptr = str + 1;`
4148    if let Some(p) = eptr {
4149        *p = close + Outpar.len_utf8();
4150    }
4151    // c:4955 — `parse_string(cmd + 2, 0)`.
4152    let prog = parse_string(&cmd[body_start..close], 0);
4153    if prog.is_none() {
4154        // c:4955
4155        zerr("parse error in process substitution"); // c:4956
4156        return None; // c:4957
4157    }
4158    prog // c:4959
4159}
4160
4161
4162/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
4163/// front of a script when probing for a `#!` shebang line.
4164pub const POUNDBANGLIMIT: usize = 128;
4165
4166/// Port of `makecline()` from `Src/exec.c:2046` — C decl `makecline(LinkList list)`.
4167///
4168/// Builds the argv array from a command's args list. The C version
4169/// allocates with a 4-slot prepad (2 reserved at the front for the
4170/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
4171/// doesn't need this since we rebuild the Vec on shebang re-exec
4172/// (see zexecve WARNING e).
4173///
4174/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
4175/// (stderr), preceded by the PS4 prefix when first command of the line.
4176pub fn makecline(list: &[String]) -> Vec<String> {
4177    // c:2046
4178    if isset(XTRACE) {
4179        // c:2055
4180        if doneps4.load(Ordering::Relaxed) == 0 {
4181            // c:2056
4182            printprompt4(); // c:2057
4183        }
4184        let mut first = true;
4185        let mut err = std::io::stderr().lock();
4186        use std::io::Write;
4187        for s in list.iter() {
4188            // c:2059
4189            if !first {
4190                let _ = err.write_all(b" "); // c:2063
4191            }
4192            first = false;
4193            let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
4194        }
4195        let _ = err.write_all(b"\n"); // c:2065
4196        let _ = err.flush(); // c:2066
4197    }
4198    list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
4199}
4200
4201/// Port of `execute()` from `Src/exec.c:723` — C decl `execute(LinkList args, int flags, int defpath)`.
4202/// The canonical "child runs the simple
4203/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
4204/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
4205/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
4206/// commandnotfound-handler fallback and the final exit-code escape
4207/// (127 not-found / 126 noperm).
4208///
4209/// =================== WARNING — DIVERGENCE ====================
4210/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
4211///     fast-path wired via cmdnamtab_lock(); jumps direct to
4212///     `cn.cmd` absolute path before the $PATH scan. Unhashed
4213///     cursor-walk (c:830-846) still falls to the full $PATH scan;
4214///     observable behavior matches C when the hash hit is HASHED.
4215/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
4216///     `doshfunc` for the `command_not_found_handler` shell function.
4217///     Routes through executor dispatch (see exec.rs:2783).
4218/// (c) `_realexit()` (c:810, 874) — `builtin::_realexit`.
4219/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
4220///     FD_CLOEXEC platform default (macOS, Linux).
4221/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
4222///     `defpath` (`command -p`) walks DEFAULT_PATH via
4223///     search_defpath (already ported).
4224/// =============================================================
4225pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
4226    // c:723
4227    let mut eno: i32 = 0;
4228    let mut ee: i32; // c:729
4229                     // c:737 — `arg0 = (char *) peekfirst(args);` — the COMMAND WORD.
4230    let arg0 = if args.is_empty() {
4231        return;
4232    } else {
4233        args[0].clone()
4234    }; // c:737
4235    // c:693-696 (5.9.2) — `if (isset(RESTRICTED) && (strchr(arg0, '/') ||
4236    // defpath)) { zerr("%s: restricted", arg0); _exit(1); }`
4237    if isset(crate::ported::zsh_h::RESTRICTED) && (arg0.contains('/') || defpath != 0) {
4238        crate::ported::utils::zerr(&format!("{}: restricted", arg0));
4239        unsafe { libc::_exit(1) };
4240    }
4241       // c:733-748 — STTY pre-exec handling.
4242    {
4243        let mut stty = STTYval.lock().unwrap();
4244        if let Some(s) = stty.take() {
4245            // c:738 — STTYval = 0 to break recursion.
4246            if !s.is_empty()
4247                && unsafe { libc::isatty(0) } != 0
4248                && unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
4249            {
4250                drop(stty);
4251                let cmd = format!("stty {}", s); // c:739
4252                execstring(&cmd, 1, 0, "stty"); // c:743
4253            }
4254        }
4255    }
4256    // c:758-777 — ARGV0 override.
4257    //   c:758-759 — /* If ARGV0 is in the commands environment, we use *
4258    //                * that as argv[0] for this external command       */
4259    // NOTE: C rewrites only `argv[0]` (the exec argv), NEVER `arg0`
4260    // (`peekfirst(args)`, c:737) — `arg0` stays the COMMAND WORD and is what
4261    // the slash-exec / cmdnamtab / $PATH scan below resolves. The previous
4262    // Rust port assigned `arg0 = args[0].clone()` in both arms, so
4263    // `ARGV0=sh $cmdvar args` looked up `sh` on $PATH instead of `$cmdvar`
4264    // (E03posix:16 — `command not found: sh`), and a `-` precommand made the
4265    // shell search for `-name`.
4266    if let Some(z) = zgetenv("ARGV0") {
4267        args[0] = z.clone(); // c:761 `argv[0] = ztrdup(z);`
4268        unsafe {
4269            let key = std::ffi::CString::new("ARGV0").unwrap();
4270            libc::unsetenv(key.as_ptr()); // c:768
4271        }
4272    } else if (flags & BINF_DASH) != 0 {
4273        // c:772-776 — /* Else if the pre-command `-' was given, we add `-' *
4274        //              * to the front of argv[0] for this command.         */
4275        args[0] = format!("-{}", arg0); // c:775-776
4276    }
4277    let argv = makecline(args); // c:771
4278    let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
4279        Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
4280    } else {
4281        None
4282    };
4283    let newenvp = newenvp_owned.as_deref();
4284    closem(FDT_XTRACE, 0); // c:779
4285                           // c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
4286    child_unblock(); // c:786
4287    if arg0.len() >= libc::PATH_MAX as usize {
4288        // c:787
4289        zerr(&format!("command too long: {}", arg0)); // c:788
4290        unsafe {
4291            libc::_exit(1);
4292        } // c:789
4293    }
4294    // c:791-801 — slash in arg0 → direct exec.
4295    if let Some(slash_pos) = arg0.find('/') {
4296        let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
4297        let is_dot = arg0.starts_with('.')
4298            && (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
4299        if slash_pos == 0 || unset(PATHDIRS) || is_dot {
4300            // c:794
4301            // c:797 — `zerr("%e: %s", lerrno, arg0)`. `%e` is zerrmsg's
4302            // errno arm (Src/utils.c:348-365): strerror with the first
4303            // letter lowercased, NOT Rust's `io::Error` Display (which
4304            // appends " (os error N)" and keeps the capital, so `~`
4305            // printed `Permission denied (os error 13): /Users/wizard`
4306            // where zsh prints `permission denied: /Users/wizard`).
4307            zerr(&format!(
4308                "{}: {}",
4309                crate::ported::utils::zsh_errno_msg(lerrno),
4310                arg0
4311            )); // c:797
4312            let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
4313                126
4314            } else {
4315                127
4316            };
4317            unsafe {
4318                libc::_exit(code);
4319            } // c:798
4320        }
4321    }
4322    if defpath != 0 {
4323        // c:804 — `command -p` default-path search.
4324        let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
4325            Some(p) => p, // c:808
4326            None => {
4327                if commandnotfound(&arg0, args) == 0 {
4328                    // c:809
4329                    unsafe {
4330                        libc::_exit(LASTVAL.load(Ordering::Relaxed));
4331                    }
4332                }
4333                zerr(&format!("command not found: {}", crate::ported::utils::nicedupstring(&arg0))); // c:818 — `%s` is nicezputs (c:Src/utils.c:316)
4334                unsafe {
4335                    libc::_exit(127);
4336                } // c:812
4337            }
4338        };
4339        ee = zexecve(&pbuf, &argv, newenvp); // c:815
4340        let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
4341        if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
4342            // c:819
4343            eno = ee;
4344        }
4345    } else {
4346        // c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
4347        // jump straight to the absolute path stored in `cn.cmd`,
4348        // skipping the full $PATH scan (one exec attempt vs N).
4349        // c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
4350        let hashed_path: Option<String> = {
4351            let tab = cmdnamtab_lock().read().ok();
4352            tab.and_then(|t| {
4353                t.get(&arg0).and_then(|cn| {
4354                    if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
4355                        // c:827-828 — `strcpy(nn, cn->u.cmd);`
4356                        cn.cmd.clone()
4357                    } else {
4358                        None
4359                    }
4360                })
4361            })
4362        };
4363        if let Some(nn) = hashed_path {
4364            // c:848 — `ee = zexecve(nn, argv, newenvp);`
4365            ee = zexecve(&nn, &argv, newenvp);
4366            let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
4367            if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
4368                eno = ee;
4369            }
4370            // If the hashed entry's exec failed without a "good" error,
4371            // we still need the $PATH fallback — fall through.
4372            if eno == 0 && ee != 0 {
4373                // Reset for the $PATH scan below.
4374                ee = 0;
4375            }
4376        }
4377        // c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
4378        // optimization but C also walks the rest of `path` if the hashed
4379        // exec failed with a non-"good" error).
4380        let path_str = getsparam("PATH").unwrap_or_default();
4381        for pp in path_str.split(':') {
4382            if pp.is_empty() || pp == "." {
4383                // c:856
4384                ee = zexecve(&arg0, &argv, newenvp); // c:857
4385                if isgooderr(ee, pp) {
4386                    eno = ee;
4387                }
4388            } else {
4389                // c:860
4390                let candidate = format!("{}/{}", pp, arg0); // c:861-864
4391                ee = zexecve(&candidate, &argv, newenvp); // c:865
4392                if isgooderr(ee, pp) {
4393                    eno = ee;
4394                }
4395            }
4396        }
4397    }
4398    // c:871-881 — final error reporting.
4399    if eno != 0 {
4400        // c:871
4401        // c:872 — `zerr("%e: %s", eno, arg0)`; same `%e` (Src/utils.c:
4402        // 348-365) rendering as the slash-in-arg0 branch above.
4403        zerr(&format!(
4404            "{}: {}",
4405            crate::ported::utils::zsh_errno_msg(eno),
4406            arg0
4407        )); // c:872
4408    } else if commandnotfound(&arg0, args) == 0 {
4409        // c:873
4410        unsafe {
4411            libc::_exit(LASTVAL.load(Ordering::Relaxed));
4412        } // c:874
4413    } else {
4414        zerr(&format!("command not found: {}", crate::ported::utils::nicedupstring(&arg0))); // c:903 — `%s` is nicezputs (c:Src/utils.c:316)
4415    }
4416    let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
4417        126
4418    } else {
4419        127
4420    }; // c:881
4421    unsafe {
4422        libc::_exit(code);
4423    }
4424}
4425
4426/// Port of `zexecve()` from `Src/exec.c:504` — C decl `zexecve(char *pth, char **argv, char **newenvp)`.
4427/// Wraps `execve(2)` with:
4428///   - `$_` env var stamped to absolute `pth` (c:514-520)
4429///   - winch signal unblock right before the syscall (c:527)
4430///   - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
4431///     bytes, parses a `#!interp arg` shebang and re-execs the
4432///     interpreter (c:534-628). For `ENOEXEC` with no shebang,
4433///     binary-safety check then falls back to `/bin/sh script` per
4434///     POSIX (c:588-628).
4435///
4436/// Returns `errno` from the failing exec — execve only returns on
4437/// failure, so success means the calling process is already replaced.
4438///
4439/// =================== WARNING — DIVERGENCE ====================
4440/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
4441///     string; Rust uses a stack `String` (consumed by `zputenv`).
4442/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
4443///     unconditionally as a no-op when FD_CLOEXEC is platform default.
4444/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
4445///     c:639-642 — handled implicitly via &str ↔ CString.
4446/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
4447///     drop the metafy and pass byte ranges to zerr directly.
4448/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
4449///     (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
4450///     `Vec<String>` with interp + optional arg + original argv tail
4451///     since Vec doesn't expose negative indexing.
4452/// (f) `environ` is FFI-loaded only when `newenvp` is None.
4453/// =============================================================
4454pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
4455    // c:504
4456    use std::ffi::CString;
4457    // c:514-520 — `_=pth` env stamping.
4458    let pth_abs = if pth.starts_with('/') {
4459        // c:516
4460        pth.to_string() // c:517
4461    } else {
4462        // c:518
4463        format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
4464    };
4465    zputenv(&format!("_={}", pth_abs)); // c:520
4466    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
4467    // `exec` replaces this process, so neither `zexit` nor `atexit` runs: finish
4468    // the recording of a script that ends in `exec` and write what is buffered.
4469    // In a forked child both are refused by their pid checks.
4470    crate::script_cache::capture_commit_all_on_exit(false);
4471    crate::script_cache::try_flush_pending();
4472    closedumps(); // c:522
4473    winch_unblock(); // c:527
4474    let cpth = match CString::new(pth) {
4475        Ok(c) => c,
4476        Err(_) => return libc::ENOENT,
4477    };
4478    let cargs: Vec<CString> = argv
4479        .iter()
4480        .filter_map(|a| CString::new(a.as_str()).ok())
4481        .collect();
4482    let mut argv_ptrs: Vec<*const libc::c_char> = cargs.iter().map(|c| c.as_ptr()).collect();
4483    argv_ptrs.push(std::ptr::null());
4484    let env_holder: Vec<CString>;
4485    let env_ptrs: Vec<*const libc::c_char>;
4486    let envp: *const *const libc::c_char = match newenvp {
4487        Some(env) => {
4488            env_holder = env
4489                .iter()
4490                .filter_map(|e| CString::new(e.as_str()).ok())
4491                .collect();
4492            env_ptrs = {
4493                let mut v: Vec<*const libc::c_char> =
4494                    env_holder.iter().map(|c| c.as_ptr()).collect();
4495                v.push(std::ptr::null());
4496                v
4497            };
4498            env_ptrs.as_ptr()
4499        }
4500        None => unsafe {
4501            extern "C" {
4502                static environ: *const *const libc::c_char;
4503            }
4504            environ
4505        },
4506    };
4507    unsafe {
4508        libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
4509    }
4510    let eno = std::io::Error::last_os_error()
4511        .raw_os_error()
4512        .unwrap_or(libc::ENOEXEC); // c:534
4513    if eno == libc::ENOEXEC || eno == libc::ENOENT {
4514        // c:534 — the `#!` / shebang-less recovery; see zexecve_recover.
4515        match crate::vm_helper::zexecve_recover(pth, argv, eno) {
4516            // c:566/571/581/585/627 — `execve(<prog>, <argv>, newenvp);`
4517            Ok((prog, argv_new)) => return zexecve(&prog, &argv_new, newenvp),
4518            Err(e) => return e, // c:643
4519        }
4520    }
4521    eno // c:643
4522}
4523
4524/// Port of `getoutputfile()` from `Src/exec.c:4910` — C decl `getoutputfile(char *cmd, char **eptr)`.
4525/// `=(cmd)` process substitution.
4526///
4527/// Substitutes the cmd's stdout into a temp file, returns the
4528/// filename. Optimised path: `=(<<<heredoc-str)` writes the
4529/// heredoc body directly without a fork.
4530///
4531/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
4532///     so the temp file gets cleaned at job exit.
4533/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
4534///     Behavior matches the `full=0` case anyway.
4535/// (c) This is a REAL fork (`zfork` below), not a collapsed one, so the
4536///     child gets the full `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)`
4537///     (c:4986) via the canonical port at exec.rs:4329 — no in-process
4538///     `SubshStateGuard` substitute is needed or wanted here.
4539/// (d) `execode` is now ported (exec.rs:6047) — the body still
4540///     re-feeds through fusevm for cache coherence with execstring.
4541/// (e) The child ends with `_realexit()` (c:4991) after flushing
4542///     Rust's in-process stdout buffer.
4543/// (f) The TMPSUFFIX link() block (c:4951-4958) hard-links the temp
4544///     file to `nam` + `$TMPSUFFIX`; both names are registered for
4545///     cleanup.
4546pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4547    // c:4910
4548    let bytes = cmd.as_bytes();
4549    let _ = bytes;
4550    // c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
4551    let mut ends_at: usize = 0;
4552    let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
4553    if let Some(p) = eptr {
4554        *p = ends_at;
4555    }
4556    let mut nam = gettempname(None, true)?; // c:4924
4557                                            // c:4927 — `simple_redir_name` opt for `=(<<<str)`.
4558    let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
4559        // c:4933
4560        let mut sub = singsub(&raw); // c:4933
4561        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4562            // c:4934
4563            String::new() // c:4935 — sentinel; checked below
4564        } else {
4565            sub = untokenize(&sub); // c:4937
4566            dyncat(&sub, "\n") // c:4938
4567        }
4568    });
4569    if let Some(ref sv) = s {
4570        if sv.is_empty() {
4571            s = None;
4572        }
4573    }
4574    if s.is_none() {
4575        // c:4942
4576        child_block(); // c:4943
4577    }
4578    // c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
4579    let c_nam = match std::ffi::CString::new(nam.clone()) {
4580        Ok(c) => c,
4581        Err(_) => {
4582            if s.is_none() {
4583                child_unblock();
4584            }
4585            return None;
4586        }
4587    };
4588    let fd = unsafe {
4589        libc::open(
4590            c_nam.as_ptr(),
4591            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
4592            0o600 as libc::c_uint,
4593        )
4594    };
4595    if fd < 0 {
4596        // c:4945
4597        zerr(&format!(
4598            "process substitution failed: {}",
4599            std::io::Error::last_os_error()
4600        )); // c:4946
4601        if s.is_none() {
4602            child_unblock(); // c:4948
4603        }
4604        return None; // c:4949
4605    }
4606    // c:4951-4958 — `char *suffix = getsparam("TMPSUFFIX"); if (suffix &&
4607    // *suffix && !strstr(suffix, "/")) { suffix = dyncat(nam,
4608    // unmeta(suffix)); if (link(nam, suffix) == 0) { addfilelist(nam, 0);
4609    // nam = suffix; } }`
4610    let mut linked_orig: Option<String> = None;
4611    if let Some(suffix) = getsparam("TMPSUFFIX") {
4612        if !suffix.is_empty() && !suffix.contains('/') {
4613            let linkname = dyncat(&nam, &unmeta(&suffix));
4614            let c_link = std::ffi::CString::new(linkname.clone());
4615            if let Ok(c_link) = c_link {
4616                if unsafe { libc::link(c_nam.as_ptr(), c_link.as_ptr()) } == 0 {
4617                    linked_orig = Some(std::mem::replace(&mut nam, linkname));
4618                }
4619            }
4620        }
4621    }
4622    // c:4960 — `addfilelist(nam, 0);` — register temp file in current
4623    // job's filelist so it's unlinked at job exit (not relying on the
4624    // OS temp-reaper).
4625    // zshrs runs commands in-process and the VM owns that job-end unlink
4626    // (the `=(cmd)` temp files the compiled path creates are parked there
4627    // too), so the parent also parks the name there on every return below.
4628    // Not before the fork: the child's own commands would drain the
4629    // inherited list and unlink the file it is writing.
4630    if let Some(jt) = JOBTAB.get() {
4631        let mut guard = jt.lock().unwrap();
4632        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4633        if tj >= 0 {
4634            if let Some(j) = guard.get_mut(tj as usize) {
4635                if let Some(orig) = &linked_orig {
4636                    crate::ported::jobs::addfilelist(j, Some(orig), 0); // c:4955
4637                }
4638                crate::ported::jobs::addfilelist(j, Some(&nam), 0);
4639            }
4640        }
4641    }
4642    if let Some(sv) = s {
4643        // c:4962 — optimised here-string write path.
4644        let mut buf: Vec<u8> = sv.into_bytes();
4645        let _len = unmetafy(&mut buf); // c:4965
4646        let _ = write_loop(fd, &buf); // c:4966
4647        unsafe {
4648            libc::close(fd);
4649        } // c:4967
4650        if let Some(orig) = &linked_orig {
4651            crate::fusevm_bridge::psub_pending_file_add(orig);
4652        }
4653        crate::fusevm_bridge::psub_pending_file_add(&nam);
4654        return Some(nam); // c:4968
4655    }
4656    // c:4971 — `cmdoutpid = pid = zfork(NULL)`.
4657    let pid = zfork(None);
4658    cmdoutpid.store(pid, Ordering::Relaxed);
4659    if pid == -1 {
4660        // c:4972
4661        unsafe {
4662            libc::close(fd);
4663        } // c:4973
4664        child_unblock(); // c:4974
4665        if let Some(orig) = &linked_orig {
4666            crate::fusevm_bridge::psub_pending_file_add(orig);
4667        }
4668        crate::fusevm_bridge::psub_pending_file_add(&nam);
4669        return Some(nam); // c:4975
4670    } else if pid != 0 {
4671        // c:4976 — parent.
4672        unsafe {
4673            libc::close(fd);
4674        } // c:4977
4675        let _ = waitforpid(pid); // c:4978
4676        cmdoutval.store(0, Ordering::Relaxed); // c:4979
4677        if let Some(orig) = &linked_orig {
4678            crate::fusevm_bridge::psub_pending_file_add(orig);
4679        }
4680        crate::fusevm_bridge::psub_pending_file_add(&nam);
4681        return Some(nam); // c:4980
4682    }
4683    // c:4983 — child.
4684    closem(FDT_UNUSED, 0); // c:4984
4685    let _ = redup(fd, 1); // c:4985
4686    entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
4687    cmdpush(CS_CMDSUBST as u8); // c:4987
4688                                // c:4988 — execode — WARNING (d).
4689    // The re-fed text is what parsecmd parsed: after the two marker chars,
4690    // up to the Outpar ending at `ends_at` (a byte offset). The markers are
4691    // multi-byte chars, so a `cmd[2..]` byte slice split them.
4692    let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4693    let body_end = ends_at.saturating_sub(Outpar.len_utf8());
4694    let body = if body_end > body_start && body_end <= cmd.len() {
4695        untokenize(&cmd[body_start..body_end])
4696    } else {
4697        String::new()
4698    };
4699    let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
4700    cmdpop(); // c:4989
4701    unsafe {
4702        libc::close(1);
4703    } // c:4990
4704    // Rust's stdout is buffered in-process; C builtins fflush their own.
4705    let _ = std::io::Write::flush(&mut std::io::stdout());
4706    crate::ported::builtin::_realexit(); // c:4991
4707    #[allow(unreachable_code)]
4708    {
4709        // c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
4710        let _ = &mut nam;
4711        unsafe {
4712            libc::kill(libc::getpid(), libc::SIGKILL);
4713        }
4714        None
4715    }
4716}
4717
4718/// Port of `getproc()` from `Src/exec.c:5025` — C decl `getproc(char *cmd, char **eptr)`.
4719/// `<(cmd)` / `>(cmd)` process substitution
4720/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
4721///
4722/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
4723///     path c:5037-5064) is omitted; modern Linux/macOS both
4724///     provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
4725///     unused here.
4726/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
4727///     the current job (c:5141-5142).
4728/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
4729///     c:5087.
4730/// (d) `entersubsh` is ported at exec.rs:4329 — wired below at
4731///     c:5095 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`) in the real
4732///     forked child.
4733/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
4734///     through fusevm for cache coherence.
4735/// (f) The child ends with `_realexit()` (c:5104) after flushing
4736///     Rust's in-process stdout buffer.
4737/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
4738pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4739    // c:5025
4740    let bytes = cmd.as_bytes();
4741    let out: i32 = if cmd.starts_with(Inang) {
4742        1 // c:5032 — `<(...)` writer-side child
4743    } else {
4744        0
4745    };
4746    // c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
4747    // proc subst needs a host job to attach the child to.
4748    let tj_check = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4749    if tj_check == -1 {
4750        zerr(&format!("process substitution {} cannot be used here", cmd)); // c:5069
4751        return None; // c:5070
4752    }
4753    // c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
4754    let mut ends_at: usize = 0;
4755    let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
4756    if let Some(p) = eptr {
4757        *p = ends_at;
4758    }
4759    let mut pipes: [i32; 2] = [-1; 2];
4760    if mpipe(&mut pipes) < 0 {
4761        // c:5075
4762        return None;
4763    }
4764    let mut bgtime: ZshTimespec = libc::timespec {
4765        tv_sec: 0,
4766        tv_nsec: 0,
4767    };
4768    let pid = zfork(Some(&mut bgtime)); // c:5077
4769    if pid != 0 {
4770        // c:5077 — parent path.
4771        let pnam = format!("{}/{}", crate::ported::config_h::PATH_DEV_FD, pipes[(1 - out) as usize]); // c:5144
4772        let _ = zclose(pipes[out as usize]); // c:5079
4773        if pid == -1 {
4774            // c:5080
4775            let _ = zclose(pipes[(1 - out) as usize]); // c:5082
4776            return None; // c:5083
4777        }
4778        let fd = pipes[(1 - out) as usize]; // c:5085
4779        fdtable_set(fd, FDT_PROC_SUBST); // c:5086
4780                                         // c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
4781                                         // pipe fd in the current job's filelist so it's closed at job exit.
4782        if let Some(jt) = JOBTAB.get() {
4783            let mut guard = jt.lock().unwrap();
4784            let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4785            if tj >= 0 {
4786                if let Some(j) = guard.get_mut(tj as usize) {
4787                    crate::ported::jobs::addfilelist(j, None, fd);
4788                }
4789            }
4790        }
4791        // c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
4792        // record the proc-subst writer-side child in the job's
4793        // auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
4794        // child), C omits the addproc — symmetric here.
4795        if out == 0 {
4796            if let Some(jt) = JOBTAB.get() {
4797                let mut guard = jt.lock().unwrap();
4798                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4799                if tj >= 0 {
4800                    if let Some(j) = guard.get_mut(tj as usize) {
4801                        crate::ported::jobs::addproc(
4802                            j,
4803                            pid,
4804                            "",
4805                            true,
4806                            Some(std::time::Instant::now()),
4807                            -1,
4808                            -1,
4809                        );
4810                    }
4811                }
4812            }
4813        }
4814        procsubstpid.store(pid, Ordering::Relaxed); // c:5092
4815        return Some(pnam); // c:5093
4816    }
4817    // c:5095 — child.
4818    entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
4819    let _ = redup(pipes[out as usize], out); // c:5096
4820    closem(FDT_UNUSED, 0); // c:5097
4821    cmdpush(CS_CMDSUBST as u8); // c:5100
4822    // The re-fed text is what parsecmd parsed: after the two marker chars,
4823    // up to the Outpar ending at `ends_at` (a byte offset). The markers are
4824    // multi-byte chars, so a `cmd[2..]` byte slice split them.
4825    let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4826    let body_end = ends_at.saturating_sub(Outpar.len_utf8());
4827    let body = if body_end > body_start && body_end <= cmd.len() {
4828        untokenize(&cmd[body_start..body_end])
4829    } else {
4830        String::new()
4831    };
4832    let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
4833    cmdpop(); // c:5102
4834    let _ = zclose(out); // c:5103
4835    let _ = std::io::Write::flush(&mut std::io::stdout());
4836    crate::ported::builtin::_realexit(); // c:5104
4837}
4838
4839/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
4840/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
4841pub mod esub {
4842    // c:1056
4843    /// `ASYNC` constant.
4844    pub const ASYNC: i32 = 0x01; // c:1058
4845    /// `PGRP` constant.
4846    pub const PGRP: i32 = 0x02; // c:1063
4847    /// `KEEPTRAP` constant.
4848    pub const KEEPTRAP: i32 = 0x04; // c:1065
4849    /// `FAKE` constant.
4850    pub const FAKE: i32 = 0x08; // c:1067
4851    /// `REVERTPGRP` constant.
4852    pub const REVERTPGRP: i32 = 0x10; // c:1069
4853    /// `NOMONITOR` constant.
4854    pub const NOMONITOR: i32 = 0x20; // c:1071
4855    /// `JOB_CONTROL` constant.
4856    pub const JOB_CONTROL: i32 = 0x40; // c:1073
4857}
4858
4859/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
4860/// Out-arg used by `entersubsh()` to hand back the group-leader pid
4861/// and the list-pipe job index the parent should track. Only filled
4862/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
4863/// groups).
4864#[allow(non_camel_case_types)]
4865#[derive(Default)]
4866pub struct entersubsh_ret {
4867    pub gleader: i32,       // c:1122
4868    pub list_pipe_job: i32, // c:1123
4869}
4870
4871/// Port of `entersubsh()` from `Src/exec.c:1084` — C decl `entersubsh(int flags, struct entersubsh_ret *retp)`.
4872/// Called by every child fork to switch the
4873/// process into subshell mode: traps reset, monitor disabled, signals
4874/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
4875/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
4876///
4877/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
4878///     1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
4879///     ESUB_PGRP+sync path establishes pipeline group-leadership
4880///     (list_pipe_job inherit or thisjob-as-leader), filling
4881///     entersubsh_ret with the chosen gleader + list_pipe_job index.
4882/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
4883///     `clearjobtab(&mut JobTable, monitor)`; we get the global table
4884///     via a TABLE handle similar to other jobs.rs entries.
4885/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
4886/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
4887///     mypgrp` — direct C parity (jobs.rs:3406 provides the call).
4888/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
4889///     uses `opts_set_off(USEZLE)`; zleactive is the atomic in
4890///     builtins/sched.rs.
4891/// =============================================================
4892pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
4893    // c:1083
4894    let monitor: i32;
4895    let job_control_ok: i32;
4896    // c:1088-1092 — reset traps unless KEEPTRAP.
4897    if (flags & esub::KEEPTRAP) == 0 {
4898        // c:1088
4899        for sig in 0..=SIGCOUNT {
4900            // c:1089
4901            let st = {
4902                let guard = sigtrapped.lock().unwrap();
4903                guard.get(sig as usize).copied().unwrap_or(0)
4904            };
4905            let func_set = (st & ZSIG_FUNC) != 0; // c:1090
4906            let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
4907            if !func_set && !posix_ignored {
4908                unsettrap(sig); // c:1092
4909            }
4910        }
4911    }
4912    monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
4913    job_control_ok = if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
4914        // c:1094
4915        1
4916    } else {
4917        0
4918    };
4919    EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
4920    if (flags & esub::NOMONITOR) != 0 {
4921        // c:1096
4922        dosetopt(MONITOR, 0, 0); // c:1097
4923    }
4924    if !isset(MONITOR) {
4925        // c:1098
4926        if (flags & esub::ASYNC) != 0 {
4927            // c:1099
4928            let _ = settrap(libc::SIGINT, None, 0); // c:1100
4929            let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
4930            if unsafe { libc::isatty(0) } != 0 {
4931                // c:1102
4932                unsafe {
4933                    libc::close(0);
4934                } // c:1103
4935                let devnull = std::ffi::CString::new("/dev/null").unwrap();
4936                if unsafe { libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) } != 0 {
4937                    // c:1104
4938                    zerr(&format!(
4939                        // c:1105
4940                        "can't open /dev/null: {}",
4941                        std::io::Error::last_os_error()
4942                    ));
4943                    unsafe {
4944                        libc::_exit(1);
4945                    } // c:1106
4946                }
4947            }
4948        }
4949    } else if (flags & esub::PGRP) != 0 {
4950        // c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
4951        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4952        if thisjob != -1 {
4953            let lpj = list_pipe_job.load(Ordering::Relaxed);
4954            let lp = list_pipe.load(Ordering::Relaxed);
4955            let lpc = list_pipe_child.load(Ordering::Relaxed);
4956            if let Some(jt) = JOBTAB.get() {
4957                let mut guard = jt.lock().unwrap();
4958                let lpj_gleader = guard.get(lpj as usize).map(|j| j.gleader).unwrap_or(0);
4959                if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
4960                    // c:1111-1124 — inherit list_pipe_job's group leader.
4961                    let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
4962                        || (unsafe { libc::killpg(lpj_gleader, 0) } == -1
4963                            && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH))
4964                    {
4965                        // c:1115-1117 — primary group leader gone; this child becomes leader.
4966                        let new_gl = if lpc != 0 {
4967                            mypgrp.load(Ordering::Relaxed)
4968                        } else {
4969                            unsafe { libc::getpid() }
4970                        };
4971                        if let Some(j) = guard.get_mut(lpj as usize) {
4972                            j.gleader = new_gl;
4973                        }
4974                        if let Some(j) = guard.get_mut(thisjob as usize) {
4975                            j.gleader = new_gl;
4976                        }
4977                        unsafe { libc::setpgid(0, new_gl) };
4978                        if (flags & esub::ASYNC) == 0 {
4979                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
4980                        }
4981                        new_gl
4982                    } else {
4983                        lpj_gleader
4984                    };
4985                    if let Some(r) = retp {
4986                        if (flags & esub::ASYNC) == 0 {
4987                            r.gleader = pgid; // c:1122
4988                            r.list_pipe_job = lpj; // c:1123
4989                        }
4990                    }
4991                } else {
4992                    // c:1126-1151 — standard group-leader-takeover path.
4993                    let thisjob_gleader =
4994                        guard.get(thisjob as usize).map(|j| j.gleader).unwrap_or(0);
4995                    if thisjob_gleader == 0 || unsafe { libc::setpgid(0, thisjob_gleader) } == -1 {
4996                        let new_gl = unsafe { libc::getpid() };
4997                        if let Some(j) = guard.get_mut(thisjob as usize) {
4998                            j.gleader = new_gl; // c:1138
4999                        }
5000                        if lpj != thisjob {
5001                            let lpj_was_unset = guard
5002                                .get(lpj as usize)
5003                                .map(|j| j.gleader == 0)
5004                                .unwrap_or(true);
5005                            if lpj_was_unset {
5006                                if let Some(j) = guard.get_mut(lpj as usize) {
5007                                    j.gleader = new_gl; // c:1140-1141
5008                                }
5009                            }
5010                        }
5011                        unsafe { libc::setpgid(0, new_gl) }; // c:1142
5012                        if (flags & esub::ASYNC) == 0 {
5013                            unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
5014                            if let Some(r) = retp {
5015                                r.gleader = new_gl; // c:1146
5016                                if lpj != thisjob {
5017                                    r.list_pipe_job = lpj; // c:1148
5018                                }
5019                            }
5020                        }
5021                    }
5022                }
5023            }
5024        } else {
5025            // No real job slot; basic setpgid fallback.
5026            unsafe { libc::setpgid(0, 0) };
5027        }
5028    }
5029    if (flags & esub::FAKE) == 0 {
5030        // c:1153
5031        subsh.store(1, Ordering::Relaxed); // c:1154
5032    }
5033    // c:1161 — `zsh_subshell++;` regardless of FAKE.
5034    zsh_subshell.fetch_add(1, Ordering::Relaxed);
5035    // c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
5036    if (flags & esub::REVERTPGRP) != 0
5037        && unsafe { libc::getpid() } == mypgrp.load(Ordering::Relaxed)
5038    {
5039        release_pgrp(); // c:1163
5040    }
5041    *shout.lock().unwrap() = 0; // c:1164 — shout = NULL
5042    if (flags & esub::NOMONITOR) != 0 {
5043        // c:1165
5044        signal_ignore(libc::SIGTTOU); // c:1171
5045        signal_ignore(libc::SIGTTIN); // c:1172
5046        signal_ignore(libc::SIGTSTP); // c:1173
5047    } else if job_control_ok == 0 {
5048        // c:1174
5049        signal_default(libc::SIGTTOU); // c:1181
5050        signal_default(libc::SIGTTIN); // c:1182
5051        signal_default(libc::SIGTSTP); // c:1183
5052    }
5053    let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
5054    if interact {
5055        signal_default(libc::SIGTERM); // c:1186
5056        let int_st = sigtrapped
5057            .lock()
5058            .unwrap()
5059            .get(libc::SIGINT as usize)
5060            .copied()
5061            .unwrap_or(0);
5062        if (int_st & ZSIG_IGNORED) == 0 {
5063            // c:1187
5064            signal_default(libc::SIGINT); // c:1188
5065        }
5066        let pipe_st = sigtrapped
5067            .lock()
5068            .unwrap()
5069            .get(libc::SIGPIPE as usize)
5070            .copied()
5071            .unwrap_or(0);
5072        if pipe_st == 0 {
5073            // c:1189
5074            signal_default(libc::SIGPIPE); // c:1190
5075        }
5076    }
5077    let quit_st = sigtrapped
5078        .lock()
5079        .unwrap()
5080        .get(libc::SIGQUIT as usize)
5081        .copied()
5082        .unwrap_or(0);
5083    if (quit_st & ZSIG_IGNORED) == 0 {
5084        // c:1192
5085        signal_default(libc::SIGQUIT); // c:1193
5086    }
5087    // c:1202-1205 — unblock any trapped signals while in `intrap`.
5088    if intrap.load(Ordering::Relaxed) != 0 {
5089        // c:1202
5090        for sig in 1..=SIGCOUNT {
5091            let st = sigtrapped
5092                .lock()
5093                .unwrap()
5094                .get(sig as usize)
5095                .copied()
5096                .unwrap_or(0);
5097            if st != 0 && st != ZSIG_IGNORED {
5098                // c:1204
5099                let m = signal_mask(sig);
5100                let _ = signal_unblock(&m); // c:1205
5101            }
5102        }
5103    }
5104    if job_control_ok == 0 {
5105        // c:1206
5106        dosetopt(MONITOR, 0, 0); // c:1207
5107    }
5108    dosetopt(USEZLE, 0, 0); // c:1208
5109    zleactive.store(0, Ordering::Relaxed); // c:1209
5110                                           // c:1214-1217 — close saved fds.
5111    let max = MAX_ZSH_FD.load(Ordering::Relaxed);
5112    for i in 10..=max {
5113        if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
5114            // c:1215
5115            let _ = zclose(i); // c:1216
5116        }
5117    }
5118    // c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
5119    // at jobs.rs:1695 which handles ALL the C body including the
5120    // oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
5121    let mut dummy_table = crate::exec_jobs::JobTable::new();
5122    crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
5123    let _ = get_usage(); // c:1220
5124    FORKLEVEL.store(
5125        // c:1221 — `forklevel = locallevel;`
5126        locallevel.load(Ordering::Relaxed),
5127        Ordering::Relaxed,
5128    );
5129}
5130
5131/// RAII guard applying the fork-safe subset of
5132/// `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` (`Src/exec.c:1083`) around a
5133/// command substitution that zshrs runs **in-process**, restoring every
5134/// touched global on drop.
5135///
5136/// C's `getoutput()` forks, and only the child runs `entersubsh`:
5137///
5138/// ```c
5139///     /* pid == 0 */
5140///     child_unblock();
5141///     zclose(pipes[0]);
5142///     redup(pipes[1], 1);
5143///     entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL);   /* c:4781 */
5144///     cmdpush(CS_CMDSUBST);
5145///     execode(prog, 0, 1, "cmdsubst");
5146/// ```
5147///
5148/// so every state delta dies with the child and the parent is untouched.
5149/// zshrs collapses that fork (`getoutput` below → `run_command_substitution`),
5150/// which means the deltas were previously never applied **at all**: inside
5151/// `$( )` the shell kept the caller's `zleactive` / `opts[USEZLE]`, so every
5152/// reader of them took the "we are inside the line editor" branch. Observed
5153/// failure: `lt=( ${(f)"$(fc -l -200)"} )` bound to a ZLE widget died with
5154/// `no interactive history within ZLE` — `bin_fc`'s guard
5155/// (`builtin.rs:2757`, a correct port of `Src/builtin.c:1523-1527`) firing on
5156/// a stale `zleactive`. Same class of bug for every other reader:
5157/// `jobs.rs:1114` (time-report suppression), `utils.rs:1785` (screen
5158/// ownership), `signals.rs:2050` (refresh-on-signal).
5159///
5160/// APPLIED (pure restorable scalar state; correct without a fork):
5161///   * c:1097 / c:1207 — `opts[MONITOR] = 0`. ESUB_NOMONITOR is set and
5162///     `job_control_ok` is 0 (no ESUB_JOB_CONTROL), so both C branches
5163///     agree. A substitution body must not do job control.
5164///   * c:1164 — `shout = NULL`. Its readers already handle the NULL case
5165///     the way the C child does (`putshout` falls back to stderr,
5166///     `checkrmall` returns 1 at `utils.rs:3565` per c:2871, `mailcheck`
5167///     stays silent per c:1670).
5168///   * c:1208 — `opts[USEZLE] = 0`.
5169///   * c:1209 — `zleactive = 0`.
5170///
5171/// DELIBERATELY SKIPPED — each would be wrong, or is already handled
5172/// elsewhere on the in-process path:
5173///   * c:1127-1131 `unsettrap(sig)` — APPLIED, but not here: the trap table is
5174///     not scalar state (trap bodies and `sigaction` dispositions), so
5175///     `run_command_substitution` resets it with the same helper `( … )`
5176///     uses (`fusevm_bridge::entersubsh_reset_traps`) and puts the parent's
5177///     flags and dispositions back on exit.
5178///   * c:1095 `exit_val = 0` — ALREADY DONE on this path:
5179///     `vm_helper.rs:3893` swaps `EXIT_VAL` to 0 before the nested VM runs
5180///     and restores it at `vm_helper.rs:4004`.
5181///   * c:1110-1151 `setpgrp`/`attachtty`/gleader bookkeeping — process-group
5182///     and tty surgery. With no fork this moves the interactive shell's own
5183///     process group and hands the terminal away.
5184///   * c:1154 `subsh = 1` — `init.rs:2289` does `if subsh != 0 { realexit() }`.
5185///     With no fork, an `exit` inside `$( )` would then hard-kill the
5186///     interactive shell instead of ending the substitution.
5187///   * c:1161 `zsh_subshell++` — ALREADY DONE on this path by
5188///     `CmdSubstSubshellBump` (`fusevm_bridge.rs:232`, entered at
5189///     `vm_helper.rs:3734`). Bumping again would make `$ZSH_SUBSHELL` read 2
5190///     inside `$( )` instead of 1.
5191///   * c:1165-1193 `signal_ignore`/`signal_default` — per-process signal
5192///     dispositions. Defaulting SIGINT/SIGQUIT/SIGTSTP in the live
5193///     interactive shell would let ^C kill and ^Z stop the shell itself.
5194///   * c:1202-1205 `signal_unblock` — the process signal mask is owned by
5195///     the trap dispatcher's block/unblock pairing; unblocking here breaks
5196///     that pairing.
5197///   * c:1214-1217 close FDT_SAVED_MASK fds — fd surgery. These are the
5198///     parent's saved descriptors for redirection restore; closing them is
5199///     unrecoverable.
5200///   * c:1219 `clearjobtab(monitor)` — would destroy the parent's job table.
5201///   * c:1220 `get_usage()` — overwrites the parent's `times` baseline with
5202///     no fork to discard it, and nothing inside the substitution reads it.
5203///   * c:1221 `forklevel = locallevel` — FORKLEVEL gates local-scope
5204///     unwinding; the in-process body pushes and pops its own locals
5205///     normally, so moving the fork boundary would strand them.
5206///
5207/// Nesting is safe: each guard saves what it observed on entry and restores
5208/// exactly that on drop, so LIFO nesting (`$( … $( … ) … )`) unwinds
5209/// correctly.
5210pub struct SubshStateGuard {
5211    saved_monitor: bool,
5212    saved_shout: usize,
5213    saved_usezle: bool,
5214    saved_zleactive: i32,
5215    saved_subsh: i32,
5216}
5217
5218/// The `zleactive` the shell had before the innermost in-process subshell
5219/// state was applied, or `-1` when no such state is in effect.
5220///
5221/// !!! WARNING: RUST-ONLY STATE !!! C has no counterpart and needs none.
5222/// `Src/exec.c:1248`'s `zleactive = 0;` is inside `entersubsh`, and every
5223/// caller reaches it in the FORKED CHILD — `getoutput`, the `$(...)` one,
5224/// calls it at `Src/exec.c:4838` under the `/* pid == 0 */` label
5225/// (c:4834), with the parent left at `waitforpid(pid, 0)` on c:4830.
5226/// The parent's `zleactive` is never touched,
5227/// so a signal handler that lands in the parent while a `$(...)` is running
5228/// still sees ZLE as active — which is the whole reason `adjustwinsize`'s
5229/// `if (zleactive && resetzle)` (`Src/utils.c:1954`) can repaint on a resize
5230/// that arrives during a completion.
5231///
5232/// `SubshStateGuard` runs that child-side write IN THE PARENT because zshrs
5233/// executes command substitutions in-process, so the parent's own handler
5234/// read 0 and declined to repaint. This publishes what C's parent would have
5235/// had, for the handler to consult. Only the outermost guard records, so a
5236/// nested substitution cannot overwrite it with its own zero.
5237pub static SUBSH_PARENT_ZLEACTIVE: std::sync::atomic::AtomicI32 =
5238    std::sync::atomic::AtomicI32::new(-1);
5239
5240/// Nesting depth of the in-process subshell state above.
5241pub static SUBSH_STATE_DEPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
5242
5243impl SubshStateGuard {
5244    /// Apply the deltas listed above and capture the values to restore.
5245    pub fn enter() -> Self {
5246        let g = SubshStateGuard {
5247            saved_monitor: isset(MONITOR),
5248            saved_shout: *shout.lock().unwrap(),
5249            saved_usezle: isset(USEZLE),
5250            saved_zleactive: zleactive.load(Ordering::Relaxed),
5251            saved_subsh: subsh.load(Ordering::Relaxed),
5252        };
5253        // c:1153-1154 — `if (!(flags & ESUB_FAKE)) subsh = 1;`. The
5254        // substitution's body is "in a subshell" for every consumer that
5255        // asks, which is what keeps PRINT_EXIT_VALUE quiet inside
5256        // `x=$(false)` (c:4309 `&& !subsh`).
5257        subsh.store(1, Ordering::Relaxed); // c:1154
5258                                           // `force = 1`: C assigns the `opts[]` slots directly, so the
5259                                           // dosetopt gatekeeping (c:743-861, which only guards turning
5260                                           // options ON) must not apply in either direction.
5261        dosetopt(MONITOR, 0, 1); // c:1097 / c:1207
5262        *shout.lock().unwrap() = 0; // c:1164
5263        dosetopt(USEZLE, 0, 1); // c:1208
5264        // Publish the pre-substitution value BEFORE zeroing it, so a signal
5265        // handler that lands in this process during the substitution can read
5266        // the `zleactive` C's parent would still have. Outermost guard only.
5267        if SUBSH_STATE_DEPTH.fetch_add(1, Ordering::SeqCst) == 0 {
5268            SUBSH_PARENT_ZLEACTIVE.store(g.saved_zleactive, Ordering::SeqCst);
5269        }
5270        zleactive.store(0, Ordering::Relaxed); // c:1209
5271        g
5272    }
5273}
5274
5275impl Drop for SubshStateGuard {
5276    fn drop(&mut self) {
5277        // Reverse order of `enter`; the C child never restores because it
5278        // `_realexit()`s, so this half has no C counterpart to cite.
5279        subsh.store(self.saved_subsh, Ordering::Relaxed);
5280        if SUBSH_STATE_DEPTH.fetch_sub(1, Ordering::SeqCst) <= 1 {
5281            SUBSH_PARENT_ZLEACTIVE.store(-1, Ordering::SeqCst);
5282        }
5283        zleactive.store(self.saved_zleactive, Ordering::Relaxed);
5284        dosetopt(USEZLE, self.saved_usezle as i32, 1);
5285        *shout.lock().unwrap() = self.saved_shout;
5286        dosetopt(MONITOR, self.saved_monitor as i32, 1);
5287    }
5288}
5289
5290/// The state a forked subshell owns a private copy of — the shell's hash
5291/// tables, its environment, its working directory and its process
5292/// attributes — taken on entry to an in-process subshell and put back on
5293/// exit.
5294///
5295/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5296/// `getoutput` (`Src/exec.c:4816`) and `( … )` (`c:2880`) run the body in
5297/// a `zfork()` child. `entersubsh` (`c:1123-1261`) resets traps, job
5298/// control, `subsh` and `zsh_subshell`, but it does not touch any of the
5299/// state below: the child simply writes its fork-copied tables, its own
5300/// `environ`, its own cwd, umask and rlimits, and all of it dies at
5301/// `_realexit()` (`c:4843`). The parent never sees
5302///     x=$(alias a=b; export v=1; cd /tmp; umask 077)
5303/// because nothing it owns was written. zshrs runs both forms IN
5304/// PROCESS, so each piece the body can write must be handed back here.
5305///
5306/// Cost of `save`, per substitution: every table is copy-on-write, so a
5307/// refcount bump each, and only a body that writes a table pays for
5308/// copying it; the environment is one byte copy (`environ_image`); the
5309/// directory stack and the two rlimit arrays are short vectors; the cwd
5310/// is one `stat` and the umask two `umask(2)` calls.
5311/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5312/// The entry-directory descriptor `SubshForkCopy` holds for the in-process
5313/// subshell. A forked child can never move its parent; zshrs's body shares
5314/// the process cwd, so the parent must be put back into the same directory
5315/// object, not the same path. The fd is recorded `FDT_INTERNAL`, like every
5316/// other descriptor the shell keeps for itself (`movefd`,
5317/// `Src/utils.c:1990-2011`), but sits past the script's `{var}` range
5318/// (`lowfd::movefd_past_script`), and is close-on-exec so an external command
5319/// run by the body never inherits it. Dropping closes it.
5320struct SubshCwdFd(i32);
5321
5322impl SubshCwdFd {
5323    fn open() -> Self {
5324        let fd = unsafe { libc::open(b".\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_CLOEXEC) };
5325        if fd < 0 {
5326            return SubshCwdFd(-1);
5327        }
5328        // Moved past the script's `{var}` range as well: a forked child has
5329        // no such descriptor, so `(exec {v}>f; print $v)` must still get 11.
5330        let moved = crate::lowfd::movefd_past_script(fd);
5331        if moved == fd {
5332            unsafe { libc::close(fd) };
5333            return SubshCwdFd(-1);
5334        }
5335        SubshCwdFd(moved)
5336    }
5337}
5338
5339impl Drop for SubshCwdFd {
5340    fn drop(&mut self) {
5341        if self.0 >= 0 {
5342            crate::ported::utils::zclose(self.0);
5343        }
5344    }
5345}
5346
5347pub struct SubshForkCopy {
5348    /// `aliastab` (`Src/hashtable.c:1177`) — `alias`, `unalias`,
5349    /// `disable -a`, `aliases[x]=…`.
5350    aliastab: crate::ported::hashtable::alias_table,
5351    /// `sufaliastab` (`Src/hashtable.c:1182`) — `alias -s`, `unalias -s`.
5352    sufaliastab: crate::ported::hashtable::alias_table,
5353    /// `nameddirtab` (`Src/hashnameddir.c:48`) — `hash -d`, `unhash -d`,
5354    /// `nameddirs[x]=…`, and the nodes a `~name` lookup adds.
5355    nameddirtab: crate::cow_map::CowArc<crate::ported::hashtable::hashtable_nodes<crate::ported::zsh_h::nameddir>>,
5356    /// `allusersadded` (`Src/hashnameddir.c:53`) — set by a
5357    /// `fillnameddirtable` that ran in the body; the parent's table
5358    /// never got those user entries, so its flag must not claim it did.
5359    allusersadded: i32,
5360    /// `cmdnamtab` (`Src/hashtable.c:590`) — `hash`, `unhash`, `hash -r`,
5361    /// `commands[x]=…`, and a fill run by reading `$commands`.
5362    cmdnamtab: crate::ported::hashtable::cmdnam_table,
5363    /// `pathchecked` (`Src/hashtable.c:595`) — how far a fill has walked
5364    /// `$path`; it describes `cmdnamtab`, so it travels with it.
5365    pathchecked: usize,
5366    /// `environ` — every `export`, `unset` of an exported name, `cd`
5367    /// (`PWD`/`OLDPWD`) and `allexport` assignment writes it with
5368    /// `setenv`/`unsetenv` (`Src/params.c:5320`, `c:5537`).
5369    environ: crate::ported::params::environ_image,
5370    /// The process working directory, as the device and inode of `.`.
5371    /// `cd`, `pushd` and `popd` in the body move it for the whole
5372    /// process; `restore` compares and moves back only if they did.
5373    cwd: Option<(u64, u64)>,
5374    /// The `oldpwd` global (`Src/builtin.c`) — a `cd` in the body sets it in
5375    /// the child only; run in-process it leaked, so `$(cd ../x; print $PWD)`
5376    /// in a completion function (`_cd`) made the parent's next `cd -` a no-op.
5377    oldpwd: Option<String>,
5378    /// An fd open on the entry directory itself. `restore` goes back
5379    /// through it, so a body that renames the directory out from under
5380    /// the parent (`( cd .. && mv foo bar )` run from `foo`) still returns
5381    /// the parent to the very directory it was in, as a fork does.
5382    cwd_fd: SubshCwdFd,
5383    /// `$PWD` on entry — the fallback when `cwd_fd` could not be opened.
5384    pwd: Option<String>,
5385    /// The `oldpwd` global `cd -` / `~-` read (Src/builtin.c:1239).
5386    oldpwd_global: Option<String>,
5387    /// `dirstack` (`Src/builtin.c:744`) — `pushd` / `popd`.
5388    dirstack: Vec<String>,
5389    /// The file-creation mask — `umask` (`Src/builtin.c:7483`).
5390    umask: libc::mode_t,
5391    /// `limits[]` and `current_limits[]` (`Src/exec.c:315`) — `ulimit`,
5392    /// `limit`, `unlimit`. `zfork` applies the child's copy to the child
5393    /// (`c:381-383` `setlimits(NULL)`), so the parent never sees it. Both
5394    /// arrays are needed and are rolled back differently: `limits[]` is
5395    /// what the shell WANTS, `current_limits[]` what `setrlimit` was
5396    /// actually given (`zsetlimit`, `c:319-332`, only calls `setrlimit`
5397    /// where they disagree), so `limit descriptors 256` without `-s`
5398    /// moves only the first.
5399    limits: Vec<libc::rlimit>,
5400    current_limits: Vec<libc::rlimit>,
5401    /// `zstyletab` (`Src/Modules/zutil.c:106`) — `zstyle`, `zstyle -d`.
5402    zstyletab: crate::ported::modules::zutil::style_table,
5403    /// `modulestab` (`Src/module.c:49`) as `(name → flags)`: `zmodload`
5404    /// flips `MOD_INIT_B` / `MOD_UNLOAD`, and loading a module the table
5405    /// has never seen adds a node (see `restore`).
5406    modules: std::collections::HashMap<String, i32>,
5407    /// Builtins carrying `DISABLED` in `builtintab` (`Src/builtin.c:146`)
5408    /// — `disable`/`enable` (`c:535-546`).
5409    builtins_disabled: std::collections::HashSet<String>,
5410    /// Reserved words carrying `DISABLED` in `reswdtab`
5411    /// (`Src/hashtable.c:1114`) — `disable -r`. Changes how the parent
5412    /// PARSES, e.g. `repeat 2 print r` stops being a loop.
5413    reswds_disabled: std::collections::HashSet<String>,
5414    /// `schedcmds` (`Src/Builtins/sched.c:52`) — `sched`.
5415    schedcmds: Option<Box<crate::ported::builtins::sched::schedcmd>>,
5416    /// `watch_fds` (`Src/Zle/zle_main.c:204`) — `zle -F`. A forked
5417    /// child's handlers die with it; kept here, a `$(cd x)` whose `chpwd`
5418    /// hook armed one (zinit's `@zinit-scheduler`: `exec {AFD}< <(…);
5419    /// zle -F $AFD @zinit-scheduler`) left the parent watching a descriptor
5420    /// the frame had closed: `failed to close file descriptor 17`.
5421    watch_fds: Vec<crate::ported::zle::zle_h::watch_fd>,
5422    /// `shtimer` (`Src/params.c:147`) — assigning `SECONDS` moves it.
5423    shtimer: std::time::Duration,
5424    /// `donetrap` (`Src/exec.c:1413`) — a ZERR trap that ran inside the body
5425    /// sets it (`c:1658`) in the forked child only. The parent's own check
5426    /// for the subshell's failing status (`c:1651-1659`) must still see the
5427    /// value it had on entry, or `(false)` runs ZERR once instead of twice.
5428    donetrap: i32,
5429    /// `zpc_disables[]` (`Src/pattern.c:4179`) — `disable -p` / `enable -p`.
5430    /// A fork leaves the parent's array alone, so `( disable -p '*' )`
5431    /// must not keep `*` disabled afterwards. Held as the
5432    /// `savepatterndisables()` bitmap (`c:4220`).
5433    pattern_disables: u32,
5434}
5435
5436impl SubshForkCopy {
5437    /// Take the parent's state. O(1) per table; see the type docs for
5438    /// the rest.
5439    pub fn save() -> Self {
5440        use std::os::unix::fs::MetadataExt;
5441        crate::ported::builtins::rlimits::ensure_limits_initialized();
5442        let rlimits = |a: &std::sync::OnceLock<std::sync::Mutex<Vec<libc::rlimit>>>| {
5443            a.get()
5444                .and_then(|l| l.lock().ok().map(|g| g.clone()))
5445                .unwrap_or_default()
5446        };
5447        SubshForkCopy {
5448            aliastab: crate::ported::hashtable::aliastab_lock()
5449                .read()
5450                .map(|t| t.snapshot())
5451                .unwrap_or_default(),
5452            sufaliastab: crate::ported::hashtable::sufaliastab_lock()
5453                .read()
5454                .map(|t| t.snapshot())
5455                .unwrap_or_default(),
5456            nameddirtab: crate::ported::hashnameddir::nameddirtab()
5457                .lock()
5458                .map(|t| t.clone())
5459                .unwrap_or_default(),
5460            allusersadded: crate::ported::hashnameddir::allusersadded.load(Ordering::Relaxed),
5461            cmdnamtab: crate::ported::hashtable::cmdnamtab_lock()
5462                .read()
5463                .map(|t| t.snapshot())
5464                .unwrap_or_default(),
5465            pathchecked: pathchecked.load(Ordering::SeqCst),
5466            environ: crate::ported::params::environ_image::save(),
5467            cwd: std::fs::metadata(".").ok().map(|m| (m.dev(), m.ino())),
5468            cwd_fd: SubshCwdFd::open(),
5469            oldpwd: crate::dash_mode::oldpwd_global(),
5470            pwd: getsparam("PWD"),
5471            oldpwd_global: crate::extensions::dash_mode::oldpwd_global_get(),
5472            dirstack: crate::ported::modules::parameter::DIRSTACK
5473                .lock()
5474                .map(|d| d.clone())
5475                .unwrap_or_default(),
5476            // umask(2) can only be read by setting it; put it straight back.
5477            umask: unsafe {
5478                let m = libc::umask(0o022);
5479                libc::umask(m);
5480                m
5481            },
5482            limits: rlimits(&crate::ported::builtins::rlimits::LIMITS),
5483            current_limits: rlimits(&crate::ported::builtins::rlimits::CURRENT_LIMITS),
5484            zstyletab: crate::ported::modules::zutil::zstyletab
5485                .lock()
5486                .map(|t| t.clone())
5487                .unwrap_or_default(),
5488            modules: crate::ported::module::MODULESTAB
5489                .lock()
5490                .map(|t| t.modules.iter().map(|(k, v)| (k.clone(), v.node.flags)).collect())
5491                .unwrap_or_default(),
5492            builtins_disabled: crate::ported::builtin::BUILTINS_DISABLED
5493                .lock()
5494                .map(|s| s.clone())
5495                .unwrap_or_default(),
5496            reswds_disabled: crate::ported::hashtable::reswdtab_lock()
5497                .read()
5498                .map(|t| {
5499                    t.iter()
5500                        .filter(|(_, r)| (r.node.flags & crate::ported::zsh_h::DISABLED as i32) != 0)
5501                        .map(|(n, _)| n.clone())
5502                        .collect()
5503                })
5504                .unwrap_or_default(),
5505            schedcmds: crate::ported::builtins::sched::schedcmd::subsh_save(),
5506            watch_fds: crate::ported::zle::zle_main::WATCH_FDS
5507                .lock()
5508                .map(|t| t.clone())
5509                .unwrap_or_default(),
5510            shtimer: crate::ported::params::shtimer_lock()
5511                .lock()
5512                .map(|t| *t)
5513                .unwrap_or_default(),
5514            donetrap: DONETRAP.load(Ordering::Relaxed),
5515            pattern_disables: crate::ported::pattern::savepatterndisables(),
5516        }
5517    }
5518
5519    /// Put the parent's state back, discarding whatever the subshell
5520    /// body did to it — the in-process stand-in for the child exiting.
5521    pub fn restore(self) {
5522        use std::os::unix::fs::MetadataExt;
5523        // modulestab first: rolling a module back runs `cleanup_module`
5524        // → `setfeatureenables` (`Src/module.c:3354`) → `deleteparamdef`
5525        // (c:1128), which looks its parameters up in the LIVE paramtab —
5526        // so callers restore this struct before they put paramtab back.
5527        //
5528        // A `zmodload zsh/X` for a module with no node yet takes
5529        // load_module's allocate-on-miss branch (c:2229-2237) and CREATES
5530        // the node. In C that node is allocated in the forked child and
5531        // dies with it; here it survives, and restoring flags alone would
5532        // never touch it — `(zmodload zsh/datetime)` left the parent with a
5533        // loaded node and `zmodload -e zsh/datetime` answering 0 where zsh
5534        // answers 1. So nodes the parent did not have are rolled back the
5535        // way a real unload does it — `cleanup_module` (c:1922) →
5536        // `finish_module` (c:1930), undoing the feature enables the load
5537        // made in the module's own statics — and dropped; then the flags
5538        // of the parent's nodes are put back.
5539        if let Ok(mut t) = crate::ported::module::MODULESTAB.lock() {
5540            let strays: Vec<String> = t
5541                .modules
5542                .keys()
5543                .filter(|n| !self.modules.contains_key(*n))
5544                .cloned()
5545                .collect();
5546            for name in &strays {
5547                let loaded = t
5548                    .modules
5549                    .get(name)
5550                    .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
5551                    .unwrap_or(false);
5552                if loaded {
5553                    let _ = crate::ported::module::cleanup_module(&mut t, name);
5554                    let _ = crate::ported::module::finish_module(&mut t, name);
5555                }
5556            }
5557            t.modules.retain(|name, _| self.modules.contains_key(name));
5558            for (name, saved_flags) in &self.modules {
5559                if let Some(m) = t.modules.get_mut(name) {
5560                    m.node.flags = *saved_flags;
5561                }
5562            }
5563        }
5564        if let Ok(mut t) = crate::ported::modules::zutil::zstyletab.lock() {
5565            *t = self.zstyletab;
5566        }
5567        if let Ok(mut s) = crate::ported::builtin::BUILTINS_DISABLED.lock() {
5568            *s = self.builtins_disabled;
5569        }
5570        if let Ok(mut t) = crate::ported::hashtable::reswdtab_lock().write() {
5571            let names: Vec<String> = t.iter().map(|(n, _)| n.clone()).collect();
5572            for n in names {
5573                if self.reswds_disabled.contains(&n) {
5574                    t.disable(&n);
5575                } else {
5576                    t.enable(&n);
5577                }
5578            }
5579        }
5580        crate::ported::builtins::sched::schedcmd::subsh_restore(self.schedcmds);
5581        if let Ok(mut t) = crate::ported::zle::zle_main::WATCH_FDS.lock() {
5582            *t = self.watch_fds;
5583        }
5584        if let Ok(mut t) = crate::ported::params::shtimer_lock().lock() {
5585            *t = self.shtimer;
5586        }
5587        DONETRAP.store(self.donetrap, Ordering::Relaxed);
5588        crate::dash_mode::restore_oldpwd_global(self.oldpwd.clone());
5589        crate::ported::pattern::restorepatterndisables(self.pattern_disables); // c:4258
5590        if let Ok(mut t) = crate::ported::hashtable::aliastab_lock().write() {
5591            t.restore(self.aliastab);
5592        }
5593        if let Ok(mut t) = crate::ported::hashtable::sufaliastab_lock().write() {
5594            t.restore(self.sufaliastab);
5595        }
5596        if let Ok(mut t) = crate::ported::hashnameddir::nameddirtab().lock() {
5597            *t = self.nameddirtab;
5598        }
5599        crate::ported::hashnameddir::allusersadded.store(self.allusersadded, Ordering::Relaxed);
5600        if let Ok(mut t) = crate::ported::hashtable::cmdnamtab_lock().write() {
5601            t.restore(self.cmdnamtab);
5602        }
5603        pathchecked.store(self.pathchecked, Ordering::SeqCst);
5604        self.environ.restore();
5605        let here = std::fs::metadata(".").ok().map(|m| (m.dev(), m.ino()));
5606        if here != self.cwd {
5607            // By descriptor first: the entry path may no longer name the
5608            // entry directory (the body renamed it), and chdir-by-path
5609            // would then leave the parent somewhere else entirely.
5610            let back = self.cwd_fd.0 >= 0 && unsafe { libc::fchdir(self.cwd_fd.0) } == 0;
5611            if !back {
5612                if let Some(pwd) = &self.pwd {
5613                    let _ = std::env::set_current_dir(pwd);
5614                }
5615            }
5616        }
5617        crate::extensions::dash_mode::oldpwd_global_put(self.oldpwd_global);
5618        if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
5619            *d = self.dirstack;
5620        }
5621        unsafe {
5622            libc::umask(self.umask);
5623        }
5624        // `zsetlimit` (c:319-332) applied to before/after: replay
5625        // `setrlimit` only for the resources whose `current_limits[]` the
5626        // BODY moved, so a `limit` the parent set without `-s` is not
5627        // suddenly installed here. A body that LOWERED a hard limit is the
5628        // one case this cannot undo — the process cannot raise it again;
5629        // C escapes that only because the child dies with it.
5630        if let Some(lock) = crate::ported::builtins::rlimits::CURRENT_LIMITS.get() {
5631            if let Ok(mut cur) = lock.lock() {
5632                for (i, want) in self.current_limits.iter().enumerate() {
5633                    let Some(have) = cur.get(i).copied() else {
5634                        continue;
5635                    };
5636                    if have.rlim_max == want.rlim_max && have.rlim_cur == want.rlim_cur {
5637                        continue; // c:321-322
5638                    }
5639                    if unsafe { libc::setrlimit(i as _, want) } == 0 {
5640                        cur[i] = *want; // c:329
5641                    }
5642                }
5643            }
5644        }
5645        if let Some(lock) = crate::ported::builtins::rlimits::LIMITS.get() {
5646            if let Ok(mut g) = lock.lock() {
5647                *g = self.limits;
5648            }
5649        }
5650    }
5651}
5652
5653/// The descriptors a bare `exec` redirection moved inside an in-process
5654/// subshell, each with a copy of what it was before, and the ones the body
5655/// opened for itself (`{var}`, `sysopen -u`, `zsystem flock -f`).
5656///
5657/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5658/// `exec 3>file` / `exec 3>&-` with no command is permanent — c:4035
5659/// "specifically *don't* restore the original fd's" — but inside
5660/// `$( … )` or `( … )` it is permanent only for the forked child
5661/// (`Src/exec.c:4816`, `c:2880`), whose fd table dies with it. zshrs runs
5662/// the body in process, so the parent's descriptors are saved here the
5663/// first time the body's permanent redirection touches each one, and put
5664/// back when the frame is dropped; a descriptor from 10 up that the body
5665/// allocated is closed, as the child's exit would (`(exec {v}>f); exec
5666/// {w}>g` gives `w` 11 in zsh). A body that redirects nothing permanently
5667/// costs nothing.
5668pub struct SubshFdFrame {
5669    /// Only `enter` builds one, so every frame on `SUBSH_FD_FRAMES` has
5670    /// exactly one owner to pop it.
5671    _entered: (),
5672}
5673
5674thread_local! {
5675    /// `(fd, copy, fdtable kind)` per frame, innermost last; a copy of -1
5676    /// means the fd was closed on entry. Thread-local: a frame is entered and left on
5677    /// the thread that runs the body.
5678    static SUBSH_FD_FRAMES: std::cell::RefCell<Vec<Vec<(i32, i32, i32)>>> =
5679        const { std::cell::RefCell::new(Vec::new()) };
5680}
5681
5682impl SubshFdFrame {
5683    /// Open a frame for a subshell body.
5684    pub fn enter() -> Self {
5685        SUBSH_FD_FRAMES.with(|f| f.borrow_mut().push(Vec::new()));
5686        SubshFdFrame { _entered: () }
5687    }
5688
5689    /// Called before a permanent redirection changes `fd`: keep a copy
5690    /// (past the script's `{var}` range — see `lowfd::movefd_past_script`)
5691    /// unless the innermost frame already has one. Outside any frame
5692    /// the redirection really is permanent and nothing is kept.
5693    pub fn touch(fd: i32) {
5694        if fd < 0 {
5695            return;
5696        }
5697        SUBSH_FD_FRAMES.with(|f| {
5698            if let Some(frame) = f.borrow_mut().last_mut() {
5699                if !frame.iter().any(|(saved_fd, _, _)| *saved_fd == fd) {
5700                    let copy = unsafe {
5701                        libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, crate::lowfd::EXTENSION_FD_FLOOR)
5702                    };
5703                    frame.push((fd, copy, crate::ported::utils::fdtable_get(fd)));
5704                }
5705            }
5706        });
5707    }
5708
5709    /// Called when the body takes a descriptor from 10 up for the script
5710    /// (fdtable_set to a user kind): it did not exist for the parent, so
5711    /// the frame closes it on the way out.
5712    pub fn opened(fd: i32) {
5713        if fd < 10 {
5714            return;
5715        }
5716        SUBSH_FD_FRAMES.with(|f| {
5717            if let Ok(mut frames) = f.try_borrow_mut() {
5718                if let Some(frame) = frames.last_mut() {
5719                    if !frame.iter().any(|(saved_fd, _, _)| *saved_fd == fd) {
5720                        frame.push((fd, -1, crate::ported::zsh_h::FDT_UNUSED));
5721                    }
5722                }
5723            }
5724        });
5725    }
5726}
5727
5728impl Drop for SubshFdFrame {
5729    /// Put the parent's descriptors back, most recent first.
5730    fn drop(&mut self) {
5731        let frame = SUBSH_FD_FRAMES.with(|f| f.borrow_mut().pop().unwrap_or_default());
5732        if frame.is_empty() {
5733            return;
5734        }
5735        // Bytes the body printed belong to the body's fd 1.
5736        let _ = std::io::Write::flush(&mut std::io::stdout());
5737        for (fd, copy, kind) in frame.into_iter().rev() {
5738            if copy >= 0 {
5739                unsafe {
5740                    libc::dup2(copy, fd);
5741                    libc::close(copy);
5742                }
5743                if fd >= 10 {
5744                    crate::ported::utils::fdtable_set(fd, kind);
5745                }
5746            } else if fd >= 10 {
5747                crate::ported::utils::zclose(fd); // also drops the fdtable mark
5748            } else {
5749                unsafe { libc::close(fd) };
5750            }
5751        }
5752    }
5753}
5754
5755/// Port of `getpipe()` from `Src/exec.c:5119` — C decl `getpipe(char *cmd, int nullexec)`.
5756///
5757/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
5758/// pipe pair: parent gets back the readable (`<(...)`) or writable
5759/// (`>(...)`) end as an fd; child runs the substituted command with
5760/// its stdio redirected into the other end.
5761///
5762/// ```c
5763/// Eprog prog;
5764/// int pipes[2], out = *cmd == Inang;
5765/// pid_t pid;
5766/// struct timespec bgtime;
5767/// char *ends;
5768/// if (!(prog = parsecmd(cmd, &ends))) return -1;
5769/// if (*ends) { zerr("invalid syntax..."); return -1; }
5770/// if (mpipe(pipes) < 0) return -1;
5771/// if ((pid = zfork(&bgtime))) {
5772///     zclose(pipes[out]);
5773///     if (pid == -1) { zclose(pipes[!out]); return -1; }
5774///     if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
5775///     procsubstpid = pid;
5776///     return pipes[!out];
5777/// }
5778/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
5779/// redup(pipes[out], out);
5780/// closem(FDT_UNUSED, 0);
5781/// cmdpush(CS_CMDSUBST);
5782/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
5783/// cmdpop();
5784/// _realexit();
5785/// ```
5786///
5787/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
5788///     procsubst pid recording site (c:5141-5142) earlier this
5789///     session; the child IS now recorded in `JOBTAB[thisjob]`.
5790/// (b) `entersubsh` IS ported (exec.rs:4329) including the
5791///     ESUB_PGRP pipeline group-leadership path, and is called from
5792///     the real forked child below for getpipe's
5793///     `entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL)`.
5794/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
5795///     can route through execode for the parsed eprog. Currently
5796///     this caller still uses the fusevm pipeline for cache
5797///     coherence with execstring; switch over when the wordcode
5798///     walker becomes the primary path.
5799/// (d) The child ends with `_realexit()` (c:5152) after flushing
5800///     Rust's in-process stdout buffer.
5801pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
5802    // c:5119
5803    let bytes = cmd.as_bytes();
5804    let out: i32 = if cmd.starts_with(Inang) {
5805        1 // c:5122 — `<(...)` reads from child, child writes to fd 1
5806    } else {
5807        0 // `>(...)` — child reads from fd 0
5808    };
5809    let mut ends_at: usize = 0;
5810    let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
5811    if prog.is_none() {
5812        // c:5127
5813        return -1; // c:5128
5814    }
5815    // c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
5816    if ends_at < bytes.len() && bytes[ends_at] != 0 {
5817        zerr("invalid syntax for process substitution in redirection"); // c:5130
5818        return -1; // c:5131
5819    }
5820    let mut pipes: [i32; 2] = [-1; 2];
5821    if mpipe(&mut pipes) < 0 {
5822        // c:5133
5823        return -1;
5824    }
5825    // c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
5826    let mut bgtime: ZshTimespec = libc::timespec {
5827        tv_sec: 0,
5828        tv_nsec: 0,
5829    };
5830    let pid = zfork(Some(&mut bgtime)); // c:5135
5831    if pid != 0 {
5832        // c:5135 — parent.
5833        let _ = zclose(pipes[out as usize]); // c:5136
5834        if pid == -1 {
5835            // c:5137
5836            let _ = zclose(pipes[(1 - out) as usize]); // c:5138
5837            return -1; // c:5139
5838        }
5839        // c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
5840        if nullexec == 0 {
5841            if let Some(jt) = JOBTAB.get() {
5842                let mut guard = jt.lock().unwrap();
5843                let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5844                if tj >= 0 {
5845                    if let Some(j) = guard.get_mut(tj as usize) {
5846                        crate::ported::jobs::addproc(
5847                            j,
5848                            pid,
5849                            "",
5850                            true, // aux=1 for proc subst
5851                            Some(std::time::Instant::now()),
5852                            -1,
5853                            -1,
5854                        );
5855                    }
5856                }
5857            }
5858        }
5859        procsubstpid.store(pid, Ordering::Relaxed); // c:5143
5860        return pipes[(1 - out) as usize]; // c:5144
5861    }
5862    // c:5146 — child path.
5863    entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
5864    let _ = redup(pipes[out as usize], out); // c:5147
5865    closem(FDT_UNUSED, 0); // c:5148
5866    cmdpush(CS_CMDSUBST as u8); // c:5149
5867                                // c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
5868    // The re-fed text is what parsecmd parsed: after the two marker chars,
5869    // up to the Outpar ending at `ends_at` (a byte offset). The markers are
5870    // multi-byte chars, so a `cmd[2..]` byte slice split them.
5871    let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
5872    let body_end = ends_at.saturating_sub(Outpar.len_utf8());
5873    let body = if body_end > body_start && body_end <= cmd.len() {
5874        untokenize(&cmd[body_start..body_end])
5875    } else {
5876        String::new()
5877    };
5878    let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
5879    cmdpop(); // c:5151
5880    let _ = std::io::Write::flush(&mut std::io::stdout());
5881    crate::ported::builtin::_realexit(); // c:5152
5882}
5883
5884/// Port of `spawnpipes()` from `Src/exec.c:5184` — C decl `spawnpipes(LinkList l, int nullexec)`.
5885///
5886/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
5887/// entry fires `getpipe(name, nullexec || varid)` and stashes the
5888/// resulting fd into `f->fd2`.
5889///
5890/// ```c
5891/// LinkNode n;
5892/// Redir f;
5893/// char *str;
5894/// n = firstnode(l);
5895/// for (; n; incnode(n)) {
5896///     f = (Redir) getdata(n);
5897///     if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
5898///         str = f->name;
5899///         f->fd2 = getpipe(str, nullexec || f->varid);
5900///     }
5901/// }
5902/// ```
5903///
5904/// =================== WARNING — DIVERGENCE ====================
5905/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
5906/// in place of `LinkList`. The walk is identical; the only behavior
5907/// difference is that LinkList iteration in C lets callers splice
5908/// nodes mid-walk — we never do that here so it's a no-op divergence.
5909/// =============================================================
5910pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
5911    // c:5184
5912    for f in l.iter_mut() {
5913        // c:5191
5914        if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
5915            // c:5193
5916            let str_ = f.name.clone().unwrap_or_default(); // c:5194
5917            let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
5918                1
5919            } else {
5920                nullexec
5921            };
5922            f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
5923        }
5924    }
5925}
5926
5927/// Port of `cancd2()` from `Src/exec.c:6411` — C decl `cancd2(char *s)`.
5928///
5929/// C body:
5930/// ```c
5931/// struct stat buf;
5932/// char *us, *us2 = NULL;
5933/// int ret;
5934/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
5935///     if (*s != '/')
5936///         us = tricat(pwd[1] ? pwd : "", "/", s);
5937///     else
5938///         us = ztrdup(s);
5939///     fixdir(us2 = us);
5940/// } else
5941///     us = unmeta(s);
5942/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
5943/// if (us2) free(us2);
5944/// return ret;
5945/// ```
5946///
5947/// True iff `s` is a directory we can `cd` into (X-perm). With
5948/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
5949/// (joining with PWD if relative) so `cd /foo/bar/..` works without
5950/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
5951pub fn cancd2(s: &str) -> i32 {
5952    // c:6411
5953    let us: String;
5954    // c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
5955    let chasedots = isset(CHASEDOTS); // c:6422
5956    let chaselinks = isset(CHASELINKS);
5957    if !chasedots && !chaselinks {
5958        // c:6422
5959        // c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
5960        let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
5961        let mut raw = if !s.starts_with('/') {
5962            // c:6423
5963            format!(
5964                "{}/{}",
5965                if pwd_str.len() > 1 { &pwd_str[..] } else { "" },
5966                s
5967            )
5968        } else {
5969            s.to_string()
5970        };
5971        // c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
5972        fixdir(&mut raw);
5973        us = raw;
5974    } else {
5975        // c:6428
5976        us = unmeta(s); // c:6429
5977    }
5978    // c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
5979    let cstr = match std::ffi::CString::new(us.as_str()) {
5980        Ok(c) => c,
5981        Err(_) => return 0,
5982    };
5983    if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
5984        return 0;
5985    }
5986    let meta = match std::fs::metadata(&us) {
5987        Ok(m) => m,
5988        Err(_) => return 0,
5989    };
5990    if !meta.file_type().is_dir() {
5991        return 0;
5992    }
5993    1
5994}
5995
5996/// Port of `cancd()` from `Src/exec.c:6370` — C decl `cancd(char *s)`.
5997///
5998/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
5999/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
6000/// it, else `None`.
6001///
6002/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
6003/// `doprintdir = -1` when a non-trivial path is found (so `cd`
6004/// echoes the resolved path).
6005pub fn cancd(s: &str) -> Option<String> {
6006    // c:6370
6007    // c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
6008    //                (s[1]=='.' && (s[2]=='/' || !s[2])))`.
6009    let bytes = s.as_bytes();
6010    let nocdpath = bytes.first().copied() == Some(b'.')
6011        && (bytes.get(1).copied() == Some(b'/')
6012            || bytes.get(1).is_none()
6013            || (bytes.get(1).copied() == Some(b'.')
6014                && (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
6015    // c:6376 — `if (*s != '/')` branch.
6016    if !s.starts_with('/') {
6017        // c:6376
6018        // c:6379-6380 — `if (cancd2(s)) return s;`
6019        if cancd2(s) != 0 {
6020            return Some(s.to_string());
6021        }
6022        // c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
6023        let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
6024        if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
6025            return None; // c:6382
6026        }
6027        // c:6383-6397 — CDPATH walk.
6028        if !nocdpath {
6029            let cdpath_str = getsparam("CDPATH").unwrap_or_default();
6030            for cp in cdpath_str.split(':') {
6031                // c:6384
6032                let sbuf = if !cp.is_empty() {
6033                    format!("{}/{}", cp, s) // c:6386
6034                } else {
6035                    s.to_string() // c:6391
6036                };
6037                if cancd2(&sbuf) != 0 {
6038                    // c:6393
6039                    DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
6040                    return Some(sbuf); // c:6395
6041                }
6042            }
6043        }
6044        // c:6398-6403 — `cd_able_vars()` fallback.
6045        if let Some(t) = cd_able_vars(s) {
6046            // c:6398
6047            if cancd2(&t) != 0 {
6048                // c:6399
6049                DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
6050                return Some(t); // c:6401
6051            }
6052        }
6053        return None; // c:6404
6054    }
6055    // c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
6056    if cancd2(s) != 0 {
6057        Some(s.to_string())
6058    } else {
6059        None
6060    }
6061}
6062
6063/// Port of `simple_redir_name()` from `Src/exec.c:4689` — C decl `simple_redir_name(Eprog prog, int redir_type)`.
6064///
6065/// Test if an Eprog encodes a single simple-command consisting of a
6066/// SINGLE redirection of the requested type with NO command body
6067/// (the `cat < foo` shape). When true, returns the redir target name
6068/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
6069/// `open(2)` instead of fork+pipe+exec.
6070///
6071/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
6072/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
6073/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
6074/// same shape — this port replicates the same offset reads.
6075pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
6076    // c:4689
6077    let pc = &prog.prog;
6078    // c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
6079    // offsets matching C's `pc[0]..pc[6]` checks.
6080    if pc.len() < 7 {
6081        return None;
6082    }
6083
6084    if wc_code(pc[0]) != WC_LIST
6085        || (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0  // c:4695
6086        || wc_code(pc[1]) != WC_SUBLIST
6087        || WC_SUBLIST_FLAGS(pc[1]) != 0  // c:4696
6088        || WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END  // c:4697
6089        || wc_code(pc[2]) != WC_PIPE
6090        || WC_PIPE_TYPE(pc[2]) != WC_PIPE_END  // c:4698
6091        || wc_code(pc[3]) != WC_REDIR
6092        || WC_REDIR_TYPE(pc[3]) != redir_type  // c:4699
6093        || WC_REDIR_VARID(pc[3]) != 0  // c:4700
6094        || pc[4] != 0  // c:4701
6095        || wc_code(pc[6]) != WC_SIMPLE
6096        || WC_SIMPLE_ARGC(pc[6]) != 0
6097    // c:4702
6098    {
6099        return None; // c:4706
6100    }
6101    // c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
6102    Some(dupstring(&ecrawstr(prog, 5, None)))
6103}
6104
6105/// Port of `getherestr()` from `Src/exec.c:4655` — C decl `getherestr(struct redir *fn)`.
6106///
6107/// C body:
6108/// ```c
6109/// char *s, *t;
6110/// int fd, len;
6111/// t = fn->name;
6112/// singsub(&t);
6113/// untokenize(t);
6114/// unmetafy(t, &len);
6115/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
6116///     t[len++] = '\n';
6117/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
6118///     return -1;
6119/// write_loop(fd, t, len);
6120/// close(fd);
6121/// fd = open(s, O_RDONLY | O_NOCTTY);
6122/// unlink(s);
6123/// return fd;
6124/// ```
6125///
6126/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
6127/// tempfile, then re-open read-only and unlink — gives the consumer a
6128/// read fd whose backing file is already cleaned up.
6129pub fn getherestr(fn_: &redir) -> i32 {
6130    // c:4655
6131    let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
6132    t = singsub(&t); // c:4661
6133    t = untokenize(&t); // c:4662
6134                        // c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
6135                        // Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
6136    let mut bytes: Vec<u8> = t.into_bytes();
6137    let _len = unmetafy(&mut bytes);
6138    // c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
6139    if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
6140        // c:4671
6141        bytes.push(b'\n'); // c:4672
6142    }
6143    // c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
6144    let (fd, s) = match gettempfile(None) {
6145        Some(p) => p,
6146        None => return -1, // c:4674
6147    };
6148    // c:4675 — `write_loop(fd, t, len);`
6149    let _ = write_loop(fd, &bytes); // c:4675
6150                                    // c:4676 — `close(fd);`
6151    let _ = zclose(fd); // c:4676
6152                        // c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
6153    let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
6154    let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
6155                                                                                        // c:4678 — `unlink(s);`
6156    unsafe {
6157        libc::unlink(cstr.as_ptr());
6158    } // c:4678
6159    new_fd // c:4679
6160}
6161
6162/// Port of `quote_tokenized_output()` from `Src/exec.c:2114` — C decl `quote_tokenized_output(char *str, FILE *file)`.
6163///
6164/// C body (abridged):
6165/// ```c
6166/// for (; *s; s++) {
6167///     switch (*s) {
6168///         case Meta: putc(*++s ^ 32, file); continue;
6169///         case Nularg: continue;
6170///         case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
6171///             putc('\\', file); break;
6172///         case '\t': fputs("$'\\t'", file); continue;
6173///         case '\n': fputs("$'\\n'", file); continue;
6174///         case '\r': fputs("$'\\r'", file); continue;
6175///         case '=': if (s == str) putc('\\', file); break;
6176///         default:
6177///             if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
6178///     }
6179///     putc(*s, file);
6180/// }
6181/// ```
6182///
6183/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
6184/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
6185/// surface as unescaped chars (`*`, `(`) while literal special chars
6186/// get backslash-escaped — round-tripping through the shell.
6187pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
6188    // c:2114
6189    let bytes = str_in.as_bytes();
6190    let mut i = 0usize;
6191    while i < bytes.len() {
6192        // c:2118 `for (; *s; s++)`
6193        let c = bytes[i];
6194        match c {
6195            x if x == Meta => {
6196                // c:2120 — `case Meta: putc(*++s ^ 32, file);`
6197                if i + 1 < bytes.len() {
6198                    file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
6199                    i += 2;
6200                } else {
6201                    i += 1;
6202                }
6203                continue; // c:2122
6204            }
6205            x if x as char == Nularg => {
6206                // c:2124
6207                i += 1;
6208                continue; // c:2126
6209            }
6210            b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']' | b'*'
6211            | b'?' | b'$' | b' ' => {
6212                // c:2128-2142
6213                file.write_all(b"\\")?; // c:2143
6214            }
6215            b'\t' => {
6216                // c:2146
6217                file.write_all(b"$'\\t'")?; // c:2147
6218                i += 1;
6219                continue;
6220            }
6221            b'\n' => {
6222                // c:2150
6223                file.write_all(b"$'\\n'")?; // c:2151
6224                i += 1;
6225                continue;
6226            }
6227            b'\r' => {
6228                // c:2154
6229                file.write_all(b"$'\\r'")?; // c:2155
6230                i += 1;
6231                continue;
6232            }
6233            b'=' => {
6234                // c:2158 — `if (s == str) putc('\\', file);`
6235                if i == 0 {
6236                    file.write_all(b"\\")?; // c:2160
6237                }
6238            }
6239            _ => {
6240                // c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
6241                if itok(c) {
6242                    // c:2164
6243                    let pound = Pound as u8;
6244                    if c >= pound {
6245                        let idx = (c - pound) as usize;
6246                        let zt = ztokens.as_bytes();
6247                        if idx < zt.len() {
6248                            file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
6249                        }
6250                    }
6251                    i += 1;
6252                    continue;
6253                }
6254            }
6255        }
6256        file.write_all(&[c])?; // c:2171
6257        i += 1;
6258    }
6259    Ok(())
6260}
6261
6262// =====================================================================
6263// Wordcode-VM control-flow dispatch — faithful ports of the C
6264// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
6265//
6266// Each function below takes `&mut estate` and returns `i32` to mirror
6267// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
6268// line `// c:NNN` citations track the C source line.
6269//
6270// zshrs's primary execution path is the fusevm bytecode VM. These
6271// wordcode-VM entries exist for C-name parity with the upstream
6272// interpreter so that future bridging code can drive zshrs through
6273// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
6274// zshrs primitives don't yet model their C counterpart (e.g.
6275// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
6276// helper is declared with a comment citing the C source file:line
6277// where the canonical body lives — same pattern as the canonical
6278// `ksh93::ksh93_wrapper` port at c:152-227.
6279// =====================================================================
6280
6281use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
6282use crate::ported::pattern::{patcompile, pattry};
6283use crate::ported::r#loop::try_tryflag;
6284
6285// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
6286use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
6287use crate::ported::linklist::LinkList;
6288use crate::ported::mem::freeheap;
6289use crate::ported::params::setloopvar;
6290use crate::ported::params::{assignaparam, assignsparam, unsetparam};
6291use crate::ported::parse::{ecgetlist, ecgetstr};
6292use crate::ported::pattern::haswilds;
6293use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
6294use crate::ported::subst::{globlist, prefork};
6295use crate::ported::zsh_h::{
6296    estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
6297    WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP,
6298    WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
6299    WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
6300    WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
6301};
6302use crate::ported::zsh_h::{
6303    ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
6304    PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
6305    WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
6306};
6307use crate::ported::zsh_h::{
6308    CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
6309    CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
6310};
6311
6312// --- exec.c ports used by the exec paths below --------------------------
6313
6314/// Port of `execsubst()` from `Src/exec.c:2684` — C decl `execsubst(LinkList strs)`.
6315///
6316/// C body (c:2684-2693):
6317/// ```c
6318/// void execsubst(LinkList strs) {
6319///     if (strs) {
6320///         prefork(strs, esprefork, NULL);
6321///         if (esglob && !errflag) {
6322///             LinkList ostrs = strs;
6323///             globlist(strs, 0);
6324///             strs = ostrs;
6325///         }
6326///     }
6327/// }
6328/// ```
6329///
6330/// `execsubst` runs `prefork` (parameter / arithmetic / command
6331/// substitution expansion + IFS-split) over the whole list, then
6332/// (when `esglob` is set) `globlist` to do filename globbing on the
6333/// result.
6334pub(crate) fn execsubst(list: &mut Vec<String>) {
6335    // c:2684
6336    if list.is_empty() {
6337        return; // c:2686 `if (strs)`
6338    }
6339    let mut ll: crate::ported::subst::LinkList = std::mem::take(list).into_iter().collect();
6340    let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
6341    let mut rf: i32 = 0;
6342    prefork(&mut ll, prefork_flags, &mut rf); // c:2687
6343    if esglob.load(Ordering::Relaxed) != 0 && errflag.load(Ordering::Relaxed) == 0 {
6344        // c:2688 `if (esglob && !errflag)`
6345        globlist(&mut ll, 0); // c:2690
6346    }
6347    *list = ll.into_iter().collect();
6348}
6349
6350/// Port of `addvars()` from `Src/exec.c:2499` — C decl `addvars(Estate state, Wordcode pc, int addflags)`.
6351/// Direct port of `static void addvars(Estate state, Wordcode pc,
6352/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
6353/// nodes stacked inline of a simple command — the `var=value` and
6354/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
6355/// at `pc`, extracts each assignment's name + value (scalar or array),
6356/// optionally preforks + globs the tokenised RHS, and routes through
6357/// `assignsparam` (scalar) or `assignaparam` (array).
6358///
6359/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
6360/// stderr (C uses xtrerr; zshrs uses eprint!).
6361///
6362/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
6363/// into the file-static `STTYval` for `execute()` to apply pre-exec.
6364fn addvars(state: &mut estate, pc: usize, addflags: i32) {
6365    // c:2501 — locals.
6366    let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
6367    let xtr: bool; // c:2502 `int xtr,`
6368    let mut isstr: bool; // c:2502 `int isstr,`
6369    let mut htok: i32 = 0; // c:2502 `int htok = 0;`
6370    let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
6371    let mut name: String;
6372    let mut flags: i32; // c:2504 `int flags;`
6373    let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
6374    let mut ac: wordcode; // c:2507 `wordcode ac;`
6375                          // c:2508 `local_list1(svl);` — stack-local one-element LinkList
6376                          // for the scalar-assignment path. Rust uses a fresh LinkList per
6377                          // iteration; equivalent semantics.
6378
6379    // c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
6380    // when the assignment list is implicitly local (ADDVAR_RESTORE).
6381    flags = if (addflags & ADDVAR_RESTORE) == 0 {
6382        ASSPM_WARN // c:2516
6383    } else {
6384        0 // c:2516
6385    };
6386    xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
6387    if xtr {
6388        // c:2518
6389        printprompt4(); // c:2519
6390        doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
6391    }
6392    state.pc = pc; // c:2522 `state->pc = pc;`
6393
6394    // c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
6395    loop {
6396        if state.pc >= state.prog.prog.len() {
6397            break;
6398        }
6399        ac = state.prog.prog[state.pc];
6400        state.pc += 1;
6401        if wc_code(ac) != WC_ASSIGN {
6402            // Step back so the WC_SIMPLE / outer dispatcher sees the
6403            // non-assignment opcode. C's `state->pc++` post-increment
6404            // already pointed past WC_ASSIGN; we need to unconsume.
6405            state.pc -= 1;
6406            break;
6407        }
6408        let mut myflags = flags; // c:2524 `int myflags = flags;`
6409        name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
6410        if htok != 0 {
6411            // c:2526 `if (htok) untokenize(name);`
6412            name = untokenize(&name).to_string(); // c:2527
6413        }
6414        if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
6415            // c:2528
6416            myflags |= ASSPM_AUGMENT; // c:2529
6417        }
6418        if xtr {
6419            // c:2530
6420            // c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
6421            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
6422                eprint!("{}+=", name); // c:2532
6423            } else {
6424                eprint!("{}=", name); // c:2532
6425            }
6426        }
6427
6428        // c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
6429        isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
6430        if isstr {
6431            // c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
6432            let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
6433            vl = LinkList::new();
6434            vl.push_back(svl_val);
6435            // c:2535 `vl = &svl;` — vl already points at the new list.
6436        } else {
6437            // c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
6438            let items = ecgetlist(
6439                state,
6440                WC_ASSIGN_NUM(ac) as usize,
6441                EC_DUPTOK,
6442                Some(&mut htok),
6443            );
6444            vl = LinkList::new();
6445            for it in items {
6446                vl.push_back(it);
6447            }
6448            if errflag.load(Ordering::Relaxed) != 0 {
6449                // c:2538-2541
6450                state.pc = opc; // c:2539
6451                return; // c:2540
6452            }
6453        }
6454
6455        // c:2544 `if (vl && htok) {`
6456        if htok != 0 {
6457            // c:2545 `int prefork_ret = 0;`
6458            let mut prefork_ret: i32 = 0;
6459            // c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
6460            //                          : PREFORK_ASSIGN), &prefork_ret);
6461            let pf_flags = if isstr {
6462                PREFORK_SINGLE | PREFORK_ASSIGN
6463            } else {
6464                PREFORK_ASSIGN
6465            };
6466            prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
6467            if errflag.load(Ordering::Relaxed) != 0 {
6468                // c:2548
6469                state.pc = opc; // c:2549
6470                return; // c:2550
6471            }
6472            if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
6473                // c:2552
6474                myflags |= ASSPM_KEY_VALUE; // c:2553
6475            }
6476            // c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
6477            //                  haswilds((char *)getdata(firstnode(vl)))))`
6478            let needs_glob = if !isstr {
6479                true
6480            } else {
6481                isset(GLOBASSIGN)
6482                    && isstr
6483                    && !vl.is_empty()
6484                    && haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
6485            };
6486            if needs_glob {
6487                globlist(&mut vl, prefork_ret); // c:2556
6488                                                // c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
6489                                                //                  unsetparam(name);`
6490                if isset(GLOBASSIGN) && isstr {
6491                    unsetparam(&name); // c:2562
6492                }
6493                if errflag.load(Ordering::Relaxed) != 0 {
6494                    // c:2563
6495                    state.pc = opc; // c:2564
6496                    return; // c:2565
6497                }
6498            }
6499        }
6500        // c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
6501        // — scalar-assignment path: zero or one element after prefork.
6502        if isstr && (vl.is_empty() || vl.len() == 1) {
6503            let val: String; // c:2571 `char *val;`
6504            if vl.is_empty() {
6505                // c:2574
6506                val = String::new(); // c:2575 `val = ztrdup("");`
6507            } else {
6508                // c:2577 `untokenize(peekfirst(vl));`
6509                let peek = vl.nodes.front().cloned().unwrap_or_default();
6510                val = untokenize(&peek).to_string(); // c:2577-2578
6511                                                     // c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
6512                                                     // we just cloned the front above. Equivalent.
6513            }
6514            if xtr {
6515                // c:2580
6516                eprint!("{}", quotedzputs(&val)); // c:2581
6517                eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
6518            }
6519            // c:2578-2586 — `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))
6520            // { if ((addflags & ADDVAR_RESTRICT) && isset(RESTRICTED) && (pm =
6521            // removenode(name)) && (pm->flags & PM_RESTRICTED)) { zerr("%s:
6522            // restricted", pm->node.nam); state->pc = opc; return; }`
6523            if (addflags & ADDVAR_EXPORT) != 0
6524                && !name.contains('[')
6525                && (addflags & ADDVAR_RESTRICT) != 0
6526                && isset(crate::ported::zsh_h::RESTRICTED)
6527                && crate::ported::params::paramtab()
6528                    .read()
6529                    .ok()
6530                    .and_then(|t| t.get(name.as_str()).map(|p| p.node.flags as u32))
6531                    .is_some_and(|f| f & crate::ported::zsh_h::PM_RESTRICTED != 0)
6532            {
6533                zerr(&format!("{}: restricted", name)); // c:2582
6534                state.pc = opc; // c:2584
6535                return; // c:2585
6536            }
6537            // c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
6538            let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
6539                // c:2585 `if (strcmp(name, "STTY") == 0)`
6540                if name == "STTY" {
6541                    // c:2586-2587 — `STTYval = ztrdup(val);`
6542                    let mut stty = STTYval.lock().unwrap();
6543                    *stty = Some(val.clone()); // c:2587
6544                }
6545                // c:2589 `allexp = opts[ALLEXPORT];`
6546                let allexp = isset(ALLEXPORT);
6547                // c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
6548                opt_state_set("allexport", true);
6549                if isset(KSHARRAYS) {
6550                    // c:2591
6551                    unsetparam(&name); // c:2592
6552                }
6553                let pm = assignsparam(&name, &val, myflags); // c:2593
6554                                                             // c:2594 `opts[ALLEXPORT] = allexp;` — restore.
6555                opt_state_set("allexport", allexp);
6556                pm
6557            } else {
6558                // c:2595
6559                assignsparam(&name, &val, myflags) // c:2596
6560            };
6561            if pm.is_none() {
6562                // c:2597 `if (!pm)`
6563                LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
6564                                                     // c:2599-2604 — "cheating" comment: don't zerr.
6565                if cmdoutval.load(Ordering::Relaxed) == 0 {
6566                    // c:2605 `if (!cmdoutval)`
6567                    cmdoutval.store(1, Ordering::Relaxed); // c:2606
6568                }
6569            }
6570            if errflag.load(Ordering::Relaxed) != 0 {
6571                // c:2608
6572                state.pc = opc; // c:2609
6573                return; // c:2610
6574            }
6575            continue; // c:2612
6576        }
6577        // c:2614 `if (vl) { ... }` — array-assignment path: drain vl
6578        // into a fresh `char **arr`.
6579        // c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
6580        arr = Vec::with_capacity(vl.len() + 1);
6581        while let Some(s) = vl.pop_front() {
6582            arr.push(s);
6583        }
6584        // c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
6585        if xtr {
6586            // c:2624
6587            eprint!("( "); // c:2625
6588            for s in &arr {
6589                // c:2626 `for (ptr = arr; *ptr; ptr++)`
6590                eprint!("{}", quotedzputs(s)); // c:2627
6591                eprint!(" "); // c:2628
6592            }
6593            eprint!(") "); // c:2630
6594        }
6595        // c:2632 `if (!assignaparam(name, arr, myflags))`
6596        if assignaparam(&name, arr, myflags).is_none() {
6597            LASTVAL.store(1, Ordering::Relaxed); // c:2633
6598                                                 // c:2634-2638 — "cheating" comment.
6599            if cmdoutval.load(Ordering::Relaxed) == 0 {
6600                // c:2639
6601                cmdoutval.store(1, Ordering::Relaxed); // c:2640
6602            }
6603        }
6604        if errflag.load(Ordering::Relaxed) != 0 {
6605            // c:2642
6606            state.pc = opc; // c:2643
6607            return; // c:2644
6608        }
6609    }
6610    state.pc = opc; // c:2647 `state->pc = opc;`
6611}
6612
6613// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
6614// match expression at the call sites in execsimple. Not a separate
6615// Rust fn — every C-side reference to
6616// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
6617
6618// --- exec.c entries ---------------------------------------------------
6619
6620/// Port of `execcursh()` from `Src/exec.c:469` — C decl `execcursh(Estate state, int do_exec)`.
6621/// Execute a `{ ... }` current-shell command
6622/// group: skip the trailing try-only word, optionally drop a stale
6623/// job slot, then run the inner list.
6624pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
6625    // c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
6626    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
6627    let end = state.pc + WC_CURSH_SKIP(prior) as usize;
6628    // c:475 — `state->pc++;` skip the try/always-only word.
6629    state.pc += 1;
6630    // c:482-486 — drop empty job slot before nested cmd: if outer-pipe
6631    // bookkeeping is clean AND thisjob is a real job that's not the
6632    // pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
6633    // leaking job-table slots when execcursh recurses.
6634    {
6635        let lp = list_pipe.load(Ordering::Relaxed);
6636        let lpj = list_pipe_job.load(Ordering::Relaxed);
6637        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
6638        if lp == 0 && tj != -1 && tj != lpj {
6639            if let Some(jt) = JOBTAB.get() {
6640                let mut guard = jt.lock().unwrap();
6641                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
6642                if !has && (tj as usize) < guard.len() {
6643                    crate::ported::jobs::deletejob(&mut guard, tj as usize, false);
6644                }
6645            }
6646        }
6647    }
6648    cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
6649    let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
6650    cmdpop(); // c:489 — `cmdpop();`
6651    state.pc = end; // c:491 — `state->pc = end;`
6652    this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
6653    LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
6654}
6655
6656// `(...)` subshell — no dedicated C function (handled inline by
6657// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
6658// In zshrs the subshell branch is folded into `execpline` and
6659// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
6660// inner-list walk since fusevm bytecode handles the forking via
6661// Op::Subshell at a higher layer.
6662
6663/// Port of `execcond()` from `Src/exec.c:5260` — C decl `execcond(Estate state, UNUSED(int do_exec))`.
6664/// Run a `[[ ... ]]` cond expression.
6665pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
6666    state.pc -= 1; // c:5265 — `state->pc--;`
6667    if isset(XTRACE) {
6668        printprompt4();
6669        eprint!("[["); // `fprintf(xtrerr, "[[");`
6670        crate::ported::cond::tracingcond.fetch_add(1, Ordering::Relaxed); // `tracingcond++;`
6671    }
6672    cmdpush(CS_COND as u8);
6673    let stat: i32 = crate::ported::cond::evalcond(state, None); // `stat = evalcond(state, NULL);`
6674                                                                          // 2 indicates a syntax error.  For compatibility, turn this into a shell error.
6675    if stat == 2 {
6676        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
6677    }
6678    cmdpop();
6679    if isset(XTRACE) {
6680        eprintln!(" ]]"); // `fprintf(xtrerr, " ]]\n");`
6681        crate::ported::cond::tracingcond.fetch_sub(1, Ordering::Relaxed); // `tracingcond--;`
6682    }
6683    stat // `return stat;`
6684}
6685
6686/// Port of `execarith()` from `Src/exec.c:5235` — C decl `execarith(Estate state, UNUSED(int do_exec))`.
6687/// Run a `(( ... ))` arithmetic command;
6688/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
6689/// parse error.
6690pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
6691    if isset(XTRACE) {
6692        printprompt4();
6693        eprint!("((");
6694    }
6695    cmdpush(CS_MATH as u8); // c:5247
6696    let mut htok: i32 = 0;
6697    let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
6698    if htok != 0 {
6699        e = singsub(&e); // c:5250 — `singsub(&e);`
6700    }
6701    if isset(XTRACE) {
6702        eprint!(" {}", e);
6703    }
6704    let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
6705    cmdpop(); // c:5256
6706    if isset(XTRACE) {
6707        eprintln!(" ))");
6708    }
6709    // c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
6710    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
6711        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
6712        return 2;
6713    }
6714    // c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
6715    let val = val_result.unwrap();
6716    if val.type_ == MN_INTEGER {
6717        if val.l == 0 {
6718            1
6719        } else {
6720            0
6721        }
6722    } else if val.d == 0.0 {
6723        1
6724    } else {
6725        0
6726    }
6727}
6728
6729/// Port of `exectime()` from `Src/exec.c:5272` — C decl `exectime(Estate state, UNUSED(int do_exec))`.
6730/// Run `time pipeline`: drives execpline with
6731/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
6732pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
6733    let jb = *THISJOB
6734        .get_or_init(|| std::sync::Mutex::new(-1))
6735        .lock()
6736        .unwrap(); // c:5283
6737    let prior = state.prog.prog[state.pc.wrapping_sub(1)];
6738    // c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
6739    if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
6740        // c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
6741        // shell+kids time deltas since last call.
6742        crate::ported::jobs::shelltime(None, None, None, 0);
6743        return 0; // c:5286
6744    }
6745    // c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
6746    let slcode = state.prog.prog[state.pc];
6747    state.pc += 1;
6748    use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
6749    let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
6750    *THISJOB
6751        .get_or_init(|| std::sync::Mutex::new(-1))
6752        .lock()
6753        .unwrap() = jb; // c:5289
6754    LASTVAL.load(Ordering::Relaxed) // c:5290
6755}
6756
6757/// Port of `execshfunc()` from `Src/exec.c:5540` — C decl `execshfunc(Shfunc shf, LinkList args)`.
6758/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
6759/// Promoted to top-level pub fn so execcmd_exec at the shfunc
6760/// dispatch site (c:4102-4105) can route through it. Owns the
6761/// queue_signals + cmdstack + sfcontext setup around `doshfunc`, as C.
6762pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
6763    // c:5546-5547 — `if (errflag) return;`
6764    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
6765    // the C line cited above tests the WHOLE errflag, so masking here let an
6766    // interrupted shell keep going where zsh stops.
6767    if errflag.load(Ordering::Relaxed) != 0 {
6768        return;
6769    }
6770    // c:5550-5557 — drop empty job slot before nested shfunc invoke:
6771    // if outer-pipe bookkeeping is clean AND thisjob is a real job
6772    // that's not the pipe-leader AND has no procs yet, deletejob()
6773    // recycles it. Avoids leaking job-table slots across recursive
6774    // function calls. Same pattern as execcursh's c:482-486.
6775    let mut last_file_list: Vec<jobfile> = Vec::new(); // c:5598
6776    {
6777        let lp = list_pipe.load(Ordering::Relaxed);
6778        let lpj = list_pipe_job.load(Ordering::Relaxed);
6779        let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
6780        if lp == 0 && tj != -1 && tj != lpj {
6781            if let Some(jt) = JOBTAB.get() {
6782                let mut guard = jt.lock().unwrap();
6783                let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
6784                if !has {
6785                    // c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
6786                    //                jobtab[thisjob].filelist = NULL;` — preserve
6787                    //                the filelist so deletejob doesn't unlink temp
6788                    //                files. Rust take()s the Vec into a local.
6789                    last_file_list = if let Some(j) = guard.get_mut(tj as usize)
6790                    {
6791                        std::mem::take(&mut j.filelist)
6792                    } else {
6793                        Vec::new()
6794                    };
6795                    if (tj as usize) < guard.len() {
6796                        crate::ported::jobs::deletejob(&mut guard, tj as usize, false); // c:5556
6797                    }
6798                }
6799            }
6800        }
6801    }
6802    // c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
6803    // emit PS4 prefix + space-separated quoted args on the trace
6804    // stream so `set -x` shows the function invocation line.
6805    if isset(XTRACE) {
6806        printprompt4();
6807        for (i, a) in args.iter().enumerate() {
6808            if i > 0 {
6809                crate::fusevm_bridge::xtrerr_fputs(" "); // c:5565 fputc(' ', xtrerr)
6810            }
6811            crate::fusevm_bridge::xtrerr_fputs(&quotedzputs(a)); // c:5566
6812        }
6813        crate::fusevm_bridge::xtrerr_fputs("\n"); // c:5568
6814        crate::fusevm_bridge::xtrerr_flush(); // c:5569
6815    }
6816    queue_signals(); // c:5627
6817    // c:5628-5631 — `ocs = cmdstack; ocsp = cmdsp; cmdstack = zalloc(CMDSTACKSZ); cmdsp = 0;`
6818    let ocs: Vec<u8> = crate::ported::prompt::CMDSTACK.with(|s| std::mem::take(&mut *s.borrow_mut()));
6819    // c:5632-5633 — `if ((osfc = sfcontext) == SFC_NONE) sfcontext = SFC_DIRECT;`
6820    let osfc = sfcontext.load(Ordering::Relaxed);
6821    if osfc == crate::ported::zsh_h::SFC_NONE {
6822        sfcontext.store(crate::ported::zsh_h::SFC_DIRECT, Ordering::Relaxed);
6823    }
6824    // c:5580 — `doshfunc(shf, args, 0);`. The C path always has
6825    // `funcdef` populated since C parses at definition time. zshrs
6826    // compiles to fusevm chunks instead, so `funcdef` is None for
6827    // user-defined functions; only `body` (source string) carries
6828    // the definition. When that's the case, build a one-shot eprog
6829    // whose `strs` carries the source so runshfunc's script-pipeline
6830    // arm (execute_script_zsh_pipeline) executes the body.
6831    let prog_owned: Option<eprog> = if shf.funcdef.is_some() {
6832        None
6833    } else if let Some(ref body) = shf.body {
6834        Some(eprog {
6835            strs: Some(body.clone()),
6836            ..Default::default()
6837        })
6838    } else if (shf.node.flags as u32 & PM_UNDEFINED) != 0 {
6839        // c:Src/builtin.c:3763 `shf->funcdef = mkautofn(shf);` — an
6840        // `autoload`ed-but-not-yet-loaded function is NOT body-less in C.
6841        // `mkautofn` (c:3786-3809) hands it a five-word program whose only
6842        // instruction is `WCB_AUTOFN()`, which `execautofn`
6843        // (Src/exec.c:5786) turns into `loadautofn`; `functions NAME`
6844        // prints that program back as `builtin autoload -X`.
6845        //
6846        // zshrs's autoload stub carries neither `funcdef` nor `body`, so
6847        // this arm produced `None` and `execshfunc` returned having done
6848        // NOTHING, status 0. That silently killed every autoloaded function
6849        // reached through `execcmd_exec` — i.e. every call whose command
6850        // word is not a compile-time literal (`$c`, `my$v`, `"$f"`), since
6851        // a literal head goes through `CallFunction` →
6852        // `dispatch_function_call`, which has its own autoload prelude.
6853        // `_sh` dispatches with `_$variant "$@"` (Completion/Unix/Command/
6854        // _sh sh:51), so `sh <TAB>` ran `_bash` as a no-op and completed
6855        // nothing.
6856        //
6857        // Emit mkautofn's program as its printed source; zshrs's
6858        // `autoload -X` arm (builtin.rs `bin_functions`, c:3613-3654) is
6859        // the port of `execautofn`'s effect and re-invokes the function
6860        // with the original arguments via `eval_autoload` (c:3167-3177).
6861        Some(eprog {
6862            strs: Some("builtin autoload -X".to_string()),
6863            ..Default::default()
6864        })
6865    } else {
6866        None
6867    };
6868    let prog_ref: Option<&eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
6869        (Some(p), _) => Some(p),
6870        (_, Some(p)) => Some(p),
6871        _ => None,
6872    };
6873    if let Some(_prog) = prog_ref {
6874        // c:5580 — `doshfunc(shf, args, 0);`. Direct doshfunc call —
6875        // noreturnval=0 means the body's return value updates LASTVAL
6876        // (caller of execfuncdef reads it back). PPARAMS swap +
6877        // restore happens INSIDE doshfunc's scope; body_runner just
6878        // runs the body.
6879        let name_for_body = shf.node.nam.clone();
6880        let body_args_owned: Vec<String> = if args.len() > 1 {
6881            args[1..].to_vec()
6882        } else {
6883            Vec::new()
6884        };
6885        let body_runner = move || -> i32 {
6886            crate::ported::exec::run_function_body(&name_for_body, &body_args_owned).unwrap_or(0)
6887        };
6888        crate::ported::utils::xtrerr.store(2, Ordering::Relaxed); // c:5634 xtrerr = stderr;
6889        let _ = doshfunc(shf, args.clone(), false, body_runner);
6890    }
6891    sfcontext.store(osfc, Ordering::Relaxed); // c:5638
6892    // c:5639-5641 — `free(cmdstack); cmdstack = ocs; cmdsp = ocsp;`
6893    crate::ported::prompt::CMDSTACK.with(|s| *s.borrow_mut() = ocs);
6894    if list_pipe.load(Ordering::Relaxed) == 0 {
6895        // c:5643-5644 — `deletefilelist(last_file_list, 0);`
6896        let mut holder = crate::ported::zsh_h::job::default();
6897        holder.filelist = last_file_list;
6898        crate::ported::jobs::deletefilelist(&mut holder, false);
6899    }
6900    unqueue_signals(); // c:5645
6901}
6902
6903/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
6904/// from `Src/exec.c:5823-6158`.
6905///
6906/// C body's scope-management sequence ported here. The C source's
6907/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
6908/// is replaced by `body_runner` — zshrs runs function bodies through
6909/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
6910/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
6911/// callback hands the live executor back to the caller (typically
6912/// the fusevm bridge) for the actual body run. Every line of scope
6913/// save/restore around the body call mirrors C exactly.
6914///
6915/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
6916/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
6917/// c:6042; zshrs delegates to a closure because the body-execution
6918/// pipeline (fusevm) differs from C's (wordcode). The closure
6919/// fully replaces the runshfunc call and returns the body's exit
6920/// status (which doshfunc reads as `lastval` for the `noreturnval`
6921/// path).
6922#[allow(non_snake_case)]
6923/// Port of the module-global `oflags` in `Src/exec.c` (set at c:5970 via
6924/// `funcsave->oflags = oflags;`). Holds the attribute flags of the function
6925/// CURRENTLY executing, so a nested call can tell whether its caller carried
6926/// PM_TAGGED_LOCAL — that is what makes `functions -T` non-recursive.
6927/// zshrs-original storage shape (atomic rather than a plain global); the
6928/// save/restore discipline matches C's funcsave. Bug #1058.
6929pub static FUNC_OFLAGS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
6930
6931/// Port of `doshfunc()` from `Src/exec.c:5823` — C decl `doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`.
6932pub fn doshfunc(
6933    shfunc: &mut shfunc,                  // c:5823
6934    doshargs: Vec<String>,                // c:5823
6935    noreturnval: bool,                    // c:5823
6936    mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
6937) -> i32 {
6938    use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
6939    use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
6940    use crate::ported::modules::parameter::FUNCSTACK;
6941    use crate::ported::params::endparamscope;
6942    use crate::ported::params::locallevel as locallevel_atomic;
6943    use crate::ported::zsh_h::{
6944        FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_TAGGED, PM_TAGGED_LOCAL, PM_UNDEFINED,
6945        PM_WARNNESTED,
6946    };
6947    use std::sync::atomic::Ordering;
6948
6949    let name = shfunc.node.nam.clone(); // c:5827
6950    let flags = shfunc.node.flags; // c:5828
6951                                   // Lineage tap, before the funcstack frame goes on: the op must
6952                                   // record where the *caller* stands, not the body about to run.
6953    if crate::provenance::active() {
6954        // c:5978-5998 — doshargs[0] is the function name, doshargs[1..] the
6955        // positionals the body will see as $1, $2, …; the chain records those,
6956        // not the name again.
6957        let prov_args: &[String] = if doshargs.len() > 1 {
6958            &doshargs[1..]
6959        } else {
6960            &[]
6961        };
6962        crate::provenance::on_func_call(
6963            &name,
6964            prov_args,
6965            shfunc.body.as_deref(),
6966            shfunc.filename.as_deref(),
6967            shfunc.lineno,
6968        );
6969    }
6970    let fname = dupstring(&name); // c:5829
6971    let _ = fname; // c:5829 (kept for parity)
6972
6973    // c:6000-6004 — FUNCNEST guard. C:
6974    //   if (zsh_funcnest >= 0 && funcstack && funcstacksz >= zsh_funcnest) {
6975    //       zerr("maximum nested function level reached; increase FUNCNEST?");
6976    //       goto undoshfunc;
6977    //   }
6978    // This was previously stubbed out on the assumption that the zshrs
6979    // fusevm "doesn't recurse via real stack frames" — but it DOES:
6980    // dispatch_function_call -> doshfunc -> dispatch_function_call -> …
6981    // nests real native frames, so an accidental infinite recursion
6982    // (e.g. a self-referential hook, a wrapped function that re-invokes
6983    // itself, a zle widget cycle) overflowed the OS stack and
6984    // SEGFAULTED instead of producing zsh's graceful error. Check at
6985    // entry — before queue_signals / inc_locallevel / the funcsave
6986    // snapshot — so the early return needs no unwinding. FUNCNEST < 0
6987    // means "unlimited" (zsh's `zsh_funcnest >= 0` gate). Depth is the
6988    // count of already-active frames on FUNCSTACK; the 501st nested
6989    // call (depth == 500) trips it, matching C's `>=` on the default
6990    // FUNCNEST=500.
6991    let funcnest = crate::ported::params::getiparam("FUNCNEST");
6992    if funcnest >= 0 {
6993        // Count only real function frames (FS_FUNC) — the raw FUNCSTACK
6994        // Vec also carries FS_EVAL/FS_SOURCE/anon frames, which inflate
6995        // the depth several× and would falsely trip on legitimately deep
6996        // (but finite) recursion. FS_FUNC-only matches zsh's nesting
6997        // depth (`${#funcstack}`) and its funcnest accounting.
6998        let depth = crate::ported::modules::parameter::FUNCSTACK
6999            .lock()
7000            .map(|s| s.iter().filter(|f| f.tp == FS_FUNC).count())
7001            .unwrap_or(0) as i64;
7002        if depth >= funcnest {
7003            zerr("maximum nested function level reached; increase FUNCNEST?");
7004            crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
7005            return 1;
7006        }
7007    }
7008
7009    // c:5835 — `queue_signals();` Lots of memory + global-state changes.
7010    queue_signals();
7011
7012    // c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
7013    // Pinned so a recursive unload doesn't free the eprog under us.
7014    // (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
7015    // already pin until call ends. C does explicit refcount on
7016    // `funcdef->nref` via useeprog.)
7017
7018    // c:5856-5916 — Funcsave allocation + per-field snapshot.
7019    let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
7020    let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
7021    let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
7022    let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
7023    let funcsave_numpipestats = {
7024        // c:5864
7025        NUMPIPESTATS
7026            .get_or_init(|| std::sync::Mutex::new(0))
7027            .lock()
7028            .map(|n| *n)
7029            .unwrap_or(0)
7030    };
7031    let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
7032                                                                // c:5866-5867 — trap_state PRIMED branch decrements trap_return.
7033    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
7034        // c:5866
7035        TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
7036    }
7037    // c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
7038    // return-suppress so a `return` inside the body fires errexit
7039    // checks normally.
7040    noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
7041
7042    // c:5872-5880 — noreturnval branch: deep-copy pipestats so the
7043    // function body's pipestats writes are restored on exit.
7044    let funcsave_pipestats: Option<Vec<i32>> = if noreturnval {
7045        // c:5872
7046        let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
7047        p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
7048    } else {
7049        None
7050    };
7051
7052    // c:5882-5896 — `if (!strcmp(fname, "TRAPEXIT")) { shcopy = copy of
7053    // *shfunc; shfunc = shcopy; name = shfunc->node.nam; }`: C copies the
7054    // node because starttrapscope() unsets the table's TRAPEXIT entry. Every
7055    // doshfunc caller passes its own `&mut shfunc` copy (not the shfunctab
7056    // node), so unsettrap() cannot invalidate it and no copy is needed.
7057
7058    // c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
7059    // tags SIGEXIT for deferred restoration at scope end.
7060    crate::ported::signals::starttrapscope();
7061    crate::extensions::emulation_output::bash_return_trap_enter();
7062    // c:5899 — `startpatternscope();`
7063    crate::ported::pattern::startpatternscope();
7064
7065    // c:5901 — `pptab = pparams;` — save outer positional params.
7066    let pptab: Vec<String> = crate::ported::builtin::PPARAMS
7067        .lock()
7068        .map(|p| p.clone())
7069        .unwrap_or_default();
7070
7071    // c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
7072    let funcsave_scriptname = crate::ported::utils::scriptname_get();
7073    if (flags as u32 & PM_UNDEFINED) == 0 {
7074        // c:5902
7075        crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
7076    }
7077
7078    // c:5964-5965 — `funcsave->zoptind = zoptind; funcsave->optcind
7079    // = optcind;`. C makes OPTIND implicitly function-local: a
7080    // `getopts` loop inside the function gets its own counter that
7081    // snaps back to the caller's on return. zshrs keeps the counter
7082    // in the `$OPTIND` param plus the ZOPTIND/OPTCIND trackers
7083    // `getopts` syncs against, so all three are snapshotted here.
7084    //
7085    // OPTARG is deliberately NOT snapshotted. The funcsave struct
7086    // (c:44-52) is `char opts[OPT_SIZE]; char *argv0; int zoptind,
7087    // lastval, optcind, numpipestats; …` — there is no `zoptarg`
7088    // member, and `grep -n zoptarg Src/exec.c` returns nothing, so C
7089    // lets a callee's OPTARG leak to its caller. Bug #513 added an
7090    // OPTARG save/restore citing "C's `funcsave->zoptind / zoptarg`
7091    // save/restore pair"; that field does not exist. The extra
7092    // restore was observable:
7093    //     g() { OPTARG=inner; }; h() { local OPTARG=Z; g; print $OPTARG }
7094    //     zsh   -> inner        zshrs (before) -> Z
7095    //     g() { getopts "a:" o -a VAL; }
7096    //     k() { local OPTARG=Z OPTIND=1; g; print $OPTARG }
7097    //     zsh   -> VAL          zshrs (before) -> Z
7098    let funcsave_optind: Option<String> = crate::ported::params::getsparam("OPTIND");
7099    let funcsave_optcind = crate::ported::builtin::OPTCIND.load(Ordering::Relaxed); // c:5965
7100                                                                                    // c:5966-5969 — `if (!isset(POSIXBUILTINS)) { zoptind = 1; optcind = 0; }`.
7101                                                                                    // The snapshot above is only half the contract: C also RESETS the
7102                                                                                    // counter on entry so an inner `getopts` loop starts fresh at the
7103                                                                                    // first positional, independent of the caller's OPTIND. Without
7104                                                                                    // this, a function whose body runs `getopts` after the caller
7105                                                                                    // advanced OPTIND mis-parses its own args — e.g. `add-zsh-hook`
7106                                                                                    // (which `getopts`-parses `precmd func`) printed its usage and
7107                                                                                    // failed when invoked from a config that had run getopts earlier.
7108                                                                                    // zshrs keeps the counter in the $OPTIND param plus the ZOPTIND/
7109                                                                                    // OPTCIND trackers getopts syncs against; reset all three.
7110    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
7111        // Only a changed counter needs the parameter written: a store runs the
7112        // whole assignment path (paramtab lock, special setfn), and the caller
7113        // usually has OPTIND at 1 already.
7114        if funcsave_optind.as_deref() != Some("1") {
7115            crate::ported::params::setiparam("OPTIND", 1); // c:5967 zoptind = 1
7116        }
7117        crate::ported::builtin::ZOPTIND.store(1, Ordering::Relaxed);
7118        crate::ported::builtin::OPTCIND.store(0, Ordering::Relaxed); // c:5968 optcind = 0
7119    }
7120
7121    // c:5911-5914 — `memcpy(funcsave->opts, opts, sizeof(opts));`
7122    //
7123    // C copies 186 bytes. The port used to clone the whole option table
7124    // as a `HashMap<String, bool>` — a heap allocation per option name,
7125    // on every call of every function, whether or not the body touches
7126    // an option. `opt_state_store` is C's array, so this is C's memcpy.
7127    let funcsave_opts = crate::ported::options::opt_state_store::save();
7128
7129    // c:5974-5975 — `funcsave->emulation = emulation;
7130    //                funcsave->sticky = sticky;`
7131    let funcsave_emulation = crate::ported::options::emulation.load(Ordering::Relaxed);
7132    let funcsave_emulation_live = crate::ported::options::EMULATION.load(Ordering::Relaxed);
7133    let funcsave_fully = crate::ported::options::FULLY_EMULATING.load(Ordering::Relaxed);
7134    let funcsave_sticky = sticky
7135        .lock()
7136        .unwrap_or_else(|e| e.into_inner())
7137        .as_ref()
7138        .map(|b| sticky_emulation_dup(b, 0));
7139
7140    // c:5977-6012 —
7141    //     if (sticky_emulation_differs(shfunc->sticky)) {
7142    //         sticky = sticky_emulation_dup(shfunc->sticky, 1);
7143    //         emulation = sticky->emulation;
7144    //         funcsave->restore_sticky = 1;
7145    //         installemulation(emulation, opts);
7146    //         if (sticky->n_on_opts) { ... opts[*onptr] = 1; }
7147    //         if (sticky->n_off_opts) { ... opts[*offptr] = 0; }
7148    //         /* All emulations start with pattern disables clear */
7149    //         clearpatterndisables();
7150    //     } else
7151    //         funcsave->restore_sticky = 0;
7152    //
7153    // This whole block was missing, so a function DEFINED inside
7154    // `emulate sh -c '...'` did not re-enter that emulation when
7155    // called (B07emulate.ztst:6,7,8,12,13,14).
7156    let mut funcsave_restore_sticky = 0; // c:6012
7157    if sticky_emulation_differs(shfunc.sticky.as_deref()) != 0 {
7158        // c:5978
7159        let newsticky = sticky_emulation_dup(shfunc.sticky.as_deref().unwrap(), 1); // c:5990
7160        let emu = newsticky.emulation; // c:5991
7161        *sticky.lock().unwrap_or_else(|e| e.into_inner()) = Some(newsticky);
7162        // C's single `emulation` global carries EMULATE_FULLY; the Rust
7163        // port splits it into EMULATION (base bits) + FULLY_EMULATING.
7164        let emu_base = emu & !crate::ported::zsh_h::EMULATE_FULLY;
7165        let emu_fully = (emu & crate::ported::zsh_h::EMULATE_FULLY) != 0;
7166        crate::ported::options::emulation.store(emu_base, Ordering::Relaxed); // c:5991
7167        crate::ported::options::EMULATION.store(emu_base, Ordering::Relaxed); // (port keeps 2 cells)
7168        crate::ported::options::FULLY_EMULATING.store(emu_fully, Ordering::Relaxed);
7169        funcsave_restore_sticky = 1; // c:5992
7170                                     // c:5993 — `installemulation(emulation, opts);`
7171        let mut new_opts = [-1i8; crate::ported::zsh_h::OPT_SIZE as usize];
7172        crate::ported::options::installemulation(emu, &mut new_opts); // c:5993
7173        for (optno, &v) in new_opts.iter().enumerate().skip(1) {
7174            if v >= 0 {
7175                opt_state_set(crate::ported::zsh_h::opt_name(optno as i32), v == 1);
7176            }
7177        }
7178        {
7179            let guard = sticky.lock().unwrap_or_else(|e| e.into_inner());
7180            if let Some(ref st) = *guard {
7181                // c:5995-6001 — `opts[*onptr] = 1;`
7182                for on in &st.on_opts {
7183                    opt_state_set(crate::ported::zsh_h::opt_name(*on as i32), true);
7184                }
7185                // c:6002-6008 — `opts[*offptr] = 0;`
7186                for off in &st.off_opts {
7187                    opt_state_set(crate::ported::zsh_h::opt_name(*off as i32), false);
7188                }
7189            }
7190        }
7191        // c:6010 — `clearpatterndisables();`
7192        crate::ported::pattern::clearpatterndisables();
7193    }
7194
7195    // c:5954-5960 — PM_TAGGED / PM_TAGGED_LOCAL turn XTRACE on for the
7196    // duration of the call:
7197    //     if (flags & (PM_TAGGED|PM_TAGGED_LOCAL))
7198    //         opts[XTRACE] = 1;
7199    //     else if (oflags & PM_TAGGED_LOCAL) {
7200    //         if (shfunc->node.nam == ANONYMOUS_FUNCTION_NAME)
7201    //             flags |= PM_TAGGED_LOCAL;
7202    //         else
7203    //             opts[XTRACE] = 0;
7204    //     }
7205    // This is what makes `functions -t f` / `-T f` trace a SINGLE function
7206    // without `setopt xtrace` globally. It was previously skipped, so both
7207    // flags parsed and stored correctly (builtin.rs c:3358-3365) but nothing
7208    // ever consulted them and the traced function ran silently.
7209    //
7210    // `-T` is the non-recursive form: a function called FROM a `-T` function
7211    // gets XTRACE turned back OFF unless it is itself tagged. C tracks the
7212    // caller's flags in the module-global `oflags`, saved per call at c:5970
7213    // (`funcsave->oflags = oflags;`) and restored on return — mirrored here by
7214    // FUNC_OFLAGS. Anonymous functions are the documented exception: they
7215    // INHERIT the local tag instead of clearing it, so `-T` tracing spans the
7216    // `(anon)` bodies inside the traced function.
7217    //
7218    // No restore of XTRACE is needed here: it is already in the
7219    // always-restored subset at the exit block below (c:6097-6101).
7220    // Bug #1058.
7221    let mut fn_flags = shfunc.node.flags as u32;
7222    let oflags_prev = FUNC_OFLAGS.load(Ordering::Relaxed);
7223    if (fn_flags & (PM_TAGGED | PM_TAGGED_LOCAL)) != 0 {
7224        opt_state_set("xtrace", true); // c:5955
7225    } else if (oflags_prev & PM_TAGGED_LOCAL) != 0 {
7226        // c:5956
7227        if shfunc.node.nam == ANONYMOUS_FUNCTION_NAME {
7228            fn_flags |= PM_TAGGED_LOCAL; // c:5958
7229        } else {
7230            opt_state_set("xtrace", false); // c:5960
7231        }
7232    }
7233    // c:6022-6029 — `functions -W` (PM_WARNNESTED) is the per-function
7234    // form of WARN_NESTED_VAR, with the same anonymous-function inheritance
7235    // as `-T` above:
7236    //     if (flags & PM_WARNNESTED)
7237    //         opts[WARNNESTEDVAR] = 1;
7238    //     else if (oflags & PM_WARNNESTED) {
7239    //         if (shfunc->node.nam == ANONYMOUS_FUNCTION_NAME)
7240    //             flags |= PM_WARNNESTED;
7241    //         else
7242    //             opts[WARNNESTEDVAR] = 0;
7243    //     }
7244    // WARNNESTEDVAR is in the always-restored subset at the exit block
7245    // (c:6161).
7246    if (fn_flags & PM_WARNNESTED) != 0 {
7247        opt_state_set("warnnestedvar", true); // c:6023
7248    } else if (oflags_prev & PM_WARNNESTED) != 0 {
7249        // c:6024
7250        if shfunc.node.nam == ANONYMOUS_FUNCTION_NAME {
7251            fn_flags |= PM_WARNNESTED; // c:6026
7252        } else {
7253            opt_state_set("warnnestedvar", false); // c:6028
7254        }
7255    }
7256    FUNC_OFLAGS.store(fn_flags, Ordering::Relaxed); // c:5970
7257
7258    // c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
7259    // for inner commands; outer flag restored on exit.
7260    opt_state_set("printexitvalue", false);
7261
7262    // c:5978-5998 — pparams swap. C reads doshargs and constructs the
7263    // function's positional-param array. First arg is the function
7264    // name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
7265    let funcsave_argv0: Option<String> = if !doshargs.is_empty() {
7266        // c:5978
7267        // c:5982-5985 — `pparams = x = zshcalloc(...)`.
7268        let positionals: Vec<String> = if doshargs.len() > 1 {
7269            doshargs[1..].to_vec()
7270        } else {
7271            Vec::new()
7272        };
7273        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7274            *pp = positionals;
7275        }
7276        // c:5984-5987 — FUNCTIONARGZERO: save argzero, install
7277        // doshargs[0] (the function name).
7278        if isset(FUNCTIONARGZERO) {
7279            // c:5984
7280            let prev = crate::ported::utils::argzero();
7281            crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
7282            prev
7283        } else {
7284            None
7285        }
7286    } else {
7287        // c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
7288        if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7289            *pp = Vec::new();
7290        }
7291        if isset(FUNCTIONARGZERO) {
7292            // c:5994
7293            let prev = crate::ported::utils::argzero();
7294            crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
7295            prev
7296        } else {
7297            None
7298        }
7299    };
7300
7301    // c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
7302    // since zshrs tracks function-call depth there.
7303    //
7304    // Plus the canonical startparamscope (c:6194 inside runshfunc).
7305    // zshrs's body_runner replaces runshfunc's `execode` call so the
7306    // startparamscope/endparamscope pair must wrap body_runner here,
7307    // not inside the closure. inc_locallevel is exactly startparamscope.
7308    inc_locallevel();
7309
7310    // c:6000-6004 — FUNCNEST check + `goto undoshfunc` on overflow.
7311    // Skip the runtime check (the zshrs fusevm doesn't recurse via
7312    // real stack frames so the depth limit is less critical), but
7313    // keep the comment so the C label `undoshfunc:` target is
7314    // visible — `goto undoshfunc;` here would jump straight to the
7315    // epilogue at the `undoshfunc:` label below.
7316
7317    // c:6005-6019 — funcstack frame push. The full C block:
7318    //   funcsave->fstack.name      = dupstring(name);
7319    //   funcsave->fstack.caller    = funcstack ? funcstack->name :
7320    //                                 dupstring(argv0 ? argv0 : argzero);
7321    //   funcsave->fstack.lineno    = lineno;
7322    //   funcsave->fstack.prev      = funcstack;
7323    //   funcsave->fstack.tp        = FS_FUNC;
7324    //   funcstack                  = &funcsave->fstack;
7325    //   funcsave->fstack.flineno   = shfunc->lineno;
7326    //   funcsave->fstack.filename  = getshfuncfile(shfunc);
7327    // c:6013 — `funcsave->fstack.lineno = lineno;` C has ONE lineno
7328    // global (params.c:123); zshrs mirrors it in both input::lineno
7329    // and lex::LEX_LINENO. The lex mirror is the one driven by
7330    // BUILTIN_SET_LINENO per statement AND zeroed for the duration
7331    // of a function body (see set_lineno(0) below), so it is the
7332    // one that matches C's value at call time: a call made INSIDE a
7333    // caller's body records the caller-relative line (0 for a
7334    // single-line fn), giving `$functrace` entries like `g:0`.
7335    // input::lineno stayed parked at the script-wide line and
7336    // produced `g:1`.
7337    let lineno_now = crate::ported::lex::lineno() as i64;
7338    let (caller, prev_tp): (Option<String>, Option<i32>) = {
7339        let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7340        if let Some(p) = stk.last() {
7341            (Some(p.name.clone()), Some(p.tp))
7342        } else {
7343            // c:6011-6012 — outermost: argv0 (saved) or argzero global.
7344            let z = funcsave_argv0
7345                .clone()
7346                .or_else(crate::ported::utils::argzero);
7347            (z, None)
7348        }
7349    };
7350    // c:6018-6019 — flineno: shfunc->lineno (function def line)
7351    let mut flineno = shfunc.lineno;
7352    // c:6079 — `funcsave->fstack.filename = getshfuncfile(shfunc);`, NOT a raw
7353    // read of `shfunc->filename`. `getshfuncfile` (c:Src/hashtable.c:1058-1068)
7354    // appends `/name` when PM_LOADDIR is set:
7355    //     if (shf->node.flags & PM_LOADDIR)
7356    //         return zhtricat(shf->filename, "/", shf->node.nam);
7357    // An fpath autoload stores the DIRECTORY in `filename` (loadautofnsetfile,
7358    // c:5657), so reading the field directly rendered a funcstack frame as the
7359    // directory — `$funcfiletrace` showed `/Users/…/.zshrs/functions:117`
7360    // where zsh has `/…/functions/_complete:117`.
7361    //
7362    // The `.or_else(Some(String::new()))` coercion is DELIBERATELY kept: C's
7363    // `getshfuncfile` can return NULL and c:5675 tests `!funcstack->filename`
7364    // for it, but several readers here assume non-`None`, so making that
7365    // distinction expressible is a separate change.
7366    let mut filename = if (shfunc.node.flags as u32 & crate::ported::zsh_h::PM_LOADDIR) != 0 {
7367        // c:1061-1062
7368        shfunc
7369            .filename
7370            .as_ref()
7371            .map(|d| format!("{}/{}", d, shfunc.node.nam))
7372            .or_else(|| Some(String::new()))
7373    } else {
7374        // c:1063-1066
7375        shfunc.filename.clone().or_else(|| Some(String::new()))
7376    };
7377    // !!! WARNING: RUST-ONLY HELPER !!!
7378    //
7379    // c:6019 — `funcsave->fstack.filename = getshfuncfile(shfunc);`, which
7380    // reads the shfunc's OWN `filename`, set by `loadautofnsetfile`
7381    // (c:5657, called from `loadautofn` at c:5735 / c:5757) when the
7382    // function was autoloaded out of `$fpath`.
7383    //
7384    // zshrs has no C counterpart for a function that exists ONLY as a Rust
7385    // port: `_main_complete`, `_normal`, `_dispatch`, … are dispatched from
7386    // `compsys::router::try_rust_dispatch` and the autoload prelude is
7387    // skipped entirely (vm_helper.rs — "no upstream shell function to
7388    // load"), so nothing ever records a `$fpath` file for the name and the
7389    // synthesized shfunc handed to us carries the caller's `scriptfilename`
7390    // ("zsh") or nothing at all. `$funcsourcetrace` / `$funcfiletrace` then
7391    // reported `zsh:1` for every completer frame where zsh names the
7392    // `$fpath` file — and `funcfiletrace` is read by real completion code
7393    // (`_git` derives its git-completion.bash search path from
7394    // `${funcsourcetrace[1]%:*}`).
7395    //
7396    // Stand in for `loadautofnsetfile` in exactly that case by resolving the
7397    // defining file out of `$fpath` the way `getfpfunc` (c:6219) does for a
7398    // real autoload. An autoload-installed function gets `shf->lineno == 0`
7399    // (c:5384-5388 only stamps a `name() { … }` STATEMENT), so the def line
7400    // is 0, matching zsh's `<fpath-file>:0` in `$funcsourcetrace`.
7401    //
7402    // The gate is `try_rust_dispatch` ALONE. That predicate is the same one
7403    // `vm_helper::dispatch_function_call` and `compcore::callcompfunc` use to
7404    // pick the body_runner, so `Some` means the frame being pushed IS the
7405    // port standing in for the stock `$fpath` file — and the router has
7406    // already refused every name a shell definition owns
7407    // (`has_fpath_override` / `has_shfunc_override`, router.rs:53,62), so
7408    // there is nothing left here to shadow.
7409    //
7410    // An additional "…and the name has no shfunctab node" clause used to
7411    // guard this, which meant it never fired in practice: `compinit` writes
7412    // one bare `autoload -Uz <every completer>` line into its dump
7413    // (Completion/compdump:113), so EVERY completer has a
7414    // PM_UNDEFINED stub by the time Tab is pressed. A bare-name stub records
7415    // neither `filename` nor PM_LOADDIR (`add_autoload_function`,
7416    // Src/builtin.c:3278-3334 — only the `/abs/dir/name` arm and the
7417    // inherited-loaddir arm set them), so `getshfuncfile` returns NULL for
7418    // it and the fallback chain in vm_helper landed on `scriptfilename`.
7419    // That is how `$funcsourcetrace` read `zsh:1` for `_vars`, `_dispatch`,
7420    // `_normal`, `_complete` where zsh reads `/usr/share/zsh/5.9/functions/
7421    // _vars:0` etc.
7422    //
7423    // Memoised, and deliberately never invalidated: C stamps `shf->filename`
7424    // ONCE, at autoload time, and a later `fpath=(…)` does not restamp it —
7425    // so a permanent per-name answer is what matches zsh, not a re-scan. It
7426    // also keeps `doshfunc` off the filesystem on the completion hot path.
7427    if crate::compsys::router::try_rust_dispatch(&name).is_some() {
7428        static RUST_PORT_FILE: std::sync::OnceLock<
7429            std::sync::Mutex<std::collections::HashMap<String, Option<String>>>,
7430        > = std::sync::OnceLock::new();
7431        let cache = RUST_PORT_FILE.get_or_init(|| std::sync::Mutex::new(Default::default()));
7432        let cached = cache
7433            .lock()
7434            .unwrap_or_else(|e| e.into_inner())
7435            .get(&name)
7436            .cloned();
7437        let resolved = match cached {
7438            Some(hit) => hit,
7439            None => {
7440                let mut fdir: Option<String> = None;
7441                let mut dump: Option<(eprog, i32)> = None;
7442                // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
7443                // autoloads.rkyv already records which `$fpath` directory
7444                // holds this name; take it from there instead of walking
7445                // `$fpath` for every native completer a new shell calls.
7446                let fpath = crate::ported::params::getaparam("fpath").unwrap_or_default();
7447                let from_shard = crate::autoload_cache::try_source_dir(&name, &fpath);
7448                // c:6219 `getfpfunc(s, ksh, test, fdir, 1)` with test_only —
7449                // a pure probe: it fills `*fdir` (c:6240 `*fdir = *pp;`)
7450                // without parsing the file.
7451                let hit = from_shard
7452                    .or_else(|| getfpfunc(&name, &mut fdir, None, 1, &mut dump).and(fdir))
7453                    // c:1061 (Src/hashtable.c, getshfuncfile) — a PM_LOADDIR
7454                    // filename renders as
7455                    // `zhtricat(shf->filename, "/", shf->node.nam)`.
7456                    .map(|d| {
7457                        if d.is_empty() {
7458                            name.clone()
7459                        } else {
7460                            format!("{d}/{name}")
7461                        }
7462                    });
7463                cache
7464                    .lock()
7465                    .unwrap_or_else(|e| e.into_inner())
7466                    .insert(name.clone(), hit.clone());
7467                hit
7468            }
7469        };
7470        if let Some(file) = resolved {
7471            filename = Some(file);
7472            flineno = 0; // c:5384-5388 — never runs for an autoload stub
7473        }
7474    }
7475    {
7476        let frame = crate::ported::zsh_h::funcstack {
7477            prev: None,             // c:6014 (Vec-stack: index encodes link)
7478            name: dupstring(&name), // c:6005
7479            filename,               // c:6019
7480            caller,                 // c:6011
7481            flineno,                // c:6018
7482            lineno: lineno_now,     // c:6013
7483            tp: FS_FUNC,            // c:6015
7484        };
7485        let _ = prev_tp; // c:6011 (informational)
7486        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7487        stack.push(frame); // c:6016 funcstack = &funcsave->fstack
7488    }
7489
7490    // c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
7491    // zshrs delegates to the body_runner closure (typically a fusevm
7492    // sub-VM run from the bridge). The closure returns the body's
7493    // exit status which becomes lastval.
7494    //
7495    // c:Src/exec.c:1251-1266 — push "shfunc" onto zsh_eval_context
7496    // so the body sees `${zsh_eval_context[*]}` containing the call
7497    // chain context. The execode-based path (c:1245-1282 port at
7498    // exec.rs:7092) already did this, but the fusevm body_runner
7499    // path skipped doshfunc's body_runner invocation without the
7500    // push. Bug #262 in docs/BUGS.md.
7501    //
7502    // Push BOTH the static `zsh_eval_context` (matches C's variable)
7503    // AND the paramtab array entry (what `${zsh_eval_context[*]}`
7504    // reads). Pop on every return path via the guard struct so
7505    // panics / early returns don't leak the entry. Inlined here
7506    // (sole caller) — `zsh_eval_context` is this module's own static.
7507    //
7508    // c:Src/exec.c:1251-1266 — `zsh_eval_context[*]` shell-visible
7509    // mirror: the array entry holds the stack; the scalar
7510    // `ZSH_EVAL_CONTEXT` holds the `:`-joined form. Both written via
7511    // the PM_READONLY bypass (`u_arr`/`u_str` direct), the same shape
7512    // the binary's `-c` ZSH_EVAL_CONTEXT init uses (bins/zshrs.rs).
7513    // `EvalContextFrame` owns both halves (push + paramtab sync, then
7514    // pop + sync on Drop) and is shared with the other C sites that
7515    // reach `execode` with a label — `bin_eval` ("eval", builtin.c:6209)
7516    // and the autoload body run ("loadautofunc", exec.c:5626).
7517    let _eval_ctx_guard = EvalContextFrame::push("shfunc");
7518    // c:Src/exec.c — function bodies execute with `lineno` reset to
7519    // the relative line within the body (incremented per WC_PIPE
7520    // from the wordcode-encoded lineno). zsh's zerrmsg
7521    // (Src/utils.c:301) emits the lineno prefix only when lineno
7522    // is non-zero AND (!SHINSTDIN || locallevel != 0). For an
7523    // inline single-line function like `f() { x=1 }`, the body's
7524    // WC_PIPE encodes lineno=1, exec sets `lineno = lineno - 1 =
7525    // 0`, and the zerrmsg path falls through to space-only ("f: ").
7526    //
7527    // zshrs's compiler doesn't thread WC_PIPE_LINENO into the
7528    // bytecode, so the global lineno stays at the script-wide
7529    // value (1 for inline `-c`). Suppress the line-number prefix
7530    // inside function bodies by saving lineno on entry and forcing
7531    // it to 0 during body execution; restore on exit. This makes
7532    // warnings inside functions emit `f: ...` matching zsh's
7533    // single-line-function format. Bug #54/#74/#86 in docs/BUGS.md.
7534    //
7535    // NOT for a PM_UNDEFINED function. Its `prog` is the `mkautofn` stub
7536    // (c:Src/builtin.c:3662, zsh-5.9.1), which carries no line marker, so in C the
7537    // `loadautofn` -> `getfpfunc` lookup it runs (c:5626-5632) still sees the
7538    // CALLER's `lineno`: a corrupt digest on `$fpath` is reported as
7539    // `_main_complete:218: invalid zwc file:` (c:Src/parse.c:3288 `zwarnnam`),
7540    // and zeroing it here printed `_main_complete: invalid zwc file:`. The load
7541    // runs inside `body_runner`; `run_function_body_only` zeroes `lineno` once
7542    // the definition is loaded, just before the body itself runs.
7543    let saved_lineno = crate::ported::lex::lineno();
7544    if (flags as u32 & PM_UNDEFINED) == 0 {
7545        crate::ported::lex::set_lineno(0);
7546    }
7547    // c:Src/exec.c:6173-6175 + c:6196-6198 — `runshfunc` saves
7548    // zunderscore before the body runs and restores it after, so
7549    // `$_` reads outside the function continue to reflect the
7550    // function CALL's last arg (set by setunderscore at c:3491
7551    // before doshfunc enters). Without this, commands inside the
7552    // body (`:`, `echo`, etc.) update `$_` to their own last arg,
7553    // and the post-call `echo "[$_]"` sees the body's residue
7554    // instead of the call's arg. Bug surfaced via
7555    // test_dollar_underscore_after_function_call.
7556    let saved_zunderscore = crate::ported::params::getsparam("_").unwrap_or_default();
7557    // c:6042 — `runshfunc(prog, wrappers, funcsave->fstack.name);` — run
7558    // the body through the module-registered wrapper chain, starting at
7559    // its head (`wrappers` == [`BODY_WRAPPERS`] index 0). `prog` is `None`
7560    // because the body arrives as `body_runner`; see [`runshfunc`] for the
7561    // split of C's c:6166-6202 between it and this function.
7562    let body_status = runshfunc(None, 0, &name, &mut body_runner);
7563    crate::ported::params::set_zunderscore(std::slice::from_ref(&saved_zunderscore));
7564    crate::ported::lex::set_lineno(saved_lineno);
7565    LASTVAL.store(body_status, Ordering::Relaxed);
7566    crate::extensions::emulation_output::bash_return_trap_exit();
7567
7568    // c:6043 — `doneshfunc:` label. The C `runshfunc` happy-path
7569    // falls through here from c:6042.
7570    // c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
7571    {
7572        let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7573        stack.pop();
7574    }
7575    // c:Src/exec.c:6260 — `endparamscope();` inside `runshfunc`, i.e.
7576    // BEFORE control returns to doshfunc's epilogue at c:6103. zshrs
7577    // hoists runshfunc's `startparamscope()` (c:6254) up to
7578    // `inc_locallevel()` above because the body arrives as a closure,
7579    // so its partner must be dropped here — at the same point in the
7580    // sequence C reaches it — not at the tail of the epilogue.
7581    //
7582    // The whole epilogue below therefore runs with the callee's locals
7583    // already popped, which is what C's own comment at c:6195-6196
7584    // asserts ("The endparamscope() has already happened, hence the
7585    // `+1` here" for the exit_pending test at c:6201). Three orderings
7586    // depend on it and all three were wrong while this call sat at the
7587    // tail:
7588    //   * `endtrapscope()` (c:6174) dispatches a function-scoped EXIT
7589    //     trap. In C the trap body runs in the CALLER's param scope:
7590    //       X=5; f(){ local X=1; trap 'print IN=$X; X=99' EXIT; }; f
7591    //       zsh -> IN=5 then X==99 outside
7592    //     With endparamscope last, zshrs printed `IN=1` and the trap's
7593    //     `X=99` landed in the doomed local shadow (outer X stayed 5).
7594    //   * the OPTIND/optcind restore (c:6120-6122) must write the
7595    //     CALLER's parameter, so it has to follow this pop — see the
7596    //     restore site below.
7597    //   * `foo() { exit 7; }; foo` needs the decrement before the
7598    //     exit_pending comparison or `exit_level >= locallevel+1` is
7599    //     off by one and the shell exits 0.
7600    endparamscope();
7601
7602    // c:6045 — `undoshfunc:` label. Reached either by fall-through
7603    // from c:6044 or by `goto undoshfunc;` from the FUNCNEST check
7604    // at c:6003. Tail epilogue follows.
7605
7606    // c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
7607    // runshfunc) lives at c:6157 below as `endparamscope()`. Removed
7608    // the dec here so locallevel only decrements once per
7609    // function-call frame; double-dec was purging level-0 globals on
7610    // function exit (the `f() { x=foo; }; f; echo $x` regression).
7611
7612    // c:6047-6053 — retflag clear. C clears retflag and restores
7613    // outer breaks if a `return` fired.
7614    if RETFLAG.load(Ordering::SeqCst) != 0 {
7615        // c:6047
7616        RETFLAG.store(0, Ordering::SeqCst); // c:6051
7617        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
7618    }
7619
7620    // ksh93 fires the ERR trap for a function CALL that returns non-zero in
7621    // addition to the failing command inside it (`f() { false; }; f` -> two
7622    // ERR, `f() { return 3; }; f` -> one, from the call). zsh's process-global
7623    // `donetrap` (c:1598) would spare the call site, so a Korn shell re-arms
7624    // it as the function returns. Verified against ksh93u+m 1.0.10.
7625    if crate::extensions::dash_mode::ksh93_mode() {
7626        DONETRAP.store(0, Ordering::SeqCst);
7627    }
7628
7629    // c:6054-6058 — pparams + argv0 restore.
7630    if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7631        *pp = pptab; // c:6059 pparams = pptab
7632    }
7633    if let Some(saved) = funcsave_argv0 {
7634        // c:6055
7635        crate::ported::utils::set_argzero(Some(saved)); // c:6057
7636    }
7637
7638    // c:6120-6123 — `if (!isset(POSIXBUILTINS)) { zoptind =
7639    // funcsave->zoptind; optcind = funcsave->optcind; }`.
7640    //
7641    // Placement is load-bearing in both directions, which is why this
7642    // sits exactly where C has it — after the pparams/argv0 restore
7643    // (c:6114-6119) and before the option restore (c:6129-6162):
7644    //
7645    //  * AFTER `endparamscope()` (already run above). In C `zoptind`
7646    //    is a plain global int (c:47); in zshrs `$OPTIND` is an
7647    //    ordinary paramtab entry, so "restore OPTIND" means "write the
7648    //    visible parameter". Written before the scope pop it landed in
7649    //    the CALLEE's own `local OPTIND` shadow, which the pop then
7650    //    discarded — re-exposing the caller's parameter that the entry
7651    //    reset at c:5967 had already clobbered to 1:
7652    //        g() { local OPTIND OPTARG; }
7653    //        f() { local OPTIND=9; g; print $OPTIND }
7654    //        zsh -> 9        zshrs (before) -> 1
7655    //    A callee with no `local OPTIND` was unaffected, which is why
7656    //    it hid for so long; it reached every compsys port that
7657    //    declares OPTIND local (`_describe` and thus every completer
7658    //    calling it).
7659    //
7660    //  * BEFORE `endtrapscope()` (c:6174). A function-scoped EXIT trap
7661    //    that assigns OPTIND must win, because in C it runs after the
7662    //    restore:
7663    //        t1(){ trap 'OPTIND=99' EXIT; }; OPTIND=5; t1; print $OPTIND
7664    //        zsh -> 99
7665    //    Restoring after endtrapscope clobbered the trap's write to 5.
7666    //
7667    //  * BEFORE the option restore (c:6129-6162), so `isset(POSIXBUILTINS)`
7668    //    reads the CALLEE's setting, matching the entry-side reset at
7669    //    c:5966 which is likewise evaluated inside the function's option
7670    //    scope:
7671    //        OPTIND=5; f(){ setopt localoptions posixbuiltins; OPTIND=42 }
7672    //        f; print $OPTIND    zsh -> 42
7673    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
7674        if let Some(saved) = funcsave_optind {
7675            // c:6121 `zoptind = funcsave->zoptind;`
7676            if let Ok(n) = saved.parse::<i64>() {
7677                // A body that left OPTIND as it found it (no `getopts`, no
7678                // assignment) needs no write-back.
7679                if crate::ported::params::getsparam("OPTIND").as_deref() != Some(saved.as_str()) {
7680                    crate::ported::params::setiparam("OPTIND", n);
7681                }
7682                crate::ported::builtin::ZOPTIND.store(n as i32, Ordering::Relaxed);
7683            } else {
7684                crate::ported::params::setsparam("OPTIND", &saved);
7685            }
7686        }
7687        // c:6122 `optcind = funcsave->optcind;`
7688        crate::ported::builtin::OPTCIND.store(funcsave_optcind, Ordering::Relaxed);
7689    }
7690
7691    // c:6064 — `scriptname = funcsave->scriptname;`
7692    crate::ported::utils::set_scriptname(funcsave_scriptname);
7693
7694    // c:6067 — `endpatternscope();`
7695    crate::ported::pattern::endpatternscope();
7696
7697    // c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
7698    // localoptions was set inside the body.
7699    // c:6129 — `if (isset(LOCALOPTIONS) || funcsave->restore_sticky)`.
7700    // A sticky-emulation call ALWAYS restores the full option set on
7701    // return, whether or not LOCAL_OPTIONS is on — otherwise the
7702    // emulation the call installed leaks into the caller.
7703    if crate::ported::options::opt_state_get("localoptions").unwrap_or(false)
7704        || funcsave_restore_sticky != 0
7705    {
7706        // c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
7707        let mut restore = funcsave_opts.clone();
7708        // c:6089 — `funcsave->opts[PRIVILEGED] = opts[PRIVILEGED];`.
7709        // PRIVILEGED is carved out of the restore in C: a function that
7710        // dropped privileges does not get them back on return. The port
7711        // restored it with everything else.
7712        restore.carry_privileged_from_live();
7713        restore.restore();
7714        // c:6136 / c:6153 — `emulation = funcsave->emulation;`
7715        crate::ported::options::emulation.store(funcsave_emulation, Ordering::Relaxed);
7716        crate::ported::options::EMULATION.store(funcsave_emulation_live, Ordering::Relaxed);
7717        crate::ported::options::FULLY_EMULATING.store(funcsave_fully, Ordering::Relaxed);
7718        // c:6137 — `sticky = funcsave->sticky;` (restore_sticky arm only)
7719        if funcsave_restore_sticky != 0 {
7720            *sticky.lock().unwrap_or_else(|e| e.into_inner()) = funcsave_sticky;
7721        }
7722    } else {
7723        // c:6097-6101 — non-LOCALOPTIONS: restore only the always-
7724        // restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
7725        // LOCALLOOPS / WARNNESTEDVAR).
7726        for opt in [
7727            "xtrace",
7728            "printexitvalue",
7729            "localoptions",
7730            "localloops",
7731            "warnnestedvar",
7732        ] {
7733            if let Some(v) = funcsave_opts.saved_get(opt) {
7734                opt_state_set(opt, v);
7735            }
7736        }
7737    }
7738
7739    // c:5970 counterpart — `oflags = funcsave->oflags;` restores the caller's
7740    // attribute set so a sibling call after this one sees the right `-T`
7741    // inheritance. Bug #1058.
7742    FUNC_OFLAGS.store(oflags_prev, Ordering::Relaxed);
7743
7744    // c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
7745    // breaks/contflag/loops snapshot.
7746    if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
7747        // c:6106-6108 —
7748        //     if (contflag) zwarn("`continue' active at end of function scope");
7749        //     if (breaks)   zwarn("`break' active at end of function scope");
7750        if CONTFLAG.load(Ordering::SeqCst) != 0 {
7751            crate::ported::utils::zwarn("`continue' active at end of function scope");
7752            // c:6106
7753        }
7754        if BREAKS.load(Ordering::SeqCst) != 0 {
7755            crate::ported::utils::zwarn("`break' active at end of function scope");
7756            // c:6107
7757        }
7758        BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
7759        CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
7760        LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
7761    }
7762
7763    // c:6174 — `endtrapscope();`
7764    //
7765    // Bug #80 in docs/BUGS.md: zshrs used to run endtrapscope while
7766    // locallevel was still at the function's own level, so savetrap
7767    // entries tagged `local == current_function_level` failed the
7768    // `local > locallevel` pop condition and nested EXIT traps never
7769    // restored — outer EXIT traps fired at script exit instead. That
7770    // was worked around by bracketing this call with a manual
7771    // decrement/re-bump of locallevel. The workaround is gone: the
7772    // real `endparamscope()` now runs at its C position above, so the
7773    // pop loop observes the genuine post-decrement level, and the
7774    // trap body executes in the caller's param scope as C does.
7775    //
7776    // The function body's "shfunc" context was pushed by runshfunc's
7777    // execode (c:Src/exec.c:5960 → c:1251) and popped when that execode
7778    // returned, before doshfunc reaches endtrapscope; a function-scoped
7779    // EXIT trap therefore sees the CALLER's context plus its own "trap".
7780    drop(_eval_ctx_guard);
7781    crate::ported::signals::endtrapscope();
7782
7783    // c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
7784    if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
7785        // c:6116
7786        TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
7787    }
7788
7789    // c:6118 — `ret = lastval;`
7790    let ret = LASTVAL.load(Ordering::Relaxed);
7791
7792    // c:6119 — `noerrexit = funcsave->noerrexit;`
7793    noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
7794
7795    // c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
7796    // the function for side-effects only; outer lastval/pipestats
7797    // should reflect the PRE-call state.
7798    if noreturnval {
7799        // c:6120
7800        LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
7801        if let Some(saved_ps) = funcsave_pipestats {
7802            let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
7803            if let Ok(mut nguard) = n.lock() {
7804                *nguard = funcsave_numpipestats; // c:6122
7805            }
7806            let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
7807            if let Ok(mut pguard) = p.lock() {
7808                for (i, v) in saved_ps.iter().enumerate() {
7809                    if i < pguard.len() {
7810                        pguard[i] = *v; // c:6123 memcpy
7811                    }
7812                }
7813            }
7814        }
7815    }
7816
7817    // c:6128 — `unqueue_signals();`
7818    unqueue_signals();
7819
7820    // c:6135-6155 — exit_pending branch: when an `exit` was queued
7821    // inside the function body and we've unwound enough scopes for
7822    // it to take effect, either keep unwinding (still inside a
7823    // nested function) or actually exit the shell.
7824    let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
7825    let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
7826    let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
7827    let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
7828    let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
7829    if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
7830        // c:6141
7831        if cur_locallevel > cur_forklevel {
7832            // c:6143 — still inside a nested function: keep unwinding.
7833            RETFLAG.store(1, Ordering::Relaxed); // c:6144
7834            BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
7835        } else {
7836            // c:6151 — out of all functions: exit for real.
7837            crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
7838            let val = EXIT_VAL.load(Ordering::Relaxed);
7839            crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
7840            // c:6152
7841        }
7842    }
7843
7844    ret // c:6157 return ret
7845}
7846
7847// ═══════════════════════════════════════════════════════════════════════
7848// `(eval)` funcstack frame — the `FS_EVAL` half of the funcstack/functrace/
7849// funcfiletrace subsystem whose `FS_FUNC` half is `doshfunc` above.
7850// ═══════════════════════════════════════════════════════════════════════
7851
7852/// !!! WARNING: RUST-ONLY HELPER !!!
7853///
7854/// C keeps `struct funcstack fstack;` as an automatic in `eval()`
7855/// (`Src/builtin.c:6148`) and unwinds it with a plain
7856/// `funcstack = funcstack->prev;` on the single return path
7857/// (`Src/builtin.c:6210`). zshrs's `FUNCSTACK` is a `Mutex<Vec<funcstack>>`
7858/// and its eval entry points have MANY early-return paths (errflag
7859/// containment, recursion backstop, `?` from `execute_script`), so the pop
7860/// is carried by `Drop` instead. There is no C counterpart for this type;
7861/// the pushed frame itself is a line-by-line port of `Src/builtin.c:6155-6193`.
7862///
7863/// Every caller that stands in for C's `eval()` — the `BUILTIN_EVAL`
7864/// handler and `eval_string` below — must hold one of these for the
7865/// duration of the eval'd body, or `$funcstack` / `$functrace` /
7866/// `$funcfiletrace` lose a frame relative to zsh.
7867pub struct EvalFuncstackFrame {
7868    /// c:6191/6193 `fpushed` — whether the frame was actually pushed.
7869    fpushed: bool,
7870    /// c:6147 — `int oineval = ineval, fpushed;`. Restored at c:6215
7871    /// (`ineval = oineval;`) by this guard's Drop.
7872    oineval: i32,
7873}
7874
7875impl EvalFuncstackFrame {
7876    /// Port of the funcstack prologue of `eval(char **argv)` from
7877    /// `Src/builtin.c:6155-6193`.
7878    ///
7879    /// ```c
7880    /// ineval = !isset(EVALLINENO);
7881    /// if (!ineval) {
7882    ///     scriptname = "(eval)";
7883    ///     fstack.prev = funcstack;
7884    ///     fstack.name = scriptname;
7885    ///     fstack.caller = funcstack ? funcstack->name : dupstring(argzero);
7886    ///     fstack.lineno = lineno;
7887    ///     fstack.tp = FS_EVAL;
7888    ///     if (!funcstack || funcstack->tp == FS_SOURCE) {
7889    ///         fstack.flineno = fstack.lineno;
7890    ///         fstack.filename = fstack.caller;
7891    ///     } else {
7892    ///         fstack.flineno = funcstack->flineno + lineno;
7893    ///         if (funcstack->tp == FS_EVAL)
7894    ///             fstack.flineno--;
7895    ///         fstack.filename = funcstack->filename;
7896    ///         if (!fstack.filename)
7897    ///             fstack.filename = "";
7898    ///     }
7899    ///     funcstack = &fstack;
7900    ///     fpushed = 1;
7901    /// } else
7902    ///     fpushed = 0;
7903    /// ```
7904    ///
7905    /// `scriptname` (c:6157) is left to the caller: the `BUILTIN_EVAL`
7906    /// handler saves/restores it around the whole builtin, which is where
7907    /// C's `oscriptname` (c:6146) / restore (c:6222) live.
7908    pub fn push() -> Self {
7909        use crate::ported::modules::parameter::FUNCSTACK;
7910        use crate::ported::zsh_h::{FS_EVAL, FS_SOURCE};
7911        // c:6147 — `int oineval = ineval, fpushed;`
7912        let oineval = crate::ported::builtin::ineval.load(std::sync::atomic::Ordering::Relaxed);
7913        // c:6155 — `ineval = !isset(EVALLINENO);`
7914        let ineval = !crate::ported::zsh_h::isset(crate::ported::zsh_h::EVALLINENO);
7915        // The global drives `Src/exec.c`'s three `!ineval` lineno gates
7916        // (c:1355 / c:1451 / c:2056); zshrs reads it in the
7917        // `BUILTIN_SET_LINENO` handler. Only the funcstack half of this
7918        // prologue was ported, so `unsetopt evallineno; eval 'print
7919        // $LINENO'` counted lines inside the eval string (1) instead of
7920        // reporting the caller's line (E01options.ztst:29).
7921        crate::ported::builtin::ineval.store(
7922            if ineval { 1 } else { 0 },
7923            std::sync::atomic::Ordering::Relaxed,
7924        );
7925        if ineval {
7926            return EvalFuncstackFrame {
7927                fpushed: false, // c:6193 fpushed = 0
7928                oineval,
7929            };
7930        }
7931        // c:6161 — `fstack.lineno = lineno;`. zshrs mirrors C's single
7932        // `lineno` global (params.c:123) in `lex::LEX_LINENO`, the one
7933        // `BUILTIN_SET_LINENO` drives per statement — the same mirror
7934        // `doshfunc` reads for its `FS_FUNC` frame (see c:6013 there).
7935        let lineno = crate::ported::lex::lineno() as i64;
7936        let frame = {
7937            let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7938            // c:6160 — `fstack.caller = funcstack ? funcstack->name
7939            //                                     : dupstring(argzero);`
7940            let caller = match stk.last() {
7941                Some(f) => Some(f.name.clone()),
7942                None => crate::ported::utils::argzero(),
7943            };
7944            // c:6174-6188 — flineno/filename deduction. Identical logic to
7945            // `funcfiletracegetfn` (Src/Modules/parameter.c:724-762): an eval
7946            // is an inlined call from a tracing perspective.
7947            let (flineno, filename) = match stk.last() {
7948                // c:6174 — `if (!funcstack || funcstack->tp == FS_SOURCE)`
7949                None => (lineno, caller.clone()), // c:6175-6176
7950                Some(p) if p.tp == FS_SOURCE => (lineno, caller.clone()), // c:6175-6176
7951                Some(p) => {
7952                    // c:6178 — `fstack.flineno = funcstack->flineno + lineno;`
7953                    let mut flineno = p.flineno + lineno;
7954                    // c:6183-6184 — `if (funcstack->tp == FS_EVAL) fstack.flineno--;`
7955                    // Line numbers in eval start from 1, not zero, so offset
7956                    // by one to get line in file.
7957                    if p.tp == FS_EVAL {
7958                        flineno -= 1;
7959                    }
7960                    // c:6185-6187 — `fstack.filename = funcstack->filename;
7961                    //                if (!fstack.filename) fstack.filename = "";`
7962                    (
7963                        flineno,
7964                        Some(p.filename.clone().unwrap_or_else(String::new)),
7965                    )
7966                }
7967            };
7968            crate::ported::zsh_h::funcstack {
7969                prev: None,                 // c:6158 (Vec-stack: index encodes link)
7970                name: "(eval)".to_string(), // c:6159 fstack.name = scriptname
7971                filename,                   // c:6176 / c:6185
7972                caller,                     // c:6160
7973                flineno,                    // c:6175 / c:6178
7974                lineno,                     // c:6161
7975                tp: FS_EVAL,                // c:6162
7976            }
7977        };
7978        FUNCSTACK
7979            .lock()
7980            .unwrap_or_else(|e| e.into_inner())
7981            .push(frame); // c:6189 funcstack = &fstack
7982        EvalFuncstackFrame {
7983            fpushed: true, // c:6191 fpushed = 1
7984            oineval,
7985        }
7986    }
7987
7988    /// c:6191/6193 `fpushed` — whether `EVALLINENO` was set and a frame
7989    /// really went on. Callers mirror C's `if (!ineval) scriptname =
7990    /// "(eval)";` (c:6157) off the same test.
7991    pub fn pushed(&self) -> bool {
7992        self.fpushed
7993    }
7994}
7995
7996impl Drop for EvalFuncstackFrame {
7997    /// c:6209-6210 — `if (fpushed) funcstack = funcstack->prev;`
7998    /// c:6215 — `ineval = oineval;`
7999    fn drop(&mut self) {
8000        crate::ported::builtin::ineval.store(self.oineval, std::sync::atomic::Ordering::Relaxed); // c:6215
8001        if self.fpushed {
8002            crate::ported::modules::parameter::FUNCSTACK
8003                .lock()
8004                .unwrap_or_else(|e| e.into_inner())
8005                .pop();
8006        }
8007    }
8008}
8009
8010/// `TRAP_STATE_PRIMED` — re-exported from the canonical enum port rather
8011/// than redefined here.
8012///
8013/// This was a LOCAL `const … = 2`, which is `TRAP_STATE_FORCE_RETURN`'s value:
8014/// `enum trap_state { TRAP_STATE_INACTIVE, TRAP_STATE_PRIMED,
8015/// TRAP_STATE_FORCE_RETURN }` (zsh.h:2947-2960) makes PRIMED 1, and
8016/// zsh_h.rs:4501-4505 already carries all three with the right values. With
8017/// the wrong number, doshfunc's two trap_return adjustments (c:5866-5867 on
8018/// entry, c:6116-6117 on exit) never fired for a primed trap, so trap_return
8019/// stayed at -1 instead of reaching the -2 sentinel bin_break tests
8020/// (builtin.c:5845). `return N` inside a TRAPxxx function therefore never
8021/// promoted to TRAP_STATE_FORCE_RETURN:
8022///     TRAPINT() { print CAUGHT; return 130 }; kill -INT $$; print DONE
8023///     zsh  : CAUGHT
8024///     zshrs: CAUGHT DONE
8025use crate::ported::zsh_h::TRAP_STATE_PRIMED;
8026
8027/// Port of `execfuncdef()` from `Src/exec.c:5309` — C decl `execfuncdef(Estate state, Eprog redir_prog)`.
8028/// Define a shell function: extract
8029/// name(s)+body from the wordcode payload, allocate the Shfunc,
8030/// install into `shfunctab` (named), or execute immediately (anon).
8031#[allow(non_snake_case)]
8032pub fn execfuncdef(state: &mut estate, mut redir_prog: Option<crate::ported::zsh_h::Eprog>) -> i32 {
8033    use crate::ported::hashtable::{dircache_set, shfunctab_lock};
8034    use crate::ported::jobs::{getsigidx, removetrapnode};
8035    use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
8036    use crate::ported::signals::settrap;
8037    use crate::ported::utils::scriptfilename_get;
8038    use crate::ported::zsh_h::{
8039        eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, Patprog,
8040        EC_DUPTOK as _, EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED,
8041        PM_TAGGED_LOCAL, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
8042    };
8043    // c:5311 — `Shfunc shf;`
8044    let mut shf: Box<shfunc_t>;
8045    // c:5312 — `char *s = NULL;`
8046    let mut s: Option<String> = None;
8047    // c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
8048    let mut signum: i32;
8049    let nprg: i32;
8050    let sbeg: i32;
8051    let nstrs: i32;
8052    let npats: i32;
8053    let do_tracing: i32;
8054    let len: i32;
8055    let plen: i32;
8056    // `i` — C loop counter for pp stamp; Rust uses .map().collect().
8057    let mut htok: i32 = 0;
8058    let mut ret: i32 = 0;
8059    // c:5314 — `int anon_func = 0;`
8060    let mut anon_func: i32 = 0;
8061    // c:5315 — `Wordcode beg = state->pc, end;`
8062    let _beg: usize = state.pc;
8063    let mut end: usize;
8064    // c:5316 — `Eprog prog;`
8065    // (allocated inline per-iter below; no upfront binding needed)
8066    // c:5317 — `Patprog *pp;` — handled by Vec construction.
8067    // c:5318 — `LinkList names;`
8068    let names: Vec<String>;
8069    // c:5319 — `int tracing_flags;`
8070    let tracing_flags: i32;
8071
8072    // c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
8073    end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
8074    // c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
8075    let num = state.prog.prog[state.pc] as usize;
8076    state.pc += 1;
8077    names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
8078    // c:5323 — `sbeg = *state->pc++;`
8079    sbeg = state.prog.prog[state.pc] as i32;
8080    state.pc += 1;
8081    // c:5324 — `nstrs = *state->pc++;`
8082    nstrs = state.prog.prog[state.pc] as i32;
8083    state.pc += 1;
8084    // c:5325 — `npats = *state->pc++;`
8085    npats = state.prog.prog[state.pc] as i32;
8086    state.pc += 1;
8087    // c:5326 — `do_tracing = *state->pc++;`
8088    do_tracing = state.prog.prog[state.pc] as i32;
8089    state.pc += 1;
8090
8091    // c:5328 — `nprg = (end - state->pc);`
8092    nprg = end.saturating_sub(state.pc) as i32;
8093    // c:5329 — `plen = nprg * sizeof(wordcode);`
8094    plen = nprg.saturating_mul(size_of::<wordcode>() as i32);
8095    // c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
8096    len = plen + npats.saturating_mul(size_of::<usize>() as i32) + nstrs;
8097    // c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
8098    tracing_flags = if do_tracing != 0 {
8099        PM_TAGGED_LOCAL as i32
8100    } else {
8101        0
8102    };
8103
8104    // c:5333-5339 — htok name substitution.
8105    let mut names_mut: Vec<String> = names;
8106    if htok != 0 && !names_mut.is_empty() {
8107        execsubst(&mut names_mut); // c:5334
8108        if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8109            // c:5335
8110            state.pc = end; // c:5336
8111            return 1; // c:5337
8112        }
8113    }
8114
8115    // c:5397-5398 — `DPUTS(!names && redir_prog, "Passing redirection to anon function definition.");`
8116    crate::DPUTS!(
8117        num == 0 && redir_prog.is_some(),
8118        "Passing redirection to anon function definition."
8119    );
8120
8121    // c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
8122    // num==0 → anon (no names); else iterate names.
8123    let mut names_iter = names_mut.into_iter();
8124    loop {
8125        let no_names = num == 0;
8126        if !no_names {
8127            // c:5343 — `s = ugetnode(names)`; break when list exhausted.
8128            match names_iter.next() {
8129                Some(nm) => s = Some(nm),
8130                None => break,
8131            }
8132        }
8133        // c:5344-5374 — Eprog alloc.
8134        let prog: Box<eprog_t>;
8135        let dump_present = state.prog.dump.is_some();
8136        let make_pat = || -> Patprog {
8137            // c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
8138            Box::new(patprog_t {
8139                startoff: 0,
8140                size: 0,
8141                mustoff: 0,
8142                patmlen: 0,
8143                globflags: 0,
8144                globend: 0,
8145                flags: 0,
8146                patnpar: 0,
8147                patstartch: 0,
8148            })
8149        };
8150        if no_names {
8151            // c:5345-5346 — `zhalloc`, `nref = -1`.
8152            // c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
8153            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8154            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
8155            // c:5365 — `prog->strs = state->strs + sbeg;`
8156            let strs_tail = state.strs.as_ref().map(|t| {
8157                let off = (sbeg as usize).min(t.len());
8158                t[off..].to_string()
8159            });
8160            prog = Box::new(eprog_t {
8161                flags: EF_HEAP,
8162                len,
8163                npats,
8164                nref: -1, // c:5346
8165                pats,
8166                prog: prog_words,
8167                strs: strs_tail,
8168                shf: None,  // c:5377
8169                dump: None, // c:5356
8170                // c:5365 — `prog->strs = state->strs + sbeg;` — the tail is
8171                // the SAME pool, so it keeps the same encoding.
8172                strs_metafied: state.prog.strs_metafied,
8173            });
8174        } else if dump_present {
8175            // c:5358-5363 — EF_MAP path: refcount the dump, allocate
8176            // pats permanent, reuse `state->pc` slice in place.
8177            if let Some(dp) = state.prog.dump.as_deref() {
8178                incrdumpcount(dp); // c:5360
8179            }
8180            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8181            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
8182            let strs_tail = state.strs.as_ref().map(|t| {
8183                let off = (sbeg as usize).min(t.len());
8184                t[off..].to_string()
8185            });
8186            prog = Box::new(eprog_t {
8187                flags: EF_MAP, // c:5359
8188                len,
8189                npats,
8190                nref: 1, // c:5349
8191                pats,
8192                prog: prog_words,
8193                strs: strs_tail,
8194                shf: None,                               // c:5377
8195                dump: state.prog.dump.clone(),           // c:5361
8196                strs_metafied: state.prog.strs_metafied, // pool copied verbatim — carry provenance
8197            });
8198        } else {
8199            // c:5366-5374 — EF_REAL: copy wordcode + strs into a
8200            // freshly-owned eprog (no shared dump backing).
8201            let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8202            let pc_end = state.pc + nprg as usize;
8203            let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
8204            // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
8205            let strs_copy = state.strs.as_ref().map(|t| {
8206                let off = (sbeg as usize).min(t.len());
8207                let n_avail = t.len().saturating_sub(off);
8208                let take = (nstrs as usize).min(n_avail);
8209                t[off..off + take].to_string()
8210            });
8211            prog = Box::new(eprog_t {
8212                flags: EF_REAL, // c:5367
8213                len,
8214                npats,
8215                nref: 1, // c:5349
8216                pats,
8217                prog: prog_words,
8218                strs: strs_copy,
8219                shf: None,  // c:5377
8220                dump: None, // c:5371
8221                // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);` —
8222                // a byte copy of the source pool, so the same encoding.
8223                strs_metafied: state.prog.strs_metafied,
8224            });
8225        }
8226
8227        // c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
8228        shf = Box::new(shfunc_t {
8229            node: hashnode {
8230                next: None,
8231                nam: String::new(),
8232                flags: tracing_flags,
8233            },
8234            filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
8235            // c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
8236            // else just lineno.
8237            lineno: {
8238                let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
8239                if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
8240                    if let Some(top) = stk.last() {
8241                        if top.tp == FS_FUNC || top.tp == FS_EVAL {
8242                            top.flineno + cur_lineno
8243                        } else {
8244                            cur_lineno
8245                        }
8246                    } else {
8247                        cur_lineno
8248                    }
8249                } else {
8250                    cur_lineno
8251                }
8252            },
8253            funcdef: Some(prog), // c:5380
8254            redir: None,
8255            sticky: None,
8256            body: None,
8257            redir_text: None,
8258        });
8259        // c:5396-5401 — redir_prog ownership.
8260        // C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
8261        // else `shf->redir = redir_prog; redir_prog = 0;`
8262        // "nonempty(names)" means there's a NEXT name still to consume —
8263        // i.e. peek the iterator.
8264        if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
8265            // c:5397 — dupe so each earlier name gets its own copy; the
8266            // last name (when iterator drains) gets the original.
8267            if let Some(rp) = redir_prog.as_deref() {
8268                shf.redir = Some(Box::new(dupeprog(rp, false)));
8269            }
8270        } else {
8271            // c:5399-5400 — last name (or anon) takes original.
8272            shf.redir = redir_prog.take();
8273        }
8274        // c:5402 — `shfunc_set_sticky(shf);`
8275        shfunc_set_sticky(&mut shf);
8276
8277        if no_names {
8278            // c:5404-5457 — anonymous function: execute immediately.
8279            // `LinkList args;` c:5409
8280            let mut args: Vec<String>;
8281
8282            anon_func = 1; // c:5411
8283            shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
8284
8285            state.pc = end; // c:5414
8286                            // c:5415 — `end += *state->pc++;`
8287            end += state.prog.prog[state.pc] as usize;
8288            state.pc += 1;
8289            // c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
8290            let arg_count = state.prog.prog[state.pc] as usize;
8291            state.pc += 1;
8292            args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
8293
8294            // c:5418-5429 — htok arg subst + cleanup-on-error.
8295            if htok != 0 && !args.is_empty() {
8296                execsubst(&mut args); // c:5419
8297                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8298                    // c:5421 — `freeeprog(shf->funcdef);`
8299                    if let Some(mut fd) = shf.funcdef.take() {
8300                        freeeprog(&mut fd);
8301                    }
8302                    if shf.redir.is_some() {
8303                        // c:5422-5423 — "shouldn't be" anon+redir, but free if so.
8304                        if let Some(mut rd) = shf.redir.take() {
8305                            freeeprog(&mut rd);
8306                        }
8307                    }
8308                    dircache_set(&mut shf.filename, None); // c:5424
8309                    drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
8310                    state.pc = end; // c:5426
8311                    return 1; // c:5427
8312                }
8313            }
8314
8315            // c:5431-5432 — `setunderscore` to last arg (or "").
8316            let under_val = if !args.is_empty() {
8317                args.last().cloned().unwrap_or_default()
8318            } else {
8319                String::new()
8320            };
8321            setunderscore(&under_val);
8322
8323            // c:5434-5435 — `if (!args) args = newlinklist();`
8324            // (Rust Vec is never null; no-op.)
8325            shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
8326                                                                // c:5437 — `pushnode(args, shf->node.nam);` — prepend.
8327            args.insert(0, shf.node.nam.clone());
8328
8329            execshfunc(&mut shf, &mut args); // c:5439
8330            ret = LASTVAL.load(Ordering::Relaxed); // c:5440
8331
8332            // c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
8333            if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
8334                eprintln!("zsh: exit {}", ret); // c:5445/5447
8335            }
8336
8337            // c:5452-5456 — cleanup.
8338            if let Some(mut fd) = shf.funcdef.take() {
8339                freeeprog(&mut fd);
8340            }
8341            if let Some(mut rd) = shf.redir.take() {
8342                // c:5453-5454 — "shouldn't be" but free if present.
8343                freeeprog(&mut rd);
8344            }
8345            dircache_set(&mut shf.filename, None); // c:5455
8346            drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
8347            break; // c:5457
8348        } else {
8349            // c:5458-5484 — named function path.
8350            let nm = s.as_deref().unwrap_or("");
8351            // c:5460-5475 — TRAP* signal-trap install.
8352            // BUGS.md #1114 — the TRAPxxx() FUNCTION-trap form is zsh-only.
8353            // bash, ksh and dash have no such concept: there `TRAPINT` is an
8354            // ordinary function whose name merely begins with TRAP, and SIGINT
8355            // keeps its default disposition. Recognising it in a drop-in mode
8356            // also SILENTLY DESTROYS an already-installed list trap for that
8357            // signal, since the two forms are alternatives in zsh.
8358            // posix_faithful() is raised only for a bare drop-in flag, so
8359            // --zsh, native zshrs and `emulate sh` are untouched.
8360            if !crate::extensions::dash_mode::posix_faithful() && nm.len() > 4 && nm.starts_with("TRAP") {
8361                if let Some(sn) = getsigidx(&nm[4..]) {
8362                    signum = sn;
8363                    // c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
8364                    if settrap(signum, None, ZSIG_FUNC) != 0 {
8365                        if let Some(mut fd) = shf.funcdef.take() {
8366                            freeeprog(&mut fd); // c:5463
8367                        }
8368                        dircache_set(&mut shf.filename, None); // c:5464
8369                        drop(shf); // c:5465
8370                        state.pc = end; // c:5466
8371                        return 1; // c:5467
8372                    }
8373                    // c:5474 — `removetrapnode(signum);`
8374                    removetrapnode(signum);
8375                    // c:Src/signals.c::settrap → unsettrap →
8376                    // removetrap also clears sigfuncs[sig] (the C
8377                    // string-form trap slot). zshrs's port stores
8378                    // string-form bodies in a separate
8379                    // `traps_table` HashMap not touched by
8380                    // removetrap. Drop the string-form entry here
8381                    // so dotrap's fallback doesn't double-dispatch
8382                    // when a TRAPxxx function REPLACES an
8383                    // existing `trap '...' SIG` registration. Bug
8384                    // #541 in docs/BUGS.md.
8385                    if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
8386                        t.remove(&nm[4..]);
8387                    }
8388                }
8389            }
8390            // c:5477-5482 — re-define-self trace flag propagate.
8391            if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
8392                if let Some(top) = stk.last() {
8393                    if top.tp == FS_FUNC && top.name == nm {
8394                        // c:5479 — `Shfunc old = shfunctab->getnode(s);`
8395                        if let Ok(rd) = shfunctab_lock().read() {
8396                            if let Some(old) = rd.get(nm) {
8397                                // c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
8398                                shf.node.flags |=
8399                                    old.node.flags & (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
8400                            }
8401                        }
8402                    }
8403                }
8404            }
8405            // c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
8406            shf.node.nam = nm.to_string();
8407            // Lineage tap: the definition is a function's origin, and a
8408            // second definition of the same name is a `redefine` op.
8409            if crate::provenance::active() {
8410                crate::provenance::on_func_define(
8411                    nm,
8412                    shf.body.as_deref(),
8413                    shf.filename.as_deref(),
8414                    shf.lineno,
8415                );
8416            }
8417            if let Ok(mut wr) = shfunctab_lock().write() {
8418                wr.add(*shf);
8419            }
8420        }
8421    }
8422    // c:5486-5487 — `if (!anon_func) setunderscore("");`
8423    if anon_func == 0 {
8424        setunderscore("");
8425    }
8426    // c:5488-5491 — leftover redir cleanup ("shouldn't happen").
8427    if let Some(mut rd) = redir_prog.take() {
8428        freeeprog(&mut rd);
8429    }
8430    // c:5492 — `state->pc = end;`
8431    state.pc = end;
8432    // c:5493 — `return ret;`
8433    ret
8434}
8435
8436/// Port of `execsimple()` from `Src/exec.c:1290` — C decl `execsimple(Estate state)`.
8437/// Fast-path for single-Simple commands that bypasses the full
8438/// `execcmd_exec` machinery.
8439pub fn execsimple(state: &mut estate) -> i32 {
8440    // c:1292 — `wordcode code = *state->pc++;`
8441    let mut code = state.prog.prog[state.pc];
8442    state.pc += 1;
8443    // c:1295-1296 — `if (errflag) return (lastval = 1);`
8444    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
8445    // the C line cited above tests the WHOLE errflag, so masking here let an
8446    // interrupted shell keep going where zsh stops.
8447    if errflag.load(Ordering::Relaxed) != 0 {
8448        LASTVAL.store(1, Ordering::Relaxed);
8449        return 1;
8450    }
8451    // c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
8452    if !isset(crate::ported::zsh_h::EXECOPT) {
8453        LASTVAL.store(0, Ordering::Relaxed);
8454        return 0;
8455    }
8456    // c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
8457    // In evaluated traps, don't modify the line number (the trap
8458    // dispatcher restores it). `code` here is the wordcode-encoded
8459    // line number from the WC_SIMPLE entry at state.pc-1.
8460    if !crate::ported::zsh_h::IN_EVAL_TRAP()
8461        && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
8462        && code != 0
8463    {
8464        crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
8465    }
8466    // c:1306 — `code = wc_code(*state->pc++);`
8467    code = wc_code(state.prog.prog[state.pc]);
8468    state.pc += 1;
8469    // c:1311-1312 — `otj = thisjob; thisjob = -1;`
8470    let otj = *THISJOB
8471        .get_or_init(|| std::sync::Mutex::new(-1))
8472        .lock()
8473        .unwrap();
8474    *THISJOB
8475        .get_or_init(|| std::sync::Mutex::new(-1))
8476        .lock()
8477        .unwrap() = -1;
8478    use crate::ported::zsh_h::{
8479        WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
8480        WC_WHILE,
8481    };
8482    use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
8483    let lv = if code == WC_ASSIGN {
8484        // c:1315-1319 — assignment-only simple cmd path.
8485        // cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
8486        addvars(state, state.pc.saturating_sub(1), 0);
8487        setunderscore(""); // c:1317
8488        if isset(XTRACE) {
8489            eprintln!();
8490        }
8491        let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
8492        if ef != 0 {
8493            ef
8494        } else {
8495            0
8496        }
8497    } else {
8498        // c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
8499        let q = queue_signal_level();
8500        dont_queue_signals();
8501        let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8502            ERRFLAG_ERROR
8503        } else if code == WC_FUNCDEF {
8504            execfuncdef(state, None)
8505        } else {
8506            // c:5499 execfuncs[] table inlined — match the WC_* tag.
8507            match code {
8508                WC_CURSH => execcursh(state, 0),
8509                WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
8510                WC_FOR => execfor(state, 0),
8511                WC_SELECT => execselect(state, 0),
8512                WC_CASE => execcase(state, 0),
8513                WC_IF => execif(state, 0),
8514                WC_WHILE => execwhile(state, 0),
8515                WC_REPEAT => execrepeat(state, 0),
8516                WC_TIMED => exectime(state, 0),
8517                WC_COND => execcond(state, 0),
8518                WC_ARITH => execarith(state, 0),
8519                WC_TRY => exectry(state, 0),
8520                _ => 0,
8521            }
8522        };
8523        restore_queue_signals(q);
8524        result
8525    };
8526    // c:1334 — `thisjob = otj;`
8527    *THISJOB
8528        .get_or_init(|| std::sync::Mutex::new(-1))
8529        .lock()
8530        .unwrap() = otj;
8531    LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
8532    lv
8533}
8534
8535/// Port of `execlist()` from `Src/exec.c:1402` — C decl `execlist(Estate state, int dont_change_job, int exiting)`.
8536/// Walks WC_LIST entries, dispatches each sublist (WC_SUBLIST chain
8537/// inlined, same as C — there is no separate execsublist function),
8538/// runs the DEBUG / ERR / EXIT traps and applies ERREXIT / ERRRETURN.
8539pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) {
8540    use crate::ported::signals_h::{SIGDEBUG, SIGEXIT, SIGZERR};
8541    use crate::ported::zsh_h::{
8542        CS_CMDAND, CS_CMDOR, DEBUGBEFORECMD, ERREXIT, ERRRETURN, SHINSTDIN, WC_LIST_SKIP,
8543        WC_PIPE_LINENO, WC_SUBLIST_AND, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SIMPLE,
8544        WC_SUBLIST_SKIP, Z_SIMPLE, Z_SYNC,
8545    };
8546    static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1404
8547
8548    // c:1415 — `int oldnoerrexit = noerrexit;`
8549    let oldnoerrexit = noerrexit.load(Ordering::Relaxed);
8550
8551    queue_signals(); // c:1417
8552
8553    let cj = *THISJOB
8554        .get_or_init(|| std::sync::Mutex::new(-1))
8555        .lock()
8556        .unwrap(); // c:1419
8557    let old_pline_level = pline_level.load(Ordering::Relaxed); // c:1420
8558    let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:1421
8559    let old_list_pipe_job = list_pipe_job.load(Ordering::Relaxed); // c:1422
8560    let old_list_pipe_text: Option<String> = {
8561        // c:1423-1426
8562        let t = LIST_PIPE_TEXT.lock().unwrap();
8563        if t.is_empty() {
8564            None
8565        } else {
8566            Some(t.clone())
8567        }
8568    };
8569    let oldlineno = crate::ported::input::lineno.with(|l| l.get()); // c:1427
8570
8571    // c:1429-1432 — `if (sourcelevel && unset(SHINSTDIN)) {...}`
8572    if crate::ported::init::sourcelevel.load(Ordering::Relaxed) != 0 && !isset(SHINSTDIN) {
8573        pline_level.store(0, Ordering::Relaxed);
8574        list_pipe.store(0, Ordering::Relaxed);
8575        list_pipe_job.store(0, Ordering::Relaxed);
8576        LIST_PIPE_TEXT.lock().unwrap().clear();
8577    }
8578
8579    // c:1436-1440 — `code = *state->pc++; if (wc_code(code) != WC_LIST) lastval = 0;`
8580    let mut code = state.prog.prog[state.pc];
8581    state.pc += 1;
8582    if wc_code(code) != WC_LIST {
8583        LASTVAL.store(0, Ordering::Relaxed);
8584    }
8585    while wc_code(code) == WC_LIST
8586        && BREAKS.load(Ordering::SeqCst) == 0
8587        && RETFLAG.load(Ordering::SeqCst) == 0
8588        && errflag.load(Ordering::Relaxed) == 0
8589    {
8590        let mut this_donetrap = 0; // c:1443
8591        this_noerrexit.store(0, Ordering::Relaxed); // c:1444
8592
8593        let ltype = WC_LIST_TYPE(code) as i32; // c:1446
8594        let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len()); // c:1447
8595
8596        if !crate::ported::zsh_h::IN_EVAL_TRAP()
8597            && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
8598        {
8599            // c:1449-1469 — valid line number for debugging.
8600            let code2 = state.prog.prog[state.pc];
8601            let mut lnp1: wordcode = 0;
8602            if (ltype & Z_SIMPLE as i32) != 0 {
8603                lnp1 = code2;
8604            } else if wc_code(code2) == WC_SUBLIST {
8605                if WC_SUBLIST_FLAGS(code2) == WC_SUBLIST_SIMPLE {
8606                    lnp1 = state.prog.prog[state.pc + 1];
8607                } else {
8608                    lnp1 = WC_PIPE_LINENO(state.prog.prog[state.pc + 1]);
8609                }
8610            }
8611            if lnp1 != 0 {
8612                crate::ported::input::lineno.with(|l| l.set(lnp1 as usize - 1)); // c:1469
8613            }
8614        }
8615
8616        let debug_trapped = sigtrapped
8617            .lock()
8618            .unwrap()
8619            .get(SIGDEBUG as usize)
8620            .copied()
8621            .unwrap_or(0)
8622            != 0;
8623        let donedebug: i32;
8624        if debug_trapped && isset(DEBUGBEFORECMD) && intrap.load(Ordering::Relaxed) == 0 {
8625            // c:1472-1496
8626            let mut pc2 = state.pc;
8627            let oerrexit_opt = isset(ERREXIT);
8628            opt_state_set("errexit", false);
8629            noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed);
8630            if (ltype & Z_SIMPLE as i32) != 0 {
8631                pc2 += 1; // c:1478 skip the line number
8632            }
8633            let pm = assignsparam(
8634                "ZSH_DEBUG_CMD",
8635                &crate::ported::text::getpermtext(state.prog.clone(), Some(pc2), 0),
8636                0,
8637            ); // c:1479
8638
8639            exiting = DONETRAP.load(Ordering::Relaxed); // c:1483
8640            let ret = LASTVAL.load(Ordering::Relaxed); // c:1484
8641            crate::ported::signals::dotrap(SIGDEBUG); // c:1485
8642            if RETFLAG.load(Ordering::SeqCst) == 0 {
8643                LASTVAL.store(ret, Ordering::Relaxed); // c:1487
8644            }
8645            DONETRAP.store(exiting, Ordering::Relaxed); // c:1488
8646            noerrexit.store(oldnoerrexit, Ordering::Relaxed); // c:1489
8647            // c:1493 — only execute the trap once per sublist, even if
8648            // the DEBUGBEFORECMD option changes.
8649            donedebug = if isset(ERREXIT) { 2 } else { 1 };
8650            opt_state_set("errexit", oerrexit_opt); // c:1494
8651            if pm.is_some() {
8652                unsetparam("ZSH_DEBUG_CMD"); // c:1496 unsetparam_pm(pm, 0, 1)
8653            }
8654        } else {
8655            donedebug = if intrap.load(Ordering::Relaxed) != 0 { 1 } else { 0 }; // c:1498
8656        }
8657
8658        // c:1502 — reset donetrap: a trap is only called once for each
8659        // sublist that fails.
8660        DONETRAP.store(0, Ordering::Relaxed);
8661        'sublist_done: {
8662            if (ltype & Z_SIMPLE as i32) != 0 {
8663                // c:1503-1509
8664                let next = state.pc + WC_LIST_SKIP(code) as usize;
8665                if donedebug != 2 {
8666                    execsimple(state);
8667                }
8668                state.pc = next;
8669                break 'sublist_done;
8670            }
8671
8672            // c:1511 — loop through code followed by &&, ||, or end of sublist.
8673            code = state.prog.prog[state.pc];
8674            state.pc += 1;
8675            if donedebug == 2 {
8676                // c:1513-1522 — skip sublist.
8677                while wc_code(code) == WC_SUBLIST {
8678                    state.pc += WC_SUBLIST_SKIP(code) as usize;
8679                    if WC_SUBLIST_TYPE(code) == WC_SUBLIST_END {
8680                        break;
8681                    }
8682                    code = state.prog.prog[state.pc];
8683                    state.pc += 1;
8684                }
8685                DONETRAP.store(1, Ordering::Relaxed);
8686                break 'sublist_done;
8687            }
8688            while wc_code(code) == WC_SUBLIST {
8689                this_noerrexit.store(0, Ordering::Relaxed); // c:1525
8690                let isandor = WC_SUBLIST_TYPE(code) != WC_SUBLIST_END; // c:1526
8691                let isnot = (WC_SUBLIST_FLAGS(code) & WC_SUBLIST_NOT) != 0; // c:1527
8692                let mut next = state.pc + WC_SUBLIST_SKIP(code) as usize; // c:1528
8693                // c:1530 — suppress errexit for commands before && and || and after !
8694                if isandor || isnot {
8695                    noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed);
8696                }
8697                let simple = (WC_SUBLIST_FLAGS(code) & WC_SUBLIST_SIMPLE) != 0;
8698                match WC_SUBLIST_TYPE(code) {
8699                    x if x == WC_SUBLIST_END => {
8700                        // c:1533-1543 — end of sublist; just execute, ignoring status.
8701                        if simple {
8702                            execsimple(state);
8703                        } else {
8704                            execpline(
8705                                state,
8706                                code,
8707                                ltype,
8708                                if (ltype & Z_END as i32) != 0 && exiting != 0 { 1 } else { 0 },
8709                            );
8710                        }
8711                        state.pc = next;
8712                        // c:1541 — suppress errexit for the command "! ..."
8713                        if isnot {
8714                            this_noerrexit.store(1, Ordering::Relaxed);
8715                        }
8716                        break 'sublist_done;
8717                    }
8718                    x if x == WC_SUBLIST_AND => {
8719                        // c:1545 — if the return code is non-zero, skip pipelines
8720                        // until we find a sublist followed by ORNEXT.
8721                        let r = if simple {
8722                            execsimple(state)
8723                        } else {
8724                            execpline(state, code, Z_SYNC, 0)
8725                        };
8726                        if r != 0 || BREAKS.load(Ordering::SeqCst) != 0 {
8727                            state.pc = next;
8728                            code = state.prog.prog[state.pc];
8729                            state.pc += 1;
8730                            next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8731                            while wc_code(code) == WC_SUBLIST
8732                                && WC_SUBLIST_TYPE(code) == WC_SUBLIST_AND
8733                            {
8734                                state.pc = next;
8735                                code = state.prog.prog[state.pc];
8736                                state.pc += 1;
8737                                next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8738                            }
8739                            if wc_code(code) != WC_SUBLIST {
8740                                // c:1562-1567 — skipped to the end of the list
8741                                // without executing the final pipeline: no error
8742                                // handling for this sublist.
8743                                this_donetrap = 1;
8744                                break 'sublist_done;
8745                            } else if WC_SUBLIST_TYPE(code) == WC_SUBLIST_END {
8746                                this_donetrap = 1;
8747                                state.pc = next;
8748                                break 'sublist_done;
8749                            }
8750                        }
8751                        cmdpush(CS_CMDAND as u8); // c:1580
8752                    }
8753                    x if x == WC_SUBLIST_OR => {
8754                        // c:1582 — if the return code is zero, skip pipelines
8755                        // until we find a sublist followed by ANDNEXT.
8756                        let r = if simple {
8757                            execsimple(state)
8758                        } else {
8759                            execpline(state, code, Z_SYNC, 0)
8760                        };
8761                        if r == 0 || BREAKS.load(Ordering::SeqCst) != 0 {
8762                            state.pc = next;
8763                            code = state.prog.prog[state.pc];
8764                            state.pc += 1;
8765                            next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8766                            while wc_code(code) == WC_SUBLIST
8767                                && WC_SUBLIST_TYPE(code) == WC_SUBLIST_OR
8768                            {
8769                                state.pc = next;
8770                                code = state.prog.prog[state.pc];
8771                                state.pc += 1;
8772                                next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8773                            }
8774                            if wc_code(code) != WC_SUBLIST {
8775                                this_donetrap = 1;
8776                                break 'sublist_done;
8777                            } else if WC_SUBLIST_TYPE(code) == WC_SUBLIST_END {
8778                                this_donetrap = 1;
8779                                state.pc = next;
8780                                break 'sublist_done;
8781                            }
8782                        }
8783                        cmdpush(CS_CMDOR as u8); // c:1617
8784                    }
8785                    _ => {}
8786                }
8787                state.pc = next; // c:1621
8788                code = state.prog.prog[state.pc];
8789                state.pc += 1;
8790                noerrexit.store(oldnoerrexit, Ordering::Relaxed); // c:1623
8791            }
8792            state.pc -= 1; // c:1625
8793        }
8794        // sublist_done:
8795        noerrexit.store(oldnoerrexit, Ordering::Relaxed); // c:1628
8796
8797        let debug_trapped = sigtrapped
8798            .lock()
8799            .unwrap()
8800            .get(SIGDEBUG as usize)
8801            .copied()
8802            .unwrap_or(0)
8803            != 0;
8804        if debug_trapped && !isset(DEBUGBEFORECMD) && donedebug == 0 {
8805            // c:1630-1648 — save and restore ERREXIT for consistency with
8806            // DEBUGBEFORECMD, even though it's not used.
8807            let oerrexit_opt = isset(ERREXIT);
8808            opt_state_set("errexit", false);
8809            noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed);
8810            exiting = DONETRAP.load(Ordering::Relaxed);
8811            let ret = LASTVAL.load(Ordering::Relaxed);
8812            crate::ported::signals::dotrap(SIGDEBUG);
8813            if RETFLAG.load(Ordering::SeqCst) == 0 {
8814                LASTVAL.store(ret, Ordering::Relaxed);
8815            }
8816            DONETRAP.store(exiting, Ordering::Relaxed);
8817            noerrexit.store(oldnoerrexit, Ordering::Relaxed);
8818            opt_state_set("errexit", oerrexit_opt);
8819        }
8820
8821        // c:1650 — `cmdsp = csp;`
8822        crate::ported::prompt::CMDSTACK.with(|s| {
8823            let mut g = s.borrow_mut();
8824            if g.len() > csp {
8825                g.truncate(csp);
8826            }
8827        });
8828
8829        // c:1652-1690 — check whether we are suppressing traps/errexit
8830        // and if we haven't already performed them for this sublist.
8831        if this_noerrexit.load(Ordering::Relaxed) == 0
8832            && DONETRAP.load(Ordering::Relaxed) == 0
8833            && this_donetrap == 0
8834        {
8835            let zerr_trapped = sigtrapped
8836                .lock()
8837                .unwrap()
8838                .get(SIGZERR as usize)
8839                .copied()
8840                .unwrap_or(0)
8841                != 0;
8842            if zerr_trapped
8843                && LASTVAL.load(Ordering::Relaxed) != 0
8844                && (noerrexit.load(Ordering::Relaxed) & NOERREXIT_EXIT) == 0
8845            {
8846                let eflag = errflag.load(Ordering::Relaxed);
8847                errflag.store(0, Ordering::Relaxed);
8848                crate::ported::signals::dotrap(SIGZERR);
8849                errflag.store(eflag, Ordering::Relaxed);
8850                DONETRAP.store(1, Ordering::Relaxed);
8851            }
8852            if LASTVAL.load(Ordering::Relaxed) != 0 {
8853                let ne = noerrexit.load(Ordering::Relaxed);
8854                let errreturn = isset(ERRRETURN)
8855                    && (isset(INTERACTIVE)
8856                        || (locallevel.load(Ordering::Relaxed) as i32) != 0
8857                        || crate::ported::init::sourcelevel.load(Ordering::Relaxed) != 0)
8858                    && (ne & NOERREXIT_RETURN) == 0;
8859                let errexit = (isset(ERREXIT) || (isset(ERRRETURN) && !errreturn))
8860                    && (ne & NOERREXIT_EXIT) == 0;
8861                if errexit {
8862                    errflag.store(0, Ordering::Relaxed);
8863                    let exit_trapped = sigtrapped
8864                        .lock()
8865                        .unwrap()
8866                        .get(SIGEXIT as usize)
8867                        .copied()
8868                        .unwrap_or(0)
8869                        != 0;
8870                    if exit_trapped {
8871                        crate::ported::signals::dotrap(SIGEXIT);
8872                    }
8873                    if crate::ported::params::mypid.load(Ordering::Relaxed)
8874                        != unsafe { libc::getpid() } as i64
8875                    {
8876                        crate::ported::builtin::_realexit();
8877                    } else {
8878                        crate::ported::builtin::realexit();
8879                    }
8880                }
8881                if errreturn {
8882                    RETFLAG.store(1, Ordering::SeqCst);
8883                    BREAKS.store(LOOPS.load(Ordering::SeqCst), Ordering::SeqCst);
8884                }
8885            }
8886        }
8887        if (ltype & Z_END as i32) != 0 {
8888            break; // c:1692
8889        }
8890        code = state.prog.prog[state.pc]; // c:1694
8891        state.pc += 1;
8892    }
8893    this_noerrexit.store(0, Ordering::Relaxed); // c:1696
8894    pline_level.store(old_pline_level, Ordering::Relaxed);
8895    list_pipe.store(old_list_pipe, Ordering::Relaxed);
8896    list_pipe_job.store(old_list_pipe_job, Ordering::Relaxed);
8897    match old_list_pipe_text {
8898        Some(t) => *LIST_PIPE_TEXT.lock().unwrap() = t, // c:1701
8899        None => LIST_PIPE_TEXT.lock().unwrap().clear(), // c:1704
8900    }
8901    crate::ported::input::lineno.with(|l| l.set(oldlineno)); // c:1706
8902    if dont_change_job != 0 {
8903        *THISJOB
8904            .get_or_init(|| std::sync::Mutex::new(-1))
8905            .lock()
8906            .unwrap() = cj; // c:1708
8907    }
8908
8909    let exit_trapped = sigtrapped
8910        .lock()
8911        .unwrap()
8912        .get(SIGEXIT as usize)
8913        .copied()
8914        .unwrap_or(0)
8915        != 0;
8916    if exiting != 0 && exit_trapped {
8917        // c:1710-1716
8918        let eflag = errflag.load(Ordering::Relaxed);
8919        errflag.store(0, Ordering::Relaxed); // clear the context for trap
8920        crate::ported::signals::dotrap(SIGEXIT);
8921        // make sure this doesn't get executed again.
8922        if let Some(slot) = sigtrapped.lock().unwrap().get_mut(SIGEXIT as usize) {
8923            *slot = 0;
8924        }
8925        errflag.store(eflag, Ordering::Relaxed);
8926    }
8927
8928    unqueue_signals(); // c:1719
8929}
8930
8931// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
8932// 1625`, the C source likewise inlines it — there's no `execsublist`
8933// function in zsh C).
8934
8935/// Port of `execcmd_getargs()` from `Src/exec.c:2791` — C decl `static void execcmd_getargs(LinkList preargs, LinkList args, int expand)`.
8936/// Transfer the first node of `args`
8937/// to `preargs`, performing `prefork` (singleton-list expansion) on
8938/// the way if `expand` is set. Used by `execcmd_exec` to pull the
8939/// command head one word at a time so prefix-modifier walking
8940/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
8941pub fn execcmd_getargs(preargs: &mut LinkList<String>, args: &mut LinkList<String>, expand: i32) {
8942    // c:2791
8943    if args.firstnode().is_none() {
8944        // c:2793 — `if (!firstnode(args)) return;`
8945        return;
8946    } else if expand != 0 {
8947        // c:2795
8948        // c:2796-2797 — `local_list0(svl); init_list0(svl);` —
8949        // stack-local single-bucket list. Rust uses a fresh
8950        // LinkList<String> per call.
8951        let mut svl: LinkList<String> = Default::default();
8952        // c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
8953        if let Some(idx) = args.firstnode() {
8954            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
8955                svl.push_back(head);
8956            }
8957        }
8958        // c:2801 — `prefork(&svl, 0, NULL);`
8959        let mut rf = 0i32;
8960        prefork(&mut svl, 0, &mut rf);
8961        // c:2802 — `joinlists(preargs, &svl);`
8962        crate::ported::linklist::joinlists(preargs, &mut svl);
8963    } else {
8964        // c:2803-2804 — no-expand path: move head verbatim.
8965        if let Some(idx) = args.firstnode() {
8966            if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
8967                preargs.push_back(head);
8968            }
8969        }
8970    }
8971}
8972
8973/// Port of `execcmd_fork()` from `Src/exec.c:2810` — C decl `execcmd_fork(Estate state, int how, int type, Wordcode varspc, LinkList *filelistp, char *text, int oautocont, int close_if_forked)`.
8974/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
8975/// int close_if_forked)` from `Src/exec.c:2810-2893`.
8976///
8977/// Fork the current command into a child process: parent records
8978/// the pid + STTY env scan + addproc; child enters subshell, writes
8979/// `entersubsh_ret` back to parent through `synch` pipe, and returns
8980/// 0 so the caller can continue with the body.
8981///
8982/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
8983/// only in the child branch (so the parent's `filelist` stays
8984/// untouched). Rust sig keeps the same C contract.
8985pub fn execcmd_fork(
8986    state: &mut estate,
8987    how: i32,
8988    typ: i32,
8989    varspc: Option<usize>,
8990    filelistp: &mut Vec<jobfile>,
8991    text: &str,
8992    oautocont: i32,
8993    close_if_forked: i32,
8994) -> i32 {
8995    use crate::ported::signals::sigtrapped as sigtrapped_static;
8996    use crate::ported::signals_h::SIGEXIT;
8997    use crate::ported::zsh_h::{
8998        AUTOCONTINUE, BGNICE, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
8999        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
9000        WC_SUBSH as ZWC_SUBSH, ZSIG_IGNORED, Z_ASYNC,
9001    };
9002    // c:2810
9003    let pid: libc::pid_t; // c:2814
9004    let mut synch: [i32; 2] = [-1, -1]; // c:2815
9005    let flags: i32; // c:2815
9006    let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
9007                                                               // c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
9008                                                               // for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
9009    let mut bgtime = ZshTimespec::default();
9010
9011    child_block(); // c:2819
9012    esret.gleader = -1; // c:2820
9013    esret.list_pipe_job = -1; // c:2821
9014
9015    // c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
9016    if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
9017        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
9018        return -1; // c:2825
9019    }
9020    // c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
9021    pid = zfork(Some(&mut bgtime));
9022    if pid == -1 {
9023        unsafe {
9024            libc::close(synch[0]); // c:2827
9025            libc::close(synch[1]); // c:2828
9026        }
9027        LASTVAL.store(1, Ordering::Relaxed); // c:2829
9028        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
9029        return -1; // c:2831
9030    }
9031    if pid != 0 {
9032        // c:2833 — parent.
9033        unsafe { libc::close(synch[1]) }; // c:2834
9034                                          // c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
9035        let mut buf = [0u8; size_of::<entersubsh_ret>()];
9036        let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
9037        // entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
9038        if buf.len() >= 8 {
9039            esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
9040            esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
9041        }
9042        unsafe { libc::close(synch[0]) }; // c:2836
9043        if (how & Z_ASYNC as i32) != 0 {
9044            // c:2837 — `lastpid = (zlong) pid;`
9045            crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
9046        } else {
9047            // c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
9048            let thisjob_idx = {
9049                if let Some(m) = THISJOB.get() {
9050                    *m.lock().unwrap()
9051                } else {
9052                    -1
9053                }
9054            };
9055            // Examine the jobtab entry under lock.
9056            let stty_already = if thisjob_idx >= 0 {
9057                if let Some(jt) = JOBTAB.get() {
9058                    let guard = jt.lock().unwrap();
9059                    guard
9060                        .get(thisjob_idx as usize)
9061                        .map(|j| j.stty_in_env != 0)
9062                        .unwrap_or(true)
9063                } else {
9064                    true
9065                }
9066            } else {
9067                true
9068            };
9069            if !stty_already && varspc.is_some() {
9070                // c:2841-2851 — walk varspc looking for STTY=...
9071                let mut p = varspc.unwrap();
9072                loop {
9073                    if p >= state.prog.prog.len() {
9074                        break;
9075                    }
9076                    let ac = state.prog.prog[p];
9077                    if wc_code(ac) != ZWC_ASSIGN {
9078                        break;
9079                    }
9080                    // c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
9081                    let name = ecrawstr(&state.prog, p + 1, None);
9082                    if name == "STTY" {
9083                        // c:2846 — `jobtab[thisjob].stty_in_env = 1;`
9084                        if let Some(jt) = JOBTAB.get() {
9085                            let mut guard = jt.lock().unwrap();
9086                            if let Some(j) = guard.get_mut(thisjob_idx as usize) {
9087                                j.stty_in_env = 1;
9088                            }
9089                        }
9090                        break; // c:2847
9091                    }
9092                    p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
9093                        3 // c:2849
9094                    } else {
9095                        (ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
9096                    };
9097                }
9098            }
9099        }
9100        // c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
9101        if let Some(jt) = JOBTAB.get() {
9102            let mut guard = jt.lock().unwrap();
9103            let tj = {
9104                if let Some(m) = THISJOB.get() {
9105                    *m.lock().unwrap()
9106                } else {
9107                    -1
9108                }
9109            };
9110            if tj >= 0 {
9111                if let Some(j) = guard.get_mut(tj as usize) {
9112                    crate::ported::jobs::addproc(
9113                        j,
9114                        pid,
9115                        text,
9116                        false,
9117                        Some(std::time::Instant::now()),
9118                        esret.gleader,
9119                        esret.list_pipe_job,
9120                    );
9121                }
9122            }
9123        }
9124        // c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
9125        if oautocont >= 0 {
9126            opt_state_set("autocontinue", oautocont != 0);
9127            let _ = AUTOCONTINUE; // const referenced for parity
9128        }
9129        // c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
9130        if let Some(jt) = JOBTAB.get() {
9131            let mut guard = jt.lock().unwrap();
9132            let tj = {
9133                if let Some(m) = THISJOB.get() {
9134                    *m.lock().unwrap()
9135                } else {
9136                    -1
9137                }
9138            };
9139            if tj >= 0 {
9140                if let Some(j) = guard.get_mut(tj as usize) {
9141                    crate::ported::jobs::pipecleanfilelist(j, true);
9142                }
9143            }
9144        }
9145        return pid; // c:2857
9146    }
9147
9148    // c:2860 — pid == 0 (child).
9149    unsafe { libc::close(synch[0]) }; // c:2861
9150    flags = (if (how & Z_ASYNC as i32) != 0 {
9151        esub::ASYNC
9152    } else {
9153        0
9154    }) | esub::PGRP; // c:2862
9155    let mut flags = flags;
9156    if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
9157        flags |= esub::KEEPTRAP; // c:2864
9158    }
9159    if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
9160        flags |= esub::JOB_CONTROL; // c:2866
9161    }
9162    // c:2867 — `*filelistp = jobtab[thisjob].filelist;`
9163    if let Some(jt) = JOBTAB.get() {
9164        let mut guard = jt.lock().unwrap();
9165        let tj = {
9166            if let Some(m) = THISJOB.get() {
9167                *m.lock().unwrap()
9168            } else {
9169                -1
9170            }
9171        };
9172        if tj >= 0 {
9173            if let Some(j) = guard.get_mut(tj as usize) {
9174                *filelistp = std::mem::take(&mut j.filelist);
9175            }
9176        }
9177    }
9178    entersubsh(flags, Some(&mut esret)); // c:2868
9179                                         // c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
9180    let mut buf = [0u8; 8];
9181    buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
9182    buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
9183    if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
9184        zerr(&format!(
9185            "Failed to send entersubsh_ret report: {}",
9186            std::io::Error::last_os_error()
9187        ));
9188        return -1; // c:2871
9189    }
9190    unsafe { libc::close(synch[1]) }; // c:2873
9191    let _ = zclose(close_if_forked); // c:2874
9192
9193    // c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
9194    let sigint_state = {
9195        let guard = sigtrapped_static.lock().unwrap();
9196        guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
9197    };
9198    if (sigint_state & ZSIG_IGNORED) != 0 {
9199        crate::ported::signals::holdintr(); // c:2877
9200    }
9201    // c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
9202    {
9203        let mut guard = sigtrapped_static.lock().unwrap();
9204        if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
9205            *slot = 0;
9206        }
9207    }
9208    // c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
9209    // Per-platform errno setter+reader: __error() on macOS,
9210    // __errno_location() on Linux. Without cfg gating Linux CI breaks.
9211    if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
9212        #[cfg(target_os = "macos")]
9213        unsafe {
9214            *libc::__error() = 0;
9215            if libc::nice(5) == -1 && *libc::__error() != 0 {
9216                zwarn(&format!(
9217                    "nice(5) failed: {}",
9218                    std::io::Error::last_os_error()
9219                ));
9220            }
9221        }
9222        #[cfg(target_os = "linux")]
9223        unsafe {
9224            *libc::__errno_location() = 0;
9225            if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
9226                zwarn(&format!(
9227                    "nice(5) failed: {}",
9228                    std::io::Error::last_os_error()
9229                ));
9230            }
9231        }
9232    }
9233    0 // c:2892
9234}
9235
9236/// Port of `execcmd_analyse()` from `Src/exec.c:2733` — C decl `execcmd_analyse(Estate state, Execcmd_params eparams)`.
9237/// Pre-execcmd_exec analysis pass:
9238/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
9239/// without expanding (no prefork, no globbing), and fills `eparams`
9240/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
9241/// can branch on the command type before the real work.
9242pub fn execcmd_analyse(state: &mut estate, eparams: &mut crate::ported::zsh_h::execcmd_params) {
9243    use crate::ported::zsh_h::{
9244        WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
9245        WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
9246        WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
9247    };
9248    // c:2733
9249    let mut code: wordcode; // c:2735
9250    let mut i: i32; // c:2736
9251    let _ = i;
9252
9253    // c:2738 — `eparams->beg = state->pc;`
9254    eparams.beg = state.pc;
9255    // c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
9256    eparams.redir =
9257        if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_REDIR {
9258            Some(crate::ported::parse::ecgetredirs(state))
9259        } else {
9260            None
9261        };
9262    // c:2741-2748 — varspc walk (WC_ASSIGN chain).
9263    if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
9264        cmdoutval.store(0, Ordering::Relaxed); // c:2742
9265        eparams.varspc = Some(state.pc); // c:2743
9266                                         // c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
9267        loop {
9268            if state.pc >= state.prog.prog.len() {
9269                break;
9270            }
9271            code = state.prog.prog[state.pc];
9272            if wc_code(code) != ZWC_ASSIGN {
9273                break;
9274            }
9275            state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
9276                3 // c:2745
9277            } else {
9278                (WC_ASSIGN_NUM(code) + 2) as usize // c:2746
9279            };
9280        }
9281    } else {
9282        eparams.varspc = None; // c:2748
9283    }
9284
9285    // c:2750 — `code = *state->pc++;`
9286    if state.pc >= state.prog.prog.len() {
9287        eparams.args = None;
9288        eparams.assignspc = None;
9289        eparams.typ = 0;
9290        eparams.postassigns = 0;
9291        eparams.htok = 0;
9292        return;
9293    }
9294    code = state.prog.prog[state.pc];
9295    state.pc += 1;
9296
9297    // c:2752 — `eparams->type = wc_code(code);`
9298    eparams.typ = wc_code(code) as i32;
9299    // c:2753 — `eparams->postassigns = 0;`
9300    eparams.postassigns = 0;
9301
9302    // c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
9303    // per the comment at c:2755-2757.
9304    match eparams.typ as wordcode {
9305        x if x == ZWC_SIMPLE => {
9306            // c:2759-2763
9307            let mut htok = 0;
9308            let argc = ZWC_SIMPLE_ARGC(code) as usize;
9309            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
9310            eparams.htok = htok;
9311            eparams.assignspc = None;
9312        }
9313        x if x == ZWC_TYPESET => {
9314            // c:2765-2777
9315            let mut htok = 0;
9316            let argc = ZWC_TYPESET_ARGC(code) as usize;
9317            eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
9318            eparams.htok = htok;
9319            // c:2768 — `eparams->postassigns = *state->pc++;`
9320            if state.pc < state.prog.prog.len() {
9321                eparams.postassigns = state.prog.prog[state.pc] as i32;
9322                state.pc += 1;
9323            }
9324            // c:2769 — `eparams->assignspc = state->pc;`
9325            eparams.assignspc = Some(state.pc);
9326            // c:2770-2776 — walk past the postassigns.
9327            let mut k = 0i32;
9328            while k < eparams.postassigns {
9329                if state.pc >= state.prog.prog.len() {
9330                    break;
9331                }
9332                code = state.prog.prog[state.pc];
9333                // c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
9334                state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
9335                    3 // c:2774
9336                } else {
9337                    (WC_ASSIGN_NUM(code) + 2) as usize // c:2775
9338                };
9339                k += 1;
9340            }
9341        }
9342        _ => {
9343            // c:2779-2783 default.
9344            eparams.args = None;
9345            eparams.assignspc = None;
9346            eparams.htok = 0;
9347        }
9348    }
9349}
9350
9351/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
9352/// Stack of `"context"` labels used by `eval`-style nested execution:
9353/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
9354/// ..., "context")` pushes its label and pops on return.
9355///
9356/// Storage is [`crate::thread_shell_state::ThreadMutex`], not a plain
9357/// `Mutex`: like `funcstack`, the stack describes where the ONE thread of
9358/// execution is, so a hook function running on a pool worker must not push
9359/// `"shfunc"` onto the shell thread's `$ZSH_EVAL_CONTEXT`.
9360pub static zsh_eval_context: crate::thread_shell_state::ThreadMutex<Vec<String>> =
9361    crate::thread_shell_state::ThreadMutex::new(Vec::new(), Vec::new);
9362
9363/// RAII form of C's `execode` context push/pop (`Src/exec.c:1265-1266`
9364/// and the matching `zsh_eval_context[alen] = NULL` at c:1281).
9365///
9366/// zshrs does not route every nested execution through `execode`, so
9367/// the callers that C reaches via `execode(prog, …, "label")` push
9368/// their label with this guard instead. Dropping it pops, on every
9369/// return path including a panic — C relies on the plain assignment at
9370/// c:1281 being reached, which the guard reproduces more robustly.
9371pub struct EvalContextFrame {
9372    pushed: bool,
9373}
9374
9375impl EvalContextFrame {
9376    /// c:1265-1266 — `zsh_eval_context[alen] = context;`
9377    pub fn push(context: &str) -> Self {
9378        let mut pushed = false;
9379        if let Ok(mut ctx) = zsh_eval_context.lock() {
9380            ctx.push(context.to_string());
9381            Self::sync(&ctx);
9382            pushed = true;
9383        }
9384        Self { pushed }
9385    }
9386
9387    /// Publish the `zsh_eval_context` stack into the shell-visible
9388    /// params. C keeps ONE `char **zsh_eval_context` that the parameter
9389    /// table points at directly (`IPDEF8`/`IPDEF9`, params.c:401/431),
9390    /// so a push is instantly visible as `$zsh_eval_context` /
9391    /// `$ZSH_EVAL_CONTEXT`. The Rust params own their storage, so every
9392    /// mutation of the static has to be mirrored — through the
9393    /// `u_arr`/`u_str` fields directly because both names are
9394    /// `PM_READONLY_SPECIAL`.
9395    pub(crate) fn sync(stack: &[String]) {
9396        // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
9397        // C has one `char **zsh_eval_context` and one thread, so the
9398        // publish below cannot be reached from anywhere but the frame that
9399        // pushed. zshrs's param table is ONE shared table that
9400        // `async_precmd` hooks deliberately write into, so a worker's push
9401        // would overwrite the shell thread's `$ZSH_EVAL_CONTEXT` with the
9402        // hook's context. The worker still tracks its own stack (the
9403        // `ThreadMutex` above); only the shell-visible parameters stay the
9404        // shell thread's.
9405        if !crate::thread_shell_state::is_shell_thread() {
9406            return;
9407        }
9408        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
9409            // C writes through the one global `zsh_eval_context` the SPECIAL
9410            // points at. `typeset -h` / `local -h` over a special leaves a
9411            // plain local shadowing it in the table (c:Src/builtin.c:2062-2073,
9412            // `newspecial` stays NS_NONE); the shadow must not receive the
9413            // publish, so walk the `old` chain down to the node that still
9414            // carries PM_SPECIAL.
9415            fn special_of<'a>(
9416                mut pm: Option<&'a mut crate::ported::zsh_h::param>,
9417            ) -> Option<&'a mut crate::ported::zsh_h::param> {
9418                while let Some(p) = pm {
9419                    if p.node.flags as u32 & crate::ported::zsh_h::PM_SPECIAL != 0 {
9420                        return Some(p);
9421                    }
9422                    pm = p.old.as_deref_mut();
9423                }
9424                None
9425            }
9426            if let Some(pm) = special_of(tab.get_mut("zsh_eval_context").map(|p| &mut **p)) {
9427                // Rebuilt in place: every `$(( ))` pushes and pops a frame, so
9428                // reusing the previous publish's buffers saves the allocations.
9429                let arr = pm.u_arr.get_or_insert_with(Vec::new);
9430                arr.clear();
9431                arr.extend_from_slice(stack);
9432                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
9433            }
9434            if let Some(pm) = special_of(tab.get_mut("ZSH_EVAL_CONTEXT").map(|p| &mut **p)) {
9435                let joined = pm.u_str.get_or_insert_with(String::new);
9436                joined.clear();
9437                for (i, frame) in stack.iter().enumerate() {
9438                    if i > 0 {
9439                        joined.push(':');
9440                    }
9441                    joined.push_str(frame);
9442                }
9443                pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
9444            }
9445        }
9446    }
9447}
9448
9449impl Drop for EvalContextFrame {
9450    fn drop(&mut self) {
9451        // c:1281 — `zsh_eval_context[alen] = NULL;`
9452        if self.pushed {
9453            if let Ok(mut ctx) = zsh_eval_context.lock() {
9454                ctx.pop();
9455                Self::sync(&ctx);
9456            }
9457        }
9458    }
9459}
9460
9461/// Port of `static int donetrap;` from `Src/exec.c:1351`. Tracks
9462/// whether the ZERR trap has already fired for the current sublist.
9463/// C source resets to 0 at sublist start (c:1455) and sets to 1
9464/// after `dotrap(SIGZERR)` (c:1602). The check
9465/// `if (!this_noerrexit && !donetrap && !this_donetrap)` at c:1598
9466/// suppresses re-firing within the same sublist AND, crucially,
9467/// carries the "already fired" state across a function-call return
9468/// boundary so the outer caller's post-command check doesn't fire
9469/// ZERR a second time for the same logical error. Bug #303 in
9470/// docs/BUGS.md.
9471///
9472/// Reset at each top-level statement boundary via
9473/// `BUILTIN_DONETRAP_RESET` emitted by `compile_list`. Set after
9474/// `dotrap(SIGZERR)` fires inside `BUILTIN_ERREXIT_CHECK`.
9475pub static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9476
9477/// Port of `save_params()` from `Src/exec.c:4410` — C decl `save_params(Estate state, Wordcode pc, LinkList *restore_p, LinkList *remove_p)`.
9478/// Walk WC_ASSIGN
9479/// chain at `pc`, snapshot each existing param into `restore_p` (so
9480/// the builtin/shfunc can restore them on return) and enqueue every
9481/// touched name in `remove_p` (so we know what to unset).
9482pub fn save_params(
9483    state: &mut estate,
9484    pc: usize,
9485    restore_p: &mut Vec<crate::ported::zsh_h::param>,
9486    remove_p: &mut Vec<String>,
9487) {
9488    use crate::ported::zsh_h::{
9489        PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
9490        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
9491    };
9492    // c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
9493    // c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
9494    let mut p = pc;
9495    // c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
9496    loop {
9497        if p >= state.prog.prog.len() {
9498            break;
9499        }
9500        let ac = state.prog.prog[p];
9501        if wc_code(ac) != WC_ASSIGN {
9502            break;
9503        }
9504        // c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
9505        let s = ecrawstr(&state.prog, p + 1, None);
9506        // c:4421 — `pm = paramtab->getnode(paramtab, s)`
9507        let pm_clone: Option<crate::ported::zsh_h::param> = {
9508            let tab = paramtab().read().unwrap();
9509            tab.get(&s).map(|b| (**b).clone())
9510        };
9511        if let Some(pm) = pm_clone {
9512            // c:4423-4424 — `if (pm->env) delenv(pm);`
9513            if pm.env.is_some() {
9514                crate::ported::params::delenv(&s);
9515            }
9516            // c:4425-4448 — copy if not readonly-special.
9517            if (pm.node.flags & PM_SPECIAL as i32) == 0 {
9518                // c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
9519                let mut tpm = pm.clone();
9520                tpm.node.nam = s.clone();
9521                // copyparam with fakecopy=0 already done by the clone()
9522                // (Clone derives a deep copy of param fields).
9523                restore_p.push(tpm); // c:4451
9524            } else if (pm.node.flags & PM_READONLY as i32) == 0
9525            {
9526                // c:4439-4448 — special-but-not-readonly: fakecopy=1.
9527                let mut tpm = pm.clone();
9528                tpm.node.nam = pm.node.nam.clone();
9529                restore_p.push(tpm); // c:4451
9530            }
9531            // c:4449 — `addlinknode(*remove_p, dupstring(s));`
9532            remove_p.push(s.clone());
9533        } else {
9534            // c:4453 — `addlinknode(*remove_p, dupstring(s));`
9535            remove_p.push(s.clone());
9536        }
9537        // c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
9538        p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
9539            3
9540        } else {
9541            (ZWC_ASSIGN_NUM(ac) + 2) as usize
9542        };
9543    }
9544}
9545
9546/// Port of `restore_params()` from `Src/exec.c:4464` — C decl `restore_params(LinkList restorelist, LinkList removelist)`.
9547/// After the builtin/shfunc returns,
9548/// unset every name in removelist, then for each saved param in
9549/// restorelist re-install its values (PM_SPECIAL go through gsu
9550/// setfn; regular params re-enter paramtab as-is).
9551pub fn restore_params(restorelist: Vec<crate::ported::zsh_h::param>, removelist: Vec<String>) {
9552    use crate::ported::zsh_h::{PM_READONLY, PM_SPECIAL};
9553    // c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
9554    for s in &removelist {
9555        // c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
9556        let flags = {
9557            let tab = paramtab().read().unwrap();
9558            tab.get(s).map(|p| p.node.flags)
9559        };
9560        if let Some(f) = flags {
9561            if (f & PM_SPECIAL as i32) == 0 {
9562                // !!! LOCK DISCIPLINE — NO GSU CALLBACK MAY RUN UNDER THE GUARD !!!
9563                // Same rule, and the same reasoning, as the long note in
9564                // `assignnparam` (params.rs). C holds no lock at all: c:4474
9565                // hands `unsetparam_pm` a pointer to the live node, so the
9566                // callee may reach anything, and this one does — its
9567                // `if (pm->env) delenv(pm)` arm (c:Src/params.c:3872) lands in
9568                // a `delenv` that re-takes `paramtab().write()`, and its
9569                // read-only rejection (c:Src/params.c:3852) calls `zerr`,
9570                // which is not a leaf either (zwarning →
9571                // zleentry(ZLE_CMD_TRASH) → zrefresh →
9572                // getaparam("zle_highlight") → this same lock). `paramtab()`
9573                // is a `std::sync::RwLock` and is NOT reentrant.
9574                //
9575                // Neither arm can fire from HERE today, and neither is
9576                // disarmed by the callee being a leaf — both are disarmed by
9577                // this caller's preconditions, one line apart:
9578                //   * the `zerr` arm, by c:4473 clearing PM_READONLY
9579                //     immediately below;
9580                //   * the `delenv` arm, by `save_params` having already run
9581                //     `if (pm->env) delenv(pm)` (c:4423-4424) over this very
9582                //     name on the way in, which left `pm.env` None.
9583                // That is a guarantee held by a different function, for its
9584                // own reasons. `bin_ztie` had the identical shape with no such
9585                // guarantee and hung outright. The previous spelling here even
9586                // said `// Drop write guard before calling unsetparam_pm.` and
9587                // then re-acquired the guard on the next line, so the dispatch
9588                // ran under it anyway.
9589                //
9590                // Detach under the guard, release, dispatch, publish — C's
9591                // order. `unsetparam_pm` mutates only the node it is handed
9592                // and never unlinks it (its own comment records that the
9593                // c:3853-3935 removenode postlude is unported), so
9594                // republishing that node is the whole of the write-back.
9595                let staged = {
9596                    // c:4473 — `pm->node.flags &= ~PM_READONLY;`
9597                    let mut tab = paramtab().write().unwrap();
9598                    tab.get_mut(s).map(|pm_mut| {
9599                        pm_mut.node.flags &= !(PM_READONLY as i32);
9600                        (**pm_mut).clone()
9601                    })
9602                };
9603                // ---- guard released; none is held across the dispatch ----
9604                if let Some(mut pm) = staged {
9605                    let _ = crate::ported::params::unsetparam_pm(&mut pm, 0, 0); // c:4474
9606                    let mut tab = paramtab().write().unwrap();
9607                    // Only if the node is still there — re-adding one the
9608                    // dispatch removed would resurrect a parameter C dropped.
9609                    if let Some(slot) = tab.get_mut(s) {
9610                        **slot = pm;
9611                    }
9612                }
9613            }
9614        }
9615    }
9616    // c:4478-4523 — restore saved params.
9617    for pm in restorelist {
9618        // c:4481-4520 — PM_SPECIAL: route through gsu setfn.
9619        // c:4521-4523 — non-special: re-install via paramtab.
9620        if (pm.node.flags & PM_SPECIAL as i32) != 0 {
9621            // c:4537 — `tpm = paramtab->getnode(paramtab, pm->node.nam);`
9622            // c:4542-4543 — `if (!pm->env && tpm->env) delenv(tpm);`
9623            if pm.env.is_none() {
9624                let live_env = {
9625                    let tab = paramtab().read().unwrap();
9626                    tab.get(&pm.node.nam).and_then(|p| p.env.clone())
9627                };
9628                if live_env.is_some() {
9629                    crate::ported::params::delenv(&pm.node.nam);
9630                }
9631            }
9632            // c:4544 — `tpm->node.flags = pm->node.flags;`
9633            {
9634                let mut tab = paramtab().write().unwrap();
9635                if let Some(slot) = tab.get_mut(&pm.node.nam) {
9636                    slot.node.flags = pm.node.flags;
9637                }
9638            }
9639            // c:4545-4563 — the value goes back through the LIVE node's own
9640            // setfn, so the special's write hook fires: `PATH=/tmp cmd` has
9641            // to leave `pathsetfn` re-deriving `$path` and the command hash
9642            // from the restored string. The previous spelling overwrote the
9643            // paramtab entry instead, which restored `$PATH` but left every
9644            // global the setfn derives sitting at the prefix assignment's
9645            // value.
9646            match crate::ported::zsh_h::PM_TYPE(pm.node.flags as u32) {
9647                crate::ported::zsh_h::PM_INTEGER => {
9648                    // c:4551 — `tpm->gsu.i->setfn(tpm, pm->u.val);`
9649                    crate::ported::params::setiparam(&pm.node.nam, pm.u_val);
9650                }
9651                crate::ported::zsh_h::PM_EFLOAT | crate::ported::zsh_h::PM_FFLOAT => {
9652                    // c:4555 — `tpm->gsu.f->setfn(tpm, pm->u.dval);`
9653                    crate::ported::params::setnparam(
9654                        &pm.node.nam,
9655                        crate::ported::zsh_h::mnumber {
9656                            l: 0,
9657                            d: pm.u_dval,
9658                            type_: crate::ported::zsh_h::MN_FLOAT,
9659                        },
9660                    );
9661                }
9662                crate::ported::zsh_h::PM_ARRAY => {
9663                    // c:4558 — `tpm->gsu.a->setfn(tpm, pm->u.arr);`
9664                    crate::ported::params::setaparam(
9665                        &pm.node.nam,
9666                        pm.u_arr.clone().unwrap_or_default(),
9667                    );
9668                }
9669                crate::ported::zsh_h::PM_HASHED => {
9670                    // c:4561 — `tpm->gsu.h->setfn(tpm, pm->u.hash);`. zshrs
9671                    // keeps an association's pairs OUTSIDE the Param, in
9672                    // `paramtab_hashed_storage` (see `stdunsetfn`'s note at
9673                    // params.rs:11044), so they are not in `pm` to hand over
9674                    // here; the caller that saved them restores them.
9675                }
9676                _ => {
9677                    // c:4548 — `tpm->gsu.s->setfn(tpm, pm->u.str);`
9678                    crate::ported::params::setsparam(
9679                        &pm.node.nam,
9680                        pm.u_str.as_deref().unwrap_or(""),
9681                    );
9682                }
9683            }
9684        } else {
9685            // c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
9686            let mut tab = paramtab().write().unwrap();
9687            tab.insert(pm.node.nam.clone(), Box::new(pm));
9688        }
9689    }
9690}
9691
9692/// Port of `execode` from `Src/exec.c:1245` — C decl `execode(Eprog p, int dont_change_job, int exiting, char *context)`. Rust fn `execode_wordcode` is the wordcode form, driving the ported `execlist` interpreter; `execode` at exec.rs:8413 is the fusevm-pipeline entry that carries the C name.
9693/// Set up an `estate`
9694/// around the given Eprog and run `execlist`. Maintains the
9695/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
9696/// call chain.
9697///
9698/// NOTE: this is the WORDCODE form (drives the ported `execlist`
9699/// interpreter). zshrs's live execution pipeline is fusevm
9700/// (`compile_zsh` → VM), so the top-level REPL `loop()` and most call
9701/// sites run through [`execode`] (the ZshProgram/fusevm form below)
9702/// instead. This wordcode entry is retained for the internal
9703/// function-body callers (`doshfunc` / autoload) that already hold an
9704/// `Eprog`.
9705pub fn execode_wordcode(
9706    p: crate::ported::zsh_h::Eprog,
9707    dont_change_job: i32,
9708    exiting: i32,
9709    context: &str,
9710) {
9711    // c:1245
9712    let prog_ref = *p;
9713    // c:1247 — `struct estate s;`
9714    let mut s = estate {
9715        prog: Box::new(prog_ref.clone()),
9716        // c:1269 — `s.pc = p->prog;` — start at index 0.
9717        pc: 0,
9718        // c:1270 — `s.strs = p->strs;`
9719        strs: prog_ref.strs.clone(),
9720        strs_offset: 0,
9721    };
9722    // c:1251-1266 — push context onto zsh_eval_context.
9723    let pushed = {
9724        if let Ok(mut ctx) = zsh_eval_context.lock() {
9725            ctx.push(context.to_string());
9726            true
9727        } else {
9728            false
9729        }
9730    };
9731    // c:1271 — `useeprog(p);`
9732    crate::ported::parse::useeprog(&mut s.prog);
9733    // c:1273 — `execlist(&s, dont_change_job, exiting);`
9734    execlist(&mut s, dont_change_job, exiting);
9735    // c:1275 — `freeeprog(p);`
9736    crate::ported::parse::freeeprog(&mut s.prog);
9737    // c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
9738    if pushed {
9739        if let Ok(mut ctx) = zsh_eval_context.lock() {
9740            ctx.pop();
9741        }
9742    }
9743}
9744
9745thread_local! {
9746    /// !!! WARNING: RUST-ONLY CARRIER !!! The VM opens a simple command's
9747    /// redirections itself and hands `execcmd_exec` an empty `redir` list
9748    /// for a run-time-expanded head (fusevm_bridge BUILTIN_EXEC_DYNAMIC).
9749    /// Set for that call when the command HAD redirections, so `trycd`
9750    /// still sees c:3596 `(!redir || empty(redir))` as false.
9751    pub(crate) static REDIRS_PREAPPLIED: std::cell::Cell<bool> =
9752        const { std::cell::Cell::new(false) };
9753}
9754
9755thread_local! {
9756    /// The long-lived interactive executor for the top-level `loop()`
9757    /// REPL (the `zsh_main` path). Set once by the bin before
9758    /// `zsh_main`; [`execode`] runs each parsed program through it.
9759    /// Persists for the whole session so variables/functions survive
9760    /// across prompts.
9761    static SESSION_EXECUTOR: std::cell::Cell<Option<*mut crate::vm_helper::ShellExecutor>> =
9762        const { std::cell::Cell::new(None) };
9763}
9764
9765/// !!! WARNING: RUST-ONLY HELPER !!!
9766///
9767/// No function of this name exists in `Src/exec.c`. This helper stands in for
9768/// installing the process-lifetime fusevm executor that `execode`
9769/// (exec.rs:8413) runs programs on. C has no analogue: its interpreter state
9770/// is the `estate` built per call in `execode` (c:1245).
9771/// Register the persistent session executor used by [`execode`] for the
9772/// interactive `loop()` REPL. The pointer must outlive the session (the
9773/// bin keeps the executor alive until `zsh_main` exits the process).
9774pub fn install_session_executor(exec: &mut crate::vm_helper::ShellExecutor) {
9775    SESSION_EXECUTOR.with(|c| c.set(Some(exec as *mut crate::vm_helper::ShellExecutor)));
9776    // Mirror the pointer into the bridge so `with_session_context` can
9777    // establish a VM context for startup rc-sourcing (run_init_scripts,
9778    // c:1914) before the loop's first execode enters one.
9779    crate::fusevm_bridge::register_session_executor(exec);
9780}
9781
9782/// Port of `execode()` from `Src/exec.c:1245` — C decl `execode(Eprog p, int dont_change_job, int exiting, char *context)`. DIVERGENCE: this entry drives the fusevm pipeline over a `ZshProgram`; the faithful wordcode walker is `execode_wordcode` (exec.rs:8338).
9783/// zshrs `execode` — run an already-parsed `ZshProgram` (Src/exec.c:1245
9784/// `execode(prog, ...)`, called from `loop()`). This is the **exec.rs
9785/// exception** to the line-by-line port: rather than walk wordcode via
9786/// `execlist`, it drives zshrs's live engine — compile the program with
9787/// `compile_zsh` and run it on the session executor's fusevm VM. The
9788/// faithful `loop()` in init.rs calls this exactly as C calls execode.
9789/// Returns `$?` (0 when no session executor is installed).
9790pub fn execode(
9791    program: &crate::parse::ZshProgram,
9792    _dont_change_job: i32,
9793    _exiting: i32,
9794    context: &str,
9795) -> i32 {
9796    // c:1319 — `zsh_eval_context_push(context)`. The wordcode twin
9797    // (`execode_wordcode`) already does this; this fusevm variant is the
9798    // one `loop()` (Src/init.c:220) calls with "toplevel", so dropping the
9799    // push left `$zsh_eval_context` / `$ZSH_EVAL_CONTEXT` empty for the
9800    // whole interactive session (a `-c` shell got "cmdarg" from its own
9801    // caller and looked correct, hiding this).
9802    // c:1334 — the matching `zsh_eval_context_pop()` runs when this
9803    // guard drops, i.e. after execution, exactly as in C.
9804    let _ctx_frame = EvalContextFrame::push(context); // c:1319
9805    SESSION_EXECUTOR.with(|c| match c.get() {
9806        // SAFETY: set by install_session_executor to an executor that
9807        // lives for the whole single-threaded interactive session;
9808        // loop() runs only after the bin installs it.
9809        Some(ptr) => unsafe { (*ptr).execute_program(program) },
9810        None => 0,
9811    })
9812}
9813
9814// =========================================================================
9815// Live-executor accessors (former `exec_hooks` OnceLock layer).
9816//
9817// These are the **exec.rs exception**: src/ported/ code reaches the
9818// live fusevm `ShellExecutor` (param store, function dispatch, nested
9819// script/cmdsubst execution) through these thin wrappers instead of the
9820// deleted `exec_hooks` fn-pointer registry. Each delegates to
9821// `fusevm_bridge::try_with_executor` — `Some` when a VM execution
9822// context is in scope, `None` in unit-test / compsys contexts with no
9823// bridge running — and reproduces the exact per-call fallback the old
9824// `exec_hooks` wrappers used when no hook was installed. Behavior is
9825// byte-for-byte identical to the OnceLock path; only the indirection is
9826// gone. See `feedback_no_shellexecutor_in_ported` /
9827// `feedback_no_exec_script_from_ported`: the bridge belongs in exec.rs
9828// (the sanctioned exception), not scattered through src/ported.
9829// =========================================================================
9830
9831/// !!! WARNING: RUST-ONLY HELPER !!!
9832///
9833/// No function of this name exists in `Src/exec.c`. This helper stands in for
9834/// reading an array parameter through the live fusevm executor, falling back
9835/// to `params::getaparam`. C reads `paramtab` directly wherever it needs
9836/// this.
9837/// Array param value via the live executor; falls back to the direct
9838/// param table (`params::getaparam`) when no executor is in scope, so
9839/// compsys / unit-test environments still observe shell-side arrays.
9840pub fn array(name: &str) -> Option<Vec<String>> {
9841    // bash special arrays (PIPESTATUS / FUNCNAME / BASH_VERSINFO) alias the
9842    // zsh-native specials in --bash mode. Checked before the stored-array
9843    // lookup so a user array of the same name can still shadow it (rare), but
9844    // after the bash-mode gate so --zsh is untouched. Guard against the
9845    // PIPESTATUS→pipestatus self-alias recursing by only aliasing uppercase.
9846    if name.starts_with(|c: char| c.is_ascii_uppercase()) {
9847        if let Some(v) = crate::dash_mode::bash_special_array(name) {
9848            return Some(v);
9849        }
9850    }
9851    if let Some(Some(v)) = crate::fusevm_bridge::try_with_executor(|exec| exec.array(name)) {
9852        return Some(v);
9853    }
9854    crate::ported::params::getaparam(name)
9855}
9856
9857/// !!! WARNING: RUST-ONLY HELPER !!!
9858///
9859/// No function of this name exists in `Src/exec.c`. This helper stands in for
9860/// reading an associative parameter through the live fusevm executor. C reads
9861/// `paramtab` directly.
9862/// Associative-array param value via the live executor (`None` when no
9863/// executor / not set).
9864pub fn assoc(name: &str) -> Option<indexmap::IndexMap<String, String>> {
9865    crate::fusevm_bridge::try_with_executor(|exec| exec.assoc(name)).flatten()
9866}
9867
9868/// !!! WARNING: RUST-ONLY HELPER !!!
9869///
9870/// No function of this name exists in `Src/exec.c`. This helper stands in for
9871/// writing an array parameter through the live fusevm executor. C calls
9872/// `setaparam` directly.
9873/// Store an array param into the live executor (no-op without one).
9874pub fn set_array(name: &str, val: Vec<String>) {
9875    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_array(name.to_string(), val));
9876}
9877
9878/// !!! WARNING: RUST-ONLY HELPER !!!
9879///
9880/// No function of this name exists in `Src/exec.c`. This helper stands in for
9881/// writing an associative parameter through the live fusevm executor. C calls
9882/// `sethparam` directly.
9883/// Store an associative-array param into the live executor (no-op
9884/// without one).
9885pub fn set_assoc(name: &str, val: indexmap::IndexMap<String, String>) {
9886    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_assoc(name.to_string(), val));
9887}
9888
9889/// !!! WARNING: RUST-ONLY HELPER !!!
9890///
9891/// No function of this name exists in `Src/exec.c`. This helper stands in for
9892/// unsetting a scalar parameter through the live fusevm executor. C calls
9893/// `unsetparam` directly.
9894/// Unset a scalar param in the live executor (no-op without one).
9895pub fn unset_scalar(name: &str) {
9896    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_scalar(name));
9897}
9898
9899/// !!! WARNING: RUST-ONLY HELPER !!!
9900///
9901/// No function of this name exists in `Src/exec.c`. This helper stands in for
9902/// unsetting an array parameter through the live fusevm executor. C calls
9903/// `unsetparam` directly.
9904/// Unset an array param in the live executor (no-op without one).
9905pub fn unset_array(name: &str) {
9906    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_array(name));
9907}
9908
9909/// !!! WARNING: RUST-ONLY HELPER !!!
9910///
9911/// No function of this name exists in `Src/exec.c`. This helper stands in for
9912/// unsetting an associative parameter through the live fusevm executor. C
9913/// calls `unsetparam` directly.
9914/// Unset an associative-array param in the live executor (no-op
9915/// without one).
9916pub fn unset_assoc(name: &str) {
9917    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_assoc(name));
9918}
9919
9920/// !!! WARNING: RUST-ONLY HELPER !!!
9921///
9922/// No function of this name exists in `Src/exec.c`. This helper stands in for
9923/// calling a shell function by name on the live fusevm executor, with the
9924/// full `doshfunc` (c:5823) scope wrap. C reaches the function through
9925/// `execshfunc` (c:5540) off a wordcode `Shfunc`.
9926/// Dispatch a shell-function call by name through the live executor
9927/// (full doshfunc scope wrap). `None` when no executor / not a
9928/// function.
9929pub fn dispatch_function_call(name: &str, args: &[String]) -> Option<i32> {
9930    // c:3490-3492 — `if (type != WC_FUNCDEF) setunderscore((args &&
9931    // nonempty(args)) ? getdata(lastnode(args)) : "")`. C's `args` list
9932    // carries the command word, so a bare `_pre` leaves `$_` == "_pre".
9933    // The compsys ports call shell functions through this shim rather
9934    // than through a parsed command, so those calls never reached
9935    // execcmd's write and the callee saw whatever `$_` the caller left:
9936    // zsh reports `_post` inside comppostfuncs where zshrs reported the
9937    // stale value.
9938    {
9939        let last = args.last().cloned().unwrap_or_else(|| name.to_string());
9940        crate::ported::params::set_zunderscore(std::slice::from_ref(&last)); // c:3491
9941    }
9942    let __ft = crate::ftime::start(name);
9943    if let Some(r) =
9944        crate::fusevm_bridge::try_with_executor(|exec| exec.dispatch_function_call(name, args))
9945    {
9946        crate::ftime::stop(__ft);
9947        return r;
9948    }
9949    // No active VM context: this is the loop()/zsh_main exit path where
9950    // `zexit` fires a `TRAPEXIT() { ... }` (via dotrap, Src/builtin.c:6043)
9951    // or the `zshexit` hook after `loop()` returned. Enter the installed
9952    // session executor so the handler runs in the shell. SAFETY per execode.
9953    SESSION_EXECUTOR.with(|c| match c.get() {
9954        Some(ptr) => {
9955            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9956            unsafe { (*ptr).dispatch_function_call(name, args) }
9957        }
9958        None => None,
9959    })
9960}
9961
9962/// !!! WARNING: RUST-ONLY HELPER !!!
9963///
9964/// No function of this name exists in `Src/exec.c`. This helper stands in for
9965/// running a function BODY on the live fusevm executor with no scope wrap,
9966/// for callers that already entered `doshfunc` (c:5823).
9967/// Body-only function dispatch (no doshfunc scope wrap) — call as the
9968/// `body_runner` of a direct `doshfunc(...)` invocation to avoid the
9969/// double-wrap of going back through [`dispatch_function_call`]. `None`
9970/// when no executor.
9971pub fn run_function_body(name: &str, args: &[String]) -> Option<i32> {
9972    if let Some(r) =
9973        crate::fusevm_bridge::try_with_executor(|exec| exec.run_function_body_only(name, args))
9974    {
9975        return r;
9976    }
9977    // Session-executor fallback for the no-VM-context exit path (e.g. the
9978    // `zshexit` hook fired by `zexit` from zsh_main). SAFETY per execode.
9979    SESSION_EXECUTOR.with(|c| match c.get() {
9980        Some(ptr) => {
9981            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9982            unsafe { (*ptr).run_function_body_only(name, args) }
9983        }
9984        None => None,
9985    })
9986}
9987
9988/// !!! WARNING: RUST-ONLY HELPER !!!
9989///
9990/// No function of this name exists in `Src/exec.c`. This helper stands in for
9991/// running a source string on the live fusevm executor. The C path is
9992/// `execstring` (c:1228) -> `parse_string` (c:283) -> `execode` (c:1245).
9993/// Run a script source string on the live executor. `Ok(0)` when no
9994/// executor is in scope.
9995pub fn execute_script(src: &str) -> Result<i32, String> {
9996    // c:1228 execstring → execode: no end-of-script hooks (see
9997    // ShellExecutor::execute_string_without_exit_hooks).
9998    if let Some(r) =
9999        crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_without_exit_hooks(src))
10000    {
10001        return r;
10002    }
10003    // No active VM context: the loop()/zsh_main exit path where `zexit`
10004    // runs a `trap '...' EXIT` raw body (Src/builtin.c:6043) after `loop()`
10005    // returned. Enter the installed session executor so the trap body runs
10006    // in the shell instead of silently no-op'ing. SAFETY per execode.
10007    SESSION_EXECUTOR.with(|c| match c.get() {
10008        Some(ptr) => {
10009            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
10010            unsafe { (*ptr).execute_string_without_exit_hooks(src) }
10011        }
10012        None => Ok(0),
10013    })
10014}
10015
10016/// !!! WARNING: RUST-ONLY HELPER !!!
10017///
10018/// No function of this name exists in `Src/exec.c`. This helper stands in for
10019/// running a source string through the fusevm zsh pipeline. Same C path as
10020/// above: `execstring` (c:1228).
10021/// Run a script source string through the live executor's zsh pipeline.
10022/// `Ok(0)` when no executor is in scope.
10023///
10024/// Executor ladder, identical to [`execute_script`] above and to
10025/// `fusevm_bridge::source_file_per_command`: the innermost ACTIVE executor
10026/// when one is in scope, the installed session executor otherwise, `Ok(0)`
10027/// with neither.
10028///
10029/// The session-executor rung is what makes `execstring` (c:1228) behave like
10030/// C, where the interpreter is plain process globals and the string ALWAYS
10031/// runs. Every caller that fires between commands rather than inside one
10032/// reaches this with `CURRENT_EXECUTOR` unset — `checksched`'s
10033/// `execstring(sch->cmd, 0, 0, "sched")` (c:Src/Builtins/sched.c:122), fired
10034/// from the pre-prompt hook or from the `addtimedfn` deadline in ZLE's read
10035/// loop, and `dotrap`'s re-parse of a non-function trap action
10036/// (signals.rs). Without it, `try_with_executor` returned `None`, the
10037/// `.unwrap_or(Ok(0))` reported success, and the command was dropped on the
10038/// floor: `sched +1 'print hi'` listed the entry, dequeued it on time, and
10039/// printed nothing.
10040pub fn execute_script_zsh_pipeline(src: &str) -> Result<i32, String> {
10041    if let Some(r) =
10042        crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_without_exit_hooks(src))
10043    {
10044        return r;
10045    }
10046    SESSION_EXECUTOR.with(|c| match c.get() {
10047        // SAFETY: set by install_session_executor to an executor that lives
10048        // for the whole single-threaded interactive session. Reached only
10049        // when `CURRENT_EXECUTOR` is unset, i.e. no chunk is running on this
10050        // thread (`run_chunk` holds an `ExecutorContext` for the whole of
10051        // `vm.run()`), so this cannot alias a live `&mut ShellExecutor`.
10052        Some(ptr) => {
10053            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
10054            unsafe { (*ptr).execute_string_without_exit_hooks(src) }
10055        }
10056        None => Ok(0),
10057    })
10058}
10059
10060/// !!! WARNING: RUST-ONLY HELPER !!!
10061///
10062/// No function of this name exists in `Src/exec.c`. This helper stands in for
10063/// running a `$(...)` body in-process on the live fusevm executor. C forks
10064/// and calls `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` inside `getoutput`
10065/// (c:4713); the in-process form applies that subshell state with a guard
10066/// instead.
10067/// Run a `$(...)` command substitution on the live executor, returning
10068/// captured stdout. Empty string when no executor is in scope.
10069///
10070/// The body runs IN-PROCESS (no fork), so the subshell state that
10071/// `Src/exec.c:4781`'s `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` would
10072/// have installed in the forked child is applied here by `SubshStateGuard`
10073/// and unwound when the substitution returns. Every caller of this funnel —
10074/// `getoutput` above and the p10k custom-segment renderer
10075/// (`extensions/p10k/segments_core.rs:917`, which is `content="$(eval
10076/// $command)"`) — is a genuine command substitution and wants it.
10077pub fn run_command_substitution(cmd: &str) -> String {
10078    // c:4781 — `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL);` in the child
10079    // arm of getoutput. Fork-safe subset only; see SubshStateGuard for the
10080    // applied/skipped breakdown.
10081    let _subsh_state = SubshStateGuard::enter();
10082    if let Some(r) =
10083        crate::fusevm_bridge::try_with_executor(|exec| exec.run_command_substitution(cmd))
10084    {
10085        return r;
10086    }
10087    // Session-executor fallback for no-VM-context callers — the native
10088    // p10k custom_* segments (p10k:1698 `content="$(eval $command)"`)
10089    // render at preprompt time, before any ExecutorContext is entered,
10090    // so try_with_executor alone returned "" and every custom segment
10091    // rendered empty. Same pattern as run_function_body above; SAFETY
10092    // per execode.
10093    SESSION_EXECUTOR.with(|c| match c.get() {
10094        Some(ptr) => {
10095            let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
10096            unsafe { (*ptr).run_command_substitution(cmd) }
10097        }
10098        None => String::new(),
10099    })
10100}
10101
10102/// !!! WARNING: RUST-ONLY HELPER !!!
10103///
10104/// No function of this name exists in `Src/exec.c`. This helper stands in for
10105/// reading the positional parameters off the live fusevm executor. C reads
10106/// the `pparams` global directly (Src/params.c).
10107/// Positional parameters ($1..$N) from the live executor; empty without
10108/// one.
10109pub fn pparams() -> Vec<String> {
10110    crate::fusevm_bridge::try_with_executor(|exec| exec.pparams()).unwrap_or_default()
10111}
10112
10113/// !!! WARNING: RUST-ONLY HELPER !!!
10114///
10115/// No function of this name exists in `Src/exec.c`. This helper stands in for
10116/// replacing the positional parameters on the live fusevm executor. C assigns
10117/// the `pparams` global directly (Src/params.c).
10118/// Replace the positional parameters in the live executor (no-op
10119/// without one).
10120pub fn set_pparams(v: Vec<String>) {
10121    let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_pparams(v));
10122}
10123
10124/// !!! WARNING: RUST-ONLY HELPER !!!
10125///
10126/// No function of this name exists in `Src/exec.c`. This helper stands in for
10127/// dropping a function from the fusevm executor's compiled-chunk and source
10128/// maps. C removes the node from `shfunctab` directly.
10129/// Drop a function from both the compiled-chunk and source maps in the
10130/// live executor. Returns true if either entry existed; false when no
10131/// executor.
10132pub fn unregister_function(name: &str) -> bool {
10133    crate::fusevm_bridge::try_with_executor(|exec| {
10134        let a = exec.functions_compiled.remove(name).is_some();
10135        let b = exec.function_source.remove(name).is_some();
10136        a || b
10137    })
10138    .unwrap_or(false)
10139}
10140
10141/// !!! WARNING: RUST-ONLY HELPER !!!
10142///
10143/// No function of this name exists in `Src/exec.c`. This helper stands in for
10144/// reading the saved outer stdout fd of an in-progress `$(...)` capture. C
10145/// has no such stack: the capture happens in a forked child (`getoutput`,
10146/// c:4713) so the parent fd is never rebound.
10147/// Saved outer stdout fd for an in-progress `$(...)` capture (top of
10148/// the bridge's CMDSUBST_OUTER_FDS stack), or `None` when not inside a
10149/// cmdsub. Used by the trap dispatcher to route a trap body's stdout to
10150/// the parent terminal instead of the cmdsub-bound pipe (Bug #56).
10151pub fn cmdsubst_outer_stdout() -> Option<i32> {
10152    crate::fusevm_bridge::cmdsubst_outer_stdout()
10153}
10154
10155/// Port of `execautofn_basic()` from `Src/exec.c:5608` — C decl `execautofn_basic(Estate state, UNUSED(int do_exec))`.
10156/// Run a pre-loaded autoload function body
10157/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
10158/// the call so `%N` / `%x` reflect the autoload target during
10159/// execution.
10160pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
10161    // c:5608
10162    // c:5613 — `shf = state->prog->shf;`
10163    let shf = match state.prog.shf.as_deref() {
10164        Some(s) => s.clone(),
10165        None => return LASTVAL.load(Ordering::Relaxed),
10166    };
10167
10168    // c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
10169    // top-of-stack tracking is in modules::parameter::FUNCSTACK.
10170    {
10171        let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
10172        if let Some(top) = stk.last_mut() {
10173            if top.filename.is_none() {
10174                // c:5620 — `funcstack->filename = getshfuncfile(shf);`
10175                top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
10176            }
10177        }
10178    }
10179
10180    // c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
10181    let oldscriptname = crate::ported::utils::scriptname_get();
10182    let oldscriptfilename = crate::ported::utils::scriptfilename_get();
10183    // c:5624 — `scriptname = dupstring(shf->node.nam);`
10184    crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
10185    // c:5625 — `scriptfilename = getshfuncfile(shf);`
10186    crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
10187        &shf.node.nam,
10188    ));
10189    // c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
10190    if let Some(funcdef) = shf.funcdef.clone() {
10191        execode_wordcode(funcdef, 1, 0, "loadautofunc");
10192    }
10193    // c:5627-5628 — restore.
10194    crate::ported::utils::set_scriptname(oldscriptname);
10195    crate::ported::utils::set_scriptfilename(oldscriptfilename);
10196
10197    LASTVAL.load(Ordering::Relaxed) // c:5630
10198}
10199
10200/// Port of `execautofn()` from `Src/exec.c:5635` — C decl `execautofn(Estate state, UNUSED(int do_exec))`.
10201/// The autoload-aware dispatch entry
10202/// for `WC_AUTOFN`: fault the function body in via `loadautofn`,
10203/// then hand off to `execautofn_basic` to actually run it.
10204///
10205/// C body:
10206/// ```c
10207/// static int
10208/// execautofn(Estate state, UNUSED(int do_exec))
10209/// {
10210///     Shfunc shf;
10211///     if (!(shf = loadautofn(state->prog->shf, 1, 0, 0)))
10212///         return 1;
10213///     state->prog->shf = shf;
10214///     return execautofn_basic(state, 0);
10215/// }
10216/// ```
10217///
10218/// Rust port: `loadautofn` mutates the `shfunc` in place via a raw
10219/// pointer and returns 0/1 (success/failure), so the explicit
10220/// `state->prog->shf = shf` assignment in C is implicit here.
10221pub fn execautofn(state: &mut estate, _do_exec: i32) -> i32 {
10222    // c:5638-5640 — `if (!(shf = loadautofn(state->prog->shf, 1, 0, 0))) return 1;`
10223    let shf_ptr: *mut shfunc = match state.prog.shf.as_mut() {
10224        Some(b) => &mut **b as *mut shfunc,
10225        None => return 1,
10226    };
10227    if loadautofn(shf_ptr, 1, 0, 0) != 0 {
10228        return 1;
10229    }
10230    // c:5643 — `return execautofn_basic(state, 0);`
10231    execautofn_basic(state, 0)
10232}
10233
10234/// Port of `execpline2()` from `Src/exec.c:1991` — C decl `execpline2(Estate state, wordcode pcode, int how, int input, int output, int last1)`.
10235/// Recursive
10236/// multi-stage pipe walker: at each step, analyse the current
10237/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
10238/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
10239/// its input fd.
10240pub fn execpline2(
10241    state: &mut estate,
10242    pcode: wordcode,
10243    how: i32,
10244    input: i32,
10245    output: i32,
10246    last1: i32,
10247) {
10248    use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
10249    use crate::ported::zsh_h::{
10250        execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
10251        WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
10252    };
10253    // c:1991
10254    let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
10255
10256    // c:1996-1997 — `if (breaks || retflag) return;`
10257    if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
10258        return;
10259    }
10260
10261    // c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
10262    //                  lineno = WC_PIPE_LINENO(pcode) - 1;`
10263    if !crate::ported::zsh_h::IN_EVAL_TRAP()
10264        && INEVAL.load(Ordering::SeqCst) == 0
10265        && ZWC_PIPE_LINENO(pcode) != 0
10266    {
10267        let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
10268        crate::ported::input::lineno.with(|l| l.set(new_lineno));
10269    }
10270
10271    // c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
10272    if pline_level.load(Ordering::Relaxed) == 1 {
10273        // c:2003
10274        if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
10275            // c:2004 — `(how & Z_ASYNC) || !sfcontext`
10276            // c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
10277            //   state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
10278            let pc_for_text = state.pc
10279                + if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
10280                    0
10281                } else {
10282                    1
10283                };
10284            let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
10285            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
10286                *lpt = text;
10287            }
10288        } else {
10289            // c:2010 — `list_pipe_text[0] = '\0';`
10290            if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
10291                lpt.clear();
10292            }
10293        }
10294    }
10295
10296    if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
10297        // c:2012-2014 — terminal stage: analyse + exec directly.
10298        execcmd_analyse(state, &mut eparams); // c:2013
10299        execcmd_exec(
10300            state,
10301            &mut eparams,
10302            input,
10303            output,
10304            how,
10305            if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
10306            -1,                             // c:2014 close_if_forked = -1
10307        );
10308    } else {
10309        // c:2015-2039 — non-terminal stage: pipe + fork + recurse.
10310        let mut pipes: [i32; 2] = [-1, -1]; // c:2016
10311        let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
10312                                                               // c:2018 — `Wordcode next = state->pc + (*state->pc);`
10313        let next = if state.pc < state.prog.prog.len() {
10314            state.pc + state.prog.prog[state.pc] as usize
10315        } else {
10316            state.pc
10317        };
10318        // c:2020 — `++state->pc;`
10319        if state.pc < state.prog.prog.len() {
10320            state.pc += 1;
10321        }
10322        execcmd_analyse(state, &mut eparams); // c:2021
10323
10324        if mpipe(&mut pipes) < 0 {
10325            // c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
10326        }
10327
10328        // c:2027 — `addfilelist(NULL, pipes[0]);`
10329        // C uses the current thisjob's filelist; Rust port wires through JOBTAB.
10330        if let Some(jt) = JOBTAB.get() {
10331            let mut guard = jt.lock().unwrap();
10332            let tj = {
10333                if let Some(m) = THISJOB.get() {
10334                    *m.lock().unwrap()
10335                } else {
10336                    -1
10337                }
10338            };
10339            if tj >= 0 {
10340                if let Some(j) = guard.get_mut(tj as usize) {
10341                    crate::ported::jobs::addfilelist(j, None, pipes[0]);
10342                }
10343            }
10344        }
10345
10346        // c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
10347        execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
10348        let _ = zclose(pipes[1]); // c:2029
10349        state.pc = next; // c:2030
10350
10351        // c:2034 — `cmdpush(CS_PIPE);`
10352        cmdpush(CS_PIPE as u8);
10353        // c:2035 — `list_pipe = 1;`
10354        list_pipe.store(1, Ordering::Relaxed);
10355        // c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
10356        let next_pcode = if state.pc < state.prog.prog.len() {
10357            state.prog.prog[state.pc]
10358        } else {
10359            0
10360        };
10361        if state.pc < state.prog.prog.len() {
10362            state.pc += 1;
10363        }
10364        execpline2(state, next_pcode, how, pipes[0], output, last1);
10365        // c:2037 — `list_pipe = old_list_pipe;`
10366        list_pipe.store(old_list_pipe, Ordering::Relaxed);
10367        // c:2038 — `cmdpop();`
10368        cmdpop();
10369    }
10370}
10371
10372/// Port of `execpline()` from `Src/exec.c:1668` — C decl `execpline(Estate state, wordcode slcode, int how, int last1)`.
10373/// Full faithful port: allocates a job-table
10374/// entry via `initjob`, sets up coproc mpipes, drives the whole
10375/// (multi-stage) pipeline through `execpline2` (which performs the real
10376/// per-stage mpipe/fork/exec), then either spawns the job asynchronously
10377/// (`Z_ASYNC` -> `spawnjob`/`deletejob`) or waits synchronously
10378/// (`waitjobs`), including the `list_pipe` stop/continue fork machinery
10379/// (SUBJOB/SUPERJOB linkage) that re-forks the shell to keep an
10380/// interactively-suspended right-hand pipeline stage running.
10381///
10382/// Divergences from C, each forced by the Rust substrate and cited
10383/// inline at the point of use:
10384///   * `initjob` here grows the `Vec`-backed jobtab on demand and does
10385///     not return -1, so C's per-pipeline table-full bailout (c:1756-1760)
10386///     is inert on THIS path. That bailout is zsh's universal recursion
10387///     backstop (`initjob` caps the table at `MAX_MAXJOBS` → `zerr("job
10388///     table full or recursion limit exceeded")`), which bounds recursion
10389///     through paths FUNCNEST does not count (sourced files, `eval` — the
10390///     doshfunc funcnest check at c:5684 counts FS_FUNC frames only). The
10391///     fusevm runtime that actually executes pipelines does not allocate a
10392///     job per pipeline, so that backstop was missing entirely — runaway
10393///     `source`/`eval` recursion overflowed the (large but finite)
10394///     main-thread stack → SIGBUS. It is reinstated inline (same ceiling,
10395///     total FUNCSTACK depth as the proxy for held job slots) at the
10396///     FS_SOURCE re-entry (init.rs::source) and FS_EVAL re-entry
10397///     (builtin.rs eval).
10398///   * `errbrk_saved` / `prev_errflag` / `prev_breaks` (jobs.c:128
10399///     globals) are the jobs.rs statics; `update_job` sets them and the
10400///     `if (errbrk_saved)` restore (c:1998-2003) reads them here.
10401pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
10402    use crate::ported::builtin::{BREAKS, LOOPS, RETFLAG};
10403    use crate::ported::init::zleentry;
10404    use crate::ported::jobs::stat as jst;
10405    use crate::ported::jobs::{
10406        addproc, clearoldjobtab, deletejob, hasprocs, initjob, makerunning, pipecleanfilelist,
10407        printjob, spawnjob, waitjobs, CURJOB, ERRBRK_SAVED, LASTVAL2, PREVJOB, PREV_BREAKS,
10408        PREV_ERRFLAG,
10409    };
10410    use crate::ported::modules::clone::{coprocin, coprocout};
10411    use crate::ported::signals::killjb;
10412    use crate::ported::signals_h::{
10413        queue_signal_level, queue_signals, restore_queue_signals, unqueue_signals,
10414    };
10415    use crate::ported::utils::read_loop;
10416    use crate::ported::zsh_h::{
10417        jobbing, INTERACTIVE, LONGLISTJOBS, STAT_SUBJOB_ORPHANED, WC_PIPE, WC_PIPE_END,
10418        WC_PIPE_TYPE, WC_SUBLIST_COPROC, WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, ZLE_CMD_TRASH, Z_ASYNC,
10419        Z_DISOWN, Z_TIMED,
10420    };
10421
10422    // c:1731 — `static int lastwj, lpforked;` (persist across calls).
10423    static LASTWJ: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10424    static LPFORKED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10425    // c:465 (exec.c) — `static struct timespec list_pipe_start;`. Used as
10426    // the addproc bgtime for the re-forked super-job leader (c:1841).
10427    static LIST_PIPE_START: std::sync::Mutex<Option<std::time::Instant>> =
10428        std::sync::Mutex::new(None);
10429    // c:128 (jobs.c) — `int prev_errflag, prev_breaks, errbrk_saved;` are the
10430    // jobs.rs statics `update_job` sets (c:598-600).
10431
10432    let jt = JOBTAB.get_or_init(|| std::sync::Mutex::new(Vec::new()));
10433    // Read/write the shared thisjob slot.
10434    let thisjob_set = |v: i32| {
10435        if let Some(m) = THISJOB.get() {
10436            *m.lock().unwrap() = v;
10437        }
10438    };
10439    let thisjob_get = || THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10440
10441    let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:1728
10442    let mut slflags = WC_SUBLIST_FLAGS(slcode); // c:1729
10443    let code = state.prog.prog[state.pc]; // c:1730 `wordcode code = *state->pc++;`
10444    state.pc += 1;
10445
10446    // c:1733-1736 — non-pipe, non-timed sublist short-circuits with the
10447    // negated-empty status.
10448    if wc_code(code) != WC_PIPE && (how & Z_TIMED) == 0 {
10449        let r = i32::from((slflags & WC_SUBLIST_NOT) != 0);
10450        LASTVAL.store(r, Ordering::Relaxed);
10451        return r;
10452    }
10453    let mut last1 = last1;
10454    if (slflags & WC_SUBLIST_NOT) != 0 {
10455        last1 = 0; // c:1736
10456    }
10457    let mut how = how;
10458
10459    queue_signals(); // c:1744
10460
10461    let pj = thisjob_get(); // c:1746 — `pj = thisjob;`
10462    let mut ipipe: [i32; 2] = [0, 0]; // c:1747
10463    let mut opipe: [i32; 2] = [0, 0];
10464    child_block(); // c:1748
10465
10466    // c:1755 — `thisjob = newjob = initjob();` (Rust jobtab grows on
10467    // demand, so initjob never fails; the -1 bailout is unreachable).
10468    let newjob = {
10469        let mut g = jt.lock().unwrap();
10470        initjob(&mut g)
10471    };
10472    thisjob_set(newjob as i32);
10473    if (how & Z_TIMED) != 0 {
10474        // c:1760-1761
10475        let mut g = jt.lock().unwrap();
10476        g[newjob].stat |= jst::TIMED;
10477    }
10478
10479    if (slflags & WC_SUBLIST_COPROC) != 0 {
10480        // c:1763-1782
10481        how = Z_ASYNC; // c:1764
10482        if coprocin.load(Ordering::Relaxed) >= 0 {
10483            zclose(coprocin.load(Ordering::Relaxed)); // c:1766
10484            zclose(coprocout.load(Ordering::Relaxed)); // c:1767
10485        }
10486        if mpipe(&mut ipipe) < 0 {
10487            // c:1769-1771
10488            coprocin.store(-1, Ordering::Relaxed);
10489            coprocout.store(-1, Ordering::Relaxed);
10490            slflags &= !WC_SUBLIST_COPROC;
10491        } else if mpipe(&mut opipe) < 0 {
10492            // c:1772-1776
10493            unsafe {
10494                libc::close(ipipe[0]);
10495                libc::close(ipipe[1]);
10496            }
10497            coprocin.store(-1, Ordering::Relaxed);
10498            coprocout.store(-1, Ordering::Relaxed);
10499            slflags &= !WC_SUBLIST_COPROC;
10500        } else {
10501            // c:1777-1781
10502            coprocin.store(ipipe[0], Ordering::Relaxed);
10503            coprocout.store(opipe[1], Ordering::Relaxed);
10504            fdtable_set(ipipe[0], FDT_UNUSED);
10505            fdtable_set(opipe[1], FDT_UNUSED);
10506        }
10507    }
10508
10509    // c:1788-1793 — `if (!pline_level++) { ... }`.
10510    let prev_pline = pline_level.fetch_add(1, Ordering::Relaxed);
10511    if prev_pline == 0 {
10512        list_pipe_pid.store(0, Ordering::Relaxed); // c:1789
10513        nowait.store(0, Ordering::Relaxed); // c:1790
10514        simple_pline.store(
10515            i32::from(WC_PIPE_TYPE(code) == WC_PIPE_END),
10516            Ordering::Relaxed,
10517        ); // c:1791
10518        list_pipe_job.store(newjob as i32, Ordering::Relaxed); // c:1792
10519    }
10520    LASTWJ.store(0, Ordering::Relaxed); // c:1794
10521    LPFORKED.store(0, Ordering::Relaxed);
10522    execpline2(state, code, how, opipe[0], ipipe[1], last1); // c:1795
10523    pline_level.fetch_sub(1, Ordering::Relaxed); // c:1796
10524
10525    if (how & Z_ASYNC) != 0 {
10526        // c:1797-1818
10527        clearoldjobtab(); // c:1798
10528        LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1799
10529
10530        if thisjob_get() == list_pipe_job.load(Ordering::Relaxed) {
10531            list_pipe_job.store(0, Ordering::Relaxed); // c:1801-1802
10532        }
10533        {
10534            let mut g = jt.lock().unwrap();
10535            let tj = thisjob_get();
10536            if tj >= 0 {
10537                g[tj as usize].stat |= jst::NOSTTY; // c:1803
10538            }
10539        }
10540        if (slflags & WC_SUBLIST_COPROC) != 0 {
10541            zclose(ipipe[1]); // c:1805
10542            zclose(opipe[0]); // c:1806
10543        }
10544        if (how & Z_DISOWN) != 0 {
10545            // c:1808-1812
10546            let tj = thisjob_get();
10547            if tj >= 0 {
10548                let mut g = jt.lock().unwrap();
10549                pipecleanfilelist(&mut g[tj as usize], false); // c:1809
10550                deletejob(&mut g, tj as usize, true); // c:1810
10551            }
10552            thisjob_set(-1); // c:1811
10553        } else {
10554            spawnjob(); // c:1814 (locks JOBTAB internally — no guard held)
10555        }
10556        child_unblock(); // c:1815
10557        unqueue_signals(); // c:1816
10558        LASTVAL.store(0, Ordering::Relaxed); // c:1818 `return lastval = 0;`
10559        return 0;
10560    }
10561
10562    // c:1819-2033 — synchronous branch.
10563    if newjob as i32 != LASTWJ.load(Ordering::Relaxed) {
10564        // c:1820
10565        let mut jn_idx = newjob; // `Job jn = jobtab + newjob;`
10566
10567        // c:1824-1825 — a list_pipe sub-shell child exits here.
10568        if newjob as i32 == list_pipe_job.load(Ordering::Relaxed)
10569            && list_pipe_child.load(Ordering::Relaxed) != 0
10570        {
10571            unsafe { libc::_exit(0) };
10572        }
10573
10574        LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1826
10575        thisjob_set(newjob as i32);
10576
10577        // c:1828-1830 — suppress the job announcement in nested pipes.
10578        {
10579            let mut g = jt.lock().unwrap();
10580            let noprint = list_pipe.load(Ordering::Relaxed) != 0
10581                || (pline_level.load(Ordering::Relaxed) != 0
10582                    && (how & Z_TIMED) == 0
10583                    && (g[jn_idx].stat & jst::NOSTTY) == 0);
10584            if noprint {
10585                g[jn_idx].stat |= jst::NOPRINT;
10586            }
10587        }
10588
10589        if nowait.load(Ordering::Relaxed) != 0 {
10590            // c:1832-1882
10591            if pline_level.load(Ordering::Relaxed) == 0 {
10592                // c:1833-1875
10593                *CURJOB
10594                    .get_or_init(|| std::sync::Mutex::new(-1))
10595                    .lock()
10596                    .unwrap() = newjob as i32; // c:1836
10597                                               // c:1838 — DPUTS(!list_pipe_pid, "invalid list_pipe_pid").
10598                                               // c:1840-1841 — record the re-forked leader in the super-job.
10599                {
10600                    let txt = LIST_PIPE_TEXT.lock().map(|s| s.clone()).unwrap_or_default();
10601                    let bgt = *LIST_PIPE_START.lock().unwrap();
10602                    let mut g = jt.lock().unwrap();
10603                    addproc(
10604                        &mut g[jn_idx],
10605                        list_pipe_pid.load(Ordering::Relaxed),
10606                        &txt,
10607                        false,
10608                        bgt,
10609                        -1,
10610                        -1,
10611                    );
10612                }
10613                {
10614                    let mut g = jt.lock().unwrap();
10615                    // c:1845 — `if (!jn->procs->next || lpforked == 2)`.
10616                    if g[jn_idx].procs.len() <= 1 || LPFORKED.load(Ordering::Relaxed) == 2 {
10617                        g[jn_idx].gleader = list_pipe_pid.load(Ordering::Relaxed); // c:1845
10618                        g[jn_idx].stat |= jst::SUBLEADER; // c:1847
10619                                                          // c:1852-1861 — adopt any orphaned subjob; we
10620                                                          // become its super-job.
10621                        for jobsub in 1..g.len() {
10622                            if (g[jobsub].stat & STAT_SUBJOB_ORPHANED) != 0 {
10623                                g[jn_idx].other = jobsub as i32; // c:1855
10624                                g[jn_idx].stat |= jst::SUPERJOB; // c:1856
10625                                g[jobsub].stat &= !STAT_SUBJOB_ORPHANED; // c:1857
10626                                g[jobsub].other = list_pipe_pid.load(Ordering::Relaxed);
10627                                // c:1858
10628                            }
10629                        }
10630                    }
10631                    // c:1863-1869 — copy a stopped proc status from the
10632                    // subjob onto our last proc.
10633                    let other = g[jn_idx].other as usize;
10634                    let stopped = if other < g.len() {
10635                        g[other]
10636                            .procs
10637                            .iter()
10638                            .find(|p| p.is_stopped())
10639                            .map(|p| p.status)
10640                    } else {
10641                        None
10642                    };
10643                    if let Some(st) = stopped {
10644                        if let Some(last) = g[jn_idx].procs.last_mut() {
10645                            last.status = st;
10646                        }
10647                    }
10648                    // c:1870-1872
10649                    g[jn_idx].stat &= !(jst::DONE | jst::NOPRINT);
10650                    g[jn_idx].stat |= jst::STOPPED | jst::CHANGED | jst::LOCKED | jst::INUSE;
10651                }
10652                // c:1875 — printjob(jn, !!isset(LONGLISTJOBS), 1).
10653                {
10654                    let mut g = jt.lock().unwrap();
10655                    crate::exec_jobs::printjob_synch(
10656                        &mut g,
10657                        jn_idx,
10658                        i32::from(isset(LONGLISTJOBS)),
10659                        1,
10660                    );
10661                }
10662            } else if newjob as i32 != list_pipe_job.load(Ordering::Relaxed) {
10663                let mut g = jt.lock().unwrap();
10664                deletejob(&mut g, jn_idx, false); // c:1878
10665            } else {
10666                LASTWJ.store(-1, Ordering::Relaxed); // c:1879
10667            }
10668        }
10669
10670        ERRBRK_SAVED.store(0, Ordering::Relaxed); // c:1883
10671                                                  // c:1884-2015 — `for (; !nowait;)` wait / continue-fork loop.
10672        loop {
10673            if nowait.load(Ordering::Relaxed) != 0 {
10674                break;
10675            }
10676            if list_pipe_child.load(Ordering::Relaxed) != 0 {
10677                // c:1886-1887
10678                let mut g = jt.lock().unwrap();
10679                g[jn_idx].stat |= jst::NOPRINT;
10680                makerunning(&mut g, jn_idx);
10681            }
10682            // c:1889-1894 — wait unless the job is LOCKED.
10683            let locked = {
10684                let g = jt.lock().unwrap();
10685                (g[jn_idx].stat & jst::LOCKED) != 0
10686            };
10687            let updated;
10688            if !locked {
10689                let tj = thisjob_get();
10690                {
10691                    let g = jt.lock().unwrap();
10692                    updated = hasprocs(&g, tj as usize); // c:1890
10693                }
10694                waitjobs(); // c:1891 — no JOBTAB guard held across the wait
10695                child_block(); // c:1892
10696            } else {
10697                updated = false; // c:1894
10698            }
10699            // c:1895-1902 — nudge the signal queue when the LHS job is
10700            // still running but we saw no update.
10701            let lpj = list_pipe_job.load(Ordering::Relaxed);
10702            let nudge = !updated && lpj != 0 && {
10703                let g = jt.lock().unwrap();
10704                (lpj as usize) < g.len()
10705                    && hasprocs(&g, lpj as usize)
10706                    && (g[lpj as usize].stat & jst::STOPPED) == 0
10707            };
10708            if nudge {
10709                let q = queue_signal_level();
10710                child_unblock();
10711                child_block();
10712                dont_queue_signals();
10713                restore_queue_signals(q);
10714            }
10715            // c:1903-1907 — forward a fatal signal from a done child.
10716            let jn_done = {
10717                let g = jt.lock().unwrap();
10718                (g[jn_idx].stat & jst::DONE) != 0
10719            };
10720            if list_pipe_child.load(Ordering::Relaxed) != 0
10721                && jn_done
10722                && (LASTVAL2.load(Ordering::Relaxed) & 0o200) != 0
10723            {
10724                unsafe {
10725                    libc::killpg(
10726                        mypgrp.load(Ordering::Relaxed),
10727                        LASTVAL2.load(Ordering::Relaxed) & !0o200,
10728                    );
10729                }
10730            }
10731            // c:1908-1921 — a pipeline with the shell running the RHS was
10732            // stopped; fork to let it continue.
10733            let stop_fork = list_pipe_child.load(Ordering::Relaxed) == 0
10734                && LPFORKED.load(Ordering::Relaxed) == 0
10735                && subsh.load(Ordering::Relaxed) == 0
10736                && jobbing()
10737                && (list_pipe.load(Ordering::Relaxed) != 0
10738                    || last1 != 0
10739                    || pline_level.load(Ordering::Relaxed) != 0)
10740                && {
10741                    let g = jt.lock().unwrap();
10742                    (g[jn_idx].stat & jst::STOPPED) != 0
10743                        || (lpj != 0
10744                            && pline_level.load(Ordering::Relaxed) != 0
10745                            && (lpj as usize) < g.len()
10746                            && (g[lpj as usize].stat & jst::STOPPED) != 0)
10747                };
10748            if stop_fork {
10749                let mut synch: [i32; 2] = [0, 0]; // c:1913
10750                let mut bgtime = ZshTimespec {
10751                    tv_sec: 0,
10752                    tv_nsec: 0,
10753                };
10754                let mut pid: libc::pid_t = 0;
10755                let pipe_failed = unsafe { libc::pipe(synch.as_mut_ptr()) } < 0; // c:1922
10756                if !pipe_failed {
10757                    pid = zfork(Some(&mut bgtime)); // c:1922
10758                }
10759                if pipe_failed || pid == -1 {
10760                    // c:1923-1935 — failure: can't suspend, resume the job.
10761                    if pid < 0 {
10762                        unsafe {
10763                            libc::close(synch[0]);
10764                            libc::close(synch[1]);
10765                        }
10766                    } else {
10767                        zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
10768                        // c:1926
10769                    }
10770                    let _ = zleentry(ZLE_CMD_TRASH); // c:1929
10771                    eprintln!("zsh: job can't be suspended"); // c:1930
10772                    {
10773                        let mut g = jt.lock().unwrap();
10774                        makerunning(&mut g, jn_idx); // c:1932
10775                    }
10776                    killjb(jn_idx, libc::SIGCONT); // c:1933
10777                    thisjob_set(newjob as i32); // c:1934
10778                } else if pid != 0 {
10779                    // c:1936-1973 — parent: job control lives here.
10780                    let gl = {
10781                        let g = jt.lock().unwrap();
10782                        g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0)
10783                    };
10784                    LPFORKED.store(
10785                        if unsafe { libc::killpg(gl, 0) } == -1 {
10786                            2
10787                        } else {
10788                            1
10789                        },
10790                        Ordering::Relaxed,
10791                    ); // c:1951-1952
10792                    list_pipe_pid.store(pid, Ordering::Relaxed); // c:1953
10793                    *LIST_PIPE_START.lock().unwrap() = Some(std::time::Instant::now()); // c:1954
10794                    nowait.store(1, Ordering::Relaxed); // c:1955
10795                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:1956
10796                    BREAKS.store(LOOPS.load(Ordering::SeqCst), Ordering::SeqCst); // c:1957
10797                    unsafe { libc::close(synch[1]) }; // c:1958
10798                    let mut dummy = [0u8; 1];
10799                    let _ = read_loop(synch[0], &mut dummy); // c:1959
10800                    unsafe { libc::close(synch[0]) }; // c:1960
10801                                                      // c:1962-1970 — link super/sub jobs if we're still live.
10802                    let jn_done2 = {
10803                        let g = jt.lock().unwrap();
10804                        (g[jn_idx].stat & jst::DONE) != 0
10805                    };
10806                    if !jn_done2 {
10807                        let mut g = jt.lock().unwrap();
10808                        g[lpj as usize].other = newjob as i32; // c:1964
10809                        g[lpj as usize].stat |= jst::SUPERJOB; // c:1965
10810                        g[jn_idx].stat |= jst::SUBJOB | jst::NOPRINT; // c:1966
10811                        g[jn_idx].other = list_pipe_pid.load(Ordering::Relaxed); // c:1967
10812                        if hasprocs(&g, lpj as usize) {
10813                            g[jn_idx].gleader = g[lpj as usize].gleader; // c:1968-1969
10814                        }
10815                    }
10816                    // c:1971-1972 — stop the LHS group so the whole pipe
10817                    // suspends together.
10818                    let (do_kill, gl2) = {
10819                        let g = jt.lock().unwrap();
10820                        (
10821                            (list_pipe.load(Ordering::Relaxed) != 0 || last1 != 0)
10822                                && hasprocs(&g, lpj as usize),
10823                            g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0),
10824                        )
10825                    };
10826                    if do_kill {
10827                        unsafe { libc::killpg(gl2, libc::SIGSTOP) };
10828                    }
10829                    break; // c:1973
10830                } else {
10831                    // c:1975-2004 — child: become our own group, stop, then
10832                    // continue as the RHS sub-shell.
10833                    unsafe { libc::close(synch[0]) }; // c:1976
10834                    entersubsh(esub::ASYNC, None); // c:1977
10835                    let mypid = unsafe { libc::getpid() };
10836                    mypgrp.store(mypid, Ordering::Relaxed);
10837                    unsafe { libc::setpgid(0, mypid) }; // c:1992 setpgrp
10838                    unsafe { libc::close(synch[1]) }; // c:1993
10839                    unsafe { libc::kill(mypid, libc::SIGSTOP) }; // c:1994
10840                    list_pipe.store(0, Ordering::Relaxed); // c:1995
10841                    list_pipe_child.store(1, Ordering::Relaxed); // c:1996
10842                    dosetopt(INTERACTIVE, 0, 0); // c:1997 `opts[INTERACTIVE] = 0`
10843                    if ERRBRK_SAVED.load(Ordering::Relaxed) != 0 {
10844                        // c:1998-2003 — restore saved break/errflag state.
10845                        errflag.store(
10846                            PREV_ERRFLAG.load(Ordering::Relaxed)
10847                                | (errflag.load(Ordering::Relaxed) & ERRFLAG_INT),
10848                            Ordering::Relaxed,
10849                        );
10850                        BREAKS.store(PREV_BREAKS.load(Ordering::Relaxed), Ordering::SeqCst);
10851                    }
10852                    break;
10853                }
10854            } else if subsh.load(Ordering::Relaxed) != 0 && {
10855                let g = jt.lock().unwrap();
10856                (g[jn_idx].stat & jst::STOPPED) != 0
10857            } {
10858                // c:2008-2012
10859                if thisjob_get() == newjob as i32 {
10860                    let mut g = jt.lock().unwrap();
10861                    makerunning(&mut g, jn_idx);
10862                } else {
10863                    thisjob_set(newjob as i32);
10864                }
10865            } else {
10866                break; // c:2015
10867            }
10868        }
10869
10870        child_unblock(); // c:2017
10871        unqueue_signals(); // c:2018
10872
10873        // c:2020-2026 — a signal-killed list_pipe: drop this job and
10874        // forward the signal to the enclosing job's group.
10875        let lastval_now = LASTVAL.load(Ordering::Relaxed);
10876        let drop_and_signal =
10877            list_pipe.load(Ordering::Relaxed) != 0 && (lastval_now & 0o200) != 0 && pj >= 0 && {
10878                let g = jt.lock().unwrap();
10879                (g[jn_idx].stat & jst::INUSE) == 0 || (g[jn_idx].stat & jst::DONE) != 0
10880            };
10881        if drop_and_signal {
10882            {
10883                let mut g = jt.lock().unwrap();
10884                deletejob(&mut g, jn_idx, false); // c:2022
10885            }
10886            jn_idx = pj as usize; // c:2023 `jn = jobtab + pj;`
10887            let gl = {
10888                let g = jt.lock().unwrap();
10889                g[jn_idx].gleader
10890            };
10891            if gl != 0 {
10892                killjb(jn_idx, lastval_now & !0o200); // c:2025
10893            }
10894        }
10895        // c:2027-2030 — final cleanup deletejob for a done/child job.
10896        let final_delete = list_pipe_child.load(Ordering::Relaxed) != 0 || {
10897            let g = jt.lock().unwrap();
10898            (g[jn_idx].stat & jst::DONE) != 0
10899                && (list_pipe.load(Ordering::Relaxed) != 0
10900                    || (pline_level.load(Ordering::Relaxed) != 0
10901                        && (g[jn_idx].stat & jst::SUBJOB) == 0))
10902        };
10903        if final_delete {
10904            let mut g = jt.lock().unwrap();
10905            deletejob(&mut g, jn_idx, false); // c:2030
10906        }
10907        thisjob_set(pj); // c:2031
10908    } else {
10909        unqueue_signals(); // c:2034
10910    }
10911
10912    // c:2035-2036 — apply `!` negation to the pipeline status.
10913    if (slflags & WC_SUBLIST_NOT) != 0
10914        && errflag.load(Ordering::Relaxed) == 0
10915        && RETFLAG.load(Ordering::SeqCst) == 0
10916    {
10917        let lv = LASTVAL.load(Ordering::Relaxed);
10918        LASTVAL.store(i32::from(lv == 0), Ordering::Relaxed);
10919    }
10920
10921    if pline_level.load(Ordering::Relaxed) == 0 {
10922        simple_pline.store(old_simple_pline, Ordering::Relaxed); // c:2039
10923    }
10924    LASTVAL.load(Ordering::Relaxed) // c:2040 `return lastval;`
10925}
10926
10927// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
10928// inlined at every call site (execsimple, execpline) as the match
10929// expression that selects the right execX function. There's no
10930// separate Rust fn for it because:
10931//   - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
10932//     occupies the canonical name (handling precommand modifiers).
10933//   - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
10934//     a table lookup at exec.c:5499 — not a separate function.
10935#[cfg(any())]
10936mod _execcmd_tail_doc_anchor {
10937    // c:2901-3700 — see inlined match in execpline + execsimple above.
10938    // c:5499 — execfuncs[] table inlined as the same match.
10939}
10940
10941
10942/// Port of `execcmd_exec()` from `Src/exec.c:2901` — C decl `execcmd_exec(Estate state, Execcmd_params eparams, int input, int output, int how, int last1, int close_if_forked)`.
10943/// int input, int output, int how, int last1, int close_if_forked)`
10944/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
10945/// level of the hierarchy.
10946///
10947/// Line-by-line port of the full 1500-line C body. Sections:
10948///   c:2904-2916  — locals
10949///   c:2917-2924  — eparams field unpacking
10950///   c:2934-2939  — Z_TIMED + doneps4 reset
10951///   c:2945-2960  — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
10952///   c:2962-2986  — %job head rewrite + AUTORESUME prefix match
10953///   c:2988-3011  — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
10954///   c:3013-3283  — precommand-modifier walk (BINF_PREFIX strip)
10955///                  + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
10956///   c:3285-3307  — prefork substitutions + magic_assign
10957///   c:3309-3406  — empty-command branch (redir / nullexec / BINF_COMMAND)
10958///   c:3409-3466  — main resolution loop (shfunc / builtin / autocd)
10959///   c:3468-3479  — errflag bail-out
10960///   c:3480-3492  — text fetch + setunderscore
10961///   c:3494-3524  — rm * safety prompt
10962///   c:3526-3591  — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
10963///   c:3593-3632  — external resolution (cmdnamtab, hashcmd, AUTOCD)
10964///   c:3634-3697  — fork decision
10965///   c:3700-3955  — redir loop + multio + addfd + xpandredir
10966///   c:3957-3961  — multio close (`mfds[i].ct >= 2` → closemn)
10967///   c:3963-3995  — nullexec branch
10968///   c:3996-4327  — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
10969///                  execbuiltin / execshfunc / execute)
10970///   c:4330-4365  — `err:` label: forked-child fd cleanup, fixfds
10971///   c:4366-4403  — `done:` label: POSIX special-builtin error escalation,
10972///                  shelltime stop, newxtrerr close, AUTOCONTINUE restore
10973///
10974/// **Helpers used from elsewhere in exec.rs:**
10975///   - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
10976///   - `restore_params(restorelist, removelist)` → Src/exec.c:4463
10977///   - `isreallycom(cn)` → Src/exec.c:2670
10978///   - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
10979///   - `execautofn_basic(state, do_exec)` → Src/exec.c:5608
10980///   - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
10981///
10982/// Called from `execpline` (wordcode tree-walker). The fusevm bytecode
10983/// VM uses `execcmd_compile_head` + `compile_simple` instead.
10984#[allow(non_snake_case)]
10985#[allow(clippy::too_many_arguments)]
10986#[allow(clippy::redundant_field_names)]
10987#[allow(unused_assignments)]
10988#[allow(unused_variables)]
10989#[allow(unused_mut)]
10990#[allow(unused_imports)]
10991#[allow(unreachable_code)]
10992#[allow(dead_code)]
10993pub fn execcmd_exec(
10994    state: &mut estate,
10995    eparams: &mut crate::ported::zsh_h::execcmd_params,
10996    input: i32,
10997    output: i32,
10998    mut how: i32,
10999    mut last1: i32,
11000    close_if_forked: i32,
11001) {
11002    use crate::ported::zsh_h::{
11003        Star, ASG_ARRAY, ASG_KEY_VALUE, AUTOCD, AUTOCONTINUE, AUTORESUME, BGNICE,
11004        BINF_ASSIGN as BINF_ASSIGN_FLAG, BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_MAGICEQUALS,
11005        BINF_NOGLOB, BINF_PREFIX, BINF_PSPECIAL, CSHNULLCMD, ERRFLAG_INT, EXECOPT, FDT_EXTERNAL,
11006        FDT_INTERNAL, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHCMDS, HFILE_USE_OPTIONS,
11007        IS_APPEND_REDIR, IS_DASH, IS_ERROR_REDIR, MAGICEQUALSUBST, NOTIFY, PM_READONLY, PM_SPECIAL,
11008        POSIXBUILTINS, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET,
11009        PRINTEXITVALUE, RCS, REDIR_CLOSE, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN,
11010        REDIR_MERGEOUT, REDIR_OUTPIPE, REDIR_READ, REDIR_READWRITE, RMSTARSILENT, SHINSTDIN,
11011        SHNULLCMD, STAT_BUILTIN, STAT_CURSH, STAT_DONE, STAT_NOPRINT, WC_ASSIGN as ZWC_ASSIGN,
11012        WC_ASSIGN_INC as ZWC_ASSIGN_INC, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
11013        WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
11014        WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2, WC_AUTOFN, WC_CURSH, WC_FUNCDEF, WC_REDIR, WC_SIMPLE,
11015        WC_SUBSH, WC_TIMED, WC_TYPESET, XTRACE, Z_ASYNC, Z_DISOWN, Z_SYNC, Z_TIMED,
11016    };
11017
11018    // c:2900
11019
11020    // c:2904-2916 — locals.
11021    let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
11022    let mut filelist: Vec<jobfile> = Vec::new(); // c:2905 LinkList filelist = NULL
11023                                                 // c:2906 LinkNode node; (loop locals)
11024                                                 // c:2907 Redir fn;       (loop locals)
11025    let mut mfds: [Option<Box<multio>>; 10] =                              // c:2908 struct multio *mfds[10]
11026        [None, None, None, None, None, None, None, None, None, None];
11027    let mut text: Option<String> = None; // c:2909 char *text
11028    let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
11029    let mut fil: i32; // c:2911 int fil
11030    let mut dfil: i32 = 0; // c:2911 int dfil
11031    let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
11032    let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
11033    let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
11034    let mut i: i32; // c:2911 int i
11035    let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
11036    let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
11037    let mut forked: i32 = 0; // c:2912 int forked = 0
11038    let mut old_lastval: i32; // c:2912 int old_lastval
11039    let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
11040    let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
11041    let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
11042    let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
11043                                  // c:2914 — `Various flags to the command.`
11044    let mut cflags: u32 = 0; // c:2915 int cflags = 0
11045    let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
11046    let mut checked: i32 = 0; // c:2915 int checked = 0
11047    let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
11048                                 // c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;`. newxtrerr
11049                                 // is only made while xtrerr == stderr (c:3767), so the
11050                                 // c:4449 restore to oxtrerr is a restore to stderr.
11051    let mut newxtrerr: Option<i32> = None; // c:2916
11052
11053    // c:2917-2924 — eparams field unpacking. `args` / `redir` are
11054    // pulled into mutable locals so the body can mutate them
11055    // independently of the eparams struct.
11056    let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
11057    let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
11058    let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
11059    let typ: i32 = eparams.typ; // c:2924 int type
11060                                // c:2925-2929 — `preargs comes from expanding the head of the args
11061                                // list in order to check for prefix commands.` declared later.
11062
11063    // c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
11064    // + struct timespec then. Rust port keeps the names so the shelltime
11065    // start+stop calls map directly. Use jobs.rs's existing types.
11066    let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
11067    let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
11068    let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
11069    if (how & Z_TIMED as i32) != 0 {
11070        // c:2936
11071        crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0);
11072        // c:2937
11073    }
11074
11075    doneps4.store(0, Ordering::Relaxed); // c:2939
11076
11077    // c:2941-2947 — `If assignment but no command get the status from
11078    // variable assignment.`
11079    old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
11080    if args.is_none() && varspc.is_some() {
11081        // c:2946
11082        let ef = errflag.load(Ordering::Relaxed);
11083        LASTVAL.store(
11084            if ef != 0 {
11085                ef
11086            } else {
11087                cmdoutval.load(Ordering::Relaxed)
11088            },
11089            Ordering::Relaxed,
11090        ); // c:2947
11091    }
11092    // c:2948-2954 — `If there are arguments, we should reset the status
11093    // for the command before execution---unless we are using the result
11094    // of a command substitution...`
11095    use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
11096
11097    // c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
11098    // Already initialised above via array literals; preserved as
11099    // comment for parity. The C loop maps to a no-op in Rust.
11100
11101    // c:2962-2973 — `%job` head rewrite.
11102    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
11103        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
11104        && args.as_ref().unwrap()[0].starts_with('%')
11105    {
11106        // c:2964-2965
11107        if (how & Z_DISOWN as i32) != 0 {
11108            // c:2966
11109            oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) {
11110                1
11111            } else {
11112                0
11113            }; // c:2967
11114            opt_state_set("autocontinue", true); // c:2968
11115        }
11116        // c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
11117        let head = if (how & Z_DISOWN as i32) != 0 {
11118            "disown".to_string()
11119        } else if (how & Z_ASYNC as i32) != 0 {
11120            "bg".to_string()
11121        } else {
11122            "fg".to_string()
11123        };
11124        if let Some(ref mut v) = args {
11125            v.insert(0, head);
11126        }
11127        how = Z_SYNC as i32; // c:2972
11128    }
11129
11130    // c:2975-2986 — AUTORESUME prefix match against jobtab.
11131    if isset(AUTORESUME)
11132        && typ == WC_SIMPLE as i32
11133        && (how & Z_SYNC as i32) != 0
11134        && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
11135        && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
11136        && input == 0
11137        && args.as_ref().unwrap().len() == 1
11138    {
11139        // c:2979-2981
11140        if unset(NOTIFY) {
11141            // c:2982 — `scanjobs();` via the canonical port
11142            // (Src/jobs.c:1993).
11143            if let Some(jt) = JOBTAB.get() {
11144                let mut guard = jt.lock().unwrap();
11145                crate::ported::jobs::scanjobs(&mut guard);
11146            }
11147        }
11148        // c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
11149        let head = args.as_ref().unwrap()[0].clone();
11150        let maxjob = JOBTAB
11151            .get()
11152            .map(|m| m.lock().unwrap().len() as i32)
11153            .unwrap_or(0);
11154        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
11155        // c:2982 — `findjobnam(s)`. Canonical port at
11156        // jobs.rs::findjobnam matches against `proc.text`, which is
11157        // the command text actually saved into the job at fork —
11158        // matching C exactly. Returns the job index if any non-
11159        // SUBJOB jobtab entry's first-proc text starts with `s`.
11160        let found = if let Some(jt) = JOBTAB.get() {
11161            let guard = jt.lock().unwrap();
11162            crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
11163        } else {
11164            false
11165        };
11166        if found {
11167            // c:2985 — `pushnode(args, dupstring("fg"));`
11168            if let Some(ref mut v) = args {
11169                v.insert(0, "fg".to_string());
11170            }
11171        }
11172    }
11173
11174    // ====================================================================
11175    // Helpers ported elsewhere in exec.rs, imported for the body below.
11176    // ====================================================================
11177    // save_params + restore_params — top-level ports in exec.rs
11178    // (c:4410 / c:4464). Both bridged via `use` below.
11179    use crate::ported::exec::{restore_params, save_params};
11180    // isreallycom — top-level port at exec.rs (c:972). Bridges the
11181    // local shadow that this fn body used pre-port.
11182    use crate::ported::exec::isreallycom;
11183    // execautofn_basic — top-level port at exec.rs (c:5608).
11184    use crate::ported::exec::execautofn_basic;
11185    // C `execerr` macro (c:2700) was a goto-equivalent:
11186    //   errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
11187    // Rust expansion: each call site inlines the errflag+LASTVAL set
11188    // and then `break`s out of the enclosing redir loop. The loop's
11189    // post-loop errflag check at c:3949 routes to execcmd_exec_err_path
11190    // for the cleanup tail. No macro needed.
11191
11192    // c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
11193    // fork-immediately fast path.
11194    if (how & Z_ASYNC as i32) != 0
11195        || output != 0
11196        || (last1 == 2 && input != 0 && {
11197            // c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
11198            // EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
11199            // ported `emulation` static at options.rs:1044 holds the
11200            // current bit; compare against EMULATE_SH (zsh_h:2883).
11201            (crate::ported::options::emulation.load(Ordering::Relaxed)
11202                & crate::ported::zsh_h::EMULATE_SH)
11203                != 0
11204        })
11205    {
11206        // c:2988
11207        // c:2999 — `text = getjobtext(state->prog, eparams->beg);`
11208        text = Some(crate::ported::text::getjobtext(
11209            state.prog.clone(),
11210            Some(eparams.beg),
11211        ));
11212        // c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
11213        let mut filelist_for_fork = filelist.clone();
11214        let pid = execcmd_fork(
11215            state,
11216            how,
11217            typ,
11218            varspc,
11219            &mut filelist_for_fork,
11220            text.as_deref().unwrap_or(""),
11221            oautocont,
11222            close_if_forked,
11223        );
11224        match pid {
11225            -1 => {
11226                // c:3002-3003 — `goto fatal;` — fall through to fatal:
11227                // label at c:4377. We model this with a flag.
11228                redir_err = 1; // pretend redir error to trigger fatal arm
11229                               // Continue to done label by setting forked + jumping forward.
11230                               // Simplified: just bail with status 1 + fatal handling at
11231                               // the bottom of the fn.
11232                return execcmd_exec_done_path(
11233                    redir_err,
11234                    oautocont,
11235                    how,
11236                    &mut shti,
11237                    &mut chti,
11238                    &mut then_ts,
11239                    forked,
11240                    &mut newxtrerr,
11241                    cflags,
11242                    orig_cflags,
11243                    is_cursh,
11244                    do_exec,
11245                );
11246            }
11247            0 => {
11248                // c:3004 — child returned 0; continue with the body.
11249            }
11250            _ => {
11251                // c:3007 — parent: `return;` — but first restore AUTOCONTINUE
11252                // and shelltime stop. Inline the done-tail equivalent.
11253                if oautocont >= 0 {
11254                    opt_state_set("autocontinue", oautocont != 0);
11255                }
11256                if (how & Z_TIMED as i32) != 0 {
11257                    crate::ported::jobs::shelltime(
11258                        Some(&mut shti),
11259                        Some(&mut chti),
11260                        Some(&mut then_ts),
11261                        1,
11262                    );
11263                }
11264                return;
11265            }
11266        }
11267        last1 = 1; // c:3009
11268        forked = 1; // c:3009
11269    } else {
11270        // c:3010-3011
11271        text = None;
11272    }
11273
11274    // ====================================================================
11275    // c:3013-3283 — precommand-modifier walk.
11276    //
11277    // The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
11278    // BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
11279    // (above this fn). Call into it to keep DRY, then convert the
11280    // returned dispatch struct's fields into the locals C uses
11281    // (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
11282    // exec_argv0, precmd_skip).
11283    //
11284    // Per [[feedback_true_port_pattern]] the C function does this
11285    // walk inline. Reusing the existing port is acceptable because
11286    // `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
11287    // citations there match. The C tree-walker and the fusevm
11288    // compile-time walker arrive at identical dispatch decisions
11289    // from the same input.
11290    // ====================================================================
11291    let mut preargs: Vec<String> = Vec::new();
11292    let mut exec_argv0: Option<String> = None;
11293    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
11294        // c:3018
11295        let head_args: Vec<String> = args.as_ref().unwrap().clone();
11296        let dispatch = execcmd_compile_head(&head_args, typ as u32);
11297        // Pull fields into local mirror of C state.
11298        cflags = dispatch.cflags;
11299        if dispatch.is_builtin {
11300            is_builtin = 1;
11301        }
11302        if dispatch.is_shfunc {
11303            is_shfunc = 1;
11304        }
11305        if dispatch.use_defpath {
11306            use_defpath = 1;
11307        }
11308        exec_argv0 = dispatch.exec_argv0;
11309        // c:3115 — `orig_cflags |= cflags;` accumulated by the head walk.
11310        orig_cflags = dispatch.orig_cflags;
11311        // c:3030-3086 — strip the precmd-modifier prefix from args.
11312        // In C, the walk pulls one arg at a time from `args` into
11313        // `preargs` via execcmd_getargs, then uremnodes each
11314        // BINF_PREFIX modifier. At loop exit C's `preargs` holds the
11315        // dispatch target (1 element) and `args` holds whatever's
11316        // left; `joinlists(preargs, args)` (c:3305-3306) splices the
11317        // target back onto the head. The net effect is `args` with
11318        // the precmd modifiers stripped. We compute that final shape
11319        // directly and leave `preargs` empty so the joinlists arm
11320        // below is a no-op. Without this, preargs=head_args[skip..]
11321        // plus a non-draining args was double-counting every word
11322        // when both held the same suffix.
11323        if let Some(ref mut v) = args {
11324            v.drain(0..dispatch.precmd_skip);
11325            // c:3154 — `pushnode(preargs, "command");` /* Leave everything
11326            // alone, dispatch to whence. We need to put the name back in
11327            // the list. */  `command -v`/`-V` keeps its whole word list and
11328            // dispatches to the `command` builtin (bin_whence with
11329            // BIN_COMMAND), so the name goes back on the front after the
11330            // precommand strip. `execcmd_compile_head` only REPORTS this
11331            // via `has_command_vv` (the fusevm compiler acts on it); the
11332            // tree-walker has to perform it. Without this the head after
11333            // the strip was `-v`, which resolves to nothing —
11334            // `x=command; $x -v cat` printed nothing.
11335            if dispatch.has_command_vv {
11336                v.insert(0, "command".to_string()); // c:3154
11337            } else if dispatch.use_defpath {
11338                // c:3165 — `if (pnode) uremnode(preargs, pnode);` /* We
11339                // don't need this node as we're not treating "command" as a
11340                // builtin this time. */  The `-p` word is consumed here,
11341                // same reason as above: compile_head drops it from its own
11342                // local list only.
11343                if v.first()
11344                    .map(|w| {
11345                        let b = w.as_bytes();
11346                        b.len() >= 2
11347                            && IS_DASH(b[0] as char)
11348                            && w[1..].chars().all(|c| c == 'p' || c == 'v' || c == 'V')
11349                    })
11350                    .unwrap_or(false)
11351                {
11352                    v.remove(0); // c:3165
11353                }
11354                // c:3176-3177 — `if (IS_DASH(argdata[0]) && IS_DASH(argdata[1])
11355                // && !argdata[2]) uremnode(preargs, argnode);`
11356                if v.first().map(|w| w == "--").unwrap_or(false) {
11357                    v.remove(0); // c:3177
11358                }
11359            }
11360        }
11361        let _ = head_args;
11362        preargs.clear();
11363        // c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
11364        // — surface via cflags check: if a typeset-family builtin
11365        // landed, BINF_MAGICEQUALS is in its flags and dispatch
11366        // surfaces it via cflags.
11367        if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
11368            magic_assign = 1;
11369        }
11370        // c:3056 — C's precmd walk sets `hn = builtintab->getnode(...)`
11371        // for the dispatch target before breaking at c:3064. The
11372        // Rust port's execcmd_compile_head returns is_builtin but
11373        // not the entry pointer, and the second resolution loop
11374        // below short-circuits on `is_builtin != 0` (c:3423-3426)
11375        // without re-resolving. Look up the dispatch target now so
11376        // `hn` is non-null at the execbuiltin call (c:4233 /
11377        // exec.rs:10177); otherwise execbuiltin returns 1 silently
11378        // on a null `bn`.
11379        hn = None;
11380        if dispatch.has_command_vv {
11381            // c:3209 — `hn = &commandbn.node;`. The `command -v`/`-V` form
11382            // dispatches through the dedicated descriptor (bin_whence /
11383            // BIN_COMMAND / optstr "pvV"), NOT through builtintab's
11384            // handler-less `BIN_PREFIX("command", …)` row — looking the name
11385            // up there gave a null handler and `command -v cat` printed
11386            // nothing.
11387            hn = Some(&*commandbn as *const builtin as *mut builtin); // c:3209
11388        } else if is_builtin != 0 {
11389            if let Some(target) = args.as_ref().and_then(|v| v.first()) {
11390                if let Some(entry) = BUILTINS.iter().find(|b| b.node.nam == *target) {
11391                    hn = Some(entry as *const builtin as *mut builtin);
11392                }
11393            }
11394        }
11395    } else {
11396        // c:3282-3283 — `else preargs = NULL;`
11397        // We use an empty preargs to model NULL — C's `preargs` is
11398        // only iterated if `nonempty(preargs)` in this branch.
11399    }
11400
11401    // c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
11402    // Sets the file-static `esprefork` (exec.rs:267) so any downstream
11403    // execsubst() call inside this command's expansion uses the same
11404    // prefork flags. Also keep a local copy for the immediate
11405    // prefork(args, esprefork, NULL) below.
11406    let esprefork_v: i32 =
11407        if magic_assign != 0 || (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32) {
11408            PREFORK_TYPESET // c:3300
11409        } else {
11410            0
11411        };
11412    esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
11413
11414    // c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
11415    if args.is_some() && eparams.htok != 0 {
11416        // c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
11417        let mut as_linklist: LinkList<String> = Default::default();
11418        if let Some(ref v) = args {
11419            for s in v {
11420                as_linklist.push_back(s.clone());
11421            }
11422        }
11423        let mut rf = 0i32;
11424        prefork(&mut as_linklist, esprefork_v, &mut rf);
11425        // Move back into args.
11426        let mut out: Vec<String> = Vec::new();
11427        while let Some(s) = as_linklist.pop_front() {
11428            out.push(s);
11429        }
11430        args = Some(out);
11431    }
11432    if !preargs.is_empty() {
11433        // c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
11434        let mut joined = preargs.clone();
11435        if let Some(ref v) = args {
11436            joined.extend(v.iter().cloned());
11437        }
11438        args = Some(joined);
11439    }
11440
11441    // c:3309-3406 — main resolution loop + empty-command branch.
11442    if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
11443        let mut unglobbed: i32 = 0; // c:3310
11444
11445        // c:3312 — `for (;;)` — main resolution loop.
11446        loop {
11447            // c:3315-3318 — globbing or untokenise sweep.
11448            if (cflags & BINF_NOGLOB) == 0 {
11449                while checked == 0
11450                    && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
11451                    && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
11452                    && crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
11453                {
11454                    // c:3318 — `zglob(args, firstnode(args), 0);`
11455                    // zglob takes &mut Vec<String>; isolate the head element
11456                    // by splitting args into [head] and [tail], then re-merging.
11457                    let mut head_vec: Vec<String> = Vec::new();
11458                    if let Some(ref mut v) = args {
11459                        head_vec.push(v.remove(0));
11460                    }
11461                    crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
11462                    if let Some(ref mut v) = args {
11463                        // Re-merge the globbed head ahead of the tail. Inserting
11464                        // one element at a time (`v.insert(i, s)`) memmoves the
11465                        // whole remaining tail on every element, so a head word
11466                        // that globs to K matches costs O(K*n) and a completion
11467                        // sweep over a large directory spins. Splicing the batch
11468                        // in at position 0 shifts the tail exactly once and is
11469                        // order-identical: element i lands at index i.
11470                        v.splice(0..0, head_vec);
11471                    }
11472                }
11473            } else if unglobbed == 0 {
11474                // c:3319-3322
11475                if let Some(ref mut v) = args {
11476                    for s in v.iter_mut() {
11477                        *s = untokenize(s); // c:3321
11478                    }
11479                }
11480                unglobbed = 1; // c:3322
11481            }
11482
11483            // c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
11484            if (cflags & BINF_EXEC) != 0 && last1 != 0 {
11485                do_exec = 1; // c:3328
11486            }
11487
11488            // c:3331-3407 — empty-command branch.
11489            if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
11490                // c:3331 — `if (!args || empty(args))`
11491                if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
11492                    // c:3332 — `if (redir && nonempty(redir))`
11493                    if do_exec != 0 {
11494                        // c:3333 — `Was this "exec < foobar"?`
11495                        nullexec = 1; // c:3335
11496                        break;
11497                    } else if varspc.is_some() {
11498                        // c:3337
11499                        nullexec = 2; // c:3338
11500                        break;
11501                    } else if {
11502                        // c:3340-3341 — `if (!nullcmd || !*nullcmd ||
11503                        //   opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
11504                        let nc = getsparam("NULLCMD");
11505                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
11506                        nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
11507                    } {
11508                        // c:3342 — `zerr("redirection with no command");`
11509                        zerr("redirection with no command");
11510                        LASTVAL.store(1, Ordering::Relaxed); // c:3343
11511                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
11512                        if forked != 0 {
11513                            // c:3345-3346
11514                            crate::ported::builtin::_realexit();
11515                        }
11516                        if (how & Z_TIMED as i32) != 0 {
11517                            // c:3347-3348
11518                            crate::ported::jobs::shelltime(
11519                                Some(&mut shti),
11520                                Some(&mut chti),
11521                                Some(&mut then_ts),
11522                                1,
11523                            );
11524                        }
11525                        return; // c:3349
11526                    } else if {
11527                        // c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
11528                        let nc = getsparam("NULLCMD");
11529                        let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
11530                        nc_empty || isset(SHNULLCMD)
11531                    } {
11532                        // c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
11533                        if args.is_none() {
11534                            args = Some(Vec::new());
11535                        }
11536                        args.as_mut().unwrap().push(":".to_string()); // c:3353
11537                    } else if {
11538                        // c:3354-3356 — `readnullcmd && *readnullcmd &&
11539                        //   peekfirst(redir).type == REDIR_READ &&
11540                        //   !nextnode(firstnode(redir))`
11541                        let rnc = getsparam("READNULLCMD");
11542                        let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
11543                        rnc_nonempty
11544                            && redir.as_ref().unwrap().len() == 1
11545                            && redir.as_ref().unwrap()[0].typ == REDIR_READ
11546                    } {
11547                        // c:3357-3359
11548                        if args.is_none() {
11549                            args = Some(Vec::new());
11550                        }
11551                        let rnc = getsparam("READNULLCMD").unwrap_or_default();
11552                        args.as_mut().unwrap().push(rnc); // c:3359
11553                    } else {
11554                        // c:3360-3364 — default: nullcmd as command.
11555                        if args.is_none() {
11556                            args = Some(Vec::new());
11557                        }
11558                        let nc = getsparam("NULLCMD").unwrap_or_default();
11559                        args.as_mut().unwrap().push(nc); // c:3363
11560                    }
11561                } else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
11562                    // c:3365 — bare `command`: lastval=0, return.
11563                    LASTVAL.store(0, Ordering::Relaxed); // c:3366
11564                    if forked != 0 {
11565                        crate::ported::builtin::_realexit(); // c:3367-3368
11566                    }
11567                    if (how & Z_TIMED as i32) != 0 {
11568                        crate::ported::jobs::shelltime(
11569                            Some(&mut shti),
11570                            Some(&mut chti),
11571                            Some(&mut then_ts),
11572                            1,
11573                        ); // c:3369-3370
11574                    }
11575                    return; // c:3371
11576                } else {
11577                    // c:3372-3406 — no arguments default arm.
11578                    // c:3378-3385 — badcshglob == 1 → no match.
11579                    if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
11580                        zerr("no match"); // c:3379
11581                        LASTVAL.store(1, Ordering::Relaxed); // c:3380
11582                        if forked != 0 {
11583                            crate::ported::builtin::_realexit(); // c:3381-3382
11584                        }
11585                        if (how & Z_TIMED as i32) != 0 {
11586                            crate::ported::jobs::shelltime(
11587                                Some(&mut shti),
11588                                Some(&mut chti),
11589                                Some(&mut then_ts),
11590                                1,
11591                            ); // c:3383-3384
11592                        }
11593                        return; // c:3385
11594                    }
11595                    // c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
11596                    cmdoutval.store(
11597                        if use_cmdoutval.load(Ordering::Relaxed) != 0 {
11598                            LASTVAL.load(Ordering::Relaxed)
11599                        } else {
11600                            0
11601                        },
11602                        Ordering::Relaxed,
11603                    );
11604                    if varspc.is_some() {
11605                        // c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
11606                        LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
11607                        addvars(state, varspc.unwrap_or(0), 0); // c:3391
11608                    }
11609                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11610                        // c:3393
11611                        LASTVAL.store(1, Ordering::Relaxed); // c:3394
11612                    } else {
11613                        // c:3395-3396
11614                        LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
11615                    }
11616                    if isset(XTRACE) {
11617                        // c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
11618                        crate::fusevm_bridge::xtrerr_fputs("\n");
11619                        crate::fusevm_bridge::xtrerr_flush();
11620                    }
11621                    if forked != 0 {
11622                        crate::ported::builtin::_realexit(); // c:3401-3402
11623                    }
11624                    if (how & Z_TIMED as i32) != 0 {
11625                        crate::ported::jobs::shelltime(
11626                            Some(&mut shti),
11627                            Some(&mut chti),
11628                            Some(&mut then_ts),
11629                            1,
11630                        ); // c:3403-3404
11631                    }
11632                    return; // c:3405
11633                }
11634            }
11635
11636            // c:3423-3426 — `if (errflag || checked || is_builtin ||
11637            //   (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
11638            // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11639            // the C line cited above tests the WHOLE errflag, so masking here let an
11640            // interrupted shell keep going where zsh stops.
11641            if errflag.load(Ordering::Relaxed) != 0
11642                || checked != 0
11643                || is_builtin != 0
11644                || if isset(POSIXBUILTINS) {
11645                    (cflags & BINF_EXEC) != 0
11646                } else {
11647                    (cflags & BINF_COMMAND) != 0
11648                }
11649            {
11650                // c:3423
11651                break; // c:3426
11652            }
11653
11654            // c:3428 — `cmdarg = (char *) peekfirst(args);`
11655            let cmdarg = args.as_ref().unwrap()[0].clone();
11656
11657            // c:3429-3433 — shfunc lookup.
11658            // c:3485 — `(hn = shfunctab->getnode(shfunctab, cmdarg))`.
11659            // Same misport as the compile-time head walk above: C's
11660            // `shfunctab->getnode` is `gethashnode`
11661            // (Src/hashtable.c:821), an O(1) probe that skips DISABLED
11662            // nodes, not a full-table walk.
11663            if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
11664                let in_shfunctab = shfunctab_lock()
11665                    .read()
11666                    .map(|t| !t.getnode(cmdarg.as_str()).is_null())
11667                    .unwrap_or(false);
11668                if in_shfunctab {
11669                    is_shfunc = 1; // c:3431
11670                    break; // c:3432
11671                }
11672            }
11673            // c:3434-3447 — builtintab lookup.
11674            let builtin_entry: Option<&'static builtin> = BUILTINS
11675                .iter()
11676                .find(|b| b.node.nam.as_str() == cmdarg.as_str());
11677            if builtin_entry.is_none() {
11678                if (cflags & BINF_BUILTIN) != 0 {
11679                    // c:3435 — `zwarn("no such builtin: %s", cmdarg);`
11680                    zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
11681                    LASTVAL.store(1, Ordering::Relaxed); // c:3437
11682                    if oautocont >= 0 {
11683                        // c:3438-3439
11684                        opt_state_set("autocontinue", oautocont != 0);
11685                    }
11686                    if forked != 0 {
11687                        crate::ported::builtin::_realexit(); // c:3440-3441
11688                    }
11689                    if (how & Z_TIMED as i32) != 0 {
11690                        crate::ported::jobs::shelltime(
11691                            Some(&mut shti),
11692                            Some(&mut chti),
11693                            Some(&mut then_ts),
11694                            1,
11695                        ); // c:3442-3443
11696                    }
11697                    return; // c:3444
11698                }
11699                break; // c:3446
11700            }
11701            let entry = builtin_entry.unwrap();
11702            // c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
11703            if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
11704                is_builtin = 1; // c:3449
11705                                // c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
11706                                // module autoload check. zshrs's BUILTINS table is
11707                                // static and pre-resolved; treat resolvebuiltin as
11708                                // pass-through.
11709                hn = Some(entry as *const builtin as *mut builtin);
11710                break; // c:3459
11711            }
11712            // c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
11713            cflags &= !(BINF_BUILTIN | BINF_COMMAND);
11714            cflags |= entry.node.flags as u32;
11715            if let Some(ref mut v) = args {
11716                v.remove(0); // c:3463 uremnode(args, firstnode(args))
11717            }
11718            hn = None; // c:3464
11719        }
11720    }
11721
11722    // c:3468-3478 — errflag bail-out.
11723    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11724    // the C line cited above tests the WHOLE errflag, so masking here let an
11725    // interrupted shell keep going where zsh stops.
11726    if errflag.load(Ordering::Relaxed) != 0 {
11727        // c:3468
11728        if LASTVAL.load(Ordering::Relaxed) == 0 {
11729            // c:3469
11730            LASTVAL.store(1, Ordering::Relaxed); // c:3470
11731        }
11732        if oautocont >= 0 {
11733            opt_state_set("autocontinue", oautocont != 0);
11734            // c:3472
11735        }
11736        if forked != 0 {
11737            crate::ported::builtin::_realexit(); // c:3473-3474
11738        }
11739        if (how & Z_TIMED as i32) != 0 {
11740            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
11741            // c:3475-3476
11742        }
11743        return; // c:3477
11744    }
11745
11746    // c:3480-3483 — `Get the text associated with this command.`
11747    if text.is_none()
11748        && sfcontext.load(Ordering::Relaxed) == 0
11749        && (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
11750    {
11751        // c:3481-3482
11752        text = Some(crate::ported::text::getjobtext(
11753            state.prog.clone(),
11754            Some(eparams.beg),
11755        )); // c:3483
11756    }
11757
11758    // c:3485-3492 — `Set up special parameter $_`.
11759    if typ != WC_FUNCDEF as i32 {
11760        // c:3490
11761        let last_str = args
11762            .as_ref()
11763            .and_then(|v| v.last())
11764            .cloned()
11765            .unwrap_or_default();
11766        setunderscore(&last_str); // c:3491-3492
11767    }
11768
11769    // c:3494-3524 — `Warn about "rm *"`.
11770    if typ == WC_SIMPLE as i32
11771        && crate::ported::zsh_h::interact()
11772        && unset(RMSTARSILENT)
11773        && isset(SHINSTDIN)
11774        && args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
11775        && args.as_ref().unwrap()[0] == "rm"
11776    {
11777        // c:3495-3497
11778        let args_v = args.as_ref().unwrap().clone();
11779        for s in args_v.iter().skip(1) {
11780            // c:3500
11781            // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11782            // the C line cited above tests the WHOLE errflag, so masking here let an
11783            // interrupted shell keep going where zsh stops.
11784            if errflag.load(Ordering::Relaxed) != 0 {
11785                break;
11786            }
11787            let l = s.len();
11788            // c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
11789            if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
11790                let pwd = getsparam("PWD").unwrap_or_default();
11791                if !crate::ported::utils::checkrmall(&pwd) {
11792                    // c:3506
11793                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
11794                    break; // c:3508
11795                }
11796            } else if l >= 2 {
11797                // c:3510 — `s[l-2] == '/' && s[l-1] == Star`
11798                let bytes = s.as_bytes();
11799                if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
11800                    let prefix = if l == 2 {
11801                        "/".to_string()
11802                    } else {
11803                        String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
11804                    };
11805                    if !crate::ported::utils::checkrmall(&prefix) {
11806                        // c:3518
11807                        errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
11808                        break; // c:3520
11809                    }
11810                }
11811            }
11812        }
11813    }
11814
11815    // c:3526-3580 — type-specific dispatch prep.
11816    if typ == WC_FUNCDEF as i32 {
11817        // c:3526
11818        if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
11819            // c:3535 — `Nonymous, don't do redirections here`
11820            redir = None; // c:3537
11821        }
11822    } else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
11823        // c:3539
11824        // c:3540-3559 — shfunc / autoload preload.
11825        if is_shfunc != 0 {
11826            // c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
11827        } else {
11828            // c:3543-3559 — autoload preload.
11829            if let Some(ref mut sh) = state.prog.shf {
11830                let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
11831                let r = loadautofn(shf_ptr, 1, 0, 0);
11832                if r != 0 {
11833                    // c:3551 — `lastval = 1;`
11834                    LASTVAL.store(1, Ordering::Relaxed);
11835                    if oautocont >= 0 {
11836                        opt_state_set("autocontinue", oautocont != 0);
11837                    }
11838                    if forked != 0 {
11839                        crate::ported::builtin::_realexit();
11840                    }
11841                    if (how & Z_TIMED as i32) != 0 {
11842                        crate::ported::jobs::shelltime(
11843                            Some(&mut shti),
11844                            Some(&mut chti),
11845                            Some(&mut then_ts),
11846                            1,
11847                        );
11848                    }
11849                    return; // c:3558
11850                }
11851            }
11852        }
11853        // c:3561-3579 — shf->redir append: a function definition can
11854        // carry extra redirs (`f() { ... } < file`), captured as a
11855        // separate Eprog in shf->redir. Walk that Eprog with a temp
11856        // estate, extract its redirs with ecgetredirs, then merge
11857        // into the live `redir` list.
11858        // Resolve shfunc by name (hn is *mut builtin so we go through
11859        // shfunctab as in the dispatch site at c:4102).
11860        let shfn_name = args
11861            .as_ref()
11862            .and_then(|v| v.first())
11863            .cloned()
11864            .unwrap_or_default();
11865        let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
11866            if let Ok(tab) = shfunctab_lock().read() {
11867                tab.get(&shfn_name).and_then(|s| s.redir.clone())
11868            } else {
11869                None
11870            }
11871        };
11872        if let Some(red_eprog) = shf_redir_eprog {
11873            // c:3566-3571 — build temp estate from shf->redir.
11874            let mut tmp_state = estate {
11875                prog: red_eprog.clone(),
11876                pc: 0,
11877                strs: red_eprog.strs.clone(),
11878                strs_offset: 0,
11879            };
11880            // c:3572 — `redir2 = ecgetredirs(&s);`
11881            let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
11882            // c:3573-3578 — merge into existing redir.
11883            if redir.is_none() {
11884                redir = Some(redir2); // c:3574
11885            } else if let Some(ref mut r) = redir {
11886                // c:3576-3577 — append.
11887                for n in redir2 {
11888                    r.push(n);
11889                }
11890            }
11891        }
11892    }
11893
11894    // c:3582-3591 — errflag bail-out (2).
11895    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11896    // the C line cited above tests the WHOLE errflag, so masking here let an
11897    // interrupted shell keep going where zsh stops.
11898    if errflag.load(Ordering::Relaxed) != 0 {
11899        // c:3582
11900        LASTVAL.store(1, Ordering::Relaxed); // c:3583
11901        if oautocont >= 0 {
11902            opt_state_set("autocontinue", oautocont != 0);
11903            // c:3584-3585
11904        }
11905        if forked != 0 {
11906            crate::ported::builtin::_realexit(); // c:3586-3587
11907        }
11908        if (how & Z_TIMED as i32) != 0 {
11909            crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
11910            // c:3588-3589
11911        }
11912        return; // c:3590
11913    }
11914
11915    // c:3593-3632 — external resolution + AUTOCD.
11916    if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
11917        // c:3593
11918        // Consumed here so a nested command dispatched from this one
11919        // (a function body) does not inherit it.
11920        let redirs_preapplied = REDIRS_PREAPPLIED.with(|c| c.replace(false));
11921        let trycd = isset(AUTOCD)
11922            && isset(SHINSTDIN)
11923            && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
11924            && !redirs_preapplied
11925            && args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
11926            && !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
11927        let mut external_found = false;
11928        if hn.is_none() {
11929            // c:3600
11930            let cmdarg = args.as_ref().unwrap()[0].clone();
11931            let mut dohashcmd = isset(HASHCMDS); // c:3604
11932                                                 // c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
11933            let mut have_cmdnam: Option<cmdnam> = {
11934                let tab = cmdnamtab_lock().read().ok();
11935                tab.and_then(|t| {
11936                    t.iter()
11937                        .find(|(k, _)| k.as_str() == cmdarg.as_str())
11938                        .map(|(_, v)| v.clone())
11939                })
11940            };
11941            if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
11942                // c:3607
11943                // c:3608-3611 — `if (!(hn->flags & HASHED)) { checkpath = path; dohashcmd = 1; }`
11944                if (have_cmdnam.as_ref().unwrap().node.flags & crate::ported::zsh_h::HASHED) == 0 {
11945                    dohashcmd = true;
11946                }
11947                // c:3612-3614 — removenode + freenode; `hn = NULL`.
11948                if let Ok(mut tab) = cmdnamtab_lock().write() {
11949                    tab.remove(&cmdarg);
11950                }
11951                have_cmdnam = None;
11952            }
11953            if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
11954                // c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
11955                let has_slash = cmdarg.contains('/'); // c:3617-3618
11956                if !has_slash {
11957                    // c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
11958                    let path_dirs = getsparam("PATH").unwrap_or_default();
11959                    let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
11960                    have_cmdnam = hashcmd(&cmdarg, &dirs);
11961                }
11962            }
11963            // C keeps the resolved command in `hn` (c:3619), which the
11964            // AUTOCD test below reads as "found": `hn` here only carries
11965            // builtins and functions, so the external lookup is carried
11966            // separately. Dropped, a found external such as `mysql` was
11967            // treated as not found, and with AUTOCD + CDABLEVARS
11968            // `${=ZPWR_LEARN_COMMAND} 2>>log` ran `cd -- /var/empty`
11969            // (user mysql's home) and printed `~mysql`.
11970            external_found = have_cmdnam.is_some();
11971        }
11972
11973        // c:3625-3631 — AUTOCD: command not found, try directory.
11974        if hn.is_none() && !external_found && trycd {
11975            let cmdarg = args.as_ref().unwrap()[0].clone();
11976            if let Some(s) = cancd(&cmdarg) {
11977                // c:3625
11978                args.as_mut().unwrap()[0] = s; // c:3626
11979                args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
11980                args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
11981                                                                    // c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
11982                let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
11983                if let Some(cd) = cd_entry {
11984                    hn = Some(cd as *const builtin as *mut builtin);
11985                    is_builtin = 1; // c:3630
11986                }
11987            }
11988        }
11989    }
11990
11991    // c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
11992    is_cursh =
11993        (is_builtin != 0 || is_shfunc != 0 || nullexec != 0 || typ >= WC_CURSH as i32) as i32;
11994
11995    // c:3659-3697 — fork decision.
11996    if forked == 0 {
11997        // c:3659
11998        if do_exec == 0
11999            && (((is_builtin != 0 || is_shfunc != 0) && output != 0)
12000                || (is_cursh == 0
12001                    && (last1 != 1
12002                        || crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
12003                        || JOBTAB
12004                            .get()
12005                            .map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
12006                            .unwrap_or(false)
12007                        || crate::ported::utils::FDTABLE_FLOCKS.load(Ordering::Relaxed) != 0)))
12008        {
12009            // c:3660-3663
12010            let mut filelist_for_fork = filelist.clone();
12011            let pid = execcmd_fork(
12012                state,
12013                how,
12014                typ,
12015                varspc,
12016                &mut filelist_for_fork,
12017                text.as_deref().unwrap_or(""),
12018                oautocont,
12019                close_if_forked,
12020            );
12021            match pid {
12022                -1 => {
12023                    // c:3666-3667 — goto fatal.
12024                    redir_err = 1;
12025                    return execcmd_exec_done_path(
12026                        redir_err,
12027                        oautocont,
12028                        how,
12029                        &mut shti,
12030                        &mut chti,
12031                        &mut then_ts,
12032                        forked,
12033                        &mut newxtrerr,
12034                        cflags,
12035                        orig_cflags,
12036                        is_cursh,
12037                        do_exec,
12038                    );
12039                }
12040                0 => {
12041                    // c:3668 — child continues.
12042                }
12043                _ => {
12044                    // c:3670-3671 — parent returns.
12045                    if oautocont >= 0 {
12046                        opt_state_set("autocontinue", oautocont != 0);
12047                    }
12048                    if (how & Z_TIMED as i32) != 0 {
12049                        crate::ported::jobs::shelltime(
12050                            Some(&mut shti),
12051                            Some(&mut chti),
12052                            Some(&mut then_ts),
12053                            1,
12054                        );
12055                    }
12056                    return;
12057                }
12058            }
12059            forked = 1; // c:3673
12060        } else if is_cursh != 0 {
12061            // c:3674
12062            // c:3678-3682 — set jobtab[thisjob] stat bits.
12063            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12064            if thisjob >= 0 {
12065                if let Some(jt) = JOBTAB.get() {
12066                    let mut guard = jt.lock().unwrap();
12067                    if let Some(j) = guard.get_mut(thisjob as usize) {
12068                        j.stat |= STAT_CURSH; // c:3678
12069                                              // c:3679-3680 — `if (!jobtab[thisjob].procs)
12070                                              //                  jobtab[thisjob].stat |= STAT_NOPRINT;`
12071                                              // Suppress the "[N] done" print for jobs that
12072                                              // never forked a real process (cursh / builtin /
12073                                              // null exec).
12074                        if j.procs.is_empty() {
12075                            j.stat |= STAT_NOPRINT; // c:3680
12076                        }
12077                        if is_builtin != 0 {
12078                            j.stat |= STAT_BUILTIN; // c:3682
12079                        }
12080                    }
12081                }
12082            }
12083        } else {
12084            // c:3683-3697 — external exec (real or fake).
12085            is_exec = 1; // c:3687
12086                         // c:3695 — `if (type == WC_SUBSH) forked = 1;`
12087            if typ == WC_SUBSH as i32 {
12088                forked = 1; // c:3696
12089            }
12090        }
12091    }
12092
12093    // c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
12094    if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
12095        // c:3700
12096        let mut oargs: LinkList<String> = Default::default();
12097        if let Some(ref v) = args {
12098            for s in v {
12099                oargs.push_back(s.clone());
12100            }
12101        }
12102        globlist(&mut oargs, 0); // c:3702
12103        let mut out: Vec<String> = Vec::new();
12104        while let Some(s) = oargs.pop_front() {
12105            out.push(s);
12106        }
12107        args = Some(out);
12108    }
12109    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12110        // c:3705
12111        LASTVAL.store(1, Ordering::Relaxed); // c:3706
12112        return execcmd_exec_err_path(
12113            forked,
12114            &mut save,
12115            &mut mfds,
12116            oautocont,
12117            how,
12118            &mut shti,
12119            &mut chti,
12120            &mut then_ts,
12121            &mut newxtrerr,
12122            cflags,
12123            orig_cflags,
12124            is_cursh,
12125            do_exec,
12126            redir_err,
12127        );
12128    }
12129
12130    // c:3765-3773 — "Make a copy of stderr for xtrace output before
12131    // redirecting". The fusevm runtime reaches the same
12132    // port when it opens a simple command's redirect scope.
12133    crate::fusevm_bridge::xtrerr_flush(); // c:3766 fflush(xtrerr)
12134    if isset(XTRACE)
12135        && crate::ported::utils::xtrerr.load(Ordering::Relaxed) == 2
12136        && (typ < WC_SUBSH as i32 || typ == WC_TIMED as i32)
12137    {
12138        // c:3767-3768
12139        let fd = crate::ported::utils::movefd(unsafe { libc::dup(2) }); // c:3769
12140        if fd >= 0 {
12141            crate::ported::utils::xtrerr.store(fd, Ordering::Relaxed); // c:3770
12142            crate::ported::utils::fdtable_set(fd, FDT_XTRACE); // c:3771
12143            newxtrerr = Some(fd);
12144        }
12145    }
12146
12147    // c:3720-3724 — pipeline input/output to mfds.
12148    if input != 0 {
12149        addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
12150    }
12151    if output != 0 {
12152        addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
12153    }
12154
12155    // c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
12156    if let Some(ref mut r) = redir {
12157        spawnpipes(r.as_mut_slice(), nullexec);
12158    }
12159
12160    // c:3731-3955 — io redirection loop. Faithful per-redir match.
12161    while let Some(redir_list) = redir.as_mut() {
12162        // c:3731 — `while (redir && nonempty(redir))`
12163        if redir_list.is_empty() {
12164            break;
12165        }
12166        let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
12167                                            // c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
12168        if fn_.typ == REDIR_INPIPE {
12169            // c:3736
12170            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
12171                // c:3737
12172                if fn_.fd2 != -1 {
12173                    let _ = zclose(fn_.fd2); // c:3738-3739
12174                }
12175                closemnodes(&mut mfds); // c:3740
12176                fixfds(&save); // c:3741
12177                {
12178                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12179                    LASTVAL.store(1, Ordering::Relaxed);
12180                } // c:3742
12181                break;
12182            }
12183            // c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
12184            addfd(
12185                forked,
12186                &mut save,
12187                &mut mfds,
12188                fn_.fd1,
12189                fn_.fd2,
12190                0,
12191                fn_.varid.as_deref(),
12192            );
12193        } else if fn_.typ == REDIR_OUTPIPE {
12194            // c:3745
12195            if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
12196                // c:3746
12197                if fn_.fd2 != -1 {
12198                    let _ = zclose(fn_.fd2); // c:3747-3748
12199                }
12200                closemnodes(&mut mfds); // c:3749
12201                fixfds(&save); // c:3750
12202                {
12203                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12204                    LASTVAL.store(1, Ordering::Relaxed);
12205                } // c:3751
12206                break;
12207            }
12208            // c:3753
12209            addfd(
12210                forked,
12211                &mut save,
12212                &mut mfds,
12213                fn_.fd1,
12214                fn_.fd2,
12215                1,
12216                fn_.varid.as_deref(),
12217            );
12218        } else {
12219            // c:3754 — non-pipe redir branch.
12220            let mut closed: i32; // c:3755
12221            // c:3756-3757 — `if (fn->type != REDIR_HERESTR && xpandredir(fn, redir)) continue;`
12222            if fn_.typ != REDIR_HERESTR && crate::ported::glob::xpandredir(&mut fn_, redir_list) != 0
12223            {
12224                continue;
12225            }
12226            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12227                // c:3758
12228                closemnodes(&mut mfds); // c:3759
12229                fixfds(&save); // c:3760
12230                {
12231                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12232                    LASTVAL.store(1, Ordering::Relaxed);
12233                } // c:3761
12234                break;
12235            }
12236            if !isset(EXECOPT) {
12237                // c:3763 — `if (unset(EXECOPT)) continue;`
12238                continue;
12239            }
12240            let fil_local: i32;
12241            match fn_.typ {
12242                t if t == REDIR_HERESTR => {
12243                    // c:3766
12244                    if checkclobberparam(&fn_) == 0 {
12245                        fil_local = -1; // c:3768
12246                    } else {
12247                        fil_local = getherestr(&fn_); // c:3770
12248                    }
12249                    if fil_local == -1 {
12250                        // c:3771
12251                        let e = std::io::Error::last_os_error();
12252                        let raw = e.raw_os_error().unwrap_or(0);
12253                        if raw != 0 && raw != libc::EINTR {
12254                            zwarn(&format!("can't create temp file for here document: {}", e));
12255                            // c:3772-3774
12256                        }
12257                        closemnodes(&mut mfds); // c:3775
12258                        fixfds(&save); // c:3776
12259                        {
12260                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12261                            LASTVAL.store(1, Ordering::Relaxed);
12262                        } // c:3777
12263                        break;
12264                    }
12265                    // c:3779
12266                    addfd(
12267                        forked,
12268                        &mut save,
12269                        &mut mfds,
12270                        fn_.fd1,
12271                        fil_local,
12272                        0,
12273                        fn_.varid.as_deref(),
12274                    );
12275                }
12276                t if t == REDIR_READ || t == REDIR_READWRITE => {
12277                    // c:3781-3782
12278                    if checkclobberparam(&fn_) == 0 {
12279                        fil_local = -1; // c:3784
12280                    } else {
12281                        let name = fn_.name.clone().unwrap_or_default();
12282                        let unmeta_name = unmeta(&name);
12283                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
12284                            Ok(c) => c,
12285                            Err(_) => {
12286                                closemnodes(&mut mfds);
12287                                fixfds(&save);
12288                                {
12289                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12290                                    LASTVAL.store(1, Ordering::Relaxed);
12291                                }
12292                                break;
12293                            }
12294                        };
12295                        if fn_.typ == REDIR_READ {
12296                            // c:3786
12297                            fil_local = unsafe {
12298                                libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
12299                            };
12300                        } else {
12301                            // c:3788-3789
12302                            fil_local = unsafe {
12303                                libc::open(
12304                                    cstr.as_ptr(),
12305                                    libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
12306                                    0o666,
12307                                )
12308                            };
12309                        }
12310                    }
12311                    if fil_local == -1 {
12312                        // c:3790
12313                        closemnodes(&mut mfds); // c:3791
12314                        fixfds(&save); // c:3792
12315                        let e = std::io::Error::last_os_error();
12316                        if e.raw_os_error().unwrap_or(0) != libc::EINTR {
12317                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
12318                            // c:3793-3794
12319                        }
12320                        {
12321                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12322                            LASTVAL.store(1, Ordering::Relaxed);
12323                        } // c:3795
12324                        break;
12325                    }
12326                    // c:3797
12327                    addfd(
12328                        forked,
12329                        &mut save,
12330                        &mut mfds,
12331                        fn_.fd1,
12332                        fil_local,
12333                        0,
12334                        fn_.varid.as_deref(),
12335                    );
12336                    // c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
12337                    if nullexec == 1
12338                        && fn_.fd1 == 0
12339                        && fn_.varid.is_none()
12340                        && isset(SHINSTDIN)
12341                        && isset(INTERACTIVE)
12342                    {
12343                        // c:3801 — `!zleactive` check ommitted (zleactive
12344                        // accessor lives in zle module; fusevm bypasses ZLE).
12345                        crate::ported::init::init_io(None); // c:3802
12346                    }
12347                }
12348                t if t == REDIR_CLOSE => {
12349                    // c:3804
12350                    // c:3805 — `if (fn->varid) { parse fd from variable }`
12351                    let mut fd1_local = fn_.fd1;
12352                    if let Some(varname) = fn_.varid.as_deref() {
12353                        // c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
12354                        // with varid. The C path resolves the named param
12355                        // to its integer-string value, parses as base-10
12356                        // (or base#NN), and rejects readonly / non-numeric
12357                        // / shell-owned-fd values.
12358                        //
12359                        //   bad=1  → "parameter %s does not contain a file descriptor"
12360                        //   bad=2  → "can't close file descriptor from readonly parameter %s"
12361                        //   bad=3  → "file descriptor %d used by shell, not closed"
12362                        //
12363                        // Substrate now available: getsparam for value,
12364                        // paramtab read for PM_READONLY, MAX_ZSH_FD +
12365                        // fdtable_get for shell-owned guard.
12366                        let mut bad: u8 = 0;
12367                        let value_opt = getsparam(varname);
12368                        let is_ro = paramtab()
12369                            .read()
12370                            .ok()
12371                            .and_then(|t| {
12372                                t.get(varname)
12373                                    .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
12374                            })
12375                            .unwrap_or(false);
12376                        if value_opt.is_none() {
12377                            bad = 1; // c:3811 getvalue failed
12378                        } else if is_ro {
12379                            bad = 2; // c:3813 PM_READONLY
12380                        } else {
12381                            let s = value_opt.as_deref().unwrap_or("");
12382                            match s.trim().parse::<i32>() {
12383                                Ok(n) => {
12384                                    fd1_local = n;
12385                                    fn_.fd1 = n;
12386                                    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
12387                                    if n >= 10
12388                                        && n <= max_fd
12389                                        && (fdtable_get(n) & FDT_TYPE_MASK) == FDT_INTERNAL
12390                                    {
12391                                        // c:3835 shell-owned-fd reject
12392                                        bad = 3;
12393                                    }
12394                                }
12395                                Err(_) => {
12396                                    bad = 1; // c:3823 strtol failure
12397                                }
12398                            }
12399                        }
12400                        if bad != 0 {
12401                            // c:3840-3849
12402                            match bad {
12403                                3 => zwarn(&format!(
12404                                    "file descriptor {} used by shell, not closed",
12405                                    fn_.fd1
12406                                )),
12407                                2 => zwarn(&format!(
12408                                    "can't close file descriptor from readonly parameter {}",
12409                                    varname
12410                                )),
12411                                _ => zwarn(&format!(
12412                                    "parameter {} does not contain a file descriptor",
12413                                    varname
12414                                )),
12415                            }
12416                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12417                            LASTVAL.store(1, Ordering::Relaxed);
12418                            break;
12419                        }
12420                    }
12421                    // c:3852-3865 — `closed`: optional movefd save.
12422                    closed = 0;
12423                    if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
12424                        // c:3856
12425                        let mv = movefd(fd1_local); // c:3857
12426                        save[fd1_local as usize] = mv;
12427                        if mv >= 0 {
12428                            closed = 1; // c:3862-3863
12429                        }
12430                    }
12431                    if fd1_local < 10 {
12432                        // c:3866
12433                        closemn(&mut mfds, fd1_local, REDIR_CLOSE);
12434                        // c:3867
12435                    }
12436                    // c:3873-3876
12437                    let _ = &mut fd1_local;
12438                    if closed == 0 && zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
12439                        zwarn(&format!(
12440                            "failed to close file descriptor {}: {}",
12441                            fn_.fd1,
12442                            std::io::Error::last_os_error()
12443                        )); // c:3873-3875
12444                    }
12445                }
12446                t if t == REDIR_MERGEIN || t == REDIR_MERGEOUT => {
12447                    // c:3878-3879
12448                    if fn_.fd2 < 10 {
12449                        closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
12450                    }
12451                    if checkclobberparam(&fn_) == 0 {
12452                        fil_local = -1; // c:3883
12453                    } else if fn_.fd2 > 9 {
12454                        // c:3884-3897 — fd table check.
12455                        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
12456                        let cin = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
12457                        let cout = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
12458                        let in_table = if fn_.fd2 <= max_fd {
12459                            let kind = fdtable_get(fn_.fd2) & FDT_TYPE_MASK;
12460                            kind != FDT_UNUSED && kind != FDT_EXTERNAL
12461                        } else {
12462                            false
12463                        };
12464                        if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
12465                            fil_local = -1; // c:3896
12466                                            // Per-platform errno setter (c:3897 `errno = EBADF;`).
12467                            #[cfg(target_os = "macos")]
12468                            unsafe {
12469                                *libc::__error() = libc::EBADF;
12470                            }
12471                            #[cfg(target_os = "linux")]
12472                            unsafe {
12473                                *libc::__errno_location() = libc::EBADF;
12474                            }
12475                        } else {
12476                            let fd = if fn_.fd2 == -2 {
12477                                // c:3900-3901
12478                                if fn_.typ == REDIR_MERGEOUT {
12479                                    crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
12480                                } else {
12481                                    crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
12482                                }
12483                            } else {
12484                                fn_.fd2
12485                            };
12486                            // c:3902 — `fil = movefd(dup(fd));`
12487                            let dup_fd = unsafe { libc::dup(fd) };
12488                            fil_local = movefd(dup_fd);
12489                        }
12490                    } else {
12491                        let fd = if fn_.fd2 == -2 {
12492                            if fn_.typ == REDIR_MERGEOUT {
12493                                crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
12494                            } else {
12495                                crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
12496                            }
12497                        } else {
12498                            fn_.fd2
12499                        };
12500                        let dup_fd = unsafe { libc::dup(fd) };
12501                        fil_local = movefd(dup_fd);
12502                    }
12503                    if fil_local == -1 {
12504                        // c:3904
12505                        closemnodes(&mut mfds); // c:3907
12506                        fixfds(&save); // c:3908
12507                        if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
12508                            let desc = if fn_.fd2 == -2 {
12509                                "coprocess".to_string()
12510                            } else {
12511                                format!("{}", fn_.fd2)
12512                            };
12513                            zwarn(&format!("{}: {}", desc, std::io::Error::last_os_error()));
12514                            // c:3911-3913
12515                        }
12516                        {
12517                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12518                            LASTVAL.store(1, Ordering::Relaxed);
12519                        } // c:3914
12520                        break;
12521                    }
12522                    // c:3916-3917
12523                    let merge_is_out = if fn_.typ == REDIR_MERGEOUT { 1 } else { 0 };
12524                    addfd(
12525                        forked,
12526                        &mut save,
12527                        &mut mfds,
12528                        fn_.fd1,
12529                        fil_local,
12530                        merge_is_out,
12531                        fn_.varid.as_deref(),
12532                    );
12533                }
12534                _ => {
12535                    // c:3919 default — write/append/error_redir.
12536                    let mut dfil: i32;
12537                    if checkclobberparam(&fn_) == 0 {
12538                        fil_local = -1; // c:3921
12539                    } else if IS_APPEND_REDIR(fn_.typ) {
12540                        // c:3922
12541                        let name = fn_.name.clone().unwrap_or_default();
12542                        let unmeta_name = unmeta(&name);
12543                        let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
12544                            Ok(c) => c,
12545                            Err(_) => {
12546                                closemnodes(&mut mfds);
12547                                fixfds(&save);
12548                                {
12549                                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12550                                    LASTVAL.store(1, Ordering::Relaxed);
12551                                }
12552                                break;
12553                            }
12554                        };
12555                        // c:3924-3927
12556                        let mode = if !isset(CLOBBER)
12557                            && !isset(crate::ported::zsh_h::APPENDCREATE)
12558                            && !IS_CLOBBER_REDIR(fn_.typ)
12559                        {
12560                            libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
12561                        } else {
12562                            libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
12563                        };
12564                        fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
12565                    } else {
12566                        // c:3929
12567                        fil_local = clobber_open(&fn_);
12568                    }
12569                    // c:3930-3933 — error_redir dup.
12570                    if fil_local != -1 && IS_ERROR_REDIR(fn_.typ) {
12571                        let dup_fd = unsafe { libc::dup(fil_local) };
12572                        dfil = movefd(dup_fd); // c:3931
12573                    } else {
12574                        dfil = 0; // c:3933
12575                    }
12576                    if fil_local == -1 || dfil == -1 {
12577                        // c:3934
12578                        if fil_local != -1 {
12579                            unsafe { libc::close(fil_local) }; // c:3935-3936
12580                        }
12581                        closemnodes(&mut mfds); // c:3937
12582                        fixfds(&save); // c:3938
12583                        let e = std::io::Error::last_os_error();
12584                        let raw = e.raw_os_error().unwrap_or(0);
12585                        if raw != 0 && raw != libc::EINTR {
12586                            zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
12587                            // c:3939-3940
12588                        }
12589                        {
12590                            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12591                            LASTVAL.store(1, Ordering::Relaxed);
12592                        } // c:3941
12593                        break;
12594                    }
12595                    // c:3943
12596                    addfd(
12597                        forked,
12598                        &mut save,
12599                        &mut mfds,
12600                        fn_.fd1,
12601                        fil_local,
12602                        1,
12603                        fn_.varid.as_deref(),
12604                    );
12605                    if IS_ERROR_REDIR(fn_.typ) {
12606                        // c:3944-3945
12607                        addfd(forked, &mut save, &mut mfds, 2, dfil, 1, None);
12608                    }
12609                    let _ = &mut dfil;
12610                }
12611            }
12612            // c:3948-3952 — addfd errflag check.
12613            if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12614                // c:3949
12615                closemnodes(&mut mfds); // c:3950
12616                fixfds(&save); // c:3951
12617                {
12618                    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12619                    LASTVAL.store(1, Ordering::Relaxed);
12620                } // c:3952
12621                break;
12622            }
12623        }
12624    }
12625
12626    // c:3957-3961 — close multios with ct >= 2.
12627    i = 0;
12628    while i < 10 {
12629        // c:3959
12630        if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
12631            if m.ct >= 2 {
12632                closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
12633            }
12634        }
12635        i += 1;
12636    }
12637
12638    // c:3963-3995 — nullexec branch.
12639    if nullexec != 0 {
12640        // c:3963
12641        if let Some(vspc) = varspc {
12642            // c:3969
12643            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
12644            let mut removelist: Vec<String> = Vec::new();
12645            if !isset(POSIXBUILTINS) && nullexec != 2 {
12646                // c:3971-3972
12647                save_params(state, vspc, &mut restorelist, &mut removelist);
12648            }
12649            addvars(state, vspc, 0); // c:3973
12650            if !restorelist.is_empty() {
12651                // c:3974
12652                restore_params(restorelist, removelist); // c:3975
12653            }
12654        }
12655        let ef = errflag.load(Ordering::Relaxed);
12656        LASTVAL.store(
12657            if ef != 0 {
12658                ef
12659            } else {
12660                cmdoutval.load(Ordering::Relaxed)
12661            },
12662            Ordering::Relaxed,
12663        ); // c:3977
12664        if nullexec == 1 {
12665            // c:3978
12666            // c:3983-3985 — close save[i].
12667            i = 0;
12668            while i < 10 {
12669                if save[i as usize] != -2 {
12670                    let _ = zclose(save[i as usize]); // c:3985
12671                }
12672                i += 1;
12673            }
12674            // c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
12675            let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12676            if thisjob >= 0 {
12677                if let Some(jt) = JOBTAB.get() {
12678                    let mut guard = jt.lock().unwrap();
12679                    if let Some(j) = guard.get_mut(thisjob as usize) {
12680                        j.stat |= STAT_DONE; // c:3989
12681                    }
12682                }
12683            }
12684            return execcmd_exec_done_path(
12685                redir_err,
12686                oautocont,
12687                how,
12688                &mut shti,
12689                &mut chti,
12690                &mut then_ts,
12691                forked,
12692                &mut newxtrerr,
12693                cflags,
12694                orig_cflags,
12695                is_cursh,
12696                do_exec,
12697            );
12698        }
12699        if isset(XTRACE) {
12700            // c:3992-3994
12701            eprintln!();
12702        }
12703    } else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
12704        // c:3996 — main dispatch branch.
12705        // c:3997 — `int q = queue_signal_level();`
12706        let _q = 0;
12707        // c:4003-4012 — entersubsh for is_exec.
12708        if is_exec != 0 {
12709            // c:4003
12710            let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
12711                esub::ASYNC
12712            } else {
12713                0
12714            } | esub::PGRP
12715                | esub::FAKE; // c:4004-4005
12716            if typ != WC_SUBSH as i32 {
12717                flags |= esub::KEEPTRAP; // c:4007
12718            }
12719            if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
12720                // c:4008-4009
12721                flags |= esub::REVERTPGRP; // c:4010
12722            }
12723            entersubsh(flags, None); // c:4011
12724        }
12725
12726        if typ == WC_FUNCDEF as i32 {
12727            // c:4013
12728            // c:4069-4091 — redirections stored with the new function when
12729            // none were taken from the parsed environment.
12730            let redir_prog: Option<crate::ported::zsh_h::Eprog> = if redir.is_none()
12731                && wc_code(state.prog.prog[eparams.beg]) == WC_REDIR
12732            {
12733                let mut s = estate {
12734                    prog: state.prog.clone(), // c:4079
12735                    pc: eparams.beg,          // c:4080
12736                    strs: state.prog.strs.clone(), // c:4081
12737                    strs_offset: 0,
12738                };
12739                // The copy uses the wordcode parsing area, so save and
12740                // restore state.
12741                crate::ported::context::zcontext_save(); // c:4087
12742                let rp = crate::ported::parse::eccopyredirs(&mut s); // c:4088
12743                crate::ported::context::zcontext_restore(); // c:4089
12744                rp.map(Box::new)
12745            } else {
12746                None // c:4091
12747            };
12748            // c:4039 — `lastval = execfuncdef(state, redir_prog);`
12749            let lv = execfuncdef(state, redir_prog);
12750            LASTVAL.store(lv, Ordering::Relaxed);
12751        } else if typ >= WC_CURSH as i32 {
12752            // c:4042
12753            if last1 == 1 {
12754                do_exec = 1; // c:4044
12755            }
12756            if typ == WC_AUTOFN as i32 {
12757                // c:4046
12758                let lv = execautofn_basic(state, do_exec); // c:4051
12759                LASTVAL.store(lv, Ordering::Relaxed);
12760            } else {
12761                // c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
12762                // dispatch_execfuncs ports the C `execfuncs[]` table
12763                // (Src/exec.c:268) by typ → exec{cursh,for,select,...}
12764                // direct call. See dispatch_execfuncs at end of file.
12765                let lv = dispatch_execfuncs(state, typ, do_exec);
12766                LASTVAL.store(lv, Ordering::Relaxed);
12767            }
12768        } else if is_builtin != 0 || is_shfunc != 0 {
12769            // c:4055
12770            let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
12771            let mut removelist: Vec<String> = Vec::new();
12772            let mut do_save: i32 = 0; // c:4057
12773
12774            if forked == 0 {
12775                // c:4060
12776                if isset(POSIXBUILTINS) {
12777                    // c:4061
12778                    if is_shfunc != 0
12779                        || (hn.map(|p| unsafe { (*p).node.flags as u32 }).unwrap_or(0)
12780                            & (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
12781                            != 0
12782                    {
12783                        // c:4067
12784                        do_save = if (orig_cflags & BINF_COMMAND) != 0 {
12785                            1
12786                        } else {
12787                            0
12788                        };
12789                    } else {
12790                        do_save = 1; // c:4070
12791                    }
12792                } else {
12793                    // c:4071
12794                    if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
12795                        // c:4076
12796                        do_save = 1; // c:4077
12797                    }
12798                }
12799                if do_save != 0 {
12800                    if let Some(vspc) = varspc {
12801                        // c:4079
12802                        save_params(state, vspc, &mut restorelist, &mut removelist);
12803                    }
12804                }
12805            }
12806            if varspc.is_some() {
12807                // c:4082
12808                let mut addflags: i32 = 0; // c:4086
12809                if is_shfunc != 0 {
12810                    addflags |= ADDVAR_EXPORT; // c:4088
12811                }
12812                if !restorelist.is_empty() {
12813                    addflags |= ADDVAR_RESTORE; // c:4090
12814                }
12815                addvars(state, varspc.unwrap_or(0), addflags); // c:4092
12816                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12817                    // c:4093
12818                    if !restorelist.is_empty() {
12819                        restore_params(restorelist, removelist); // c:4094-4095
12820                    }
12821                    LASTVAL.store(1, Ordering::Relaxed); // c:4096
12822                    fixfds(&save); // c:4097
12823                    return execcmd_exec_done_path(
12824                        redir_err,
12825                        oautocont,
12826                        how,
12827                        &mut shti,
12828                        &mut chti,
12829                        &mut then_ts,
12830                        forked,
12831                        &mut newxtrerr,
12832                        cflags,
12833                        orig_cflags,
12834                        is_cursh,
12835                        do_exec,
12836                    );
12837                }
12838            }
12839
12840            if is_shfunc != 0 {
12841                // c:4102-4105
12842                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
12843                // c:4104 — `execshfunc((Shfunc) hn, args);` C casts
12844                // HashNode hn to Shfunc; zshrs's hn is *mut builtin so
12845                // we re-resolve the shfunc by name from shfunctab and
12846                // dispatch through the top-level execshfunc port at
12847                // exec.rs:4978 (which routes to runshfunc).
12848                let name = args
12849                    .as_ref()
12850                    .and_then(|v| v.first())
12851                    .cloned()
12852                    .unwrap_or_default();
12853                let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
12854                    tab.get(&name).cloned()
12855                } else {
12856                    None
12857                };
12858                if let Some(ref mut shf) = shf_clone {
12859                    execshfunc(shf, &mut a_vec);
12860                }
12861                // c:4105 — `pipecleanfilelist(filelist, 0);` — clean
12862                // out the proc_subst entries from the current job's
12863                // filelist after the shfunc body ran. Route through
12864                // `JOBTAB[thisjob]`.
12865                if let Some(jt) = JOBTAB.get() {
12866                    let mut guard = jt.lock().unwrap();
12867                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12868                    if tj >= 0 {
12869                        if let Some(j) = guard.get_mut(tj as usize) {
12870                            crate::ported::jobs::pipecleanfilelist(j, false);
12871                        }
12872                    }
12873                }
12874            } else {
12875                // c:4107 — builtin path.
12876                let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
12877                let postassigns = eparams.postassigns; // c:4109
12878                if forked != 0 {
12879                    closem(FDT_INTERNAL, 0); // c:4111
12880                }
12881                if postassigns != 0 {
12882                    // c:4112-4230 — typeset post-assignment processing.
12883                    use crate::ported::zsh_h::{
12884                        ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
12885                        PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
12886                        WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
12887                    };
12888                    let opc = state.pc; // c:4113
12889                    state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
12890                                                                      // c:4115 — `assigns = newlinklist();` — already declared above.
12891                    let mut pa_remaining = postassigns;
12892                    while pa_remaining > 0 {
12893                        // c:4116 — `while (postassigns--)`
12894                        pa_remaining -= 1;
12895                        let mut pa_htok: i32 = 0; // c:4117
12896                        if state.pc >= state.prog.prog.len() {
12897                            break;
12898                        }
12899                        let ac = state.prog.prog[state.pc]; // c:4118
12900                        state.pc += 1;
12901                        let mut name = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut pa_htok)); // c:4119
12902                                                                                            // c:4123-4124 DPUTS — debug assertion skipped.
12903                        if pa_htok != 0 {
12904                            // c:4126 — `init_list1(svl, name);`
12905                            let mut svl: LinkList<String> = Default::default();
12906                            svl.push_back(name.clone());
12907                            // c:4127-4166 — INC-scalar special case (typeset $ass form).
12908                            if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
12909                                && WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
12910                            {
12911                                // c:4141 — `(void)ecgetstr(...)` — dummy.
12912                                let mut dummy_htok: i32 = 0;
12913                                let _ = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut dummy_htok));
12914                                let mut rf = 0i32;
12915                                prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
12916                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12917                                    // c:4143
12918                                    state.pc = opc; // c:4144
12919                                    break;
12920                                }
12921                                let mut rf2 = 0i32;
12922                                globlist(&mut svl, rf2); // c:4147
12923                                let _ = &mut rf2;
12924                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12925                                    // c:4148
12926                                    state.pc = opc; // c:4149
12927                                    break;
12928                                }
12929                                // c:4152-4165 — drain svl into assigns.
12930                                while let Some(data) = svl.pop_front() {
12931                                    let (asg_name, asg_val): (String, Option<String>) =
12932                                        if let Some(eq_pos) = data.find('=') {
12933                                            // c:4156-4159
12934                                            (
12935                                                data[..eq_pos].to_string(),
12936                                                Some(data[eq_pos + 1..].to_string()),
12937                                            )
12938                                        } else {
12939                                            // c:4161-4162
12940                                            (data, None)
12941                                        };
12942                                    assigns.push(crate::ported::zsh_h::asgment {
12943                                        node: crate::ported::zsh_h::linknode {
12944                                            next: None,
12945                                            prev: None,
12946                                            dat: 0,
12947                                        },
12948                                        name: asg_name,
12949                                        flags: 0,
12950                                        scalar: asg_val,
12951                                        array: None,
12952                                    });
12953                                }
12954                                continue; // c:4166
12955                            }
12956                            // c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
12957                            let mut rf = 0i32;
12958                            prefork(&mut svl, PREFORK_SINGLE, &mut rf);
12959                            // c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
12960                            name = if svl.is_empty() {
12961                                String::new()
12962                            } else {
12963                                svl.pop_front().unwrap_or_default()
12964                            };
12965                        }
12966                        // c:4172 — `untokenize(name);`
12967                        // (untokenize is destructive on bytes; Rust untokenize
12968                        // returns a new String — call and rebind.)
12969                        name = untokenize(&name);
12970                        let mut asg = crate::ported::zsh_h::asgment {
12971                            node: crate::ported::zsh_h::linknode {
12972                                next: None,
12973                                prev: None,
12974                                dat: 0,
12975                            },
12976                            name,
12977                            flags: 0,
12978                            scalar: None,
12979                            array: None,
12980                        };
12981                        if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
12982                            // c:4175
12983                            let mut val_htok: i32 = 0;
12984                            let mut val = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut val_htok)); // c:4176
12985                            asg.flags = 0; // c:4177
12986                            if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
12987                                // c:4178-4180 — fake assignment, no value.
12988                                asg.scalar = None;
12989                            } else {
12990                                if val_htok != 0 {
12991                                    // c:4183
12992                                    let mut svl: LinkList<String> = Default::default();
12993                                    svl.push_back(val.clone());
12994                                    let mut rf = 0i32;
12995                                    prefork(&mut svl, PREFORK_SINGLE | PREFORK_ASSIGN, &mut rf); // c:4184-4186
12996                                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12997                                        // c:4187
12998                                        state.pc = opc; // c:4188
12999                                        break;
13000                                    }
13001                                    // c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
13002                                    val = if svl.is_empty() {
13003                                        String::new()
13004                                    } else {
13005                                        svl.pop_front().unwrap_or_default()
13006                                    };
13007                                }
13008                                // c:4198 — `untokenize(val);`
13009                                asg.scalar = Some(untokenize(&val));
13010                            }
13011                        } else {
13012                            // c:4202 — array assignment.
13013                            asg.flags = ASG_ARRAY; // c:4202
13014                            let mut arr_htok: i32 = 0;
13015                            let arr_words = ecgetlist(
13016                                state,
13017                                WC_ASSIGN_NUM(ac) as usize,
13018                                ECDUPTOK_LOCAL,
13019                                Some(&mut arr_htok),
13020                            ); // c:4204
13021                            let mut arr_list: LinkList<String> = Default::default();
13022                            for s in arr_words {
13023                                arr_list.push_back(s);
13024                            }
13025                            if !arr_list.is_empty()
13026                                && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
13027                            {
13028                                // c:4209 — `int prefork_ret = 0;`
13029                                let mut prefork_ret = 0i32;
13030                                prefork(&mut arr_list, PREFORK_ASSIGN, &mut prefork_ret); // c:4210-4211
13031                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
13032                                    // c:4212
13033                                    state.pc = opc; // c:4213
13034                                    break;
13035                                }
13036                                if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
13037                                    // c:4216
13038                                    asg.flags |= ASG_KEY_VALUE; // c:4217
13039                                }
13040                                globlist(&mut arr_list, prefork_ret); // c:4218
13041                                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
13042                                    // c:4220
13043                                    state.pc = opc; // c:4221
13044                                    break;
13045                                }
13046                            }
13047                            asg.array = Some(arr_list);
13048                        }
13049                        // c:4227 — `uaddlinknode(assigns, &asg->node);`
13050                        assigns.push(asg);
13051                    }
13052                    state.pc = opc; // c:4229
13053                }
13054                if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
13055                    // c:4232
13056                    // c:Src/builtin.c:262 — `name = (char *) ugetnode(args);`
13057                    // C's execbuiltin consumes args[0] (the command name)
13058                    // at entry. zshrs's execbuiltin reads the name from
13059                    // `bn->node.nam` instead, so we strip args[0] here
13060                    // before the call to match C's post-ugetnode argv
13061                    // shape. Without this, e.g. `cmd=pwd; $cmd` reached
13062                    // execbuiltin with args=["pwd"] and pwd's
13063                    // maxargs=0 check rejected the empty call as
13064                    // "too many arguments".
13065                    let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
13066                    if !a_vec.is_empty() {
13067                        a_vec.remove(0);
13068                    }
13069                    let ret = crate::ported::builtin::execbuiltin(
13070                        a_vec,
13071                        assigns,
13072                        hn.unwrap_or(std::ptr::null_mut()),
13073                    ); // c:4233
13074                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
13075                        // c:4238
13076                        LASTVAL.store(ret, Ordering::Relaxed); // c:4239
13077                    }
13078                }
13079                if (do_save & BINF_COMMAND as i32) != 0 {
13080                    // c:4241
13081                    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
13082                }
13083                // c:4244 fflush(stdout) — Rust stdio auto-flushes.
13084                // c:4245-4251 — write-error check on save[1].
13085            }
13086            if isset(PRINTEXITVALUE)
13087                && isset(SHINSTDIN)
13088                && LASTVAL.load(Ordering::Relaxed) != 0
13089                && subsh.load(Ordering::Relaxed) == 0
13090            {
13091                // c:4253-4255
13092                eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
13093            }
13094
13095            if do_exec != 0 {
13096                // c:4263
13097                if subsh.load(Ordering::Relaxed) != 0 {
13098                    crate::ported::builtin::_realexit(); // c:4264-4265
13099                }
13100                if isset(RCS)
13101                    && crate::ported::zsh_h::interact()
13102                    && nohistsave.load(Ordering::Relaxed) == 0
13103                {
13104                    // c:4269
13105                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
13106                    // c:4270
13107                }
13108                crate::ported::builtin::realexit(); // c:4271
13109            }
13110            if !restorelist.is_empty() {
13111                // c:4273
13112                restore_params(restorelist, removelist); // c:4274
13113            }
13114        } else {
13115            // c:4276 — external command execute.
13116            if subsh.load(Ordering::Relaxed) == 0 {
13117                // c:4277
13118                if forked == 0 {
13119                    // c:4280 — `setiparam("SHLVL", --shlvl);`
13120                    let cur = getsparam("SHLVL")
13121                        .and_then(|s| s.parse::<i64>().ok())
13122                        .unwrap_or(1);
13123                    setiparam("SHLVL", cur - 1); // c:4281
13124                }
13125                if do_exec != 0
13126                    && isset(RCS)
13127                    && crate::ported::zsh_h::interact()
13128                    && nohistsave.load(Ordering::Relaxed) == 0
13129                {
13130                    // c:4285
13131                    crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
13132                    // c:4286
13133                }
13134            }
13135            if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
13136                // c:4288
13137                if varspc.is_some() {
13138                    // c:4289
13139                    let mut addflags: i32 = ADDVAR_EXPORT | ADDVAR_RESTRICT; // c:4257/c:4290
13140                    if forked != 0 {
13141                        addflags |= ADDVAR_RESTORE; // c:4292
13142                    }
13143                    addvars(state, varspc.unwrap_or(0), addflags); // c:4293
13144                    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
13145                        // c:4294
13146                        std::process::exit(1); // c:4295
13147                    }
13148                }
13149                closem(FDT_INTERNAL, 0); // c:4297
13150                                         // c:4298-4305 — close coprocin/coprocout.
13151                let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
13152                if cpi != -1 {
13153                    let _ = zclose(cpi); // c:4299
13154                    crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed);
13155                    // c:4300
13156                }
13157                let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
13158                if cpo != -1 {
13159                    let _ = zclose(cpo); // c:4303
13160                    crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed);
13161                    // c:4304
13162                }
13163                if forked == 0 {
13164                    // c:4307
13165                    setlimits(""); // c:4308
13166                }
13167                if (how & Z_ASYNC as i32) != 0 {
13168                    // c:4310 — `zsfree(STTYval); STTYval = 0;`
13169                    let mut guard = STTYval.lock().unwrap();
13170                    *guard = None; // c:4311-4312
13171                }
13172                // c:4314 — `execute(args, cflags, use_defpath);`
13173                let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
13174                execute(&mut a_vec, cflags, use_defpath); // c:4314
13175            } else {
13176                // c:4315 — `( ... )` — WC_SUBSH.
13177                list_pipe.store(0, Ordering::Relaxed); // c:4318
13178                                                       // c:4319 — `pipecleanfilelist(filelist, 0);` — clean
13179                                                       // proc-subst entries from the current job's filelist
13180                                                       // before recursing into the subshell body.
13181                if let Some(jt) = JOBTAB.get() {
13182                    let mut guard = jt.lock().unwrap();
13183                    let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
13184                    if tj >= 0 {
13185                        if let Some(j) = guard.get_mut(tj as usize) {
13186                            crate::ported::jobs::pipecleanfilelist(j, false);
13187                        }
13188                    }
13189                }
13190                state.pc += 1; // c:4324 — `state->pc++;`
13191                let _ = execlist(state, 0, 1); // c:4325
13192            }
13193        }
13194    }
13195
13196    // c:4330-4404 — err: + done: + fatal:.
13197    return execcmd_exec_done_path(
13198        redir_err,
13199        oautocont,
13200        how,
13201        &mut shti,
13202        &mut chti,
13203        &mut then_ts,
13204        forked,
13205        &mut newxtrerr,
13206        cflags,
13207        orig_cflags,
13208        is_cursh,
13209        do_exec,
13210    );
13211}
13212
13213/// !!! WARNING: RUST-ONLY HELPER !!!
13214///
13215/// No function of this name exists in `Src/exec.c`. This helper stands in for
13216/// the `done:` label tail of `execcmd_exec` (c:4366-4403), which C reaches by
13217/// `goto` and Rust cannot.
13218/// Internal helper modelling the C `done:` label tail of
13219/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
13220/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
13221/// shelltime stop, and newxtrerr close.
13222#[allow(clippy::too_many_arguments)]
13223fn execcmd_exec_done_path(
13224    redir_err: i32,
13225    oautocont: i32,
13226    how: i32,
13227    shti: &mut crate::ported::jobs::timeinfo,
13228    chti: &mut crate::ported::jobs::timeinfo,
13229    then_ts: &mut std::time::Instant,
13230    forked: i32,
13231    newxtrerr: &mut Option<i32>,
13232    cflags: u32,
13233    orig_cflags: u32,
13234    is_cursh: i32,
13235    do_exec: i32,
13236) {
13237    use crate::ported::zsh_h::{
13238        AUTOCONTINUE, BINF_COMMAND, BINF_EXEC, BINF_PSPECIAL, INTERACTIVE, POSIXBUILTINS, Z_TIMED,
13239    };
13240    // c:4366
13241    // c:4367-4386 — POSIX special-builtin error escalation.
13242    if isset(POSIXBUILTINS)
13243        && (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
13244        && (orig_cflags & BINF_COMMAND) == 0
13245    {
13246        // c:4367-4369
13247        let _forked_or_subsh = forked | zsh_subshell.load(Ordering::Relaxed); // c:4376
13248                                                                              // fatal: label entry point — same handling.
13249        if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
13250            // c:4378
13251            if !isset(INTERACTIVE) {
13252                // c:4379
13253                if _forked_or_subsh != 0 {
13254                    unsafe { libc::_exit(1) }; // c:4381
13255                } else {
13256                    std::process::exit(1); // c:4383
13257                }
13258            }
13259            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
13260        }
13261    }
13262    // c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
13263    if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
13264        crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1);
13265        // c:4389
13266    }
13267    // c:4445-4452 — newxtrerr close.
13268    if let Some(fd) = newxtrerr.take() {
13269        // c:4445
13270        crate::fusevm_bridge::xtrerr_flush(); // c:4448 fclose(newxtrerr)
13271        crate::ported::utils::xtrerr.store(2, Ordering::Relaxed); // c:4449 xtrerr = oxtrerr
13272        let _ = zclose(fd); // c:4451
13273    }
13274    // c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
13275    {
13276        let mut guard = STTYval.lock().unwrap();
13277        *guard = None;
13278    }
13279    // c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
13280    if oautocont >= 0 {
13281        opt_state_set("autocontinue", oautocont != 0);
13282    }
13283}
13284
13285/// !!! WARNING: RUST-ONLY HELPER !!!
13286///
13287/// No function of this name exists in `Src/exec.c`. This helper stands in for
13288/// the `err:` label tail of `execcmd_exec` (c:4330-4365), which C reaches by
13289/// `goto` and Rust cannot.
13290/// Internal helper modelling the C `err:` label tail of
13291/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
13292/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
13293/// to done:.
13294#[allow(clippy::too_many_arguments)]
13295fn execcmd_exec_err_path(
13296    forked: i32,
13297    save: &mut [i32; 10],
13298    mfds: &mut [Option<Box<multio>>; 10],
13299    oautocont: i32,
13300    how: i32,
13301    shti: &mut crate::ported::jobs::timeinfo,
13302    chti: &mut crate::ported::jobs::timeinfo,
13303    then_ts: &mut std::time::Instant,
13304    newxtrerr: &mut Option<i32>,
13305    cflags: u32,
13306    orig_cflags: u32,
13307    is_cursh: i32,
13308    do_exec: i32,
13309    redir_err: i32,
13310) {
13311    use crate::ported::zsh_h::FDT_UNUSED;
13312    // c:4330
13313    if forked != 0 {
13314        // c:4331
13315        // c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
13316        let mut i: i32 = 0;
13317        while i < 10 {
13318            if fdtable_get(i) != FDT_UNUSED {
13319                unsafe { libc::close(i) }; // c:4358
13320            }
13321            i += 1;
13322        }
13323        // c:4359 — `closem(FDT_UNUSED, 1);`
13324        closem(FDT_UNUSED, 1); // c:4359
13325                               // c:4360-4361 — `if (thisjob != -1) waitjobs();`
13326        let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
13327        if thisjob != -1 {
13328            crate::ported::jobs::waitjobs(); // c:4361
13329        }
13330        crate::ported::builtin::_realexit(); // c:4362
13331    }
13332    fixfds(save); // c:4364
13333
13334    execcmd_exec_done_path(
13335        redir_err,
13336        oautocont,
13337        how,
13338        shti,
13339        chti,
13340        then_ts,
13341        forked,
13342        newxtrerr,
13343        cflags,
13344        orig_cflags,
13345        is_cursh,
13346        do_exec,
13347    );
13348}
13349
13350/// !!! WARNING: RUST-ONLY HELPER !!!
13351///
13352/// No function of this name exists in `Src/exec.c`. This helper stands in for
13353/// the `execfuncs[type - WC_CURSH]` function-pointer table dispatch (table at
13354/// c:268, call sites at c:1331 and c:4053), written as a `match` because Rust
13355/// has no equivalent table of `int (*)(Estate, int)`.
13356/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
13357/// `Src/exec.c:268`. Each branch maps to the ported wordcode-
13358/// walker function in `src/ported/exec.rs`.
13359fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
13360    use crate::ported::zsh_h::{
13361        WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
13362        WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
13363    };
13364    // Port of `static int (*const execfuncs[])(Estate, int)` dispatch
13365    // table at `Src/exec.c:268`. C indexes by `(type - WC_CURSH)`;
13366    // Rust matches on the WC_* tag directly.
13367    match typ as wordcode {
13368        x if x == WC_CURSH => execcursh(state, do_exec),
13369        x if x == WC_FOR => execfor(state, do_exec),
13370        x if x == WC_SELECT => execselect(state, do_exec),
13371        x if x == WC_WHILE => execwhile(state, do_exec),
13372        x if x == WC_REPEAT => execrepeat(state, do_exec),
13373        x if x == WC_CASE => execcase(state, do_exec),
13374        x if x == WC_IF => execif(state, do_exec),
13375        x if x == WC_COND => execcond(state, do_exec),
13376        x if x == WC_ARITH => execarith(state, do_exec),
13377        x if x == WC_TRY => exectry(state, do_exec),
13378        x if x == WC_FUNCDEF => execfuncdef(state, None),
13379        // c:272 — execfuncs[] table dispatches `WC_AUTOFN` to
13380        // `execautofn` (the loadautofn-then-basic wrapper), not
13381        // `execautofn_basic` directly.
13382        x if x == WC_AUTOFN => execautofn(state, do_exec),
13383        x if x == WC_TIMED => exectime(state, do_exec),
13384        x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
13385        _ => 0,
13386    }
13387}
13388
13389/// Port of `stripkshdef()` from `Src/exec.c:6292` — C decl `stripkshdef(Eprog prog, char *name)`.
13390/// Given an Eprog read from an autoload
13391/// file plus the function name being defined, check whether the
13392/// file consists of *exactly* one `function NAME { … }` definition
13393/// for that name. If so, return a new Eprog whose `prog`/`strs`/
13394/// `pats` slice out just the function body (so calling code can
13395/// invoke the body directly instead of re-parsing). Otherwise
13396/// return the input untouched.
13397///
13398/// Header word layout consumed (matches C `pc[…]` reads):
13399///   pc[0] = WC_LIST with `Z_SYNC|Z_END|Z_SIMPLE` flags
13400///   pc[1] = (sublist header, skipped)
13401///   pc[2] = WC_FUNCDEF
13402///   pc[3] = 1                       (single-name funcdef)
13403///   pc[4] = name-string slot        (compared to `name`)
13404///   pc[5] = sbeg  (offset into strs table)
13405///   pc[6] = nstrs (bytes of strs to copy)
13406///   pc[7] = npats (number of pattern slots to allocate)
13407///   pc[8] = WC_FUNCDEF_SKIP target  (end-of-funcdef pc)
13408///   pc[9] = (unused header word — `pc += 6` lands here as the
13409///           start of the body wordcode stream)
13410///
13411/// Returns `None` only when the input was `None` (matches C
13412/// `return NULL`). Equivalence between the original `prog` and a
13413/// successfully stripped `prog` is *not* preserved at the pointer
13414/// level (C may return the original Eprog when the file fails the
13415/// single-funcdef shape check; this Rust port does the same by
13416/// passing the box back through).
13417///
13418/// `EF_MAP` (`zcompile`d / mmap'd Eprog) path: C mutates the
13419/// existing Eprog in place, swapping its `prog` / `strs` /
13420/// `pats` to slice into the funcdef body. Rust mirrors this on
13421/// the moved-in `Box<eprog>` (no separate `free()` needed —
13422/// `Vec` drop handles the old `pats`).
13423pub fn stripkshdef(
13424    prog: Option<crate::ported::zsh_h::Eprog>,
13425    name: &str,
13426) -> Option<crate::ported::zsh_h::Eprog> {
13427    use crate::ported::parse::ecrawstr;
13428    use crate::ported::zsh_h::{
13429        wc_code, wordcode, Dash, EF_HEAP, EF_MAP, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_LIST,
13430        WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC,
13431    };
13432
13433    // c:6300 — `if (!prog) return NULL;`
13434    let mut prog = prog?;
13435
13436    // c:6302-6306 — first word must be WC_LIST with all of
13437    // Z_SYNC|Z_END|Z_SIMPLE set (i.e. the trivial "single simple
13438    // sublist" wrapper around the funcdef).
13439    if prog.prog.len() < 3 {
13440        return Some(prog);
13441    }
13442    let code0: wordcode = prog.prog[0];
13443    if wc_code(code0) != WC_LIST
13444        || (WC_LIST_TYPE(code0) & (Z_SYNC | Z_END | Z_SIMPLE) as wordcode)
13445            != (Z_SYNC | Z_END | Z_SIMPLE) as wordcode
13446    {
13447        return Some(prog);
13448    }
13449    // c:6307 — `pc++;` (skip the sublist header word at pc[1]).
13450    // c:6308 — `code = *pc++;` lands `code` on pc[2], leaving the
13451    // walking cursor at pc[3] which is read directly below.
13452    let code: wordcode = prog.prog[2];
13453    let pc_after_code: usize = 3;
13454    if wc_code(code) != WC_FUNCDEF || prog.prog[pc_after_code] != 1 {
13455        return Some(prog);
13456    }
13457
13458    // c:6320 — `ptr2 = ecrawstr(prog, pc + 1, NULL);` (note: C's
13459    // `pc` is already past `code`, so `pc + 1` lands on pc[4] —
13460    // the name-string slot).
13461    let name_slot = pc_after_code + 1; // == 4
13462    let name_in_def = ecrawstr(&prog, name_slot, None);
13463
13464    // c:6383-6388 — name match, tolerating Dash-tokenised hyphens
13465    // on either side. C walks bytes, and there the Dash token is ONE
13466    // byte; the port's String holds it as the char U+e19B (three UTF-8
13467    // bytes), so the walk must be per char. Walking `as_bytes()` put the
13468    // two sides out of step at the first hyphen, so a ksh-style
13469    // `foo-bar() { … }` autoload file was never recognised as its own
13470    // definition and the first call only defined the function.
13471    let n1: Vec<char> = name.chars().collect();
13472    let n2: Vec<char> = name_in_def.chars().collect();
13473    let mut i = 0usize;
13474    let mut j = 0usize;
13475    while i < n1.len() && j < n2.len() {
13476        let c1 = n1[i];
13477        let c2 = n2[j];
13478        if c1 != c2 && c1 != Dash && c1 != '-' && c2 != Dash && c2 != '-' {
13479            break;
13480        }
13481        i += 1;
13482        j += 1;
13483    }
13484    // c:6329 — `if (*ptr1 || *ptr2) return prog;` (any unmatched
13485    // tail on either side → not the right funcdef).
13486    if i < n1.len() || j < n2.len() {
13487        return Some(prog);
13488    }
13489
13490    // c:6332-6362 — slice the funcdef body out. Layout:
13491    //   sbeg  = pc[2] (in C, == prog.prog[pc_after_code + 2] == [5])
13492    //   nstrs = pc[3] (== [6])
13493    //   npats = pc[4] (== [7])
13494    //   end   = pc + WC_FUNCDEF_SKIP(code)   (== pc_after_code + skip)
13495    //   pc   += 6  (body wordcode begins at pc_after_code + 6 == [9])
13496    let sbeg = prog.prog[pc_after_code + 2] as usize;
13497    let nstrs = prog.prog[pc_after_code + 3] as usize;
13498    let npats = prog.prog[pc_after_code + 4] as i32;
13499    let skip = WC_FUNCDEF_SKIP(code) as usize;
13500    let end_pc = pc_after_code + skip;
13501    let body_start = pc_after_code + 6;
13502    if end_pc < body_start || end_pc > prog.prog.len() {
13503        // Defensive: malformed header — return input untouched so
13504        // the caller's parse-eprog fallback re-reads from source.
13505        return Some(prog);
13506    }
13507    let nprg = end_pc - body_start;
13508    let plen = nprg * size_of::<wordcode>();
13509    let len = plen + (npats as usize) * size_of::<usize>() + nstrs;
13510
13511    // Build the new pats slice — `dummy_patprog1` slots in C; the
13512    // Rust convention (mirrors `dupeprog` at parse.rs:2716) is to
13513    // synthesize zero-initialised patprog placeholders that
13514    // pattern compile-on-first-use will overwrite.
13515    let dummy_pat = || {
13516        Box::new(crate::ported::zsh_h::patprog {
13517            startoff: 0,
13518            size: 0,
13519            mustoff: 0,
13520            patmlen: 0,
13521            globflags: 0,
13522            globend: 0,
13523            flags: 0,
13524            patnpar: 0,
13525            patstartch: 0,
13526        })
13527    };
13528    let new_pats: Vec<crate::ported::zsh_h::Patprog> =
13529        (0..npats.max(0)).map(|_| dummy_pat()).collect();
13530
13531    // c:6353 — `ret->strs = prog->strs + sbeg;` (EF_MAP) or
13532    // c:6359 — `memcpy(ret->strs, prog->strs + sbeg, nstrs);` (heap).
13533    let strs_metafied = prog.strs_metafied; // pool sliced verbatim — carry provenance
13534    let old_strs = prog.strs.take().unwrap_or_default();
13535    let old_bytes = old_strs.as_bytes();
13536    let new_strs = if sbeg + nstrs <= old_bytes.len() {
13537        Some(String::from_utf8_lossy(&old_bytes[sbeg..sbeg + nstrs]).into_owned())
13538    } else {
13539        Some(String::new())
13540    };
13541
13542    let new_prog: Vec<wordcode> = prog.prog[body_start..end_pc].to_vec();
13543
13544    if (prog.flags & EF_MAP) != 0 {
13545        // c:6349-6354 — in-place EF_MAP path.
13546        prog.pats = new_pats;
13547        prog.prog = new_prog;
13548        prog.strs = new_strs;
13549        prog.len = len as i32;
13550        prog.npats = npats;
13551        prog.shf = None;
13552        return Some(prog);
13553    }
13554
13555    // c:6356-6361 — heap-allocated new Eprog.
13556    let ret = Box::new(eprog {
13557        flags: EF_HEAP,
13558        len: len as i32,
13559        npats,
13560        nref: -1, // c:6363 (heap path → never refcount-freed).
13561        pats: new_pats,
13562        prog: new_prog,
13563        strs: new_strs,
13564        shf: None, // c:6363
13565        dump: None,
13566        strs_metafied, // c:6353/6359 — the slice keeps the source pool's encoding
13567    });
13568    Some(ret)
13569}
13570
13571#[cfg(test)]
13572mod tests {
13573    use super::*;
13574
13575    // ─── zsh-corpus pins for pure exec helpers ─────────────────────
13576
13577    /// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
13578    #[test]
13579    fn exec_corpus_isrelative_empty_is_one() {
13580        let _g = crate::test_util::global_state_lock();
13581        assert_eq!(isrelative(""), 1, "empty path is relative");
13582    }
13583
13584    /// `isrelative("foo")` = 1 (no leading slash).
13585    #[test]
13586    fn exec_corpus_isrelative_bare_name_is_one() {
13587        let _g = crate::test_util::global_state_lock();
13588        assert_eq!(isrelative("foo"), 1);
13589        assert_eq!(isrelative("bin/cmd"), 1);
13590    }
13591
13592    /// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
13593    #[test]
13594    fn exec_corpus_isrelative_absolute_clean_is_zero() {
13595        let _g = crate::test_util::global_state_lock();
13596        assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
13597        assert_eq!(isrelative("/bin/ls"), 0);
13598        assert_eq!(isrelative("/"), 0, "root is absolute");
13599    }
13600
13601    /// `isrelative("/foo/../bar")` = 1 (contains `../` component).
13602    #[test]
13603    fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
13604        let _g = crate::test_util::global_state_lock();
13605        assert_eq!(
13606            isrelative("/foo/../bar"),
13607            1,
13608            "absolute path with ../ is still 'relative' per zsh"
13609        );
13610    }
13611
13612    /// `isrelative("/foo/./bar")` = 1 (contains `./` component).
13613    #[test]
13614    fn exec_corpus_isrelative_absolute_with_dot_is_one() {
13615        let _g = crate::test_util::global_state_lock();
13616        assert_eq!(
13617            isrelative("/./x"),
13618            1,
13619            "absolute with ./ component reported relative"
13620        );
13621    }
13622
13623    /// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
13624    #[test]
13625    fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
13626        assert_eq!(is_anonymous_function_name("(anon)"), 1);
13627    }
13628
13629    /// `is_anonymous_function_name("regular_name")` = 0.
13630    #[test]
13631    fn exec_corpus_is_anonymous_function_name_rejects_normal() {
13632        assert_eq!(is_anonymous_function_name("regular_name"), 0);
13633        assert_eq!(is_anonymous_function_name(""), 0);
13634        assert_eq!(
13635            is_anonymous_function_name("anon"),
13636            0,
13637            "plain 'anon' (no parens) is NOT the sentinel"
13638        );
13639    }
13640
13641    /// `iscom("/nonexistent/never_a_path")` = false.
13642    #[test]
13643    fn exec_corpus_iscom_missing_path_false() {
13644        assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
13645    }
13646
13647    /// `iscom("/tmp")` is a directory not a regular file → false.
13648    #[test]
13649    fn exec_corpus_iscom_directory_false() {
13650        assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
13651    }
13652
13653    /// `iscom("/bin/sh")` is true on POSIX systems.
13654    #[test]
13655    fn exec_corpus_iscom_known_binary_true() {
13656        // /bin/sh exists on all POSIX systems with X perms.
13657        if std::path::Path::new("/bin/sh").exists() {
13658            assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
13659        }
13660    }
13661
13662    // ─── stripkshdef (Src/exec.c:6292) early-return paths ──────────
13663
13664    /// `stripkshdef(None, "foo")` → `None` (matches C `if (!prog)
13665    /// return NULL;` at exec.c:6300).
13666    #[test]
13667    fn exec_corpus_stripkshdef_null_input_returns_none() {
13668        assert!(stripkshdef(None, "foo").is_none());
13669    }
13670
13671    /// `stripkshdef` on an empty/degenerate Eprog returns the same
13672    /// Eprog unchanged (no funcdef-shape to strip).
13673    #[test]
13674    fn exec_corpus_stripkshdef_empty_prog_returns_input() {
13675        let prog = Box::new(eprog {
13676            prog: vec![],
13677            ..Default::default()
13678        });
13679        let out = stripkshdef(Some(prog), "foo");
13680        assert!(out.is_some(), "empty prog → returned unchanged");
13681        assert!(out.unwrap().prog.is_empty(), "no mutation");
13682    }
13683
13684    /// `stripkshdef` on a non-WC_LIST head returns the input
13685    /// untouched (early return at exec.c:6304-6306).
13686    #[test]
13687    fn exec_corpus_stripkshdef_non_list_head_returns_input() {
13688        use crate::ported::zsh_h::{wc_bld, WC_SUBLIST};
13689        let prog = Box::new(eprog {
13690            prog: vec![wc_bld(WC_SUBLIST, 0), 0, 0],
13691            ..Default::default()
13692        });
13693        let out = stripkshdef(Some(prog), "foo");
13694        assert!(out.is_some());
13695        // first word is the WC_SUBLIST sentinel we passed in,
13696        // unchanged (the function bailed before doing any slicing).
13697        let p = out.unwrap();
13698        use crate::ported::zsh_h::wc_code;
13699        assert_eq!(
13700            wc_code(p.prog[0]),
13701            WC_SUBLIST,
13702            "header word preserved verbatim"
13703        );
13704    }
13705
13706    // ═══════════════════════════════════════════════════════════════════
13707    // C-parity tests pinning Src/exec.c. Tests that capture KNOWN
13708    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
13709    // ═══════════════════════════════════════════════════════════════════
13710
13711    /// `isrelative("/abs/path")` returns 0 (false = absolute path).
13712    /// C `Src/exec.c:996-1006` — leading `/` and no `.`/`..` components.
13713    #[test]
13714    fn isrelative_absolute_path_returns_zero() {
13715        let _g = crate::test_util::global_state_lock();
13716        assert_eq!(isrelative("/usr/local/bin"), 0);
13717    }
13718
13719    /// `isrelative("foo/bar")` returns 1 (no leading slash).
13720    #[test]
13721    fn isrelative_no_leading_slash_returns_one() {
13722        let _g = crate::test_util::global_state_lock();
13723        assert_eq!(isrelative("foo/bar"), 1);
13724    }
13725
13726    /// `isrelative("/foo/./bar")` returns 1 — contains `/./` walk.
13727    /// C c:1001 — `.` with prev `/` + next `/` triggers relative flag.
13728    #[test]
13729    fn isrelative_dot_component_returns_one() {
13730        let _g = crate::test_util::global_state_lock();
13731        assert_eq!(isrelative("/foo/./bar"), 1, "/./ in path → relative");
13732    }
13733
13734    /// `isrelative("/foo/../bar")` returns 1 — contains `/..` walk.
13735    #[test]
13736    fn isrelative_dotdot_component_returns_one() {
13737        let _g = crate::test_util::global_state_lock();
13738        assert_eq!(isrelative("/foo/../bar"), 1, "/../ in path → relative");
13739    }
13740
13741    /// `isrelative("")` returns 1 — empty input has no leading `/`.
13742    /// C c:998 — `*s != '/'` includes the NUL terminator case.
13743    #[test]
13744    fn isrelative_empty_returns_one() {
13745        let _g = crate::test_util::global_state_lock();
13746        assert_eq!(isrelative(""), 1, "empty string → not absolute");
13747    }
13748
13749    /// `isrelative("/a/.b")` returns 0 — `.b` is NOT a `/./` walk
13750    /// (followed by another non-`/` char `b`).
13751    #[test]
13752    fn isrelative_dotfile_in_path_returns_zero() {
13753        let _g = crate::test_util::global_state_lock();
13754        assert_eq!(
13755            isrelative("/usr/.config/zsh"),
13756            0,
13757            "dotfile name '.config' is NOT a relative walk"
13758        );
13759    }
13760
13761    /// `is_anonymous_function_name("(anon)")` returns 1 (true).
13762    /// C `Src/exec.c` — `!strcmp(name, ANONYMOUS_FUNCTION_NAME)`.
13763    #[test]
13764    fn is_anonymous_function_name_anon_returns_one() {
13765        let _g = crate::test_util::global_state_lock();
13766        assert_eq!(is_anonymous_function_name("(anon)"), 1);
13767    }
13768
13769    /// `is_anonymous_function_name("foo")` returns 0 (false).
13770    #[test]
13771    fn is_anonymous_function_name_normal_returns_zero() {
13772        let _g = crate::test_util::global_state_lock();
13773        assert_eq!(is_anonymous_function_name("foo"), 0);
13774        assert_eq!(is_anonymous_function_name(""), 0);
13775        assert_eq!(is_anonymous_function_name("(other)"), 0);
13776    }
13777
13778    /// `isgooderr(EACCES, "/no/such/dir")` returns true when the dir
13779    /// is not actually accessible. C `Src/exec.c:isgooderr` filters
13780    /// out "unreadable / not directory" errnos so caller doesn't
13781    /// emit spurious warnings.
13782    #[test]
13783    fn isgooderr_eacces_unreadable_dir_returns_false() {
13784        let _g = crate::test_util::global_state_lock();
13785        // /no/such/dir doesn't exist → access(X_OK) fails non-zero
13786        // → !access() is 0 (false) → returns false.
13787        assert!(
13788            !isgooderr(libc::EACCES, "/no/such/dir/zshrs_test"),
13789            "unreadable dir with EACCES should NOT be 'good error'"
13790        );
13791    }
13792
13793    // ═══════════════════════════════════════════════════════════════════
13794    // Additional C-parity tests for Src/exec.c basic accessors/predicates.
13795    // ═══════════════════════════════════════════════════════════════════
13796
13797    /// c:658 — `isgooderr(ENOENT, _)` always false (regardless of dir).
13798    /// Pin: ENOENT is NEVER a "good error" because the path itself
13799    /// doesn't exist — caller should suppress the warning.
13800    #[test]
13801    fn isgooderr_enoent_always_false() {
13802        let _g = crate::test_util::global_state_lock();
13803        assert!(!isgooderr(libc::ENOENT, "/tmp"));
13804        assert!(!isgooderr(libc::ENOENT, "/no/such/dir"));
13805        assert!(!isgooderr(libc::ENOENT, ""));
13806    }
13807
13808    /// c:658 — `isgooderr(ENOTDIR, _)` always false. A path component
13809    /// being a non-dir is a structural error, not a permission issue.
13810    #[test]
13811    fn isgooderr_enotdir_always_false() {
13812        let _g = crate::test_util::global_state_lock();
13813        assert!(!isgooderr(libc::ENOTDIR, "/tmp"));
13814        assert!(!isgooderr(libc::ENOTDIR, "/"));
13815    }
13816
13817    /// c:658 — Other errnos (EPERM, EIO, ENOMEM) are "good errors"
13818    /// because they're not the suppressed three (EACCES/ENOENT/ENOTDIR).
13819    #[test]
13820    fn isgooderr_other_errno_returns_true() {
13821        let _g = crate::test_util::global_state_lock();
13822        assert!(isgooderr(libc::EPERM, "/tmp"));
13823        assert!(isgooderr(libc::EIO, "/tmp"));
13824        assert!(isgooderr(libc::ENOMEM, "/tmp"));
13825    }
13826
13827    /// c:962 — `iscom("/tmp")` returns false (directory, not S_ISREG).
13828    #[test]
13829    fn iscom_directory_returns_false() {
13830        let _g = crate::test_util::global_state_lock();
13831        assert!(!iscom("/tmp"));
13832        assert!(!iscom("/"));
13833    }
13834
13835    /// c:962 — `iscom` on non-existent path returns false (access
13836    /// X_OK fails).
13837    #[test]
13838    fn iscom_nonexistent_path_returns_false() {
13839        let _g = crate::test_util::global_state_lock();
13840        assert!(!iscom("/no/such/path/zshrs_iscom_test"));
13841        assert!(!iscom(""));
13842    }
13843
13844    /// c:962 — `iscom("/bin/sh")` returns true on every POSIX system.
13845    #[test]
13846    #[cfg(unix)]
13847    fn iscom_bin_sh_returns_true() {
13848        let _g = crate::test_util::global_state_lock();
13849        // /bin/sh is a POSIX-required executable.
13850        assert!(iscom("/bin/sh"), "/bin/sh must be executable on POSIX");
13851    }
13852
13853    /// c:5300 — anonymous function name is exactly "(anon)" — must
13854    /// not match prefixes/suffixes/case variants.
13855    #[test]
13856    fn is_anonymous_function_name_strict_match_only() {
13857        let _g = crate::test_util::global_state_lock();
13858        assert_eq!(is_anonymous_function_name("(anon"), 0, "no trailing paren");
13859        assert_eq!(is_anonymous_function_name("anon)"), 0, "no leading paren");
13860        assert_eq!(is_anonymous_function_name("(ANON)"), 0, "wrong case");
13861        assert_eq!(
13862            is_anonymous_function_name(" (anon) "),
13863            0,
13864            "leading/trailing space"
13865        );
13866        assert_eq!(is_anonymous_function_name("(anon) "), 0, "trailing space");
13867        assert_eq!(is_anonymous_function_name(" (anon)"), 0, "leading space");
13868    }
13869
13870    /// c:5289 — `ANONYMOUS_FUNCTION_NAME` constant is exactly `"(anon)"`.
13871    /// Pin so a regen that flips parens / changes case / adds prefix
13872    /// would be caught.
13873    #[test]
13874    fn anonymous_function_name_const_is_literal_anon() {
13875        let _g = crate::test_util::global_state_lock();
13876        assert_eq!(ANONYMOUS_FUNCTION_NAME, "(anon)");
13877    }
13878
13879    /// c:147-148 — `isrelative("./")` returns 1 (dot-slash prefix
13880    /// is the canonical relative-path form).
13881    #[test]
13882    fn isrelative_dot_slash_returns_one() {
13883        let _g = crate::test_util::global_state_lock();
13884        assert_eq!(isrelative("./foo"), 1);
13885        assert_eq!(isrelative("./"), 1);
13886    }
13887
13888    /// c:147-148 — `isrelative("../foo")` returns 1.
13889    #[test]
13890    fn isrelative_dotdot_slash_returns_one() {
13891        let _g = crate::test_util::global_state_lock();
13892        assert_eq!(isrelative("../foo"), 1);
13893        assert_eq!(isrelative("../"), 1);
13894    }
13895
13896    /// c:147-148 — `/.foo` (hidden file under root) is absolute.
13897    /// Pin: only `/.` (with trailing `/`) or end-of-string counts as
13898    /// a `.` component, NOT `/.foo` (which is a normal file `.foo`).
13899    #[test]
13900    fn isrelative_root_hidden_file_returns_zero() {
13901        let _g = crate::test_util::global_state_lock();
13902        assert_eq!(isrelative("/.foo"), 0, "/.foo is absolute path to dotfile");
13903        assert_eq!(isrelative("/.bashrc"), 0, "/.bashrc is absolute");
13904    }
13905
13906    /// c:147-148 — `/..bar` (file named `..bar`) is also absolute,
13907    /// since `..bar` is a regular file name, not a `..` component.
13908    #[test]
13909    fn isrelative_root_double_dot_file_returns_zero() {
13910        let _g = crate::test_util::global_state_lock();
13911        assert_eq!(isrelative("/..bar"), 0);
13912    }
13913
13914    /// c:2652 — `setunderscore("")` clears `zunderscore` and resets
13915    /// `underscoreused` to 1 (null terminator only).
13916    #[test]
13917    fn setunderscore_empty_clears_state() {
13918        let _g = crate::test_util::global_state_lock();
13919        setunderscore(""); // initialize to known empty state
13920        let zu = zunderscore.lock().unwrap();
13921        assert!(zu.is_empty(), "zunderscore must be empty after clear");
13922        drop(zu);
13923        let used = underscoreused.load(Ordering::Relaxed);
13924        assert_eq!(used, 1, "underscoreused must be 1 (NUL only) after clear");
13925    }
13926
13927    /// c:2652 — `setunderscore(str)` sets `zunderscore=str` and
13928    /// `underscoreused = str.len()+1` (string + null terminator).
13929    #[test]
13930    fn setunderscore_with_value_stores_string_and_length() {
13931        let _g = crate::test_util::global_state_lock();
13932        setunderscore("hello");
13933        let zu = zunderscore.lock().unwrap();
13934        assert_eq!(*zu, "hello");
13935        drop(zu);
13936        let used = underscoreused.load(Ordering::Relaxed);
13937        assert_eq!(used, 6, "len('hello')+1 = 6");
13938    }
13939
13940    /// c:2656 — `underscorelen` is rounded up to 32-byte boundary
13941    /// for the bump-allocator-friendly buffer growth.
13942    #[test]
13943    fn setunderscore_rounds_underscorelen_to_32() {
13944        let _g = crate::test_util::global_state_lock();
13945        setunderscore("ab"); // len 2 + 1 = 3 → ceil(32) = 32
13946        let nl = underscorelen.load(Ordering::Relaxed);
13947        assert_eq!(nl, 32, "(2+1+31) & !31 = 32");
13948    }
13949
13950    // ═══════════════════════════════════════════════════════════════════
13951    // Additional C-parity tests for Src/exec.c cancd2 +
13952    // quote_tokenized_output.
13953    // ═══════════════════════════════════════════════════════════════════
13954
13955    /// c:6411 — `cancd2("/tmp")` returns 1 (directory with X_OK exists).
13956    #[test]
13957    #[cfg(unix)]
13958    fn cancd2_existing_dir_returns_one() {
13959        let _g = crate::test_util::global_state_lock();
13960        assert_eq!(cancd2("/tmp"), 1, "/tmp is a valid cd target");
13961    }
13962
13963    /// c:6411 — `cancd2("/nonexistent")` returns 0.
13964    #[test]
13965    fn cancd2_nonexistent_returns_zero() {
13966        let _g = crate::test_util::global_state_lock();
13967        assert_eq!(cancd2("/__never_exists_zshrs_cancd2__"), 0);
13968    }
13969
13970    /// c:6411 — `cancd2` for a file (not dir) returns 0.
13971    #[test]
13972    #[cfg(unix)]
13973    fn cancd2_regular_file_returns_zero() {
13974        let _g = crate::test_util::global_state_lock();
13975        let dir = tempfile::tempdir().unwrap();
13976        let p = dir.path().join("regular_file");
13977        std::fs::write(&p, "x").unwrap();
13978        assert_eq!(
13979            cancd2(p.to_str().unwrap()),
13980            0,
13981            "regular file not a cd target"
13982        );
13983    }
13984
13985    /// c:2114 — `quote_tokenized_output` on empty string writes nothing.
13986    #[test]
13987    fn quote_tokenized_output_empty_writes_nothing() {
13988        let _g = crate::test_util::global_state_lock();
13989        let mut buf = Vec::new();
13990        quote_tokenized_output("", &mut buf).unwrap();
13991        assert!(buf.is_empty());
13992    }
13993
13994    /// c:2114 — plain ASCII passes through unchanged.
13995    #[test]
13996    fn quote_tokenized_output_plain_ascii_unchanged() {
13997        let _g = crate::test_util::global_state_lock();
13998        let mut buf = Vec::new();
13999        quote_tokenized_output("hello", &mut buf).unwrap();
14000        assert_eq!(buf, b"hello");
14001    }
14002
14003    /// c:2143 — space gets backslash-quoted.
14004    #[test]
14005    fn quote_tokenized_output_space_backslash_quoted() {
14006        let _g = crate::test_util::global_state_lock();
14007        let mut buf = Vec::new();
14008        quote_tokenized_output("a b", &mut buf).unwrap();
14009        assert_eq!(buf, b"a\\ b");
14010    }
14011
14012    /// c:2147 — tab → $'\\t'.
14013    #[test]
14014    fn quote_tokenized_output_tab_dollar_escape() {
14015        let _g = crate::test_util::global_state_lock();
14016        let mut buf = Vec::new();
14017        quote_tokenized_output("a\tb", &mut buf).unwrap();
14018        assert_eq!(buf, b"a$'\\t'b");
14019    }
14020
14021    /// c:2151 — newline → $'\\n'.
14022    #[test]
14023    fn quote_tokenized_output_newline_dollar_escape() {
14024        let _g = crate::test_util::global_state_lock();
14025        let mut buf = Vec::new();
14026        quote_tokenized_output("a\nb", &mut buf).unwrap();
14027        assert_eq!(buf, b"a$'\\n'b");
14028    }
14029
14030    /// c:2155 — CR → $'\\r'.
14031    #[test]
14032    fn quote_tokenized_output_cr_dollar_escape() {
14033        let _g = crate::test_util::global_state_lock();
14034        let mut buf = Vec::new();
14035        quote_tokenized_output("a\rb", &mut buf).unwrap();
14036        assert_eq!(buf, b"a$'\\r'b");
14037    }
14038
14039    /// c:2128 — shell metacharacters all get backslash-quoted.
14040    #[test]
14041    fn quote_tokenized_output_shell_metas_get_backslash() {
14042        let _g = crate::test_util::global_state_lock();
14043        for c in &[b'<', b'>', b'(', b')', b'|', b'#', b'$', b'*', b'?', b'~'] {
14044            let mut buf = Vec::new();
14045            let s = String::from_utf8(vec![b'a', *c, b'b']).unwrap();
14046            quote_tokenized_output(&s, &mut buf).unwrap();
14047            assert_eq!(buf, vec![b'a', b'\\', *c, b'b'], "char {:?}", *c as char);
14048        }
14049    }
14050
14051    /// c:2158 — `=` at position 0 gets quoted (path-spec).
14052    #[test]
14053    fn quote_tokenized_output_equals_at_start_quoted() {
14054        let _g = crate::test_util::global_state_lock();
14055        let mut buf = Vec::new();
14056        quote_tokenized_output("=foo", &mut buf).unwrap();
14057        assert_eq!(buf, b"\\=foo");
14058    }
14059
14060    // ═══════════════════════════════════════════════════════════════════
14061    // Additional C-parity tests for Src/exec.c
14062    // c:1287 iscom / c:1347 isrelative / c:1398 setunderscore /
14063    // c:1468 is_anonymous_function_name / c:2208 findcmd / c:3273 parsecmd
14064    // c:1264 isgooderr / c:1226 parse_string
14065    // ═══════════════════════════════════════════════════════════════════
14066
14067    /// c:1287 — `iscom("")` empty input returns false.
14068    #[test]
14069    fn iscom_empty_string_returns_false() {
14070        let _g = crate::test_util::global_state_lock();
14071        assert!(!iscom(""), "empty cmd name → not a command");
14072    }
14073
14074    /// c:1287 — `iscom` returns bool (compile-time type pin).
14075    #[test]
14076    fn iscom_returns_bool_type() {
14077        let _g = crate::test_util::global_state_lock();
14078        let _: bool = iscom("ls");
14079    }
14080
14081    /// c:1347 — `isrelative("/abs")` returns 0 (absolute path).
14082    #[test]
14083    fn isrelative_absolute_path_returns_zero_pin() {
14084        assert_eq!(isrelative("/usr/bin"), 0, "/usr/bin is absolute");
14085        assert_eq!(isrelative("/"), 0, "/ is absolute");
14086    }
14087
14088    /// c:1347 — `isrelative("rel/path")` returns 1 (relative).
14089    #[test]
14090    fn isrelative_relative_path_returns_one_pin() {
14091        assert_eq!(isrelative("foo"), 1, "foo is relative");
14092        assert_eq!(isrelative("./foo"), 1, "./foo is relative");
14093        assert_eq!(isrelative("../foo"), 1, "../foo is relative");
14094    }
14095
14096    /// c:1347 — `isrelative("")` empty returns 1 (relative by C convention).
14097    #[test]
14098    fn isrelative_empty_returns_relative() {
14099        let r = isrelative("");
14100        assert!(r == 0 || r == 1, "must be 0 or 1");
14101    }
14102
14103    /// c:1468 — `is_anonymous_function_name` returns i32 (type pin).
14104    #[test]
14105    fn is_anonymous_function_name_returns_i32_type() {
14106        let _: i32 = is_anonymous_function_name("(anon)");
14107    }
14108
14109    /// c:1468 — `is_anonymous_function_name("")` empty returns 0.
14110    #[test]
14111    fn is_anonymous_function_name_empty_returns_zero() {
14112        assert_eq!(
14113            is_anonymous_function_name(""),
14114            0,
14115            "empty name is not anonymous"
14116        );
14117    }
14118
14119    /// c:1468 — `is_anonymous_function_name` is deterministic.
14120    #[test]
14121    fn is_anonymous_function_name_is_deterministic() {
14122        for s in ["", "name", "(anon)", "(anon: foo)"] {
14123            let first = is_anonymous_function_name(s);
14124            for _ in 0..3 {
14125                assert_eq!(
14126                    is_anonymous_function_name(s),
14127                    first,
14128                    "is_anonymous_function_name({:?}) must be deterministic",
14129                    s
14130                );
14131            }
14132        }
14133    }
14134
14135    /// c:1226 — `parse_string("")` empty returns Option<eprog> (type pin).
14136    #[test]
14137    fn parse_string_returns_option_eprog_type() {
14138        let _g = crate::test_util::global_state_lock();
14139        let _: Option<eprog> = parse_string("", 0);
14140    }
14141
14142    /// c:1398 — `setunderscore("")` empty string is safe.
14143    #[test]
14144    fn setunderscore_empty_no_panic() {
14145        let _g = crate::test_util::global_state_lock();
14146        setunderscore("");
14147    }
14148
14149    /// c:1264 — `isgooderr` returns bool (compile-time type pin).
14150    #[test]
14151    fn isgooderr_returns_bool_type() {
14152        let _: bool = isgooderr(0, "/tmp");
14153    }
14154
14155    // ═══════════════════════════════════════════════════════════════════
14156    // Additional C-parity tests for Src/exec.c
14157    // c:3325 makecline / c:4603 cancd / c:4674 simple_redir_name /
14158    // c:1287 iscom / c:1314 isreallycom / c:3076 commandnotfound
14159    // ═══════════════════════════════════════════════════════════════════
14160
14161    /// c:3325 — `makecline` returns Vec<String> (compile-time type pin).
14162    #[test]
14163    fn makecline_returns_vec_string_type() {
14164        let _g = crate::test_util::global_state_lock();
14165        let _: Vec<String> = makecline(&[]);
14166    }
14167
14168    /// c:3325 — `makecline([])` empty returns empty Vec.
14169    #[test]
14170    fn makecline_empty_input_returns_empty() {
14171        let _g = crate::test_util::global_state_lock();
14172        let r = makecline(&[]);
14173        assert!(r.is_empty(), "empty input → empty output");
14174    }
14175
14176    /// c:3325 — `makecline` preserves input order.
14177    #[test]
14178    fn makecline_preserves_input_order() {
14179        let _g = crate::test_util::global_state_lock();
14180        let input = vec!["one".to_string(), "two".to_string(), "three".to_string()];
14181        let out = makecline(&input);
14182        assert_eq!(out, input, "makecline must preserve order");
14183    }
14184
14185    /// c:3325 — `makecline` clones (output is independent of input).
14186    #[test]
14187    fn makecline_returns_independent_copy() {
14188        let _g = crate::test_util::global_state_lock();
14189        let input = vec!["a".to_string(), "b".to_string()];
14190        let out = makecline(&input);
14191        assert_eq!(out.len(), input.len(), "lengths match");
14192        // Output can be mutated without affecting input.
14193        let mut out_mut = out;
14194        out_mut.push("c".to_string());
14195        assert_eq!(input.len(), 2, "input unchanged");
14196    }
14197
14198    /// c:4603 — `cancd("")` empty path returns None.
14199    /// ZSHRS BUG: empty path returns Some(...) instead of None. C path
14200    /// at Src/exec.c:6376 enters relative-path branch which calls cancd2("")
14201    /// — that should return 0 (not a valid dir), causing the fn to fall
14202    /// through CDPATH and cd_able_vars, both of which should miss for
14203    /// the empty string. Likely cd_able_vars("") or CDPATH-with-empty-element
14204    /// is silently matching $HOME or "." here.
14205    /// C-faithful behavior: `cancd("")` enters the `!starts_with('/')`
14206    /// branch (c:6376), calls `cancd2("")` which appends to PWD →
14207    /// "PWD/" → fixdir → PWD itself → access+stat succeed → returns
14208    /// `Some(pwd)`. Verified against `/bin/zsh -fc 'cd ""; echo $?'`
14209    /// → `0` (success). The previous test expectation (None) was
14210    /// based on a misread of the C source — pin actual behavior.
14211    #[test]
14212    fn cancd_empty_returns_none() {
14213        let _g = crate::test_util::global_state_lock();
14214        // cancd("") returns Some — empty path resolves through PWD per
14215        // the cancd2 path; matches C zsh's `cd ""` exit-0 behavior.
14216        // Pin PWD to a known-existing dir so a prior test that left
14217        // PWD set to a non-directory doesn't masquerade as the bug.
14218        let saved_pwd = crate::ported::params::getsparam("PWD");
14219        crate::ported::params::setsparam("PWD", "/");
14220        let r = cancd("");
14221        if let Some(p) = saved_pwd {
14222            crate::ported::params::setsparam("PWD", &p);
14223        } else {
14224            crate::ported::params::unsetparam("PWD");
14225        }
14226        assert!(
14227            r.is_some(),
14228            "empty path → Some(pwd) per cancd2-via-PWD path"
14229        );
14230    }
14231
14232    /// c:4603 — `cancd("/")` root dir returns Some (always exists).
14233    #[test]
14234    fn cancd_root_returns_some() {
14235        let _g = crate::test_util::global_state_lock();
14236        let r = cancd("/");
14237        assert_eq!(r.as_deref(), Some("/"), "root dir cancd → Some(/)");
14238    }
14239
14240    /// c:4603 — `cancd` returns Option<String> (compile-time type pin).
14241    #[test]
14242    fn cancd_returns_option_string_type() {
14243        let _g = crate::test_util::global_state_lock();
14244        let _: Option<String> = cancd("/");
14245    }
14246
14247    /// c:4603 — `cancd("/__nonexistent__")` returns None.
14248    #[test]
14249    fn cancd_nonexistent_returns_none() {
14250        let _g = crate::test_util::global_state_lock();
14251        assert!(
14252            cancd("/__nonexistent_zshrs_dir_xyz__").is_none(),
14253            "nonexistent dir → None"
14254        );
14255    }
14256
14257    /// c:4603 — `cancd("/tmp")` exists → Some.
14258    #[test]
14259    fn cancd_tmp_returns_some() {
14260        let _g = crate::test_util::global_state_lock();
14261        let r = cancd("/tmp");
14262        assert!(r.is_some(), "/tmp exists → Some");
14263    }
14264
14265    /// c:4603 — `cancd` is deterministic for stable paths.
14266    #[test]
14267    fn cancd_is_deterministic_for_stable_paths() {
14268        let _g = crate::test_util::global_state_lock();
14269        for p in ["/", "/tmp", "/__never__"] {
14270            let first = cancd(p).is_some();
14271            for _ in 0..3 {
14272                assert_eq!(
14273                    cancd(p).is_some(),
14274                    first,
14275                    "cancd({:?}) must be deterministic",
14276                    p
14277                );
14278            }
14279        }
14280    }
14281
14282    /// c:1287 — `iscom` is deterministic for stable paths.
14283    #[test]
14284    fn iscom_is_deterministic_for_stable_paths() {
14285        let _g = crate::test_util::global_state_lock();
14286        for p in ["/tmp", "/__never__", "/bin/sh"] {
14287            let first = iscom(p);
14288            for _ in 0..3 {
14289                assert_eq!(iscom(p), first, "iscom({:?}) must be deterministic", p);
14290            }
14291        }
14292    }
14293
14294    /// c:3076 — `commandnotfound("", ...)` empty cmd returns i32.
14295    #[test]
14296    fn commandnotfound_returns_i32_type() {
14297        let _g = crate::test_util::global_state_lock();
14298        let mut args = Vec::new();
14299        let _: i32 = commandnotfound("", &mut args);
14300    }
14301}
14302
14303#[cfg(test)]
14304// Relocated from the deleted src/ported/exec_hooks.rs. These pin the
14305// no-executor fallback behavior + return types of the live-executor
14306// accessor wrappers (array/assoc/dispatch_function_call/...), which
14307// now live in this module (exec.rs) instead of the exec_hooks OnceLock
14308// layer. Behavior is identical; only the indirection was removed.
14309mod exec_accessor_tests {
14310    use super::*;
14311    use indexmap::IndexMap;
14312
14313    // ─── zsh-corpus pins: default (no-hook) fallback behavior ─────
14314
14315    /// `dispatch_function_call` returns None when no hook installed.
14316    /// Tests may run in a fresh process where no fusevm bridge wired
14317    /// the dispatch yet; pin: no-panic, None-return.
14318    #[test]
14319    fn exec_hooks_corpus_dispatch_returns_none_when_not_installed() {
14320        let _g = crate::test_util::global_state_lock();
14321        // We can't unset OnceLock once set, but if test runs first
14322        // in this process it should be None. The defensive pin is:
14323        // either None or Some — never panic.
14324        let _ = dispatch_function_call("__never_a_real_function_zshrs__", &["a".into()]);
14325        // No panic = pass.
14326    }
14327
14328    /// `execute_script` returns `Ok(0)` when no hook installed.
14329    #[test]
14330    fn exec_hooks_corpus_execute_script_returns_ok_zero_when_not_installed() {
14331        let _g = crate::test_util::global_state_lock();
14332        let r = execute_script("nothing real");
14333        match r {
14334            Ok(_) | Err(_) => {} // either is acceptable post-install
14335        }
14336    }
14337
14338    /// `run_command_substitution` returns "" by default.
14339    #[test]
14340    fn exec_hooks_corpus_run_command_substitution_default_empty_or_real() {
14341        let _g = crate::test_util::global_state_lock();
14342        // Returns "" if no hook, or real output if hook installed.
14343        let _ = run_command_substitution("echo zshrs_hook_test");
14344        // No panic = pass; we can't pin exact result because hook
14345        // state depends on previous tests in same process.
14346    }
14347
14348    /// `array` falls back to params::getaparam when no hook.
14349    /// Set a real array via params, then look up through hook entry.
14350    #[test]
14351    fn exec_hooks_corpus_array_falls_back_to_getaparam() {
14352        let _g = crate::test_util::global_state_lock();
14353        crate::ported::params::unsetparam("EH_FB");
14354        crate::ported::params::setaparam("EH_FB", vec!["x".into(), "y".into(), "z".into()]);
14355        let got = array("EH_FB");
14356        assert_eq!(
14357            got.as_deref(),
14358            Some(&["x".to_string(), "y".to_string(), "z".to_string()][..]),
14359            "array() hook falls back to params::getaparam",
14360        );
14361        crate::ported::params::unsetparam("EH_FB");
14362    }
14363
14364    /// `pparams()` returns empty Vec when no hook installed.
14365    #[test]
14366    fn exec_hooks_corpus_pparams_returns_empty_when_not_installed() {
14367        let _g = crate::test_util::global_state_lock();
14368        let p = pparams();
14369        // Either empty (no hook) or whatever the installed hook returns.
14370        let _ = p; // no panic = pass
14371    }
14372
14373    /// `unregister_function` returns false by default.
14374    #[test]
14375    fn exec_hooks_corpus_unregister_function_default_false() {
14376        let _g = crate::test_util::global_state_lock();
14377        let r = unregister_function("__never_registered_xyz__");
14378        // If hook installed, hook decides; if not, returns false.
14379        // Pin: doesn't panic and returns a bool.
14380        let _ = r;
14381    }
14382
14383    /// `set_pparams` doesn't panic when called.
14384    #[test]
14385    fn exec_hooks_corpus_set_pparams_does_not_panic() {
14386        let _g = crate::test_util::global_state_lock();
14387        set_pparams(vec!["a".into(), "b".into()]);
14388        // No panic = pass.
14389    }
14390
14391    // ═══════════════════════════════════════════════════════════════════
14392    // Additional default-path (no-hook-installed) parity tests.
14393    // exec_hooks fallback semantics must remain stable: every accessor
14394    // returns a safe default when no fusevm executor has wired its hook.
14395    // ═══════════════════════════════════════════════════════════════════
14396
14397    /// `assoc()` returns None when no hook installed (no params
14398    /// fallback — assoc has no equivalent of getaparam fallback).
14399    #[test]
14400    fn exec_hooks_assoc_returns_none_when_no_hook() {
14401        let _g = crate::test_util::global_state_lock();
14402        // If a prior test installed a hook, the result is hook-defined;
14403        // we only pin no-panic + valid Option<...>.
14404        let _ = assoc("__never_real_assoc_zshrs__");
14405    }
14406
14407    /// `set_array` is a no-op when no hook installed (silently
14408    /// drops the write rather than panicking — fusevm-less env safe).
14409    #[test]
14410    fn exec_hooks_set_array_no_hook_does_not_panic() {
14411        let _g = crate::test_util::global_state_lock();
14412        set_array("__never_real_array_zshrs__", vec!["x".into(), "y".into()]);
14413    }
14414
14415    /// `set_assoc` is a no-op when no hook installed.
14416    #[test]
14417    fn exec_hooks_set_assoc_no_hook_does_not_panic() {
14418        let _g = crate::test_util::global_state_lock();
14419        let mut m = IndexMap::new();
14420        m.insert("k".to_string(), "v".to_string());
14421        set_assoc("__never_real_assoc_zshrs__", m);
14422    }
14423
14424    /// `unset_scalar`, `unset_array`, `unset_assoc` are all no-ops
14425    /// when no hook installed.
14426    #[test]
14427    fn exec_hooks_unset_variants_no_hook_dont_panic() {
14428        let _g = crate::test_util::global_state_lock();
14429        unset_scalar("__never_real_scalar_zshrs__");
14430        unset_array("__never_real_array_zshrs__");
14431        unset_assoc("__never_real_assoc_zshrs__");
14432    }
14433
14434    /// `run_function_body` returns None when no hook installed.
14435    #[test]
14436    fn exec_hooks_run_function_body_returns_none_when_no_hook() {
14437        let _g = crate::test_util::global_state_lock();
14438        let _ = run_function_body("__never_a_real_fn_zshrs__", &["a".into()]);
14439        // No panic = pass; result is Option-typed.
14440    }
14441
14442    /// `execute_script_zsh_pipeline` returns Ok(0) when no hook installed.
14443    #[test]
14444    fn exec_hooks_execute_script_zsh_pipeline_default_ok_zero() {
14445        let _g = crate::test_util::global_state_lock();
14446        let r = execute_script_zsh_pipeline("no hook");
14447        // If hook installed, result is hook-defined; if not, Ok(0).
14448        let _ = r;
14449    }
14450
14451    /// `array()` returns None when name doesn't exist in params either
14452    /// (no fallback hits).
14453    #[test]
14454    fn exec_hooks_array_returns_none_for_missing_name() {
14455        let _g = crate::test_util::global_state_lock();
14456        crate::ported::params::unsetparam("__no_such_array_zshrs__");
14457        let got = array("__no_such_array_zshrs__");
14458        // Either None (no hook + no param) or hook-returned value.
14459        let _ = got;
14460    }
14461
14462    /// `array()` empty name doesn't panic (some callers pass "" for
14463    /// special parameter probes).
14464    #[test]
14465    fn exec_hooks_array_empty_name_does_not_panic() {
14466        let _g = crate::test_util::global_state_lock();
14467        let _ = array("");
14468    }
14469
14470    /// Idempotent: calling array() twice with same name yields same
14471    /// result (no observable side effect on the fallback path).
14472    #[test]
14473    fn exec_hooks_array_is_idempotent() {
14474        let _g = crate::test_util::global_state_lock();
14475        crate::ported::params::unsetparam("EH_IDEMPOTENT");
14476        crate::ported::params::setaparam("EH_IDEMPOTENT", vec!["a".into(), "b".into()]);
14477        let first = array("EH_IDEMPOTENT");
14478        let second = array("EH_IDEMPOTENT");
14479        assert_eq!(first, second);
14480        crate::ported::params::unsetparam("EH_IDEMPOTENT");
14481    }
14482
14483    /// `pparams()` returns an empty vec (not None) when no hook —
14484    /// callers can iterate without an Option-check.
14485    #[test]
14486    fn exec_hooks_pparams_returns_vec_not_none() {
14487        let _g = crate::test_util::global_state_lock();
14488        // Type assertion: result is Vec<String>, not Option<Vec<String>>.
14489        let p: Vec<String> = pparams();
14490        let _ = p; // either [] or hook-installed value
14491    }
14492
14493    /// Round-trip via params fallback: set then read returns same value.
14494    #[test]
14495    fn exec_hooks_array_set_get_roundtrip_via_params() {
14496        let _g = crate::test_util::global_state_lock();
14497        crate::ported::params::unsetparam("EH_RT");
14498        let vals = vec!["one".to_string(), "two".to_string(), "three".to_string()];
14499        crate::ported::params::setaparam("EH_RT", vals.clone());
14500        let got = array("EH_RT").expect("set then get should hit params fallback");
14501        assert_eq!(got, vals);
14502        crate::ported::params::unsetparam("EH_RT");
14503    }
14504
14505    /// `unregister_function` is consistently a bool — pin the return
14506    /// type so accidental refactor to () would fail the type check.
14507    #[test]
14508    fn exec_hooks_unregister_function_returns_bool() {
14509        let _g = crate::test_util::global_state_lock();
14510        let r: bool = unregister_function("__never_xyz_zshrs__");
14511        let _ = r;
14512    }
14513
14514    // ═══════════════════════════════════════════════════════════════════
14515    // Additional contract-pin tests for exec_hooks default behavior.
14516    // ═══════════════════════════════════════════════════════════════════
14517
14518    /// `run_command_substitution` returns String (never None / never Option).
14519    #[test]
14520    fn exec_hooks_run_command_substitution_returns_string_type() {
14521        let _g = crate::test_util::global_state_lock();
14522        let _: String = run_command_substitution("anything");
14523    }
14524
14525    /// `execute_script` returns Result<i32, String>. Pin signature.
14526    #[test]
14527    fn exec_hooks_execute_script_returns_result_type() {
14528        let _g = crate::test_util::global_state_lock();
14529        let _: Result<i32, String> = execute_script("anything");
14530    }
14531
14532    /// `execute_script_zsh_pipeline` returns Result<i32, String>.
14533    #[test]
14534    fn exec_hooks_execute_script_zsh_pipeline_returns_result_type() {
14535        let _g = crate::test_util::global_state_lock();
14536        let _: Result<i32, String> = execute_script_zsh_pipeline("anything");
14537    }
14538
14539    /// `dispatch_function_call` returns Option<i32>.
14540    #[test]
14541    fn exec_hooks_dispatch_function_call_returns_option_i32() {
14542        let _g = crate::test_util::global_state_lock();
14543        let _: Option<i32> = dispatch_function_call("__never_real__", &[]);
14544    }
14545
14546    /// `run_function_body` returns Option<i32>.
14547    #[test]
14548    fn exec_hooks_run_function_body_returns_option_i32() {
14549        let _g = crate::test_util::global_state_lock();
14550        let _: Option<i32> = run_function_body("__never_real__", &[]);
14551    }
14552
14553    /// `array` returns Option<Vec<String>>.
14554    #[test]
14555    fn exec_hooks_array_returns_option_vec_string() {
14556        let _g = crate::test_util::global_state_lock();
14557        let _: Option<Vec<String>> = array("anything");
14558    }
14559
14560    /// `assoc` returns Option<IndexMap<String, String>>.
14561    #[test]
14562    fn exec_hooks_assoc_returns_option_indexmap() {
14563        let _g = crate::test_util::global_state_lock();
14564        let _: Option<IndexMap<String, String>> = assoc("anything");
14565    }
14566
14567    /// Empty-string args to `dispatch_function_call` doesn't panic.
14568    #[test]
14569    fn exec_hooks_dispatch_empty_args_no_panic() {
14570        let _g = crate::test_util::global_state_lock();
14571        let _ = dispatch_function_call("", &[]);
14572        let _ = dispatch_function_call("name", &[]);
14573    }
14574
14575    /// Empty-string args to `run_function_body` doesn't panic.
14576    #[test]
14577    fn exec_hooks_run_function_body_empty_args_no_panic() {
14578        let _g = crate::test_util::global_state_lock();
14579        let _ = run_function_body("", &[]);
14580        let _ = run_function_body("name", &[]);
14581    }
14582
14583    /// Empty-string name to `execute_script` doesn't panic and returns
14584    /// Result.
14585    #[test]
14586    fn exec_hooks_execute_script_empty_src_no_panic() {
14587        let _g = crate::test_util::global_state_lock();
14588        let _ = execute_script("");
14589        let _ = execute_script_zsh_pipeline("");
14590    }
14591
14592    /// Empty-string cmd to `run_command_substitution` doesn't panic.
14593    #[test]
14594    fn exec_hooks_run_command_substitution_empty_no_panic() {
14595        let _g = crate::test_util::global_state_lock();
14596        let _: String = run_command_substitution("");
14597    }
14598
14599    /// `unregister_function("")` doesn't panic.
14600    #[test]
14601    fn exec_hooks_unregister_function_empty_name_no_panic() {
14602        let _g = crate::test_util::global_state_lock();
14603        let _: bool = unregister_function("");
14604    }
14605
14606    /// `unset_*` with empty name does not panic.
14607    #[test]
14608    fn exec_hooks_unset_with_empty_name_no_panic() {
14609        let _g = crate::test_util::global_state_lock();
14610        unset_scalar("");
14611        unset_array("");
14612        unset_assoc("");
14613    }
14614
14615    // ═══════════════════════════════════════════════════════════════════
14616    // Additional contract-pin tests for exec_hooks fallback semantics
14617    // c:213 pparams / c:217 set_pparams / c:223 unregister_function
14618    // ═══════════════════════════════════════════════════════════════════
14619
14620    /// `pparams()` is deterministic for repeated calls without state changes.
14621    #[test]
14622    fn pparams_deterministic_without_changes() {
14623        let _g = crate::test_util::global_state_lock();
14624        let first = pparams();
14625        for _ in 0..3 {
14626            assert_eq!(
14627                pparams(),
14628                first,
14629                "pparams() must be deterministic across reads"
14630            );
14631        }
14632    }
14633
14634    /// `pparams()` returns Vec<String> (not Option) — pin type contract.
14635    #[test]
14636    fn pparams_returns_vec_string_no_option() {
14637        let _g = crate::test_util::global_state_lock();
14638        let _: Vec<String> = pparams();
14639    }
14640
14641    /// `array(name)` with name containing null-byte doesn't panic.
14642    #[test]
14643    fn array_with_special_chars_in_name_no_panic() {
14644        let _g = crate::test_util::global_state_lock();
14645        let _ = array("name with spaces");
14646        let _ = array("name/with/slashes");
14647        let _ = array("$dollarsigns");
14648    }
14649
14650    /// `assoc(name)` empty name no panic.
14651    #[test]
14652    fn assoc_empty_name_no_panic() {
14653        let _g = crate::test_util::global_state_lock();
14654        let _ = assoc("");
14655    }
14656
14657    /// `set_array(empty, ...)` empty name no panic.
14658    #[test]
14659    fn set_array_empty_name_no_panic() {
14660        let _g = crate::test_util::global_state_lock();
14661        set_array("", vec![]);
14662    }
14663
14664    /// `set_assoc(empty, ...)` empty name no panic.
14665    #[test]
14666    fn set_assoc_empty_name_no_panic() {
14667        let _g = crate::test_util::global_state_lock();
14668        let m = indexmap::IndexMap::new();
14669        set_assoc("", m);
14670    }
14671
14672    /// `set_pparams(empty)` is safe.
14673    #[test]
14674    fn set_pparams_empty_vec_no_panic() {
14675        let _g = crate::test_util::global_state_lock();
14676        set_pparams(vec![]);
14677    }
14678
14679    /// Repeated `pparams()` doesn't allocate growing state.
14680    #[test]
14681    fn pparams_repeated_doesnt_grow_state() {
14682        let _g = crate::test_util::global_state_lock();
14683        let first_len = pparams().len();
14684        for _ in 0..10 {
14685            let n = pparams().len();
14686            assert_eq!(n, first_len, "len must not grow across reads");
14687        }
14688    }
14689
14690    /// `unregister_function` is deterministic for nonexistent name.
14691    #[test]
14692    fn unregister_function_unknown_deterministic() {
14693        let _g = crate::test_util::global_state_lock();
14694        let first = unregister_function("__never_real_xyz__");
14695        for _ in 0..3 {
14696            assert_eq!(unregister_function("__never_real_xyz__"), first);
14697        }
14698    }
14699
14700    /// `array(name)` repeated reads of nonexistent name are deterministic.
14701    #[test]
14702    fn array_unknown_is_deterministic() {
14703        let _g = crate::test_util::global_state_lock();
14704        let first = array("__never_real_array_xyz__").is_none();
14705        for _ in 0..3 {
14706            assert_eq!(array("__never_real_array_xyz__").is_none(), first);
14707        }
14708    }
14709
14710    /// `assoc(name)` repeated reads of nonexistent name are deterministic.
14711    #[test]
14712    fn assoc_unknown_is_deterministic() {
14713        let _g = crate::test_util::global_state_lock();
14714        let first = assoc("__never_real_assoc_xyz__").is_none();
14715        for _ in 0..3 {
14716            assert_eq!(assoc("__never_real_assoc_xyz__").is_none(), first);
14717        }
14718    }
14719
14720    // ═══════════════════════════════════════════════════════════════════
14721    // Additional C-parity tests for src/ported/exec_hooks.rs
14722    // c:134 array / c:147 assoc / c:181 dispatch_function_call /
14723    // c:188 run_function_body / c:192 execute_script /
14724    // c:206 run_command_substitution / c:213 pparams / c:223 unregister_function
14725    // ═══════════════════════════════════════════════════════════════════
14726
14727    /// `array(name)` returns Option<Vec<String>> (compile-time pin).
14728    #[test]
14729    fn array_returns_option_vec_string_type() {
14730        let _g = crate::test_util::global_state_lock();
14731        let _: Option<Vec<String>> = array("any");
14732    }
14733
14734    /// `assoc(name)` returns Option<IndexMap<String,String>> (compile-time pin).
14735    #[test]
14736    fn assoc_returns_option_indexmap_type() {
14737        let _g = crate::test_util::global_state_lock();
14738        let _: Option<indexmap::IndexMap<String, String>> = assoc("any");
14739    }
14740
14741    /// `dispatch_function_call` returns Option<i32> (compile-time pin).
14742    #[test]
14743    fn dispatch_function_call_returns_option_i32_type() {
14744        let _g = crate::test_util::global_state_lock();
14745        let _: Option<i32> = dispatch_function_call("__never__", &[]);
14746    }
14747
14748    /// `run_function_body` returns Option<i32> (compile-time pin).
14749    #[test]
14750    fn run_function_body_returns_option_i32_type() {
14751        let _g = crate::test_util::global_state_lock();
14752        let _: Option<i32> = run_function_body("__never__", &[]);
14753    }
14754
14755    /// `execute_script` returns Result<i32, String> (compile-time pin).
14756    #[test]
14757    fn execute_script_returns_result_type() {
14758        let _g = crate::test_util::global_state_lock();
14759        let _: Result<i32, String> = execute_script("");
14760    }
14761
14762    /// `execute_script_zsh_pipeline` returns Result<i32, String> (compile-time pin).
14763    #[test]
14764    fn execute_script_zsh_pipeline_returns_result_type() {
14765        let _g = crate::test_util::global_state_lock();
14766        let _: Result<i32, String> = execute_script_zsh_pipeline("");
14767    }
14768
14769    /// `run_command_substitution` returns String (compile-time pin).
14770    #[test]
14771    fn run_command_substitution_returns_string_type() {
14772        let _g = crate::test_util::global_state_lock();
14773        let _: String = run_command_substitution("");
14774    }
14775
14776    /// `pparams` returns Vec<String> (compile-time pin).
14777    #[test]
14778    fn pparams_returns_vec_string_type() {
14779        let _g = crate::test_util::global_state_lock();
14780        let _: Vec<String> = pparams();
14781    }
14782
14783    /// `unregister_function` returns bool (compile-time pin).
14784    #[test]
14785    fn unregister_function_returns_bool_type() {
14786        let _g = crate::test_util::global_state_lock();
14787        let _: bool = unregister_function("__never__");
14788    }
14789
14790    /// `unset_scalar`/`unset_array`/`unset_assoc` for nonexistent name safe.
14791    #[test]
14792    fn unset_variants_nonexistent_no_panic() {
14793        let _g = crate::test_util::global_state_lock();
14794        unset_scalar("__never_unset_scalar__");
14795        unset_array("__never_unset_array__");
14796        unset_assoc("__never_unset_assoc__");
14797    }
14798
14799    /// `set_pparams` with no hook installed is a silent no-op
14800    /// (c:217-220 — `if let Some(f) = PPARAMS_SET.get() { f(v); }`).
14801    /// Pin the no-hook contract so a refactor that panics on missing
14802    /// hook gets caught.
14803    #[test]
14804    fn set_pparams_without_hook_is_silent_noop() {
14805        let _g = crate::test_util::global_state_lock();
14806        // No hook installed in test context — must not panic.
14807        set_pparams(vec!["a".into(), "b".into(), "c".into()]);
14808        set_pparams(vec![]);
14809    }
14810
14811    // ═══════════════════════════════════════════════════════════════════
14812    // Additional contract pins for exec_hooks.rs
14813    // No-hook-installed contract: every accessor must be safe + deterministic
14814    // ═══════════════════════════════════════════════════════════════════
14815
14816    /// `array("")` empty name returns deterministic value (no panic).
14817    #[test]
14818    fn array_empty_name_no_panic_deterministic() {
14819        let _g = crate::test_util::global_state_lock();
14820        let a = array("");
14821        let b = array("");
14822        assert_eq!(a, b, "array(\"\") must be deterministic");
14823    }
14824
14825    /// `set_array` then `array` without hook should not panic.
14826    #[test]
14827    fn set_array_then_get_no_hook_safe() {
14828        let _g = crate::test_util::global_state_lock();
14829        set_array("__test_hook_arr__", vec!["a".into(), "b".into()]);
14830        let _ = array("__test_hook_arr__");
14831    }
14832
14833    /// `set_assoc` then `assoc` without hook should not panic.
14834    #[test]
14835    fn set_assoc_then_get_no_hook_safe() {
14836        let _g = crate::test_util::global_state_lock();
14837        let mut m = IndexMap::new();
14838        m.insert("k".to_string(), "v".to_string());
14839        set_assoc("__test_hook_assoc__", m);
14840        let _ = assoc("__test_hook_assoc__");
14841    }
14842
14843    /// `run_function_body` with no hook returns `Option<i32>` (type pin, alt).
14844    #[test]
14845    fn run_function_body_returns_option_i32_type_alt() {
14846        let _g = crate::test_util::global_state_lock();
14847        let _: Option<i32> = run_function_body("foo", &[]);
14848    }
14849
14850    /// `run_function_body` is deterministic for the same input when no hook.
14851    #[test]
14852    fn run_function_body_no_hook_deterministic() {
14853        let _g = crate::test_util::global_state_lock();
14854        let a = run_function_body("__never__", &[]);
14855        let b = run_function_body("__never__", &[]);
14856        assert_eq!(a, b);
14857    }
14858
14859    /// `dispatch_function_call` is deterministic across calls.
14860    #[test]
14861    fn dispatch_function_call_no_hook_deterministic() {
14862        let _g = crate::test_util::global_state_lock();
14863        let a = dispatch_function_call("__never__", &[]);
14864        let b = dispatch_function_call("__never__", &[]);
14865        assert_eq!(a, b);
14866    }
14867
14868    /// `pparams` returns `Vec<String>` (compile-time pin, alt).
14869    #[test]
14870    fn pparams_returns_vec_string_type_alt() {
14871        let _g = crate::test_util::global_state_lock();
14872        let _: Vec<String> = pparams();
14873    }
14874
14875    /// `pparams` is deterministic across repeated reads when no hook installed
14876    /// (this is observably true only if no other test has installed it; pin
14877    /// the no-mutation invariant).
14878    #[test]
14879    fn pparams_repeated_reads_are_observable_type() {
14880        let _g = crate::test_util::global_state_lock();
14881        // Just type pin — value depends on whether another test installed a
14882        // hook between these calls (PPARAMS_GET is OnceLock).
14883        let _a = pparams();
14884        let _b = pparams();
14885    }
14886
14887    /// `run_command_substitution` returns `String` type pin (alt).
14888    #[test]
14889    fn run_command_substitution_returns_string_type_alt() {
14890        let _g = crate::test_util::global_state_lock();
14891        let _: String = run_command_substitution("echo x");
14892    }
14893
14894    /// `run_command_substitution` empty command doesn't panic.
14895    #[test]
14896    fn run_command_substitution_empty_cmd_no_panic() {
14897        let _g = crate::test_util::global_state_lock();
14898        let _ = run_command_substitution("");
14899    }
14900
14901    /// `execute_script` returns `Result<i32, String>` type pin.
14902    #[test]
14903    fn execute_script_returns_result_i32_string_type() {
14904        let _g = crate::test_util::global_state_lock();
14905        let _: Result<i32, String> = execute_script("foo");
14906    }
14907
14908    /// `unregister_function("")` empty name returns bool safely.
14909    #[test]
14910    fn unregister_function_empty_name_returns_bool() {
14911        let _g = crate::test_util::global_state_lock();
14912        let _: bool = unregister_function("");
14913    }
14914
14915    /// c:3115 — `orig_cflags |= cflags` records the modifier flags consumed
14916    /// by the head walk: `builtin print` leaves `BINF_BUILTIN` in it, while a
14917    /// plain command accumulates nothing.
14918    #[test]
14919    fn execcmd_compile_head_accumulates_orig_cflags() {
14920        let _g = crate::test_util::global_state_lock();
14921        let words = |w: &[&str]| w.iter().map(|s| s.to_string()).collect::<Vec<_>>();
14922        let d = execcmd_compile_head(&words(&["builtin", "print", "x"]), WC_SIMPLE as u32);
14923        assert_ne!(d.orig_cflags & BINF_BUILTIN as u32, 0);
14924        let d = execcmd_compile_head(&words(&["print", "x"]), WC_SIMPLE as u32);
14925        assert_eq!(d.orig_cflags, 0);
14926    }
14927}