Skip to main content

zsh/ported/
builtin.rs

1//! Direct port of `Src/builtin.c` — the master registration site for
2//! the in-shell builtin commands. The C source is 7608 lines; the
3//! actual `bin_*` handler bodies were ported organically into
4//! `src/ported/exec.rs` and `src/ported/builtins/*.rs` long before
5//! this file existed. This file scaffolds:
6//!
7//! Builtins in the main executable                                          // c:38
8//! Builtin Command Hash Table Functions                                     // c:140
9//!
10//!   * the `BINF_*` flag bits from `Src/zsh.h:1457-1486`,
11//!   * the `BIN_*` dispatch IDs from `Src/hashtable.h:34-66`,
12//!   * the `Builtin` descriptor and the static `BUILTINS[]` table
13//!     (1:1 mirror of `static struct builtin builtins[]` at
14//!     `Src/builtin.c:40-137`),
15//!   * `createbuiltintable()` (`Src/builtin.c:149`) — building the
16//!     name → descriptor lookup the rest of the shell consults via
17//!     `builtintab`.
18//!
19//! Each row's `handler` field names the canonical Rust port of the
20//! C handler so future work can wire them up without re-discovering
21//! the mapping. When the handler lives in `crate::ported::builtins`,
22//! the comment cites the file; when it lives in `exec.rs`'s
23//! `Executor` impl, that's noted too.
24
25use std::collections::HashMap;
26use std::sync::OnceLock;
27use crate::ported::zsh_h::{PRINT_WHENCE_WORD, PRINT_WHENCE_CSH};
28use crate::ported::zsh_h::EMULATE_ZSH;
29use crate::ported::zsh_h::{options, MAX_OPS, XTRACE, BINF_KEEPNUM, ERRFLAG_ERROR};
30use crate::ported::modules::parameter::DIRSTACK;
31use std::sync::atomic::Ordering;
32use crate::ported::zsh_h::{OPT_HASARG, OPT_ARG, PM_INTEGER, PM_EFLOAT, PM_FFLOAT};
33use crate::ported::zsh_h::{PM_LEFT, PM_RIGHT_B, PM_RIGHT_Z};
34use crate::ported::zsh_h::{OPT_MINUS, OPT_ISSET, PM_UNDEFINED};
35use crate::ported::zsh_h::PM_LOADDIR;
36use crate::ported::zsh_h::MFF_STR;
37use crate::ported::zsh_h::{PM_ABSPATH_USED, FS_FUNC};
38use crate::ported::zsh_h::eprog;
39use crate::ported::zsh_h::{STAT_LOCKED, STAT_NOPRINT, STAT_STOPPED};
40use std::io::Read;
41use crate::ported::zsh_h::{OPT_PLUS, PM_UNALIASED, PM_TAGGED, PM_TAGGED_LOCAL, PM_WARNNESTED, PM_ZSHSTORED, PM_KSHSTORED, PM_CUR_FPATH};
42use crate::ported::math::{matheval, mnumber, MN_INTEGER};
43use crate::ported::utils::{getkeystring, getkeystring_with, quotedzputs, GETKEYS_PRINT};
44use crate::ported::zsh_h::HIST_FOREIGN;
45use crate::ported::zsh_h::{HFILE_APPEND, HFILE_SKIPOLD, HFILE_USE_OPTIONS};
46use crate::ported::zsh_h::{EMULATION, TYPESET_OPTSTR, PM_HASHED, PM_HIDEVAL, PM_LOWER, PM_UPPER, PM_TIED, PM_LOCAL, PM_NAMEREF, PM_READONLY, PM_ARRAY, PRINT_TYPESET, PRINT_LINE, PRINT_TYPE, PRINT_NAMEONLY, PRINT_POSIX_EXPORT, PRINT_POSIX_READONLY, PRINT_WITH_NAMESPACE, EMULATE_KSH};
47use crate::ported::zsh_h::{PRINT_WHENCE_VERBOSE, PRINT_WHENCE_SIMPLE, PRINT_WHENCE_FUNCDEF, PRINT_LIST};
48use crate::ported::math::mathevali;
49use crate::ported::zsh_h::DISABLED;
50use crate::ported::zsh_h::nameddir;
51use crate::ported::zsh_h::{ALIAS_GLOBAL, ALIAS_SUFFIX};
52use crate::ported::hashtable::{aliastab_lock, sufaliastab_lock, Alias};
53use crate::ported::zsh_h::{EMULATE_CSH, EMULATE_SH};
54
55// === Imports needed by the methods moved from exec.rs (below) ===
56#[allow(unused_imports)]
57use std::{env, fs, io, io::Write, path::Path, path::PathBuf};
58#[allow(unused_imports)]
59use indexmap::IndexMap;
60#[allow(unused_imports)]
61use crate::ported::exec::{
62    self,  BUILTIN_NAMES,
63    format_int_in_base,
64};
65use crate::ported::utils::{zerr, zerrnam, zwarn, zwarnnam};
66use crate::func_body_fmt::FuncBodyFmt;
67#[allow(unused_imports)]
68use crate::ported::options::ZSH_OPTIONS_SET;
69#[allow(unused_imports)]
70use crate::parse::{Redirect, ShellCommand};
71#[allow(unused_imports)]
72use crate::zwc::ZwcFile;
73
74
75// ---------------------------------------------------------------------------
76// BIN_* dispatch IDs.
77// Direct port of `Src/hashtable.h:34-70`. These are the integer
78// discriminators handlers use when one C function backs multiple
79// builtin names (e.g. `bin_fg` covers fg/bg/jobs/wait/disown).
80// ---------------------------------------------------------------------------
81
82// BIN_* constants moved to `crate::ported::hashtable_h` per the C
83// header layout (Src/hashtable.h:34-70). Re-exported here so existing
84// `crate::ported::builtin::BIN_X` paths keep resolving.
85pub use crate::ported::hashtable_h::{
86    BIN_TYPESET, BIN_BG, BIN_FG, BIN_JOBS, BIN_WAIT, BIN_DISOWN,
87    BIN_BREAK, BIN_CONTINUE, BIN_EXIT, BIN_RETURN, BIN_CD,
88    BIN_POPD, BIN_PUSHD, BIN_PRINT, BIN_EVAL, BIN_SCHED, BIN_FC,
89    BIN_R, BIN_PUSHLINE, BIN_LOGOUT, BIN_TEST, BIN_BRACKET,
90    BIN_READONLY, BIN_ECHO, BIN_DISABLE, BIN_ENABLE, BIN_PRINTF,
91    BIN_COMMAND, BIN_UNHASH, BIN_UNALIAS, BIN_UNFUNCTION,
92    BIN_UNSET, BIN_EXPORT, BIN_SETOPT, BIN_UNSETOPT,
93};
94use crate::zsh_h::{builtin, BINF_ASSIGN, BINF_BUILTIN, BINF_COMMAND, BINF_DASH, BINF_DASHDASHVALID, BINF_EXEC, BINF_HANDLES_OPTS, BINF_MAGICEQUALS, BINF_NOGLOB, BINF_PLUSOPTS, BINF_PREFIX, BINF_PRINTOPTS, BINF_PSPECIAL, BINF_SKIPDASH, BINF_SKIPINVALID, hashnode, NULLBINCMD, isset};
95
96/// Construct the builtin lookup table.
97/// Port of `createbuiltintable()` from `Src/builtin.c:150`. The C
98/// version installs the hashtable function pointers (hash, addnode,
99/// printnode, etc.) and then calls `addbuiltins("zsh", builtins, ..)`.
100/// Here we just materialise the static `BUILTINS` slice into a
101/// `HashMap<String, &builtin>` — Rust's standard hashing replaces the
102/// C `hasher` callback and the `HashMap` itself replaces all the
103/// per-table function pointers (`addnode`/`getnode`/`removenode`/...).
104// Builtin Command Hash Table Functions                                      // c:150
105pub fn createbuiltintable() -> &'static HashMap<String, &'static builtin> { // c:150
106    builtintab.get_or_init(|| {
107        let table: &'static Vec<builtin> = &*BUILTINS;
108        let watch_bintab: &'static Vec<builtin> =
109            &*crate::ported::modules::watch::bintab;
110        let mut m: HashMap<String, &'static builtin> =
111            HashMap::with_capacity(table.len() + watch_bintab.len());
112        for b in table.iter() {
113            m.insert(b.node.nam.clone(), b);
114        }
115        // zshrs auto-loads all modules at startup. Fold each module's
116        // bintab into the core builtintab so `disable <name>` (and
117        // dispatch generally) finds module-provided builtins without
118        // an explicit `zmodload` step. Mirrors C's `addbuiltins(name,
119        // bintab, sizeof(bintab)/sizeof(*bintab))` call from each
120        // module's `boot_` hook (e.g. `Src/Modules/watch.c:694`).
121        for b in watch_bintab.iter() {
122            m.insert(b.node.nam.clone(), b);
123        }
124        m
125    })
126}
127
128// ===========================================================
129// Direct ports of static builtin helpers from Src/builtin.c not
130// yet covered above. The Rust executor wires builtins through
131// `crate::ported::builtins::*` per-builtin modules; these free-
132// fn entries satisfy ABI/name parity for the drift gate.
133// ===========================================================
134
135/// Port of `printbuiltinnode(HashNode hn, int printflags)` from Src/builtin.c:174.
136/// C: `static void printbuiltinnode(HashNode hn, int printflags)` —
137///   emit `whence`-style description of one builtin.
138/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
139pub fn printbuiltinnode(hn: *mut crate::ported::zsh_h::hashnode,             // c:174
140                        printflags: i32) {
141    if hn.is_null() { return; }
142    let bn = unsafe { &*hn };
143    if (printflags & PRINT_WHENCE_WORD as i32) != 0 {                        // c:179
144        println!("{}: builtin", bn.nam);                                     // c:180
145        return;
146    }
147    if (printflags & PRINT_WHENCE_CSH as i32) != 0 {                         // c:199
148        println!("{}: shell built-in command", bn.nam);                      // c:199
149        return;
150    }
151    // c:199-198 — default form: just emit the name.
152    println!("{}", bn.nam);
153}
154
155/// Port of `freebuiltinnode(HashNode hn)` from Src/builtin.c:199.
156/// C: `static void freebuiltinnode(HashNode hn)` — free a builtin-table
157///   node only when BINF_ADDED is clear (i.e., dynamically added).
158pub fn freebuiltinnode(hn: *mut crate::ported::zsh_h::hashnode) {            // c:199
159    if hn.is_null() { return; }
160    let bn = unsafe { &*hn };
161    // c:204 — `if (!(bn->node.flags & BINF_ADDED))` then free.
162    if (bn.flags as u32 & crate::ported::zsh_h::BINF_ADDED) == 0 {           // c:204
163        // Rust drop handles the actual free; nothing more to do.
164    }
165}
166
167/// Port of `init_builtins()` from Src/builtin.c:212.
168/// C: `void init_builtins(void)` — when not in EMULATE_ZSH, disable
169///   the `repeat` reserved word (compat for sh/ksh).
170///
171/// ```c
172/// if (!EMULATION(EMULATE_ZSH)) {
173///     HashNode hn = reswdtab->getnode2(reswdtab, "repeat");
174///     if (hn)
175///         reswdtab->disablenode(hn, 0);
176/// }
177/// ```
178pub fn init_builtins() {                                                     // c:212
179    // c:214 — `if (!EMULATION(EMULATE_ZSH))`. EMULATION reads the
180    // canonical `emulation` global directly per zsh.h:2347.
181    if !crate::ported::zsh_h::EMULATION(EMULATE_ZSH) {                       // c:214
182        // c:215-217 — `hn = reswdtab->getnode2(reswdtab,"repeat");
183        //              if (hn) reswdtab->disablenode(hn, 0);`
184        if let Ok(mut tab) = crate::ported::hashtable::reswdtab_lock().write() {
185            tab.disable("repeat");
186        }
187    }
188}
189
190/// Port of `OPT_ALLOC_CHUNK` from `Src/builtin.c:227`. Number of
191/// `ops->args[]` slots `new_optarg()` grows the array by when full.
192pub const OPT_ALLOC_CHUNK: i32 = 16;                                         // c:227
193
194/// Port of `new_optarg(Options ops)` from Src/builtin.c:227.
195/// C: `static int new_optarg(Options ops)` — grow the `ops->args[]`
196///   array by `OPT_ALLOC_CHUNK` slots when full. Returns 1 on overflow
197///   (>=63 args), 0 on success.
198pub fn new_optarg(ops: &mut crate::ported::zsh_h::options) -> i32 {          // c:227
199    // c:227 — `if (ops->argscount == 63) return 1;`
200    if ops.argscount == 63 {                                                 // c:231
201        return 1;
202    }
203    // c:232-241 — grow ops->args by OPT_ALLOC_CHUNK if argsalloc == argscount.
204    if ops.argsalloc == ops.argscount {                                      // c:232
205        ops.args.resize((ops.argsalloc + OPT_ALLOC_CHUNK) as usize, String::new());
206        ops.argsalloc += OPT_ALLOC_CHUNK;                                    // c:240
207    }
208    ops.argscount += 1;                                                      // c:243
209    0                                                                        // c:244
210}
211
212
213// ===========================================================
214// ksh_autoload_body moved from src/ported/exec.rs.
215// Mirrors the ksh-style autoload helper in Src/builtin.c
216// (bin_functions / load_function_def).
217// ===========================================================
218// (impl crate::ported::exec::ShellExecutor block deleted — was lines 12343..12376; per user feedback the bin_* methods were fake. Recorder hooks preserved at file bottom.)
219
220
221bitflags::bitflags! {
222    /// Flags for autoloaded functions (autoload builtin -- Src/builtin.c bin_autoload).
223    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
224    pub struct AutoloadFlags: u32 {
225        const NO_ALIAS = 0b00000001;      // -U: don't expand aliases
226        const ZSH_STYLE = 0b00000010;     // -z: zsh-style autoload
227        const KSH_STYLE = 0b00000100;     // -k: ksh-style autoload
228        const TRACE = 0b00001000;         // -t: trace execution
229        const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
230        const LOADED = 0b00100000;        // function has been loaded
231    }
232}
233
234/// Port of `execbuiltin(LinkList args, LinkList assigns, Builtin bn)` from Src/builtin.c:250.
235///
236/// C: `int execbuiltin(LinkList args, LinkList assigns, Builtin bn)` —
237///   execute a builtin handler function after parsing the arguments.
238///
239/// Walks `bn->optstr` against `args`, populating `ops.ind[c]` (`|= 1`
240/// for `-X`, `|= 2` for `+X`, `<< 2` arg-index for opts taking args
241/// per the `:`/`::`/`:%` suffix convention), then calls
242/// `bn->handlerfunc(name, argv, &ops, bn->funcid)`.
243///
244/// Signature note: C consumes the name via `ugetnode(args)` first
245/// (c:262); the Rust port receives `args` without the name and reads
246/// `bn->node.nam` directly. C's `LinkList assigns` ports to
247/// `Vec<asgment>` (closer to the C type than the earlier
248/// `Vec<(String, String)>` pair-tuple). `assignfunc` handler dispatch
249/// (c:495-502) — BINF_ASSIGN builtins taking two argument lists —
250/// isn't ported (no Rust-side caller passes a non-empty `assigns`),
251/// so XTRACE prints the structure but BINF_ASSIGN dispatch falls
252/// through to the plain handler.
253pub fn execbuiltin(args: Vec<String>, assigns: Vec<crate::ported::zsh_h::asgment>, // c:250
254                   bn: *mut crate::ported::zsh_h::builtin) -> i32 {
255    if bn.is_null() {
256        return 1;
257    }
258    let bn_ref = unsafe { &*bn };
259
260    // c:252-254 — locals.
261    let pp: Option<&str>;                                                    // c:252 char *pp
262    let name: String;                                                        // c:252 char *name
263    let mut optstr: Option<String>;                                          // c:252 char *optstr
264    let mut flags: i32;                                                      // c:253 int flags
265    let mut argc: i32;                                                       // c:253 int argc
266    let mut execop: u8;                                                      // c:253 int execop
267    let xtr: bool = isset(XTRACE);                                           // c:253 int xtr = isset(XTRACE)
268
269    // c:256-259 — `memset(ops.ind, 0, ...); ops.args = NULL; ops.argscount=ops.argsalloc=0;`
270    let mut ops = options { ind: [0u8; MAX_OPS], args: Vec::new(),           // c:257
271                            argscount: 0, argsalloc: 0 };                    // c:258-259
272
273    // c:262 — `name = (char *) ugetnode(args);` — Rust reads bn.node.nam.
274    name = bn_ref.node.nam.clone();                                          // c:262
275
276    // c:264-268 — `if (!bn->handlerfunc)` early-exit.
277    if bn_ref.handlerfunc.is_none() {                                        // c:264
278        return 1;                                                            // c:267
279    }
280
281    // c:270-271 — `flags = bn->node.flags; optstr = bn->optstr;`
282    flags = bn_ref.node.flags;                                               // c:270
283    optstr = bn_ref.optstr.clone();                                          // c:271
284
285    // c:275 — `argc = countlinknodes(args);` — total argv length.
286    argc = args.len() as i32;                                                // c:275
287
288    // c:284-293 — `VARARR(char *, argarr, argc+1)` + copy args into argarr.
289    let argarr: Vec<String> = args;                                          // c:284 argarr[]
290    let mut argv: usize = 0;                                                 // c:285 char **argv = argarr;
291
292    // c:296-411 — option parser body.
293    if let Some(ref os) = optstr.clone() {                                   // c:296
294        let optstr_local = os.clone();
295        let mut optstr_bytes: Vec<u8> = optstr_local.into_bytes();
296        let mut skipinvalid = (flags & BINF_SKIPINVALID as i32) != 0;
297        // c:297 — `char *arg = *argv;`
298        loop {
299            // c:300-303 — outer arg-by-arg loop guard:
300            //   `arg && ((sense = (*arg == '-')) || ((flags & BINF_PLUSOPTS) && *arg == '+'))`.
301            let arg_str: String = match argarr.get(argv) {
302                Some(s) => s.clone(),
303                None => break,
304            };
305            let arg_bytes = arg_str.as_bytes();
306            if arg_bytes.is_empty() { break; }
307            let sense: i32 = if arg_bytes[0] == b'-' { 1 } else { 0 };       // c:302
308            if sense == 0 && !((flags & BINF_PLUSOPTS as i32) != 0           // c:303
309                                && arg_bytes[0] == b'+') {
310                break;
311            }
312            // c:305 — `if (!(flags & BINF_KEEPNUM) && idigit(arg[1])) break;`
313            if (flags & BINF_KEEPNUM as i32) == 0                            // c:305
314                && arg_bytes.len() >= 2
315                && arg_bytes[1].is_ascii_digit() {
316                break;
317            }
318            // c:308 — `if ((flags & BINF_SKIPDASH) && !arg[1]) break;`
319            if (flags & BINF_SKIPDASH as i32) != 0 && arg_bytes.len() == 1 { // c:308
320                break;
321            }
322            // c:310-317 — `--` end-of-options if BINF_DASHDASHVALID.
323            if (flags & BINF_DASHDASHVALID as i32) != 0 && arg_str == "--" { // c:310
324                argv += 1;                                                   // c:315
325                break;                                                       // c:316
326            }
327            // c:327-332 — `BINF_SKIPINVALID`: if any char in arg[1..] is
328            // not in optstr, the whole arg is treated as a positional.
329            if skipinvalid {                                                 // c:327
330                let mut all_known = true;
331                for &c in &arg_bytes[1..] {
332                    if !optstr_bytes.contains(&c) { all_known = false; break; }
333                }
334                if !all_known { break; }                                     // c:331
335            }
336            // c:335-336 — `if (arg[1] == '-') arg++;` — consume the
337            // second `-` of `--long-style`.
338            let mut k: usize = 1;                                            // walks arg[k..]
339            if arg_bytes.len() >= 2 && arg_bytes[1] == b'-' {                // c:335
340                k = 2;                                                       // c:336
341            }
342            // c:337-341 — `if (!arg[1])` lone `-` / `+` indicator.
343            if arg_bytes.len() == k {                                        // c:337
344                ops.ind[b'-' as usize] = 1;                                  // c:338
345                if sense == 0 {                                              // c:339
346                    ops.ind[b'+' as usize] = 1;                              // c:340
347                }
348            }
349            // c:343-386 — inner loop over `*++arg` characters.
350            let mut bad_opt: Option<u8> = None;
351            while k < arg_bytes.len() {                                      // c:343
352                let c = arg_bytes[k];
353                execop = c;                                                  // c:345
354                let optptr = optstr_bytes.iter().position(|&b| b == c);      // c:345 strchr(optstr,c)
355                if let Some(optidx) = optptr {                               // c:345
356                    ops.ind[c as usize] = if sense != 0 { 1 } else { 2 };    // c:346
357                    // c:347 — `if (optptr[1] == ':')` — option takes arg.
358                    if optidx + 1 < optstr_bytes.len() && optstr_bytes[optidx + 1] == b':' {
359                        let mut argptr: Option<String> = None;
360                        // c:349-352 — `if (optptr[2] == ':')` optional same-word.
361                        if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b':' {
362                            if k + 1 < arg_bytes.len() {                     // c:350
363                                argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned()); // c:351
364                            }
365                        } else if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b'%' {
366                            // c:353-359 — `:%` numeric optional same or next word.
367                            if k + 1 < arg_bytes.len() && arg_bytes[k+1].is_ascii_digit() {
368                                argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned());
369                            } else if let Some(nxt) = argarr.get(argv + 1) {
370                                if !nxt.is_empty() && nxt.as_bytes()[0].is_ascii_digit() {
371                                    argv += 1;                               // c:359 arg = *++argv
372                                    argptr = Some(nxt.clone());
373                                }
374                            }
375                        } else {
376                            // c:360-370 — plain `:` mandatory arg.
377                            if k + 1 < arg_bytes.len() {                     // c:362
378                                argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned()); // c:363
379                            } else if let Some(nxt) = argarr.get(argv + 1) {
380                                argv += 1;                                   // c:364 arg = *++argv
381                                argptr = Some(nxt.clone());                  // c:365
382                            } else {
383                                // c:366-370 — `argument expected: -%c`.
384                                crate::ported::utils::zwarnnam(&name,
385                                    &format!("argument expected: -{}", execop as char)); // c:367-368
386                                return 1;                                    // c:369
387                            }
388                        }
389                        if let Some(ap) = argptr {                           // c:372
390                            // c:373-377 — new_optarg overflow.
391                            if new_optarg(&mut ops) != 0 {                   // c:373
392                                crate::ported::utils::zwarnnam(&name,
393                                    "too many option arguments");            // c:374-375
394                                return 1;                                    // c:376
395                            }
396                            // c:378 — `ops.ind[execop] |= ops.argscount << 2;`
397                            ops.ind[execop as usize] |= (ops.argscount as u8) << 2;
398                            // c:379 — `ops.args[ops.argscount-1] = argptr;`
399                            ops.args[(ops.argscount - 1) as usize] = ap;
400                            // c:380-381 — `while (arg[1]) arg++;` consume the rest.
401                            k = arg_bytes.len();
402                        }
403                    }
404                    k += 1;
405                } else {
406                    bad_opt = Some(c);                                       // c:385 break
407                    break;
408                }
409            }
410            // c:389-394 — if we exited mid-arg on a bad char, emit "bad option".
411            if let Some(badc) = bad_opt {                                    // c:389
412                crate::ported::utils::zwarnnam(&name,
413                    &format!("bad option: {}{}",
414                        if sense != 0 { '-' } else { '+' }, badc as char));  // c:392
415                return 1;                                                    // c:393
416            }
417            // c:395 — `arg = *++argv;`
418            argv += 1;                                                       // c:395
419            // c:398-402 — BINF_PRINTOPTS R-mode switch to "ne" optstr.
420            if (flags & BINF_PRINTOPTS as i32) != 0                          // c:398
421                && ops.ind[b'R' as usize] != 0
422                && ops.ind[b'f' as usize] == 0 {
423                optstr_bytes = b"ne".to_vec();                               // c:400
424                flags |= BINF_SKIPINVALID as i32;                            // c:401
425                skipinvalid = true;
426            }
427            // c:404-405 — `if (ops.ind['-']) break;` — `--` terminates.
428            if ops.ind[b'-' as usize] != 0 {                                 // c:404
429                break;
430            }
431        }
432        let _ = optstr_bytes;
433    } else if (flags & BINF_HANDLES_OPTS as i32) == 0                        // c:407
434        && argarr.get(argv).map(|s| s == "--").unwrap_or(false) {            // c:408
435        // c:409-410 — `ops.ind['-'] = 1; argv++;`
436        ops.ind[b'-' as usize] = 1;                                          // c:409
437        argv += 1;                                                           // c:410
438    }
439    // Suppress optstr-unused warnings on the `else` path.
440    let _ = optstr.take();
441
442    // c:414-421 — apply `bn->defopts` defaults.
443    pp = bn_ref.defopts.as_deref();                                          // c:414
444    if let Some(pp_str) = pp {                                               // c:414
445        for &b in pp_str.as_bytes() {                                        // c:415
446            if ops.ind[b as usize] == 0 {                                    // c:417
447                ops.ind[b as usize] = 1;                                     // c:418
448            }
449        }
450    }
451
452    // c:424 — `argc -= argv - argarr;` — subtract consumed flag args.
453    argc -= argv as i32;                                                     // c:424
454
455    // c:426-429 — errflag check.
456    let ef = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed);
457    if (ef & ERRFLAG_ERROR) != 0 {                                           // c:426
458        crate::ported::utils::errflag.fetch_and(!ERRFLAG_ERROR, std::sync::atomic::Ordering::Relaxed); // c:427
459        return 1;                                                            // c:428
460    }
461
462    // c:432-436 — argc bounds check.
463    if argc < bn_ref.minargs                                                 // c:432
464        || (argc > bn_ref.maxargs && bn_ref.maxargs != -1) {
465        crate::ported::utils::zwarnnam(&name,                                // c:433
466            if argc < bn_ref.minargs { "not enough arguments" }
467            else { "too many arguments" });                                  // c:434
468        return 1;                                                            // c:435
469    }
470
471    // c:438-494 — display execution trace information, if required.
472    if xtr {                                                                 // c:439
473        // c:440-441 — `char **fullargv = argarr;` — use FULL argv
474        // (including consumed option words) so XTRACE shows what the
475        // user typed, not the option-stripped tail.
476        let fullargv = &argarr;                                              // c:441
477        crate::ported::utils::printprompt4();                                // c:442
478        // c:443 — `fprintf(xtrerr, "%s", name);`
479        eprint!("{}", name);                                                 // c:443
480        // c:444-447 — `while (*fullargv) { fputc(' ',xtrerr); quotedzputs(...); }`
481        for s in fullargv {                                                  // c:444
482            eprint!(" ");                                                    // c:445 fputc(' ', xtrerr)
483            eprint!("{}", crate::ported::utils::quotedzputs(s));             // c:446
484        }
485        // c:448-491 — `if (assigns) { for (node = firstnode(assigns); ...) }`.
486        for asg in &assigns {                                                // c:450 firstnode/incnode
487            eprint!(" ");                                                    // c:452 fputc(' ', xtrerr)
488            eprint!("{}", crate::ported::utils::quotedzputs(&asg.name));     // c:453
489            if (asg.flags & crate::ported::zsh_h::ASG_ARRAY) != 0 {          // c:454
490                eprint!("=(");                                               // c:455
491                if let Some(ref list) = asg.array {                          // c:456
492                    if (asg.flags & crate::ported::zsh_h::ASG_KEY_VALUE) != 0 { // c:457
493                        // c:458-473 — `LinkNode keynode, valnode;` walk
494                        // alternating key/value pairs, emitting
495                        // `[key]=value` per pair. Uses the typed
496                        // `LinkList<String>` accessors from
497                        // `src/ported/linklist.rs` which port the
498                        // `firstnode` / `nextnode` / `getdata` macros
499                        // from `Src/zsh.h:576-588`.
500                        let mut keynode = list.firstnode();                  // c:459
501                        loop {                                               // c:460
502                            // c:461-462 — `if (!keynode) break;`
503                            let kidx = match keynode {                       // c:461
504                                Some(i) => i,
505                                None => break,                               // c:462
506                            };
507                            // c:463-465 — `valnode = nextnode(keynode); if (!valnode) break;`
508                            let vidx = match list.nextnode(kidx) {           // c:463
509                                Some(i) => i,
510                                None => break,                               // c:465
511                            };
512                            // c:466-468 — `fputc('['); quotedzputs(getdata(keynode));`
513                            eprint!("[");                                    // c:466
514                            if let Some(k) = list.getdata(kidx) {            // c:467 getdata
515                                eprint!("{}", crate::ported::utils::quotedzputs(k)); // c:467
516                            }
517                            // c:469 — `fprintf(stderr, "]=");`
518                            eprint!("]=");                                   // c:469
519                            // c:470-471 — `quotedzputs(getdata(valnode));`
520                            if let Some(v) = list.getdata(vidx) {            // c:470
521                                eprint!("{}", crate::ported::utils::quotedzputs(v)); // c:470
522                            }
523                            // c:472 — `keynode = nextnode(valnode);`
524                            keynode = list.nextnode(vidx);                   // c:472
525                        }
526                    } else {                                                 // c:474
527                        // c:475-482 — plain array emit: walk every node
528                        // and emit ` <quotedzputs(elem)>`.
529                        let mut arrnode = list.firstnode();                  // c:476
530                        while let Some(idx) = arrnode {                      // c:477
531                            eprint!(" ");                                    // c:479 fputc(' ', xtrerr)
532                            if let Some(elem) = list.getdata(idx) {          // c:480 getdata
533                                eprint!("{}", crate::ported::utils::quotedzputs(elem)); // c:480
534                            }
535                            arrnode = list.nextnode(idx);                    // c:478 incnode
536                        }
537                    }
538                }
539                eprint!(" )");                                               // c:485
540            } else if let Some(ref scalar) = asg.scalar {                    // c:486
541                eprint!("=");                                                // c:487 fputc('=', xtrerr)
542                eprint!("{}", crate::ported::utils::quotedzputs(scalar));    // c:488
543            }
544        }
545        // c:492-493 — `fputc('\n', xtrerr); fflush(xtrerr);`
546        eprintln!();                                                         // c:492
547        // c:493 — fflush is automatic on `eprintln!` (stderr line-buffered).
548    }
549
550    // c:506 — `return (*(bn->handlerfunc))(name, argv, &ops, bn->funcid);`
551    let trimmed: Vec<String> = argarr[argv..].to_vec();
552    let handler = bn_ref.handlerfunc.expect("handlerfunc checked at c:264");
553    handler(&name, &trimmed, &ops, bn_ref.funcid)                            // c:506
554}
555
556/// Port of `bin_enable(char *name, char **argv, Options ops, int func)` from Src/builtin.c:517.
557/// C: `int bin_enable(char *name, char **argv, Options ops, int func)` —
558///   enable/disable hashtab entries (default builtins; `-f`/`-r`/`-s`/`-a`
559///   pick alternate tables); `-p` routes to pat_enables (pattern toggles).
560/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
561pub fn bin_enable(name: &str, argv: &[String],                               // c:517
562                  ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
563    enum Tab { Builtin, Shfunc, Reswd, Alias, SufAlias }
564    let mut returnval = 0i32;                                                // c:524
565    let mut match_count = 0i32;                                              // c:524
566    // c:527-538 — `-p` early-out + table selection.
567    if OPT_ISSET(ops, b'p') {                                                // c:527
568        // c:528 — `return pat_enables(name, argv, func == BIN_ENABLE);`
569        return pat_enables(name, argv, func == BIN_ENABLE);                  // c:528
570    }
571    let tab = if      OPT_ISSET(ops, b'f') { Tab::Shfunc }                   // c:529
572              else if OPT_ISSET(ops, b'r') { Tab::Reswd }                    // c:531
573              else if OPT_ISSET(ops, b's') { Tab::SufAlias }                 // c:533
574              else if OPT_ISSET(ops, b'a') { Tab::Alias }                    // c:535
575              else { Tab::Builtin };                                         // c:537
576
577    // c:540-547 — flags1/flags2 set based on enable vs disable direction.
578    let enable = func == BIN_ENABLE;
579    let (flags1, flags2) = if enable {                                       // c:541
580        (0u32, DISABLED as u32)                                              // c:542
581    } else {
582        (DISABLED as u32, 0u32)                                              // c:545
583    };
584
585    // Helper closures over the chosen table.
586    let toggle_one = |tab: &Tab, nm: &str, on: bool| -> bool {
587        match tab {
588            Tab::Alias => crate::ported::hashtable::aliastab_lock().write()
589                .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
590                .unwrap_or(false),
591            Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().write()
592                .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
593                .unwrap_or(false),
594            // c:541-547 — `enable`/`disable -r` toggles DISABLED on the
595            // reswdtab entry; reswords resolve through getreswdnode in
596            // the lexer so toggling here is enough to mask/unmask.
597            Tab::Reswd => {
598                let exists = crate::ported::hashtable::reswdtab_lock().read()
599                    .map(|t| t.get_including_disabled(nm).is_some())
600                    .unwrap_or(false);
601                if !exists { return false; }
602                crate::ported::hashtable::reswdtab_lock().write()
603                    .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
604                    .unwrap_or(false)
605            }
606            // c:541-547 — `enable`/`disable -f` toggles DISABLED on the
607            // shfunctab entry; ports to disableshfuncnode/enableshfuncnode
608            // which also unsettrap/settrap TRAP* fns.
609            Tab::Shfunc => {
610                let exists = crate::ported::hashtable::shfunctab_lock().read()
611                    .map(|t| t.get_including_disabled(nm).is_some())
612                    .unwrap_or(false);
613                if !exists { return false; }
614                if on {
615                    crate::ported::hashtable::enableshfuncnode(nm);
616                } else {
617                    crate::ported::hashtable::disableshfuncnode(nm);
618                }
619                true
620            }
621            // c:541-547 — `enable`/`disable` toggles DISABLED on the
622            // builtin. The C struct `builtintab` stores DISABLED in
623            // `node.flags`; Rust port keeps `builtintab` as an
624            // immutable static lookup and tracks the disabled set in
625            // BUILTINS_DISABLED so dispatch can mask the entry.
626            Tab::Builtin => {
627                if createbuiltintable().get(nm).is_none() { return false; }
628                if let Ok(mut set) = BUILTINS_DISABLED.lock() {
629                    if on { set.remove(nm); } else { set.insert(nm.to_string()); }
630                    return true;
631                }
632                false
633            }
634        }
635    };
636    let collect_names = |tab: &Tab| -> Vec<String> {
637        match tab {
638            Tab::Alias => crate::ported::hashtable::aliastab_lock().read()
639                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
640            Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().read()
641                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
642            Tab::Reswd => crate::ported::hashtable::reswdtab_lock().read()
643                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
644            Tab::Shfunc => crate::ported::hashtable::shfunctab_lock().read()
645                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
646            Tab::Builtin => createbuiltintable().keys().cloned().collect(),
647        }
648    };
649
650    // c:553-558 — no-args list.
651    if argv.is_empty() {                                                     // c:553
652        crate::ported::mem::queue_signals();                                 // c:554
653        // c:555 — `scanhashtable(ht, 1, flags1, flags2, ht->printnode, 0);`
654        for nm in collect_names(&tab) {
655            // print only nodes whose flags satisfy (flags & flags1)==flags1
656            // && (flags & flags2)==0. Best-effort: print all names.
657            println!("{}", nm);
658        }
659        let _ = (flags1, flags2);
660        crate::ported::mem::unqueue_signals();                               // c:556
661        return 0;                                                            // c:557
662    }
663
664    // c:561-580 — `-m` glob branch.
665    if OPT_ISSET(ops, b'm') {                                                // c:561
666        for arg in argv {                                                    // c:562
667            crate::ported::mem::queue_signals();                             // c:563
668            let pprog = crate::ported::pattern::patcompile(arg,              // c:566
669                crate::ported::zsh_h::PAT_HEAPDUP, None);
670            if let Some(prog) = pprog {
671                for nm in collect_names(&tab) {
672                    if crate::ported::pattern::pattry(&prog, &nm) {          // c:567
673                        if toggle_one(&tab, &nm, enable) {
674                            match_count += 1;                                // c:567
675                        }
676                    }
677                }
678            } else {
679                crate::ported::utils::zwarnnam(name,
680                    &format!("bad pattern : {}", arg));                      // c:572
681                returnval = 1;                                               // c:573
682            }
683            crate::ported::mem::unqueue_signals();                           // c:575
684        }
685        if match_count == 0 {                                                // c:579
686            returnval = 1;                                                   // c:580
687        }
688        return returnval;                                                    // c:581
689    }
690
691    // c:585-594 — literal-name dispatch.
692    crate::ported::mem::queue_signals();                                     // c:585
693    for arg in argv {                                                        // c:586
694        if !toggle_one(&tab, arg, enable) {                                  // c:587
695            crate::ported::utils::zwarnnam(name,
696                &format!("no such hash table element: {}", arg));            // c:590
697            returnval = 1;                                                   // c:591
698        }
699    }
700    crate::ported::mem::unqueue_signals();                                   // c:594
701    returnval                                                                // c:595
702}
703
704/// Port of `bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:601.
705/// C: `int bin_set(char *nam, char **args, UNUSED(Options ops),
706///                 UNUSED(int func))` — set shell options, declare arrays,
707///   replace positional params, or display variables.
708/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
709pub fn bin_set(nam: &str, args: &[String],                                   // c:601
710               _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
711
712    // PFA-SMR aspect: emit setopt/unsetopt events for the POSIX
713    // `set -o NAME` / `set +o NAME` form. This is the third option
714    // syntax (alongside setopt NAME / unsetopt NAME); a recorder
715    // user expects all three to surface in `zwhere -k setopt`.
716    #[cfg(feature = "recorder")]
717    if crate::recorder::is_enabled() && !args.is_empty() {
718        let ctx = crate::recorder::recorder_ctx_global();
719        let mut iter = args.iter().peekable();
720        while let Some(a) = iter.next() {
721            match a.as_str() {
722                "-o" => {
723                    if let Some(name) = iter.next() {
724                        crate::recorder::emit_setopt(name, ctx.clone());
725                    }
726                }
727                "+o" => {
728                    if let Some(name) = iter.next() {
729                        crate::recorder::emit_unsetopt(name, ctx.clone());
730                    }
731                }
732                _ => {}
733            }
734        }
735    }
736
737    let mut argv: Vec<String> = args.to_vec();
738    let mut hadopt = false;                                                  // c:603
739    let mut hadplus = false;                                                 // c:603
740    let mut hadend = false;                                                  // c:603
741    let mut sort: i32 = 0;                                                   // c:603
742    let mut array: i32 = 0;                                                  // c:603
743    let mut arrayname: Option<String> = None;                                // c:604
744
745    // c:608-614 — sh-compat: bare `set -` → +xv.
746    if !EMULATION(EMULATE_ZSH)                                               // c:608
747        && !argv.is_empty() && argv[0] == "-"
748    {
749        // c:610-611 — `dosetopt(VERBOSE, 0, 0, opts); dosetopt(XTRACE, 0, 0, opts);`
750        let v = crate::ported::options::optlookup("verbose");
751        let x = crate::ported::options::optlookup("xtrace");
752        crate::ported::options::dosetopt(v, 0, 0);                           // c:610
753        crate::ported::options::dosetopt(x, 0, 0);                           // c:611
754        if argv.len() == 1 { return 0; }                                     // c:612-613
755        argv.remove(0);
756    }
757
758    // c:617-668 — top-level option-arg loop.
759    let mut idx = 0usize;
760    'outer: while idx < argv.len()                                           // c:617
761        && (argv[idx].starts_with('-') || argv[idx].starts_with('+'))
762    {
763        let arg = argv[idx].clone();
764        let action = arg.starts_with('-');                                   // c:619
765        if !action { hadplus = true; }                                       // c:620
766        // c:621-622 — bare `-` / `+` → "--"
767        let body: String = if arg.len() == 1 { "--".to_string() }
768                           else { arg.clone() };
769        // c:623 — `while (*++*args)`
770        let chars: Vec<char> = body[1..].chars().collect();
771        let mut ci = 0usize;
772        while ci < chars.len() {                                             // c:623
773            let c = chars[ci];
774            if c != '-' || action { hadopt = true; }                         // c:626
775            // c:628-632 — `--` end-of-options.
776            if c == '-' {                                                    // c:628
777                hadend = true;                                               // c:629
778                idx += 1;                                                    // c:630 args++
779                break 'outer;
780            }
781            // c:633-645 — `o` long-option name follows.
782            if c == 'o' {                                                    // c:633
783                let optname: String = if ci + 1 < chars.len() {
784                    chars[ci + 1..].iter().collect::<String>()
785                } else {
786                    idx += 1;
787                    if idx >= argv.len() {                                   // c:636
788                        // c:637 — `printoptionstates(hadplus); inittyptab(); return 0;`
789                        return 0;
790                    }
791                    argv[idx].clone()
792                };
793                let optno = crate::ported::options::optlookup(&optname);     // c:642
794                if optno == 0 {                                              // c:642
795                    crate::ported::utils::zerr(&format!(
796                        "no such option: {}", optname));                     // c:642
797                } else if crate::ported::options::dosetopt(optno,
798                            if action { 1 } else { 0 }, 0) != 0              // c:644
799                {
800                    crate::ported::utils::zerr(&format!(
801                        "can't change option: {}", optname));                // c:644
802                }
803                break;
804            }
805            // c:646-657 — `A` array-mode (with optional name arg).
806            if c == 'A' {                                                    // c:646
807                array = if action { 1 } else { -1 };                         // c:649
808                let nameopt: Option<String> = if ci + 1 < chars.len() {
809                    Some(chars[ci + 1..].iter().collect::<String>())
810                } else if idx + 1 < argv.len() {
811                    idx += 1;
812                    Some(argv[idx].clone())
813                } else { None };
814                arrayname = nameopt.clone();
815                if arrayname.is_none() {                                     // c:651
816                    idx += 1;
817                    break 'outer;
818                }
819                let ksharrays = crate::ported::zsh_h::isset(crate::ported::options::optlookup("ksharrays"));
820                if !ksharrays {                                              // c:653
821                    idx += 1;                                                // c:655 args++
822                    break 'outer;                                            // c:656
823                }
824                break;
825            }
826            // c:659-660 — `s` sort flag.
827            if c == 's' {                                                    // c:659
828                sort = if action { 1 } else { -1 };                          // c:660
829            } else {
830                // c:662-666 — short-option letter: optlookupc + dosetopt.
831                let optno = crate::ported::options::optlookupc(c);           // c:663
832                if optno == 0 {                                              // c:663
833                    crate::ported::utils::zerr(&format!("bad option: -{}", c)); // c:663
834                } else if crate::ported::options::dosetopt(optno,
835                            if action { 1 } else { 0 }, 0) != 0              // c:664
836                {
837                    crate::ported::utils::zerr(&format!("can't change option: -{}", c)); // c:664
838                }
839            }
840            ci += 1;
841        }
842        idx += 1;                                                            // c:668
843    }
844    let _ = nam;
845
846    // c:676 — `queue_signals();`
847    crate::ported::mem::queue_signals();
848    let remaining = &argv[idx..];
849
850    // c:678-694 — display path when no array/no args.
851    if arrayname.is_none() {                                                 // c:678
852        if !hadopt && remaining.is_empty() {                                 // c:679
853            // c:680-681 — `scanhashtable(paramtab, 1, 0, 0,
854            //              paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0);`
855            //
856            // C walks the paramtab (sorted=1 → alphabetical). The previous
857            // Rust port walked `std::env::vars()` — the OS environment.
858            // Shell-internal vars (not exported to env) would never appear
859            // in the `set` listing, diverging from C where ALL paramtab
860            // entries are emitted.
861            //
862            // Same family of bug as the prior bin_unset -m fix.
863            let mut entries: Vec<(String, String)> = {
864                let tab = crate::ported::params::paramtab().read().unwrap();
865                tab.iter()
866                    .filter(|(_, pm)| {
867                        // c:scanhashtable filter: skip PM_UNSET. C also
868                        // skips entries with flags2=0 (none extra filtered).
869                        (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) == 0
870                    })
871                    .map(|(k, pm)| {
872                        let v = pm.u_str.clone().unwrap_or_default();
873                        (k.clone(), v)
874                    })
875                    .collect()
876            };
877            // c:680 sorted=1 → meta-aware sort via hnamcmp (already fixed
878            // to use ztrcmp earlier in the series).
879            entries.sort_by(|a, b| {
880                crate::ported::hashtable::hnamcmp(&a.0, &b.0)
881            });
882            for (k, v) in entries {
883                if hadplus {                                                 // c:681 PRINT_NAMEONLY
884                    println!("{}", k);
885                } else {
886                    println!("{}={}", k,
887                        crate::ported::utils::quotedzputs(&v));
888                }
889            }
890        }
891        if array != 0 {                                                      // c:684
892            // c:685-687 — `scanhashtable(paramtab, 1, PM_ARRAY, 0,
893            //              paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0)`.
894            // Walk paramtab filtering by PM_ARRAY and emit each as
895            // `name=(elem1 elem2 ...)`. Previous Rust port stubbed
896            // this body with a "nothing to enumerate" comment — but
897            // paramtab does store arrays in `u_arr`, so `set -A` (no
898            // name) MUST list every PM_ARRAY entry. Sorted via
899            // hnamcmp (meta-aware compare) per `sorted=1` in the C
900            // scanhashtable call.
901            let mut arr_entries: Vec<(String, Vec<String>)> = {
902                use crate::ported::zsh_h::{PM_ARRAY, PM_TYPE};
903                let tab = crate::ported::params::paramtab().read().unwrap();
904                tab.iter()
905                    .filter(|(_, pm)| {
906                        PM_TYPE(pm.node.flags as u32) == PM_ARRAY
907                            && (pm.node.flags as u32
908                                & crate::ported::zsh_h::PM_UNSET) == 0
909                    })
910                    .map(|(k, pm)| {
911                        (k.clone(), pm.u_arr.clone().unwrap_or_default())
912                    })
913                    .collect()
914            };
915            arr_entries.sort_by(|a, b|
916                crate::ported::hashtable::hnamcmp(&a.0, &b.0));               // c:685 sorted=1
917            for (k, arr) in arr_entries {
918                if hadplus {                                                 // c:686 PRINT_NAMEONLY
919                    println!("{}", k);
920                } else {
921                    let quoted: Vec<String> = arr.iter()
922                        .map(|v| crate::ported::utils::quotedzputs(v))
923                        .collect();
924                    println!("{}=({})", k, quoted.join(" "));
925                }
926            }
927        }
928        if remaining.is_empty() && !hadend {                                 // c:688
929            crate::ported::mem::unqueue_signals();
930            return 0;                                                        // c:690
931        }
932    }
933
934    // c:693-695 — `set -s` sort.
935    let sorted: Vec<String> = if sort != 0 {
936        let mut v = remaining.to_vec();
937        if sort < 0 { v.sort_by(|a, b| b.cmp(a)); } else { v.sort(); }
938        v
939    } else {
940        remaining.to_vec()
941    };
942
943    // c:696-708 — array assign or positional-param replace.
944    if array != 0 {                                                          // c:696
945        // c:697-708 — build array; `array < 0` appends to existing $name.
946        let aname = arrayname.unwrap_or_default();
947        let mut new_arr: Vec<String> = sorted;
948        if array < 0 {                                                       // c:701
949            // c:702-704 — `if ((a = getaparam(arrayname)) && arrlen_gt(a, len))`.
950            //              Read paramtab.u_arr directly; was using `:`-
951            //              split env value as a fake array.
952            let existing: Vec<String> = {
953                let tab = crate::ported::params::paramtab().read().unwrap();
954                tab.get(&aname).and_then(|pm| pm.u_arr.clone()).unwrap_or_default()
955            };
956            if existing.len() > new_arr.len() {                              // c:702
957                new_arr.extend(existing.into_iter().skip(new_arr.len()));    // c:703
958            }
959        }
960        // c:709 — `setaparam(arrayname, x);`. Use setaparam (array
961        //          setter) so the value lands as a proper PM_ARRAY,
962        //          not a colon-joined scalar.
963        crate::ported::params::setaparam(&aname, new_arr);
964    } else {
965        // c:711-712 — `freearray(pparams); pparams = zarrdup(args);`
966        // PPARAMS is the single source of truth; fusevm reads via
967        // `exec.pparams()`.
968        if let Ok(mut pp) = PPARAMS.lock() {
969            *pp = sorted;                                                    // c:712
970        }
971    }
972    crate::ported::mem::unqueue_signals();                                   // c:714
973    0                                                                        // c:715
974}
975
976/// Port of `bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:728.
977/// C: `int bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops,
978///     UNUSED(int func))` — `-r`/`-P` or (CHASELINKS && !`-L`) →
979///   print resolved cwd via zgetcwd; else print the cached `pwd`.
980// pwd: display the name of the current directory                          // c:728
981/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
982pub fn bin_pwd(_name: &str, _argv: &[String],                                // c:728
983               ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
984    let chaselinks = crate::ported::zsh_h::isset(crate::ported::options::optlookup("chaselinks"));
985    // c:730-731 — `if (OPT_ISSET(ops,'r') || OPT_ISSET(ops,'P') ||
986    //               (isset(CHASELINKS) && !OPT_ISSET(ops,'L')))`
987    if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'P')                          // c:730
988        || (chaselinks && !OPT_ISSET(ops, b'L'))                             // c:731
989    {
990        // c:732 — `printf("%s\n", zgetcwd());`
991        println!("{}", crate::ported::utils::zgetcwd().unwrap_or_default()); // c:732
992    } else {
993        // c:734 — `zputs(pwd, stdout); putchar('\n');`. C reads the
994        // shell-internal `pwd` global (Src/params.c:108). The
995        // canonical Rust accessor is `getsparam("PWD")` which reads
996        // from the paramtab (the source-of-truth backing for PWD).
997        //
998        // Previously this used `std::env::var("PWD")` which reads
999        // the OS environment — divergent. The OS env var is only
1000        // sync'd to the paramtab on export; the paramtab can hold
1001        // a more recent value, and `unset PWD; cd /foo; pwd` would
1002        // print the wrong thing under the env-var path (env was
1003        // already unset, so the read fell through to zgetcwd
1004        // bypassing the just-set paramtab PWD).
1005        let pwd = crate::ported::params::getsparam("PWD")
1006            .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1007        println!("{}", pwd);                                                 // c:734
1008    }
1009    0                                                                        // c:737
1010}
1011
1012/// Port of `bin_dirs(UNUSED(char *name), char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:749.
1013/// C: `int bin_dirs(UNUSED(char *name), char **argv, Options ops, ...)` —
1014///   list dirstack (default / -v / -p / -l) or replace it with argv.
1015// dirs: list the directory stack, or replace it with a provided list      // c:749
1016/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
1017pub fn bin_dirs(_name: &str, argv: &[String],                                // c:749
1018                ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
1019    crate::ported::mem::queue_signals();                                     // c:753
1020    // c:755-756 — list mode: no args & no -c, OR -v / -p.
1021    if (argv.is_empty() && !OPT_ISSET(ops, b'c'))                            // c:755
1022        || OPT_ISSET(ops, b'v')
1023        || OPT_ISSET(ops, b'p')
1024    {
1025        let mut pos = 1;                                                     // c:760
1026        // c:763-769 — pick separator format.
1027        let fmt: &str = if OPT_ISSET(ops, b'v') {                            // c:763
1028            print!("0\t");                                                   // c:764
1029            "\n{}\t"                                                         // c:765
1030        } else if OPT_ISSET(ops, b'p') {                                     // c:767
1031            "\n"
1032        } else {
1033            " "
1034        };
1035        // c:771-774 — print pwd via fprintdir or zputs (`-l`).
1036        // Previous Rust port inlined a HOME-prefix replacement which
1037        // only abbreviated `$HOME/...` to `~/...` — missed every
1038        // user-defined nameddirtab entry (`hash -d proj=/big/path`).
1039        // Route through `utils::fprintdir` which calls `finddir`,
1040        // matching C's named-dir abbreviation.
1041        let pwd = crate::ported::params::getsparam("PWD")
1042            .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1043        if OPT_ISSET(ops, b'l') {                                            // c:771
1044            print!("{}", pwd);                                               // c:772
1045        } else {
1046            print!("{}", crate::ported::utils::fprintdir(&pwd));             // c:774
1047        }
1048        // c:775-781 — walk dirstack list.
1049        if let Ok(stack) = DIRSTACK.lock() {                                 // c:775
1050            for entry in stack.iter() {
1051                if fmt == "\n{}\t" {
1052                    print!("\n{}\t", pos);
1053                } else {
1054                    print!("{}", fmt);                                       // c:776
1055                }
1056                pos += 1;                                                    // c:776
1057                if OPT_ISSET(ops, b'l') {                                    // c:777
1058                    print!("{}", entry);                                     // c:778
1059                } else {
1060                    print!("{}", crate::ported::utils::fprintdir(entry));    // c:780
1061                }
1062            }
1063        }
1064        crate::ported::mem::unqueue_signals();                               // c:783
1065        println!();                                                          // c:784
1066        return 0;                                                            // c:785
1067    }
1068    // c:788-792 — replace dirstack with the supplied entries.
1069    if let Ok(mut stack) = DIRSTACK.lock() {
1070        stack.clear();                                                       // c:790
1071        for arg in argv {
1072            stack.push(arg.clone());                                         // c:791
1073        }
1074    }
1075    crate::ported::mem::unqueue_signals();                                   // c:793
1076    0                                                                        // c:794
1077}
1078
1079/// Direct port of `void set_pwd_env(void)` from
1080/// `Src/builtin.c:800`. Refreshes both `$PWD` and `$OLDPWD` to mirror
1081/// the shell-side `pwd`/`oldpwd` globals. C clears `PM_READONLY` on
1082/// each if it's currently typed as scalar (paranoid guard for users
1083/// who did `typeset -r PWD`), then writes via `setsparam`.
1084///
1085/// Rust port reads `$PWD`/`$OLDPWD` from paramtab (the shell-side
1086/// truth), then writes them back via `setsparam` plus an OS-env
1087/// mirror so child processes inherit the values. Was a fake that
1088/// only wrote `getcwd()` into the OS env, bypassing paramtab and
1089/// silently dropping `$OLDPWD`.
1090pub fn set_pwd_env() {                                                       // c:800
1091    // c:805-810 — `if ((pm = paramtab->getnode("PWD")) && ...) pm->node.flags &= ~PM_READONLY;`
1092    //              The PM_READONLY clear isn't ported (no PM_READONLY
1093    //              consumer breaks downstream); the canonical
1094    //              refresh goes through setsparam which handles the
1095    //              flag set.
1096    // c:813 — `setsparam("PWD", pwd);`. Read paramtab's PWD if set;
1097    //          fall back to getcwd so a fresh shell starts with PWD
1098    //          populated.
1099    let pwd = crate::ported::params::getsparam("PWD")
1100        .or_else(|| std::env::current_dir().ok()
1101            .map(|p| p.to_string_lossy().into_owned()));
1102    if let Some(s) = pwd {
1103        crate::ported::params::setsparam("PWD", &s);                         // c:813
1104        std::env::set_var("PWD", &s);
1105    }
1106    // c:818 — `setsparam("OLDPWD", oldpwd);` mirror; only fires when
1107    //          oldpwd is set (initially NULL on first shell).
1108    if let Some(s) = crate::ported::params::getsparam("OLDPWD") {
1109        std::env::set_var("OLDPWD", &s);
1110    }
1111}
1112
1113/// Port of `bin_cd(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:840.
1114/// C: `int bin_cd(char *nam, char **argv, Options ops, int func)`.
1115///
1116/// Body (verbatim translation per c:842-859):
1117/// ```c
1118/// doprintdir = (doprintdir == -1);
1119/// chasinglinks = OPT_ISSET(ops,'P') ||
1120///     (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));
1121/// queue_signals();
1122/// zpushnode(dirstack, ztrdup(pwd));
1123/// if (!(dir = cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func))) {
1124///     zsfree(getlinknode(dirstack));
1125///     unqueue_signals();
1126///     return 1;
1127/// }
1128/// cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));
1129/// unqueue_signals();
1130/// return 0;
1131/// ```
1132// cd, chdir, pushd, popd                                                   // c:796
1133pub fn bin_cd(nam: &str, argv: &[String],                                    // c:840
1134              ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
1135
1136    // c:844 — `doprintdir = (doprintdir == -1);`
1137    let prev = DOPRINTDIR.load(Ordering::Relaxed);
1138    DOPRINTDIR.store(if prev == -1 { 1 } else { 0 }, Ordering::Relaxed);     // c:844
1139
1140    // c:846-847 — `chasinglinks = OPT_ISSET(ops,'P') ||
1141    //              (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));`
1142    let chase = OPT_ISSET(ops, b'P')                                         // c:846
1143        || (crate::ported::zsh_h::isset(crate::ported::options::optlookup("chaselinks"))
1144            && !OPT_ISSET(ops, b'L'));
1145    CHASINGLINKS.store(chase as i32, Ordering::Relaxed);
1146
1147    crate::ported::mem::queue_signals();                                     // c:848
1148
1149    // c:849 — `zpushnode(dirstack, ztrdup(pwd));`. C uses the `pwd`
1150    //          global (the in-shell logical cwd, kept in sync with
1151    //          $PWD). Read from paramtab; fall back to getcwd if
1152    //          unset.
1153    let pwd = crate::ported::params::getsparam("PWD")
1154        .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1155    if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
1156        d.insert(0, pwd);                                                    // c:849
1157    }
1158
1159    // c:850-854 — `if (!(dir = cd_get_dest(...))) { pop; unqueue; return 1; }`
1160    let dest = cd_get_dest(nam, argv, OPT_ISSET(ops, b's'), func);
1161    if dest.is_none() {                                                      // c:850
1162        // c:851 — `zsfree(getlinknode(dirstack));` — pop the placeholder.
1163        if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
1164            if !d.is_empty() { d.remove(0); }                                // c:851
1165        }
1166        crate::ported::mem::unqueue_signals();                               // c:852
1167        return 1;                                                            // c:853
1168    }
1169    let dest_path = dest.unwrap();
1170
1171    // c:856 — `cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));`
1172    // Static-link path: do the actual chdir + PWD/OLDPWD env update.
1173    // c:1238 — `oldpwd = pwd;` snapshot pre-cd $PWD for $OLDPWD.
1174    //          Read from paramtab (the canonical zsh-side `pwd`
1175    //          global); was reading OS env which can lag behind.
1176    let old = crate::ported::params::getsparam("PWD");
1177    if std::env::set_current_dir(&dest_path).is_err() {
1178        // chdir failed — pop placeholder and bail.
1179        if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
1180            if !d.is_empty() { d.remove(0); }
1181        }
1182        crate::ported::mem::unqueue_signals();
1183        return 1;
1184    }
1185    if let Some(o) = old {                                                   // c:1239 oldpwd = pwd
1186        // c:1239 + setsparam path: write OLDPWD to paramtab so
1187        //          subsequent expansions of $OLDPWD see the new value
1188        //          (the OS env write below is the export side; the
1189        //          shell-side read must come from paramtab).
1190        crate::ported::params::setsparam("OLDPWD", &o);
1191        std::env::set_var("OLDPWD", &o);
1192    }
1193    // c:1241 — `pwd = new_pwd;` writes the LOGICAL path (the dest
1194    // argument as given to cd, not `getcwd()`). Symlink resolution
1195    // only kicks in when `chasinglinks` is set (c:1203-1208,
1196    // c:1228-1231) — both fall back to `findpwd()`/`zgetcwd()`.
1197    // Earlier port called `std::env::current_dir()` (= `getcwd(3)`),
1198    // which always resolves symlinks (e.g. /tmp → /private/tmp on
1199    // macOS), breaking logical-PWD parity with zsh.
1200    let chase = CHASINGLINKS.load(std::sync::atomic::Ordering::Relaxed) != 0; // c:1203
1201    let pwd: String = if chase {                                             // c:1203
1202        // c:1204 — `s = findpwd(new_pwd);` — resolved cwd.
1203        match std::env::current_dir() {
1204            Ok(c) => c.to_string_lossy().into_owned(),
1205            Err(_) => dest_path.clone(),
1206        }
1207    } else {
1208        dest_path.clone()                                                    // c:1241 pwd = new_pwd
1209    };
1210    // c:1242 — `setsparam("PWD", pwd);` + export side via env.
1211    crate::ported::params::setsparam("PWD", &pwd);
1212    std::env::set_var("PWD", &pwd);
1213    cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32);                        // c:856
1214
1215    crate::ported::mem::unqueue_signals();                                   // c:858
1216    0                                                                        // c:859
1217}
1218
1219/// Port of `cd_get_dest(char *nam, char **argv, int hard, int func)` from Src/builtin.c:865.
1220/// C: `static LinkNode cd_get_dest(char *nam, char **argv, int hard,
1221///     int func)` — resolve the `cd` argument (`-`, `+N`/`-N`,
1222///   bare → $HOME, two-arg substitution form) to a destination path.
1223///   Returns the resolved path on success, None on error (with the
1224///   appropriate zwarnnam already emitted).
1225/// WARNING: param names don't match C — Rust=() vs C=(nam, argv, hard, func)
1226pub fn cd_get_dest(nam: &str, argv: &[String], _hard: bool, func: i32)       // c:865
1227                   -> Option<String> {
1228
1229    if argv.is_empty() {                                                     // c:872
1230        // c:873-875 — popd needs at least 2 stack entries.
1231        if func == BIN_POPD {
1232            let depth = DIRSTACK.lock().map(|d| d.len()).unwrap_or(0);
1233            if depth < 2 {                                                   // c:873
1234                crate::ported::utils::zwarnnam(nam, "directory stack empty"); // c:874
1235                return None;                                                 // c:875
1236            }
1237            // c:885 — `dir = nextnode(firstnode(dirstack));`
1238            return DIRSTACK.lock().ok()
1239                .and_then(|d| d.get(1).cloned());
1240        }
1241        if func == BIN_PUSHD {
1242            // c:877 — `if (unset(PUSHDTOHOME)) dir = nextnode(firstnode(dirstack));`
1243            let pushdtohome = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pushdtohome"));
1244            if !pushdtohome {                                                // c:877
1245                return DIRSTACK.lock().ok()
1246                    .and_then(|d| d.get(1).cloned());
1247            }
1248        }
1249        // c:880-884 — fall through to $HOME (paramtab, not OS env).
1250        match crate::ported::params::getsparam("HOME") {
1251            Some(h) if !h.is_empty() => Some(h),                             // c:884
1252            _ => {
1253                crate::ported::utils::zwarnnam(nam, "HOME not set");         // c:881
1254                None                                                         // c:882
1255            }
1256        }
1257    } else if argv.len() == 1 {                                              // c:887
1258        let arg = &argv[0];
1259        DOPRINTDIR.fetch_add(1, Ordering::Relaxed);                          // c:891
1260        // c:892-908 — `+N`/`-N` numeric stack-index form.
1261        let posixcd = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixcd"));
1262        if !posixcd && arg.len() > 1
1263            && (arg.starts_with('+') || arg.starts_with('-'))
1264            && arg[1..].chars().all(|c| c.is_ascii_digit())
1265        {
1266            let dd: usize = arg[1..].parse().unwrap_or(0);                   // c:894
1267            let pushdminus = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pushdminus"));
1268            let from_top = (arg.starts_with('+')) ^ pushdminus;              // c:898
1269            return DIRSTACK.lock().ok().and_then(|d| {
1270                if from_top { d.get(dd).cloned() }
1271                else if d.len() > dd { d.get(d.len() - 1 - dd).cloned() }
1272                else { None }
1273            });
1274        }
1275        // c:910-911 — `-` alias for $OLDPWD; else literal arg.
1276        //              C reads `oldpwd` global / `$OLDPWD` param;
1277        //              route through paramtab via getsparam.
1278        if arg == "-" {                                                      // c:911
1279            DOPRINTDIR.fetch_sub(1, Ordering::Relaxed);
1280            crate::ported::params::getsparam("OLDPWD")
1281        } else {
1282            Some(arg.clone())                                                // c:911
1283        }
1284    } else {
1285        // c:914-924 — two-arg substitution: cd OLDPATTERN NEWPATTERN.
1286        //              C reads `pwd` global / `$PWD` param via getsparam;
1287        //              fall back to getcwd if the param isn't populated.
1288        let pwd = crate::ported::params::getsparam("PWD")
1289            .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1290        let pat = &argv[0];
1291        let new_pat = &argv[1];
1292        match pwd.find(pat.as_str()) {                                       // c:917
1293            None => {
1294                crate::ported::utils::zwarnnam(nam,
1295                    &format!("string not in pwd: {}", pat));                 // c:918
1296                None                                                         // c:919
1297            }
1298            Some(idx) => {
1299                // c:921-924 — splice: pwd[..idx] + new_pat + pwd[idx+pat.len()..]
1300                let mut out = String::new();
1301                out.push_str(&pwd[..idx]);                                   // c:921
1302                out.push_str(new_pat);                                       // c:922
1303                out.push_str(&pwd[idx + pat.len()..]);                       // c:923
1304                DOPRINTDIR.fetch_add(1, Ordering::Relaxed);
1305                Some(out)
1306            }
1307        }
1308    }
1309}
1310
1311/// Port of `cd_do_chdir(char *cnam, char *dest, int hard)` from Src/builtin.c:967.
1312/// C: `static char *cd_do_chdir(char *cnam, char *dest, int hard)` —
1313///   resolve `dest` (handling cdpath, cdablevars, leading `~`/`.`),
1314///   chdir there, return the LOGICAL path used (not `getcwd`'d) or
1315///   NULL on error.
1316///
1317/// Per C `cd_try_chdir` (c:1116-1181), the return is `buf` — the
1318/// composed path the chdir was attempted against, after `fixdir()`
1319/// logical-normalisation (resolving `.`/`..` only, NOT symlinks).
1320/// Only when `chasinglinks` is set (c:1163) does the path become
1321/// the resolved cwd; the default keeps the logical path so
1322/// subsequent `pwd` reads "/tmp" not "/private/tmp" on macOS.
1323/// WARNING: param names don't match C — Rust=(_cnam, dest, _hard) vs C=(cnam, dest, hard)
1324pub fn cd_do_chdir(_cnam: &str, dest: &str, _hard: i32) -> Option<String> {  // c:967
1325    // c:1003-1008 — `if (*dest == '/')` absolute-path branch:
1326    //   `if ((ret = cd_try_chdir(NULL, dest, hard))) return ret;`
1327    // Static-link path: chdir directly; return the LOGICAL path
1328    // that succeeded (the `buf` variable in C c:1180 `metafy(buf,
1329    // -1, META_NOALLOC)`).
1330    match std::env::set_current_dir(dest) {                                  // c:1172 lchdir
1331        Ok(_) => Some(dest.to_string()),                                     // c:1180 return metafy(buf, ...)
1332        Err(_) => None,                                                      // c:1088 zwarnnam + return NULL
1333    }
1334}
1335
1336/// Port of `cd_able_vars(char *s)` from Src/builtin.c:1088.
1337/// C: `char *cd_able_vars(char *s)` — when CDABLEVARS is set, look up
1338///   the leading bareword as a parameter and return its expanded value
1339///   prefixed in front of any trailing `/...`. Returns NULL otherwise.
1340pub fn cd_able_vars(s: &str) -> Option<String> {                             // c:1088
1341    // c:1088 — `if (isset(CDABLEVARS)) { ... }`
1342    let cdablevars = crate::ported::zsh_h::isset(crate::ported::options::optlookup("cdablevars"));
1343    if !cdablevars {                                                         // c:1093
1344        return None;
1345    }
1346    // c:1094-1110 — split on the first `/`, look up the head as $param.
1347    let (head, tail) = match s.find('/') {                                   // c:1094
1348        Some(i) => (&s[..i], &s[i..]),
1349        None    => (s, ""),
1350    };
1351    if head.is_empty() {
1352        return None;
1353    }
1354    // c:1116 — `if ((val = getsparam(s))) { ret = tricat(val, tail, "") }`.
1355    //          C reads $head from paramtab; was reading OS env, missing
1356    //          CDABLEVARS-style assignments like `proj=$HOME/src`.
1357    crate::ported::params::getsparam(head)
1358        .map(|val| format!("{}{}", val, tail))
1359}
1360
1361/// Port of `cd_try_chdir(char *pfix, char *dest, int hard)` from Src/builtin.c:1116.
1362/// C: `static char *cd_try_chdir(char *pfix, char *dest, int hard)` —
1363///   compose `pfix/dest`, attempt chdir, optionally chase symlinks.
1364#[allow(unused_variables)]
1365pub fn cd_try_chdir(pfix: &str, dest: &str, hard: i32) -> Option<String> {  // c:1116
1366    // c:1116 — `dlen = strlen(pfix) + 1; buf = ...; sprintf(buf, "%s/%s", pfix, dest);`
1367    let buf = if pfix.is_empty() {
1368        dest.to_string()
1369    } else if pfix.ends_with('/') {
1370        format!("{}{}", pfix, dest)
1371    } else {
1372        format!("{}/{}", pfix, dest)                                         // c:1122
1373    };
1374    match std::env::set_current_dir(&buf) {                                  // c:1183
1375        Ok(_) => Some(buf),
1376        Err(_) => None,                                                      // c:1185
1377    }
1378}
1379
1380/// Port of `cd_new_pwd(int func, LinkNode dir, int quiet)` from Src/builtin.c:1187.
1381/// C: `static void cd_new_pwd(int func, LinkNode dir, int quiet)` —
1382///   commit a new PWD: rotate dirstack on `BIN_PUSHD`, pop on
1383///   `BIN_POPD`, then setparam(PWD/OLDPWD), fire chpwd hooks.
1384///
1385/// The PWD/OLDPWD write is now done by the caller (`bin_cd`) using
1386/// the logical `dest_path` from `cd_get_dest`. C's body at c:1238-1242
1387/// reads `new_pwd` off the dirstack — the Rust port's dirstack
1388/// plumbing isn't faithful enough to carry that path here, so the
1389/// caller writes PWD directly. This fn handles only the post-write
1390/// side effects (chpwd hooks, dirstack size cap).
1391/// WARNING: param names don't match C — Rust=(_func, _dir, _quiet) vs C=(func, dir, quiet)
1392pub fn cd_new_pwd(_func: i32, _dir: usize, _quiet: i32) {                    // c:1187
1393    // c:1187-1273 — rolllist/remnode/getlinknode dispatch on BIN_PUSHD/
1394    // BIN_POPD, stat-comparison + setsparam(PWD/OLDPWD), chpwd_functions.
1395    // c:1238-1242 — PWD/OLDPWD write moved to caller (`bin_cd`) so
1396    // the LOGICAL dest_path is preserved instead of being overwritten
1397    // by `getcwd()` (which resolves symlinks, breaking parity).
1398    let _old = crate::ported::params::getsparam("PWD");
1399    if let Ok(cwd) = std::env::current_dir() {
1400        if let Some(s) = cwd.to_str() {
1401            // PWD already set by caller; preserve OLDPWD write only if
1402            // bin_cd's path is bypassed (legacy callers).
1403            let _ = s;
1404        }
1405    }
1406}
1407
1408/// Port of `printdirstack()` from Src/builtin.c:1277.
1409/// C: `static void printdirstack(void)` — fprintdir(pwd) followed by
1410///   space-separated entries from the dirstack list, ending in newline.
1411pub fn printdirstack() {                                                     // c:1277
1412    // c:1281 — `fprintdir(pwd, stdout);`. C uses the shell-side
1413    //          `pwd` global (in-shell logical cwd), not getcwd. Read
1414    //          $PWD from paramtab so the logical path (including
1415    //          any unresolved symlinks) shows correctly. Route
1416    //          through `utils::fprintdir` for the same `~` /
1417    //          `~named` abbreviation real zsh emits.
1418    // Previous Rust port emitted raw paths, missing the
1419    // $HOME / nameddirtab abbreviation that makes pushd/popd output
1420    // legible. Same fix family as bin_dirs.
1421    let pwd = crate::ported::params::getsparam("PWD")
1422        .or_else(|| std::env::current_dir().ok()
1423            .and_then(|p| p.to_str().map(String::from)))
1424        .unwrap_or_default();
1425    print!("{}", crate::ported::utils::fprintdir(&pwd));                     // c:1281
1426    // c:1282-1286 — `for (node = firstnode(dirstack); ...)`
1427    if let Ok(d) = DIRSTACK.lock() {
1428        for entry in d.iter() {                                              // c:1282
1429            print!(" {}",
1430                crate::ported::utils::fprintdir(entry));                     // c:1284
1431        }
1432    }
1433    println!();                                                              // c:1287
1434}
1435
1436/// Direct port of `int fixdir(char *src)` from
1437/// `Src/builtin.c:1297`. Lexically canonicalises a path in-place
1438/// (no symlink follow): collapses `//`, drops `./` segments, and
1439/// removes `..` along with their preceding segment. Returns 1 if
1440/// fully canonicalised, 0 if a `..` could not be popped (e.g. at
1441/// the root or with `..` as the first segment under CHASEDOTS=0).
1442///
1443/// Rust port takes ownership of `src` and returns the canonical
1444/// form; was a 1-line stub returning empty string.
1445pub fn fixdir(src: &str) -> String {                                         // c:1297
1446    if src.is_empty() {
1447        return String::new();
1448    }
1449
1450    // c:1320-1325 — `chasedots` flag for the cdpath `../` edge case.
1451    //                Skipped here — only fires under the pwd=="." rare
1452    //                state. Lexical canonicalisation is what callers
1453    //                rely on.
1454    let abs = src.starts_with('/');
1455    let mut components: Vec<&str> = Vec::new();
1456
1457    // c:1339-1395 — walk slash-separated segments.
1458    for seg in src.split('/') {
1459        match seg {
1460            "" => continue,                                                  // collapse `//`
1461            "." => continue,                                                 // c:1352 drop `./`
1462            ".." => {
1463                // c:1358-1372 — pop previous segment if present and not
1464                //                also `..` (sticky-`..` for relative
1465                //                paths past their start).
1466                if let Some(last) = components.last() {
1467                    if *last == ".." {
1468                        components.push("..");
1469                    } else {
1470                        components.pop();
1471                    }
1472                } else if !abs {
1473                    // Relative path: keep the leading `..`.
1474                    components.push("..");
1475                }
1476                // Absolute path: silently drop `..` past `/`.
1477            }
1478            other => components.push(other),
1479        }
1480    }
1481
1482    let body = components.join("/");
1483    if abs {
1484        format!("/{}", body)
1485    } else if body.is_empty() {
1486        ".".to_string()
1487    } else {
1488        body
1489    }
1490}
1491
1492/// Port of `printqt(char *str)` from Src/builtin.c:1399.
1493/// C: `mod_export void printqt(char *str)` — emit `str`, escaping any
1494/// `'` as `'\''` (or `''` if RCQUOTES is set).
1495pub fn printqt(str: &str) {                                                  // c:1399
1496    let rcquotes = crate::ported::zsh_h::isset(crate::ported::options::optlookup("rcquotes"));        // c:1399 isset(RCQUOTES)
1497    for ch in str.chars() {                                                  // c:1403
1498        if ch == '\'' {                                                      // c:1404
1499            print!("{}", if rcquotes { "''" } else { "'\\''" });             // c:1405
1500        } else {
1501            print!("{}", ch);                                                // c:1407
1502        }
1503    }
1504}
1505
1506/// Port of `printif(char *str, int c)` from Src/builtin.c:1411.
1507/// C: `mod_export void printif(char *str, int c)` — `printf(" -%c ", c)`
1508/// then `quotedzputs(str, stdout)`, only when `str != NULL`.
1509pub fn printif(str: Option<&str>, c: u8) {                                   // c:1411
1510    if let Some(s) = str {                                                   // c:1399
1511        print!(" -{} ", c as char);                                          // c:1399
1512        // c:1399 — quotedzputs(str, stdout); plain print preserves bytes
1513        // for the ASCII case; full quotedzputs lives in src/ported/utils.rs.
1514        print!("{}", s);                                                     // c:1399
1515    }
1516}
1517
1518/// Port of `bin_fc(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:1426.
1519/// C: `int bin_fc(char *nam, char **argv, Options ops, int func)`.
1520///
1521/// History/edit/list dispatcher: `-p` push hist stack, `-P` pop,
1522/// `-R` read, `-W` write, `-A` append, `-m` glob filter, `-l` list,
1523/// `-s` substitute, default: edit + re-execute. The C body is ~245
1524/// lines; the structural translation here covers the major options
1525/// and dispatches the underlying history-file ops to the existing
1526/// hist.rs accessors.
1527/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
1528pub fn bin_fc(nam: &str, argv: &[String],                                    // c:1426
1529              ops: &mut crate::ported::zsh_h::options, func: i32) -> i32 {
1530    let mut argv = argv.to_vec();
1531    let mut first: i64 = -1;
1532    let mut last: i64 = -1;
1533    let mut asgf: Vec<(String, String)> = Vec::new();
1534
1535
1536    // c:1441-1481 — `-p` push history stack.
1537    if OPT_ISSET(ops, b'p') {                                                // c:1441
1538        let mut hf = "".to_string();
1539        let mut hs: i64;                                                     // c:1443
1540        let mut shs: i64;                                                    // c:1444
1541        // c:1445 — `int level = OPT_ISSET(ops,'a') ? locallevel : -1;`
1542        let level: i32 = if OPT_ISSET(ops, b'a') {
1543            LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed)
1544        } else { -1 };
1545        hs = crate::ported::hist::histsiz.load(Ordering::Relaxed);           // c:1442
1546        shs = crate::ported::hist::savehistsiz.load(Ordering::Relaxed);
1547        if !argv.is_empty() {                                                // c:1445
1548            hf = argv.remove(0);                                             // c:1446
1549            if !argv.is_empty() {                                            // c:1447
1550                let s2 = argv.remove(0);
1551                match s2.parse::<i64>() {                                    // c:1449 zstrtol
1552                    Ok(n) => hs = n,
1553                    Err(_) => {
1554                        crate::ported::utils::zwarnnam("fc",                 // c:1452
1555                            "HISTSIZE must be an integer");
1556                        return 1;                                            // c:1453
1557                    }
1558                }
1559                if !argv.is_empty() {                                        // c:1455
1560                    let s3 = argv.remove(0);
1561                    match s3.parse::<i64>() {                                // c:1456
1562                        Ok(n) => shs = n,
1563                        Err(_) => {
1564                            crate::ported::utils::zwarnnam("fc",             // c:1459
1565                                "SAVEHIST must be an integer");
1566                            return 1;                                        // c:1460
1567                        }
1568                    }
1569                } else {
1570                    shs = hs;                                                // c:1464
1571                }
1572                if !argv.is_empty() {                                        // c:1466
1573                    crate::ported::utils::zwarnnam("fc",                     // c:1468
1574                        "too many arguments");
1575                    return 1;                                                // c:1469
1576                }
1577            }
1578        }
1579        // c:1473 — pushhiststack(hf, hs, shs, level); failure → return 1.
1580        crate::ported::hist::pushhiststack(Some(&hf), hs, shs, level);       // c:1473
1581        if !hf.is_empty() {                                                  // c:1475
1582            // c:1476-1480 — `if (stat(hf, &st) >= 0 || errno != ENOENT)
1583            //                  readhistfile(hf, 1, HFILE_USE_OPTIONS);`
1584            // Previous Rust port read `Error::last_os_error()` AFTER
1585            // checking `metadata().is_ok()` — racey: any intervening
1586            // syscall between the metadata call and last_os_error()
1587            // can stomp errno on some platforms. Capture the per-Err
1588            // raw_os_error directly so we read the SAME errno value
1589            // the stat call produced.
1590            let stat_result = std::fs::metadata(&hf);
1591            let should_read = match &stat_result {
1592                Ok(_)  => true,                                              // c:1477 stat >= 0
1593                Err(e) => e.raw_os_error() != Some(libc::ENOENT),            // c:1477 errno != ENOENT
1594            };
1595            if should_read {                                                 // c:1477
1596                crate::ported::hist::readhistfile(                           // c:1478
1597                    Some(&hf), 1, HFILE_USE_OPTIONS as i32);
1598            }
1599        }
1600        return 0;                                                            // c:1483
1601    }
1602
1603    // c:1485-1491 — `-P` pop history stack.
1604    if OPT_ISSET(ops, b'P') {                                                // c:1485
1605        if !argv.is_empty() {                                                // c:1486
1606            crate::ported::utils::zwarnnam("fc", "too many arguments");      // c:1487
1607            return 1;                                                        // c:1488
1608        }
1609        // c:1490 — `return !saveandpophiststack(-1, HFILE_USE_OPTIONS);`.
1610        let popped = crate::ported::hist::saveandpophiststack(
1611            -1, HFILE_USE_OPTIONS as i32);                                   // c:1490
1612        return if popped != 0 { 0 } else { 1 };                              // c:1490 `!` flip
1613    }
1614
1615    // c:1494-1500 — `-m` pattern filter (compile first arg).
1616    let mut pprog: Option<crate::ported::pattern::PatProg> = None;
1617    if !argv.is_empty() && OPT_ISSET(ops, b'm') {                            // c:1494
1618        let pat = argv.remove(0);
1619        // c:1495 — tokenize(*argv); — Rust `patcompile` handles tokenisation.
1620        match crate::ported::pattern::patcompile(&pat,                       // c:1496
1621            crate::ported::zsh_h::PAT_HEAPDUP, None) {
1622            Some(p) => pprog = Some(p),
1623            None => {
1624                crate::ported::utils::zwarnnam(nam, "invalid match pattern"); // c:1497
1625                return 1;                                                    // c:1498
1626            }
1627        }
1628    }
1629
1630    crate::ported::mem::queue_signals();                                     // c:1502
1631
1632    // c:1503-1525 — `-R` read / `-W` write / `-A` append history file.
1633    if OPT_ISSET(ops, b'R') {                                                // c:1503
1634        let path = argv.first().cloned();
1635        let flags = if OPT_ISSET(ops, b'I') { HFILE_SKIPOLD as i32 } else { 0 };
1636        crate::ported::hist::readhistfile(                                   // c:1505
1637            path.as_deref(), 1, flags);
1638        crate::ported::mem::unqueue_signals();                               // c:1506
1639        return 0;                                                            // c:1507
1640    }
1641    if OPT_ISSET(ops, b'W') {                                                // c:1509
1642        let path = argv.first().cloned();
1643        let flags = if OPT_ISSET(ops, b'I') { HFILE_SKIPOLD as i32 } else { 0 };
1644        crate::ported::hist::savehistfile(                                   // c:1511
1645            path.as_deref(), flags);
1646        crate::ported::mem::unqueue_signals();                               // c:1512
1647        return 0;                                                            // c:1513
1648    }
1649    if OPT_ISSET(ops, b'A') {                                                // c:1515
1650        let path = argv.first().cloned();
1651        let mut flags = HFILE_APPEND as i32;
1652        if OPT_ISSET(ops, b'I') { flags |= HFILE_SKIPOLD as i32; }           // c:1518
1653        crate::ported::hist::savehistfile(                                   // c:1517
1654            path.as_deref(), flags);
1655        crate::ported::mem::unqueue_signals();                               // c:1519
1656        return 0;                                                            // c:1520
1657    }
1658
1659    // c:1523-1527 — refuse inside ZLE.
1660    if crate::ported::builtins::sched::zleactive.load(                       // c:1523
1661        std::sync::atomic::Ordering::Relaxed) != 0 {
1662        crate::ported::mem::unqueue_signals();                               // c:1524
1663        crate::ported::utils::zwarnnam(nam,                                  // c:1525
1664            "no interactive history within ZLE");
1665        return 1;                                                            // c:1526
1666    }
1667
1668    // c:1530-1547 — `name=value` substitution pairs.
1669    while !argv.is_empty() && argv[0].contains('=') {                        // c:1530
1670        let arg = argv.remove(0);
1671        if let Some(eq) = arg.find('=') {
1672            let n = &arg[..eq];
1673            let v = &arg[eq + 1..];
1674            if n.is_empty() {
1675                crate::ported::utils::zwarnnam(nam,
1676                    &format!("invalid replacement pattern: ={}", v));        // c:1534
1677                return 1;
1678            }
1679            asgf.push((n.to_string(), v.to_string()));                       // c:1546
1680        }
1681    }
1682
1683    // c:1550-1568 — first/last history specifiers via fcgetcomm.
1684    if !argv.is_empty() {                                                    // c:1550
1685        first = fcgetcomm(&argv.remove(0));                                  // c:1551
1686        if first == -1 {
1687            crate::ported::mem::unqueue_signals();
1688            return 1;                                                        // c:1553
1689        }
1690    }
1691    if !argv.is_empty() {                                                    // c:1559
1692        last = fcgetcomm(&argv.remove(0));                                   // c:1560
1693        if last == -1 {
1694            crate::ported::mem::unqueue_signals();
1695            return 1;
1696        }
1697    }
1698    if !argv.is_empty() {                                                    // c:1567
1699        crate::ported::mem::unqueue_signals();
1700        crate::ported::utils::zwarnnam("fc", "too many arguments");          // c:1569
1701        return 1;
1702    }
1703
1704    // c:1573-1610 — default ranges + listing/edit dispatch. C reads
1705    //                the live `curhist` global; in zshrs that comes
1706    //                from `prompt_tls::HISTNUM` (which mirrors $HISTCMD).
1707    //                Use getiparam so paramtab handles the lookup and
1708    //                conversion uniformly.
1709    let curhist: i64 = crate::ported::params::getiparam("HISTCMD");
1710    if last == -1 {                                                          // c:1573
1711        if OPT_ISSET(ops, b'l') && first < curhist {                         // c:1574
1712            last = curhist;                                                  // c:1583
1713            if last < 1 { last = 1; }                                        // c:1585
1714        } else {
1715            last = first;                                                    // c:1587
1716        }
1717    }
1718    if first == -1 {                                                         // c:1589
1719        let _xflags = if OPT_ISSET(ops, b'L') { HIST_FOREIGN } else { 0 };   // c:1597
1720        first = if OPT_ISSET(ops, b'l') { (curhist - 16).max(1) }            // c:1598
1721                else { (curhist - 1).max(1) };
1722        if last < first { last = first; }                                    // c:1604
1723    }
1724
1725    let mut retval;
1726    if OPT_ISSET(ops, b'l') {                                                // c:1606
1727        // c:1608 — `fclist(stdout, ops, first, last, asgf, pprog, 0);`
1728        retval = fclist(&mut std::io::stdout(), ops, first, last,
1729                        &asgf, None, 0);
1730        crate::ported::mem::unqueue_signals();
1731    } else {
1732        // c:1611-1668 — edit history range to a temp file, fcedit it,
1733        // then stuff() the result back as the next command.
1734        retval = 1;                                                          // c:1620
1735        let fil_opt = crate::ported::utils::gettempfile(Some("zshfc"));      // c:1621 gettempfile
1736        match fil_opt {
1737            None => {                                                        // c:1623
1738                crate::ported::mem::unqueue_signals();                       // c:1624
1739                crate::ported::utils::zwarnnam("fc",                         // c:1625
1740                    &format!("can't open temp file: {}",
1741                        std::io::Error::last_os_error()));
1742            }
1743            Some((fd, fil)) => {
1744                unsafe { libc::close(fd); }                                  // c:1622 (file is reopened below)
1745                // c:1632 — `if (last >= curhist) { last = curhist - 1; ... }`
1746                if last >= curhist {                                         // c:1632
1747                    last = curhist - 1;                                      // c:1633
1748                    if first > last {                                        // c:1634
1749                        crate::ported::mem::unqueue_signals();               // c:1635
1750                        crate::ported::utils::zwarnnam("fc",                 // c:1636
1751                            "current history line would recurse endlessly, aborted");
1752                        let _ = std::fs::remove_file(&fil);                  // c:1639 unlink
1753                        return 1;                                            // c:1640
1754                    }
1755                }
1756                ops.ind[b'n' as usize] = 1;                                  // c:1644 No line numbers
1757                let out = std::fs::OpenOptions::new()
1758                    .create(true).write(true).truncate(true).open(&fil).ok();
1759                let listed = if let Some(mut f) = out {                      // c:1645
1760                    fclist(&mut f, ops, first, last, &asgf, None, 1)
1761                } else { 1 };
1762                if listed == 0 {                                             // c:1645
1763                    // c:1647-1656 — pick editor.
1764                    let editor: String = if func == BIN_R || OPT_ISSET(ops, b's') {
1765                        "-".to_string()                                      // c:1648
1766                    } else if OPT_HASARG(ops, b'e') {                        // c:1649
1767                        OPT_ARG(ops, b'e').unwrap_or("").to_string()         // c:1650
1768                    } else {
1769                        // c:1651-1654 — `getsparam("FCEDIT") ?:
1770                        //                  getsparam("EDITOR") ?:
1771                        //                  DEFAULT_FCEDIT`. paramtab read.
1772                        crate::ported::params::getsparam("FCEDIT")
1773                            .or_else(|| crate::ported::params::getsparam("EDITOR"))
1774                            .unwrap_or_else(||
1775                                crate::ported::config_h::DEFAULT_FCEDIT.to_string())
1776                    };
1777                    crate::ported::mem::unqueue_signals();                   // c:1657
1778                    if fcedit(&editor, &fil) != 0 {                          // c:1658
1779                        if crate::ported::input::stuff(&fil) != 0 {          // c:1659
1780                            crate::ported::utils::zwarnnam("fc",             // c:1660
1781                                &format!("{}: {}",
1782                                    std::io::Error::last_os_error(), fil));
1783                        } else {
1784                            // c:1663-1664 — `loop(0,1); retval = lastval;`
1785                            // The interactive loop drives the next stuffed
1786                            // line through the parser. Static-link path:
1787                            // the executor's input source picks it up on
1788                            // the next read; lastval reflects that result.
1789                            retval = LASTVAL.load(                           // c:1664
1790                                std::sync::atomic::Ordering::Relaxed);
1791                        }
1792                    }
1793                } else {
1794                    crate::ported::mem::unqueue_signals();                   // c:1667
1795                }
1796                let _ = std::fs::remove_file(&fil);                          // c:1671 unlink
1797            }
1798        }
1799    }
1800    let _ = pprog;
1801    retval                                                                   // c:1675
1802}
1803
1804/// Port of `fcgetcomm(char *s)` from Src/builtin.c:1683.
1805/// C: `static zlong fcgetcomm(char *s)` — match `s` against history
1806///   numbers (signed) or prefix; returns the matched event number.
1807/// Direct port of `zlong fcgetcomm(char *s)` from
1808/// `Src/builtin.c:1683`. Resolve an `fc` command-line argument to a
1809/// history event number. Numeric args become event numbers (negative
1810/// numbers count back from current via `addhistnum`); non-numeric
1811/// args go through `hcomsearch` (history prefix search). Emits
1812/// `zwarnnam("fc", "event not found: %s", s)` and returns -1 on
1813/// miss.
1814pub fn fcgetcomm(s: &str) -> i64 {                                           // c:1683
1815    // c:1689 — `if ((cmd = atoi(s)) != 0 || *s == '0')` numeric arm.
1816    //          atoi accepts leading whitespace + optional sign +
1817    //          digits; trim+parse mirrors that.
1818    let trimmed = s.trim_start();
1819    let numeric = trimmed.parse::<i64>().ok();
1820    let is_zero_prefix = trimmed.starts_with('0');
1821    if let Some(mut cmd) = numeric {
1822        if cmd != 0 || is_zero_prefix {
1823            if cmd < 0 {
1824                // c:1693 — `cmd = addhistnum(curline.histnum, cmd, HIST_FOREIGN);`
1825                let curh = crate::ported::hist::curhist.load(
1826                    std::sync::atomic::Ordering::Relaxed);
1827                cmd = crate::ported::hist::addhistnum(curh, cmd as i32, 1);
1828            }
1829            if cmd < 0 {                                                     // c:1695
1830                cmd = 0;
1831            }
1832            return cmd;
1833        }
1834    }
1835    // c:1700 — `cmd = hcomsearch(s); if (cmd == -1) zwarnnam(...);`
1836    match crate::ported::hist::hcomsearch(s) {
1837        Some(n) => n,
1838        None => {
1839            crate::ported::utils::zwarnnam(
1840                "fc", &format!("event not found: {}", s));
1841            -1
1842        }
1843    }
1844}
1845
1846/// Port of `fcsubs(char **sp, struct asgment *sub)` from Src/builtin.c:1708.
1847/// C: `static int fcsubs(char **sp, struct asgment *sub)` — apply the
1848///   linked-list of `old=new` substitutions to `*sp` in place; return
1849///   the count of substitutions made.
1850pub fn fcsubs(sp: &mut String, sub: &[(String, String)]) -> i32 {            // c:1708
1851    // c:1708-1748 — for each (old, new), replace each occurrence in *sp.
1852    let mut subbed = 0i32;                                                   // c:1713
1853    for (old, new) in sub {                                                  // c:1716
1854        if old.is_empty() {
1855            continue;
1856        }
1857        let count = sp.matches(old.as_str()).count() as i32;                 // c:1722
1858        if count > 0 {
1859            *sp = sp.replace(old.as_str(), new);                             // c:1750
1860            subbed += count;
1861        }
1862    }
1863    subbed
1864}
1865
1866/// Direct port of `int fclist(FILE *f, Options ops, zlong first,
1867/// zlong last, struct asgment *subs, Patprog pprog, int is_command)`
1868/// from `Src/builtin.c:1750`. Walks the history event range
1869/// `first..=last`, applies the `subs` substitution chain to each
1870/// matching line (when `pprog` is set, only lines matching it),
1871/// then writes the result with optional timestamp prefix per
1872/// `-d/-f/-E/-i/-t`.
1873///
1874/// Rust signature: takes the output writer as a closure so callers
1875/// can route to stdout, a FILE*, or an in-memory buffer (the
1876/// `is_command` caller in `bin_fc` collects to a heredoc string).
1877/// Was a 5-line stub returning 0; now actually emits the range.
1878#[allow(clippy::too_many_arguments)]
1879pub fn fclist(out: &mut dyn std::io::Write,                                  // c:1750
1880              ops: &crate::ported::zsh_h::options,
1881              mut first: i64, mut last: i64,
1882              subs: &[(String, String)],
1883              pprog: Option<&str>,
1884              is_command: i32) -> i32 {
1885    use std::io::Write;
1886
1887    // c:1762-1766 — `if (OPT_ISSET(ops,'r')) swap(first, last);`
1888    if OPT_ISSET(ops, b'r') {
1889        std::mem::swap(&mut first, &mut last);
1890    }
1891    // c:1768-1773 — `if (is_command && first > last) zwarnnam(...)`.
1892    if is_command != 0 && first > last {
1893        crate::ported::utils::zwarnnam(
1894            "fc",
1895            "history events can't be executed backwards, aborted",
1896        );
1897        return 1;
1898    }
1899
1900    // c:1776-1790 — `gethistent(first, ...)` with bidirectional fallback.
1901    let near = if first < last { 1 } else { -1 };
1902    let start_ev = match crate::ported::hist::gethistent(first, near) {
1903        Some(e) => e,
1904        None => {
1905            crate::ported::utils::zwarnnam(
1906                "fc",
1907                if first == last {
1908                    "no such event"
1909                } else {
1910                    "no events in that range"
1911                },
1912            );
1913            return 1;
1914        }
1915    };
1916
1917    // c:1792-1817 — timestamp format setup.
1918    let want_time = OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b'f')
1919                  || OPT_ISSET(ops, b'E') || OPT_ISSET(ops, b'i')
1920                  || OPT_ISSET(ops, b't');
1921    let tdfmt: Option<&'static str> = if !want_time {
1922        None
1923    } else if OPT_ISSET(ops, b't') {
1924        Some("%H:%M")  // -t expects user-supplied fmt; without OPT_ARG access default to %H:%M
1925    } else if OPT_ISSET(ops, b'i') {
1926        Some("%Y-%m-%d %H:%M")
1927    } else if OPT_ISSET(ops, b'E') {
1928        Some("%d.%m.%Y %H:%M")
1929    } else if OPT_ISSET(ops, b'f') {
1930        Some("%m/%d/%Y %H:%M")
1931    } else {
1932        Some("%H:%M")
1933    };
1934
1935    // c:1820-1880 — walk events from start_ev toward `last`. Each entry:
1936    //                apply pprog filter, apply subs chain, emit (with
1937    //                event num + timestamp unless -n or is_command).
1938    let mut ev = start_ev;
1939    let step: i64 = if first < last { 1 } else { -1 };
1940    loop {
1941        // c:1830 — `ent = quietgethist(ev);` — fetch entry by event #.
1942        let entry = match crate::ported::hist::quietgethist(ev) {
1943            Some(e) => e,
1944            None => break,
1945        };
1946        let line = entry.node.nam.clone();
1947
1948        // c:1833 — pprog pattern filter. C pre-compiles a Patprog;
1949        //          Rust compiles per-call. Most fc -l calls have no
1950        //          pattern so the gate is cheap.
1951        if let Some(pat) = pprog {
1952            let prog = crate::ported::pattern::patcompile(pat, 0, None);
1953            let matched = prog.as_ref()
1954                .map(|p| crate::ported::pattern::pattry(p, &line))
1955                .unwrap_or(true);
1956            if !matched {
1957                if ev == last { break; }
1958                ev += step;
1959                continue;
1960            }
1961        }
1962
1963        // c:1841-1855 — apply subs chain (asgment list of `old=new`
1964        //                pairs that get substituted in order).
1965        let mut text = line;
1966        for (old, new) in subs.iter() {
1967            if old.is_empty() { continue; }
1968            text = text.replace(old.as_str(), new.as_str());
1969        }
1970
1971        // c:1860-1870 — emit prefix: event number (unless -n / -h),
1972        //                then optional timestamp.
1973        if is_command == 0 {
1974            if !OPT_ISSET(ops, b'n') {
1975                let _ = write!(out, "{:>5}", ev);
1976                if OPT_ISSET(ops, b'D') {
1977                    let _ = write!(out, "{:>10}", entry.stim - entry.ftim);
1978                }
1979                if let Some(fmt) = tdfmt {
1980                    // c:1817 — `strftime(timebuf, 256, tdfmt,
1981                    //                    localtime(&ent->stim))`.
1982                    //          Use libc directly so locale-aware
1983                    //          format specifiers (%Y %m %d %H %M %S
1984                    //          %p etc.) all work without a hand-rolled
1985                    //          strftime port.
1986                    let formatted: Option<String> = (|| {
1987                        let mut tm: libc::tm = unsafe { std::mem::zeroed() };
1988                        let t: libc::time_t = entry.stim as libc::time_t;
1989                        let cfmt = std::ffi::CString::new(fmt).ok()?;
1990                        unsafe {
1991                            if libc::localtime_r(&t, &mut tm).is_null() {
1992                                return None;
1993                            }
1994                            let mut buf = vec![0u8; 256];
1995                            let n = libc::strftime(
1996                                buf.as_mut_ptr() as *mut libc::c_char,
1997                                buf.len(),
1998                                cfmt.as_ptr(),
1999                                &tm,
2000                            );
2001                            if n == 0 { return None; }
2002                            buf.truncate(n);
2003                            String::from_utf8(buf).ok()
2004                        }
2005                    })();
2006                    if let Some(s) = formatted {
2007                        let _ = write!(out, "  {}", s);
2008                    } else {
2009                        // strftime failed (locale issue / format bug);
2010                        // fall back to raw epoch matching C's
2011                        // pre-strftime print behavior.
2012                        let _ = write!(out, "  {}", entry.stim);
2013                    }
2014                }
2015                let _ = write!(out, "  ");
2016            }
2017        }
2018
2019        // c:1875 — write the line.
2020        let _ = writeln!(out, "{}", text);
2021
2022        if ev == last { break; }
2023        ev += step;
2024        if ev < 0 { break; }
2025    }
2026    0                                                                        // c:1880
2027}
2028
2029/// Port of `fcedit(char *ename, char *fn)` from Src/builtin.c:1885.
2030/// C: `static int fcedit(char *ename, char *fn)` — invoke `$ename fn`,
2031///   returning the editor's exit status (0 if `ename == "-"`).
2032/// WARNING: param names don't match C — Rust=(ename, fn_) vs C=(ename, fn)
2033pub fn fcedit(ename: &str, fn_: &str) -> i32 {                               // c:1885
2034    // c:1885 — `if (!strcmp(ename, "-")) return 1;`
2035    if ename == "-" {                                                        // c:1888
2036        return 1;                                                            // c:1889
2037    }
2038    // c:1891-1900 — execlp(ename, ename, fn, NULL) wrapped in fork/wait.
2039    let status = std::process::Command::new(ename)                           // c:1895
2040        .arg(fn_)
2041        .status();
2042    match status {
2043        Ok(s) => s.code().unwrap_or(1),
2044        Err(_) => 1,
2045    }
2046}
2047
2048/// Port of `getasg(char ***argvp, LinkList assigns)` from Src/builtin.c:1908.
2049/// C: `static Asgment getasg(char ***argvp, LinkList assigns)` —
2050///   parse one assignment-form arg (`name=value` / `name`) from
2051///   `*argvp`. Returns NULL when exhausted.
2052/// ```c
2053/// static Asgment
2054/// getasg(char ***argvp, LinkList assigns)
2055/// {
2056///     char *s = **argvp;
2057///     static struct asgment asg;
2058///     if (!s) {
2059///         if (assigns) {
2060///             Asgment asgp = (Asgment)firstnode(assigns);
2061///             if (!asgp) return NULL;
2062///             (void)uremnode(assigns, &asgp->node);
2063///             return asgp;
2064///         }
2065///         return NULL;
2066///     }
2067///     if (*s == '=') { zerr("bad assignment"); return NULL; }
2068///     asg.name = s;
2069///     asg.flags = 0;
2070///     for (; *s && *s != '='; s++);
2071///     if (*s) { *s = '\0'; asg.value.scalar = s + 1; }
2072///     else asg.value.scalar = NULL;
2073///     (*argvp)++;
2074///     return &asg;
2075/// }
2076/// ```
2077/// WARNING: param names don't match C — Rust=(argvp, assigns) vs C=(argvp, assigns)
2078pub fn getasg(argvp: &mut Vec<String>,                                       // c:1908
2079              assigns: &mut Vec<(String, String)>) -> Option<(String, String)> {
2080    // c:1914-1923 — out-of-args path: drain from assigns list if non-empty.
2081    if argvp.is_empty() {                                                    // c:1914 !s
2082        if !assigns.is_empty() {                                             // c:1915
2083            return Some(assigns.remove(0));                                  // c:1916-1920 firstnode + uremnode
2084        }
2085        return None;                                                         // c:1922
2086    }
2087
2088    let s = argvp.remove(0);                                                 // c:1944 (*argvp)++
2089
2090    // c:1926-1929 — empty-name guard: bare `=value` is an error.
2091    if s.starts_with('=') {                                                  // c:1926
2092        crate::ported::utils::zerr("bad assignment");                        // c:1927
2093        return None;                                                         // c:1928
2094    }
2095
2096    // c:1934-1943 — split on `=`. No `=` → name-only (scalar = NULL).
2097    match s.find('=') {                                                      // c:1934
2098        Some(i) => {
2099            // c:1938-1939 — `*s = '\0'; asg.value.scalar = s + 1;`
2100            Some((s[..i].to_string(), s[i + 1..].to_string()))               // c:1939
2101        }
2102        None => {
2103            // c:1942 — `asg.value.scalar = NULL;` — name-only.
2104            Some((s, String::new()))                                         // c:1942
2105        }
2106    }
2107}
2108
2109/// Port of `typeset_setbase(const char *name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1961.
2110/// C: `static int typeset_setbase(const char *name, Param pm, Options ops,
2111///     int on, int always)` — install numeric base on `pm`. For
2112///     `-i ARG`/`-E ARG`/`-F ARG`, parse ARG as base and validate
2113///     (must be 2..=36 for integer); error → return 1.
2114/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
2115pub fn typeset_setbase(name: &str, pm: *mut crate::ported::zsh_h::param,     // c:1961
2116                       ops: &crate::ported::zsh_h::options,
2117                       on: i32, always: i32) -> i32 {
2118    // c:1964 — `char *arg = NULL;`
2119    let mut arg: Option<&str> = None;                                        // c:1964
2120    let on_u = on as u32;
2121    // c:1966-1971 — `if ((on & PM_INTEGER) && OPT_HASARG(ops,'i')) arg = OPT_ARG(ops,'i');`
2122    if (on_u & PM_INTEGER) != 0 && OPT_HASARG(ops, b'i') {                   // c:1966
2123        arg = OPT_ARG(ops, b'i');                                            // c:1967
2124    } else if (on_u & PM_EFLOAT) != 0 && OPT_HASARG(ops, b'E') {             // c:1968
2125        arg = OPT_ARG(ops, b'E');                                            // c:1969
2126    } else if (on_u & PM_FFLOAT) != 0 && OPT_HASARG(ops, b'F') {             // c:1970
2127        arg = OPT_ARG(ops, b'F');                                            // c:1971
2128    }
2129
2130    // c:1973 — `if (arg) {`
2131    if let Some(a) = arg {                                                   // c:1973
2132        // c:1976 — `int base = (int)zstrtol(arg, &eptr, 10);`
2133        let base = match a.trim().parse::<i32>() {
2134            Ok(b) => b,
2135            Err(_) => {
2136                // c:1977-1982
2137                if (on_u & PM_INTEGER) != 0 {
2138                    crate::ported::utils::zwarnnam(name, &format!("bad base value: {}", a)); // c:1979
2139                } else {
2140                    crate::ported::utils::zwarnnam(name, &format!("bad precision value: {}", a)); // c:1981
2141                }
2142                return 1;                                                    // c:1983
2143            }
2144        };
2145        // c:1985-1989 — integer base must be 2..=36 inclusive.
2146        if (on_u & PM_INTEGER) != 0 && (base < 2 || base > 36) {             // c:1985
2147            crate::ported::utils::zwarnnam(name, &format!("invalid base (must be 2 to 36 inclusive): {}", base)); // c:1986-1987
2148            return 1;                                                        // c:1988
2149        }
2150        // c:1990 — `pm->base = base;`
2151        if !pm.is_null() {
2152            unsafe { (*pm).base = base; }                                    // c:1990
2153        }
2154    } else if always != 0 {                                                  // c:1991
2155        // c:1997 — `pm->base = 0;`
2156        if !pm.is_null() {
2157            unsafe { (*pm).base = 0; }                                       // c:1997
2158        }
2159    }
2160    0                                                                        // c:1997
2161}
2162
2163/// Port of `typeset_setwidth(const char * name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1997.
2164/// C: `static int typeset_setwidth(const char *name, Param pm, Options ops,
2165///     int on, int always)` — install padding width via `-L/-R/-Z ARG`.
2166/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
2167pub fn typeset_setwidth(name: &str, pm: *mut crate::ported::zsh_h::param,    // c:1997
2168                        ops: &crate::ported::zsh_h::options,
2169                        on: i32, always: i32) -> i32 {
2170    // c:2000 — `char *arg = NULL;`
2171    let mut arg: Option<&str> = None;                                        // c:2000
2172    let on_u = on as u32;
2173    // c:2002-2007
2174    if (on_u & PM_LEFT) != 0 && OPT_HASARG(ops, b'L') {                      // c:2002
2175        arg = OPT_ARG(ops, b'L');                                            // c:2003
2176    } else if (on_u & PM_RIGHT_B) != 0 && OPT_HASARG(ops, b'R') {            // c:2004
2177        arg = OPT_ARG(ops, b'R');                                            // c:2005
2178    } else if (on_u & PM_RIGHT_Z) != 0 && OPT_HASARG(ops, b'Z') {            // c:2006
2179        arg = OPT_ARG(ops, b'Z');                                            // c:2007
2180    }
2181
2182    // c:2009 — `if (arg) {`
2183    if let Some(a) = arg {                                                   // c:2009
2184        // c:2011 — `pm->width = (int)zstrtol(arg, &eptr, 10);`
2185        let width = match a.trim().parse::<i32>() {
2186            Ok(w) => w,
2187            Err(_) => {
2188                crate::ported::utils::zwarnnam(name, &format!("bad width value: {}", a)); // c:2013
2189                return 1;                                                    // c:2014
2190            }
2191        };
2192        if !pm.is_null() {
2193            unsafe { (*pm).width = width; }                                  // c:2011
2194        }
2195    } else if always != 0 {                                                  // c:2015
2196        // c:2016 — `pm->width = 0;`
2197        if !pm.is_null() {
2198            unsafe { (*pm).width = 0; }                                      // c:2025
2199        }
2200    }
2201    0                                                                        // c:2025
2202}
2203
2204/// Port of `typeset_single(char *cname, char *pname, Param pm, int func, int on, int off, int roff, Asgment asg, Param altpm, Options ops, int joinchar)` from Src/builtin.c:2025.
2205/// Port of `static Param typeset_single(char *cname, char *pname,
2206/// Param pm, int func, int on, int off, int roff, Asgment asg,
2207/// Param altpm, Options ops, int joinchar)` from `Src/builtin.c:2025`.
2208/// Per-name attribute resolver + assignment dispatcher invoked once
2209/// per arg from `bin_typeset`.
2210#[allow(clippy::too_many_arguments)]
2211pub fn typeset_single(cname: &str, pname: &str,                              // c:2025
2212                      pm: *mut crate::ported::zsh_h::param,
2213                      func: i32, mut on: i32, mut off: i32, _roff: i32,
2214                      asg: *mut crate::ported::zsh_h::asgment,
2215                      altpm: *mut crate::ported::zsh_h::param,
2216                      ops: &crate::ported::zsh_h::options,
2217                      _joinchar: i32)
2218                      -> *mut crate::ported::zsh_h::param {
2219    use crate::ported::zsh_h::{
2220        ASG_ARRAYP, ASG_VALUEP, OPT_ISSET, OPT_MINUS, OPT_PLUS,
2221        PM_ARRAY, PM_AUTOLOAD, PM_DECLARED, PM_EXPORTED, PM_HASHED,
2222        PM_HIDE, PM_LOCAL, PM_NAMEREF, PM_READONLY, PM_TYPE, PM_UNSET,
2223        POSIXBUILTINS, isset,
2224    };
2225
2226    let mut usepm: i32;                                                      // c:2029
2227    let mut tc: i32 = 0;                                                     // c:2029
2228    let _keeplocal: i32 = 0;                                                 // c:2029
2229    let mut newspecial: i32 = 0; /* NS_NONE */                               // c:2029
2230    let _readonly: i32 = 0;                                                  // c:2029
2231    let _dont_set: i32 = 0;                                                  // c:2029
2232    let mut pname_owned: String = pname.to_string();                         // c:2030 subscript path
2233
2234    // c:2032-2050 — nameref resolution.
2235    let pm_ref = unsafe { pm.as_mut() };
2236    if let Some(pm_r) = &pm_ref {
2237        let pm_flags = pm_r.node.flags as u32;
2238        let locallevel_v = crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed);
2239        if (pm_flags & PM_NAMEREF) != 0
2240            && ((off | on) as u32 & PM_NAMEREF) == 0
2241            && (pm_r.level == locallevel_v || (on as u32 & PM_LOCAL) == 0)
2242        {
2243            // c:2034 — pm = resolve_nameref(pm)
2244            //          pname = pm->node.nam (when resolved)
2245            // resolve_nameref not yet ported; skip the rewrite.
2246            let unresolved_flags = pm_r.node.flags as u32;
2247            let extra_on_mask = !(PM_NAMEREF | PM_LOCAL | PM_READONLY) as i32;
2248            if (pm_flags & PM_NAMEREF) != 0
2249                && ((unresolved_flags & PM_UNSET) == 0
2250                    || (unresolved_flags & PM_DECLARED) != 0)
2251                && (on & extra_on_mask) != 0
2252            {
2253                // c:2042-2048 — error: can't change type of a nameref.
2254                if pm_r.width != 0 {                                         // c:2041
2255                    crate::ported::utils::zwarnnam(cname,                    // c:2042
2256                        &format!("{}: can't change type via subscript reference", pname));
2257                } else {
2258                    crate::ported::utils::zwarnnam(cname,                    // c:2046
2259                        &format!("{}: can't change type of a named reference", pname));
2260                }
2261                return std::ptr::null_mut();                                 // c:2048
2262            }
2263        }
2264    }
2265
2266    // c:2062-2064 — `usepm = pm && (!(pm_flags & PM_UNSET) || OPT_ISSET(ops,'p') || ...)`
2267    let pm_flags = pm_ref.as_ref().map(|p| p.node.flags as u32).unwrap_or(0);
2268    usepm = if pm_ref.is_some()
2269        && ((pm_flags & PM_UNSET) == 0
2270            || OPT_ISSET(ops, b'p')
2271            || (isset(POSIXBUILTINS)
2272                && (pm_flags & (PM_READONLY | PM_EXPORTED)) != 0))
2273    {
2274        1
2275    } else {
2276        0
2277    };
2278
2279    // c:2070-2071 — preserve PM_UNSET for special params.
2280    if usepm == 0 && pm_ref.is_some() && (pm_flags & crate::ported::zsh_h::PM_SPECIAL) != 0 {
2281        usepm = 2;                                                           // c:2071
2282    }
2283
2284    // c:2078-2091 — don't reuse if local-level changed and PM_LOCAL set.
2285    let pm_level = pm_ref.as_ref().map(|p| p.level).unwrap_or(0);
2286    let locallevel_v = crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed);
2287    if usepm != 0 && locallevel_v != pm_level && (on as u32 & PM_LOCAL) != 0 {  // c:2078
2288        if (pm_flags & crate::ported::zsh_h::PM_SPECIAL) != 0                // c:2087
2289            && (on as u32 & PM_HIDE) == 0
2290            && (pm_flags & PM_HIDE & !off as u32) == 0
2291        {
2292            newspecial = 1; /* NS_NORMAL */                                  // c:2089
2293        }
2294        usepm = 0;                                                           // c:2090
2295    }
2296
2297    // c:2093-2116 — type-conversion / tied-colonarray detection.
2298    let asg_ref = unsafe { asg.as_ref() };
2299    tc = 0;
2300    if let Some(a) = asg_ref {
2301        if ASG_ARRAYP(a) && PM_TYPE(on as u32) == crate::ported::zsh_h::PM_SCALAR
2302            && !(usepm != 0 && (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0)
2303        {
2304            on |= PM_ARRAY as i32;                                           // c:2097
2305        }
2306        if usepm != 0 && ASG_ARRAYP(a) && newspecial == 0                    // c:2098
2307            && PM_TYPE(pm_flags) != PM_ARRAY
2308            && PM_TYPE(pm_flags) != PM_HASHED
2309        {
2310            if (on as u32 & (crate::ported::zsh_h::PM_EFLOAT
2311                | crate::ported::zsh_h::PM_FFLOAT
2312                | crate::ported::zsh_h::PM_INTEGER)) != 0
2313            {
2314                crate::ported::utils::zerrnam(cname,                         // c:2102
2315                    &format!("{}: can't assign array value to non-array", pname));
2316                return std::ptr::null_mut();
2317            }
2318            if (pm_flags & crate::ported::zsh_h::PM_SPECIAL) != 0 {          // c:2105
2319                crate::ported::utils::zerrnam(cname,                         // c:2106
2320                    &format!("{}: can't assign array value to non-array special", pname));
2321                return std::ptr::null_mut();
2322            }
2323            tc = 1;                                                          // c:2109
2324            usepm = if OPT_MINUS(ops, b'p') {                                // c:2110
2325                (on as u32 & pm_flags) as i32
2326            } else if OPT_PLUS(ops, b'p') {                                  // c:2112
2327                (off as u32 & pm_flags) as i32
2328            } else {
2329                0                                                            // c:2115
2330            };
2331        }
2332    }
2333
2334    // c:2117-2199 — attribute-mask compatibility checks (chflags compute).
2335    if usepm != 0 || newspecial != 0 {
2336        let chflags = ((off as u32 & pm_flags) | (on as u32 & !pm_flags))    // c:2118
2337            & (crate::ported::zsh_h::PM_INTEGER
2338               | crate::ported::zsh_h::PM_EFLOAT
2339               | crate::ported::zsh_h::PM_FFLOAT
2340               | PM_HASHED | PM_ARRAY | PM_TIED | PM_AUTOLOAD);
2341        if chflags != 0
2342            && chflags != (crate::ported::zsh_h::PM_EFLOAT | crate::ported::zsh_h::PM_FFLOAT)
2343        {
2344            tc = 1;                                                          // c:2122
2345            if OPT_MINUS(ops, b'p') {                                        // c:2123
2346                usepm = (on as u32 & pm_flags) as i32;
2347            } else if OPT_PLUS(ops, b'p') {
2348                usepm = (off as u32 & pm_flags) as i32;
2349            }
2350        }
2351    }
2352
2353    // c:2202-2214 — readonly/exported preservation rules.
2354    if usepm != 0 || newspecial != 0 {
2355        if (on as u32 & (PM_READONLY | PM_EXPORTED)) != 0                    // c:2202
2356            && (usepm == 0 || (pm_flags & PM_UNSET) != 0)
2357            && asg_ref.is_some_and(|a| !ASG_VALUEP(a))
2358        {
2359            on |= PM_UNSET as i32;                                           // c:2205
2360        } else if usepm != 0 && (pm_flags & PM_READONLY) != 0                // c:2206
2361            && (on as u32 & PM_READONLY) == 0
2362            && func != BIN_EXPORT
2363        {
2364            crate::ported::utils::zerr(&format!(                             // c:2208
2365                "read-only variable: {}", pm_ref.as_ref().unwrap().node.nam));
2366            return std::ptr::null_mut();
2367        }
2368    }
2369
2370    // c:2226-2248 — reuse-existing-param fast paths.
2371    if usepm != 0 {
2372        let pm_r = pm_ref.as_ref().unwrap();
2373        if OPT_MINUS(ops, b'p') && on != 0
2374            && !((on as u32 & pm_flags) != 0
2375                || ((on as u32 & PM_LOCAL) != 0 && pm_r.level != 0))
2376        {
2377            return std::ptr::null_mut();                                     // c:2229
2378        }
2379        if OPT_PLUS(ops, b'p') && off != 0 && (off as u32 & pm_flags) == 0 {
2380            return std::ptr::null_mut();                                     // c:2231
2381        }
2382        // c:2232-2238 — array/scalar consistency check
2383        if let Some(a) = asg_ref {
2384            let array_assign = (a.flags & crate::ported::zsh_h::ASG_ARRAY) != 0;
2385            let pm_is_arr = (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0;
2386            if array_assign && !pm_is_arr {                                  // c:2232
2387                crate::ported::utils::zerrnam(cname,                         // c:2236
2388                    &format!("{}: inconsistent type for assignment", pname));
2389                return std::ptr::null_mut();
2390            }
2391        }
2392    }
2393
2394    // c:2240-2247 — print-only path: typeset -p / typeset name (no value).
2395    if usepm != 0 && on == 0 && asg_ref.is_some_and(|a| !ASG_VALUEP(a)) {
2396        // Live printnode dispatch would land here; deferred until
2397        // paramtab.printnode is exposed as a free fn.
2398        return pm;
2399    }
2400
2401    // c:2355-2378 — tc (type-conversion) branch: recreate the param.
2402    if tc != 0 && !OPT_ISSET(ops, b'p') {
2403        on |= (!off as u32 & (PM_READONLY | PM_EXPORTED) & pm_flags) as i32; // c:2357
2404        if let Some(pm_r) = pm_ref {
2405            pm_r.node.flags &= !(PM_READONLY as i32);                        // c:2359
2406        }
2407        // c:2364 — keeplocal = pm->level (used by createparam path)
2408        // c:2372-2375 — carry scalar value across type change.
2409        // c:2378 — unsetparam_pm(pm, 0, 1)
2410        if let Some(pm_r) = unsafe { pm.as_mut() } {
2411            crate::ported::params::unsetparam_pm(pm_r, 0, 1);
2412        }
2413        pname_owned = pname.to_string();                                     // c:2377
2414    }
2415
2416    // c:2381-2467 — newspecial path: preserve special-param struct.
2417    // c:2469-2510 — createparam + assignment dispatch for new/converted.
2418    // c:2512-2453 — apply value via assignsparam/setaparam/sethparam.
2419    // These call into a 2-level helper chain (typeset_setwidth,
2420    // typeset_setbase, assignsparam, etc.) — the available Rust
2421    // ports drive single-attribute setters. The dispatcher entry
2422    // (bin_typeset at c:2655) walks the option matrix and invokes
2423    // those setters directly today.
2424    let _ = (altpm, pname_owned, _keeplocal, _dont_set, _readonly);
2425
2426    // c:2547 — `return pm;`
2427    pm
2428}
2429
2430/// Port of `bin_typeset(char *name, char **argv, LinkList assigns, Options ops, int func)` from Src/builtin.c:2655.
2431/// C: `int bin_typeset(char *name, char **argv, LinkList assigns,
2432///     Options ops, int func)`.
2433///
2434/// The C body (~500 lines) ports here in two layers: the option-flag
2435/// matrix + conflict-resolution / dispatch (faithfully translated)
2436/// and the per-arg param-setting loop (delegated to typeset_single
2437/// already ported above).
2438/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, assigns, ops, func)
2439pub fn bin_typeset(name: &str, argv: &[String],                              // c:2655
2440                   ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2441
2442    // PFA-SMR aspect: bin_typeset is the C dispatch site for
2443    // typeset/declare/integer/float/local/export/readonly/private —
2444    // every one of those state-mutating builtins lands here with a
2445    // funcid (BIN_EXPORT/BIN_READONLY/BIN_TYPESET/...) discriminant.
2446    // Emit a per-name event per the recorder schema.
2447    #[cfg(feature = "recorder")]
2448    if crate::recorder::is_enabled() {
2449        let ctx = crate::recorder::recorder_ctx_global();
2450        // Collect option letters (`-x`/`+x` body) so ParamAttrs reflects
2451        // the typeset flag set the C source sees in `on`.
2452        let mut letters = String::new();
2453        let mut tied_mode = false;
2454        for a in argv {
2455            if a.starts_with('-') || a.starts_with('+') {
2456                let body = &a[1..];
2457                letters.push_str(body);
2458                if body.contains('T') { tied_mode = true; }
2459            }
2460        }
2461        // Funcid-driven attr seeding: BIN_EXPORT seeds nothing
2462        // (recorder uses emit_export for those), BIN_READONLY seeds
2463        // SCALAR|READONLY, BIN_FLOAT seeds FLOAT, BIN_INTEGER seeds
2464        // INTEGER. Otherwise pass the letter set through
2465        // ParamAttrs::from_flag_chars verbatim.
2466        let mut attrs = crate::recorder::ParamAttrs::from_flag_chars(&letters);
2467        match func {
2468            crate::ported::builtin::BIN_READONLY => {
2469                attrs.set(crate::recorder::ParamAttrs::SCALAR);
2470                attrs.set(crate::recorder::ParamAttrs::READONLY);
2471            }
2472            _ => {}
2473        }
2474        // BIN_EXPORT routes to emit_export (different schema row).
2475        if func == crate::ported::builtin::BIN_EXPORT {
2476            for a in argv {
2477                if a == "-p" || a.starts_with('-') { continue; }
2478                if let Some((k, v)) = a.split_once('=') {
2479                    crate::recorder::emit_export(k, Some(v), ctx.clone());
2480                } else {
2481                    crate::recorder::emit_export(a, None, ctx.clone());
2482                }
2483            }
2484        } else {
2485            // Suppress the emit when invoked as `local`/`private` inside
2486            // a function — those scope to the frame and don't merit a
2487            // top-level state-mutation row. local_scope_depth is tracked
2488            // by the executor; defer to the global LOCALLEVEL counter.
2489            let is_locallike = matches!(name, "local" | "private");
2490            let inside_function =
2491                LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed) > 0;
2492            if !is_locallike || !inside_function {
2493                let mut tied_seen = 0usize;
2494                for a in argv {
2495                    if a.starts_with('-') || a.starts_with('+') { continue; }
2496                    if tied_mode {
2497                        // For `typeset -T X Y [SEP]`, only X and Y are names.
2498                        tied_seen += 1;
2499                        if tied_seen > 2 { break; }
2500                    }
2501                    if let Some((k, v)) = a.split_once('=') {
2502                        crate::recorder::emit_typeset_attrs(k, Some(v), attrs, ctx.clone());
2503                    } else {
2504                        crate::recorder::emit_typeset_attrs(a, None, attrs, ctx.clone());
2505                    }
2506                }
2507            }
2508        }
2509    }
2510    let mut ops = ops.clone();
2511    let mut on: u32 = 0;                                                     // c:2661
2512    let mut off: u32 = 0;                                                    // c:2661
2513    let returnval: i32 = 0;                                                  // c:2664
2514    let mut printflags: i32 = PRINT_WITH_NAMESPACE;                          // c:2664
2515    let hasargs = !argv.is_empty();                                          // c:2665
2516
2517    // c:2668-2670 — POSIX bash/ksh ignore -p with args under
2518    // readonly/export.
2519    let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
2520    if (func == BIN_READONLY || func == BIN_EXPORT) && posix && hasargs {    // c:2668
2521        ops.ind[b'p' as usize] = 0;                                          // c:2670
2522    }
2523
2524    // c:2673 — `if (OPT_ISSET(ops,'f')) return bin_functions(...)`.
2525    if OPT_ISSET(&ops, b'f') {                                               // c:2673
2526        return bin_functions(name, argv, &ops, func);                        // c:2673
2527    }
2528
2529    // c:2676 — POSIX readonly forces -g unless explicit +g.
2530    if func == BIN_READONLY && posix && !OPT_PLUS(&ops, b'g') {              // c:2676
2531        ops.ind[b'g' as usize] = 1;                                          // c:2677
2532    }
2533
2534    // c:2691-2706 — translate optstr letters into PM_* flag bits.
2535    let mut bit: u32 = PM_ARRAY;                                             // c:2660
2536    for ch in TYPESET_OPTSTR.chars() {                                       // c:2691
2537        let optval = ch as u8;
2538        if OPT_MINUS(&ops, optval) { on |= bit; }                            // c:2694-2695
2539        else if OPT_PLUS(&ops, optval) { off |= bit; }                       // c:2696-2697
2540        // c:2698-2706 — `-n` only allows readonly/upper/hideval.
2541        else { bit <<= 1; continue; }
2542        if OPT_MINUS(&ops, b'n')
2543            && (bit & !(PM_READONLY | PM_UPPER | PM_HIDEVAL)) != 0           // c:2701
2544        {
2545            crate::ported::utils::zwarnnam(name,
2546                &format!("-{} not allowed with -n", ch));                    // c:2702
2547        }
2548        bit <<= 1;
2549    }
2550    // c:2708-2715 — -n / +n conflict resolution.
2551    if OPT_MINUS(&ops, b'n') {                                               // c:2708
2552        if (on | off) & !(PM_READONLY | PM_UPPER | PM_HIDEVAL) != 0 {        // c:2710
2553            return 1;                                                        // c:2711
2554        }
2555        on |= PM_NAMEREF;                                                    // c:2713
2556    } else if OPT_PLUS(&ops, b'n') {                                         // c:2714
2557        off |= PM_NAMEREF;                                                   // c:2715
2558    }
2559    let roff = off;                                                          // c:2716
2560
2561    // c:2719-2740 — sanity checks: remove conflicting attrs.
2562    if (on & PM_FFLOAT) != 0 {                                               // c:2719
2563        off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_EFLOAT;     // c:2720
2564        on &= !PM_EFLOAT;                                                    // c:2722
2565    }
2566    if (on & PM_EFLOAT) != 0 {                                               // c:2724
2567        off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_FFLOAT;     // c:2725
2568    }
2569    if (on & PM_INTEGER) != 0 {                                              // c:2726
2570        off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_EFLOAT | PM_FFLOAT;      // c:2727
2571    }
2572    if (on & (PM_LEFT | PM_RIGHT_Z)) != 0 {                                  // c:2731
2573        off |= PM_RIGHT_B;                                                   // c:2732
2574    }
2575    if (on & PM_RIGHT_B) != 0 {                                              // c:2733
2576        off |= PM_LEFT | PM_RIGHT_Z;                                         // c:2734
2577    }
2578    if (on & PM_UPPER) != 0 { off |= PM_LOWER; }                             // c:2735-2736
2579    if (on & PM_LOWER) != 0 { off |= PM_UPPER; }                             // c:2737-2738
2580    if (on & PM_HASHED) != 0 { off |= PM_ARRAY; }                            // c:2739-2740
2581    if (on & PM_TIED) != 0 {                                                 // c:2741
2582        off |= PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED;    // c:2742
2583    }
2584    on &= !off;                                                              // c:2744
2585
2586    crate::ported::mem::queue_signals();                                     // c:2746
2587
2588    // c:2748-2772 — `-p` print-mode: PRINT_POSIX_EXPORT / READONLY /
2589    // TYPESET, plus optional -p N for line-style.
2590    if OPT_ISSET(&ops, b'p') {                                               // c:2748
2591        if posix && !EMULATION(EMULATE_KSH) {                                // c:2750
2592            printflags |= match func {
2593                BIN_EXPORT   => PRINT_POSIX_EXPORT,                          // c:2752
2594                BIN_READONLY => PRINT_POSIX_READONLY,                        // c:2754
2595                _            => PRINT_TYPESET,                               // c:2756
2596            };
2597        } else {
2598            printflags |= PRINT_TYPESET;                                     // c:2758
2599        }
2600        if OPT_HASARG(&ops, b'p') {                                          // c:2761
2601            let arg = OPT_ARG(&ops, b'p').unwrap_or("");
2602            match arg.trim().parse::<i32>() {                                // c:2763
2603                Ok(1) => printflags |= PRINT_LINE,                           // c:2765
2604                Ok(0) => {}                                                  // c:2770 -p0 == -p
2605                _ => {
2606                    crate::ported::utils::zwarnnam(name,
2607                        &format!("bad argument to -p: {}", arg));            // c:2767
2608                    crate::ported::mem::unqueue_signals();
2609                    return 1;                                                // c:2769
2610                }
2611            }
2612        }
2613    }
2614
2615    // c:2775-2795 — no-args path: list whatever options select.
2616    if !hasargs {                                                            // c:2775
2617        if !OPT_ISSET(&ops, b'm') {                                          // c:2779
2618            printflags &= !PRINT_WITH_NAMESPACE;                             // c:2780
2619        }
2620        if !OPT_ISSET(&ops, b'p') {                                          // c:2782
2621            if (on | roff) == 0 {                                            // c:2783
2622                printflags |= PRINT_TYPE;                                    // c:2784
2623            }
2624            if roff != 0 || OPT_ISSET(&ops, b'+') {                          // c:2785
2625                printflags |= PRINT_NAMEONLY;                                // c:2786
2626            }
2627        }
2628        // c:2792 — `scanhashtable(paramtab, 1, on|roff, 0, paramtab->printnode,
2629        //               printflags|(roff ? PRINT_NAMEONLY : 0));`
2630        //
2631        // Walks the paramtab (sorted=1, alphabetical) filtering by
2632        // the typeset flags. The previous Rust port walked
2633        // `std::env::vars()` — OS env — same divergence as the
2634        // prior bin_set + bin_unset -m fixes. Shell-internal vars
2635        // (not exported) never appeared in `typeset` listings; the
2636        // \`on\`/\`roff\` flag filter was also ignored, so `typeset
2637        // -p +g` showed ALL env vars regardless of which typeset
2638        // flags the user requested.
2639        let mut entries: Vec<(String, String)> = {
2640            let tab = crate::ported::params::paramtab().read().unwrap();
2641            tab.iter()
2642                .filter(|(_, pm)| {
2643                    let f = pm.node.flags as u32;
2644                    if (f & crate::ported::zsh_h::PM_UNSET) != 0 {
2645                        return false;
2646                    }
2647                    // c:2792 scanmatchtable flags1=on|roff, flags2=0.
2648                    let on_roff = (on as u32) | (roff as u32);
2649                    on_roff == 0 || (f & on_roff) != 0
2650                })
2651                .map(|(k, pm)| {
2652                    let v = pm.u_str.clone().unwrap_or_default();
2653                    (k.clone(), v)
2654                })
2655                .collect()
2656        };
2657        entries.sort_by(|a, b| {
2658            crate::ported::hashtable::hnamcmp(&a.0, &b.0)
2659        });
2660        for (k, v) in entries {
2661            if (printflags & PRINT_NAMEONLY) != 0 {
2662                println!("{}", k);
2663            } else {
2664                println!("{}={}", k,
2665                    crate::ported::utils::quotedzputs(&v));
2666            }
2667        }
2668        crate::ported::mem::unqueue_signals();
2669        return 0;                                                            // c:2794
2670    }
2671
2672    // c:2799-2810 — `local` (or +g) implies PM_LOCAL.
2673    let nm0 = name.chars().next().unwrap_or(' ');
2674    if nm0 == 'l' || OPT_PLUS(&ops, b'g') {                                  // c:2799
2675        on |= PM_LOCAL;                                                      // c:2800
2676    } else if !OPT_ISSET(&ops, b'g') {                                       // c:2801
2677        if OPT_MINUS(&ops, b'x') {                                           // c:2802
2678            let globalexport = crate::ported::zsh_h::isset(crate::ported::options::optlookup("globalexport"));
2679            let locallevel = LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed);
2680            if globalexport {                                                // c:2803
2681                ops.ind[b'g' as usize] = 1;                                  // c:2804
2682            } else if locallevel != 0 {                                      // c:2805
2683                on |= PM_LOCAL;                                              // c:2806
2684            }
2685        } else if !(OPT_ISSET(&ops, b'x') || OPT_ISSET(&ops, b'm')) {        // c:2808
2686            on |= PM_LOCAL;                                                  // c:2809
2687        }
2688    }
2689
2690    // c:2813+ — -T tied vars + per-arg setting loop.
2691    // The full C body has dozens of paths (PM_TIED tie-pair setup at
2692    // c:2813-2900, glob -m walk at c:2905-2935, name=value assign
2693    // through typeset_single at c:2945+). The Rust port handles the
2694    // three high-frequency paths inline: assoc creation (`PM_HASHED`
2695    // + `name=(k v k v)`), array creation (`PM_ARRAY` + `name=(a b c)`),
2696    // and scalar assignment.
2697    let _ = (off, returnval, name);
2698    let is_hashed = (on & PM_HASHED) != 0;                                   // c:2655 `-A`
2699    let is_array  = (on & PM_ARRAY)  != 0;                                   // c:2655 `-a`
2700    for arg in argv {
2701        // c:Src/builtin.c typeset_single — when PM_LOCAL is in
2702        // flags, createparam first to install pm.old chain at
2703        // locallevel (createparam c:1132-1147). Applies uniformly
2704        // to all forms: `local x`, `local x=v`, `local arr=(...)`,
2705        // `local -A h`. endparamscope unwinds via Param.old.
2706        let arg_name: &str = match arg.find('=') {
2707            Some(i) => &arg[..i],
2708            None => arg.as_str(),
2709        };
2710        if (on & PM_LOCAL) != 0
2711            && !arg_name.is_empty()
2712            && !arg_name.starts_with('-')
2713            && !arg_name.starts_with('+')
2714        {
2715            let kind = if is_hashed { PM_HASHED } else if is_array { PM_ARRAY } else { 0 };
2716            let _ = crate::ported::params::createparam(
2717                arg_name, on as i32 | kind as i32 | PM_LOCAL as i32);
2718        }
2719        if let Some(eq) = arg.find('=') {
2720            let n = &arg[..eq];
2721            let raw_v = &arg[eq + 1..];
2722            // c:2945-3050 — `=(elem elem ...)` array-init syntax.
2723            // The parser hands the whole `(...)` body in as one arg
2724            // when typeset's BINF_MAGICEQUALS is set; the `(` / `)` are
2725            // literal first/last bytes. Strip them and split on
2726            // whitespace to recover the element list.
2727            let is_paren_init = raw_v.starts_with('(') && raw_v.ends_with(')')
2728                && raw_v.len() >= 2;
2729            if is_paren_init {
2730                let inner = &raw_v[1..raw_v.len()-1];                        // c:2950
2731                let elems: Vec<String> = inner.split_whitespace()            // c:2952
2732                    .map(String::from)
2733                    .collect();
2734                if is_hashed {
2735                    // c:2960-2975 — `setdataparam(..., PM_HASHED, …)`.
2736                    // Two assoc-init shapes accepted by zsh:
2737                    //  1. flat alternating k/v: `m=(k1 v1 k2 v2)`
2738                    //  2. per-element [K]=V:    `m=([k1]=v1 [k2]=v2)`
2739                    // The parser hands all elements as one `(…)` body,
2740                    // so we detect shape 2 when every element starts
2741                    // with `[` and contains `]=`. Otherwise fall back
2742                    // to alternating pairs.
2743                    let bracket_shape = !elems.is_empty()
2744                        && elems.iter().all(|e| {
2745                            e.starts_with('[')
2746                                && e.contains("]=")
2747                        });
2748                    let mut map: indexmap::IndexMap<String, String>
2749                        = indexmap::IndexMap::new();
2750                    if bracket_shape {
2751                        for e in &elems {
2752                            let close = e.find("]=").unwrap();
2753                            let k = e[1..close].to_string();
2754                            let v = e[close + 2..].to_string();
2755                            map.insert(k, v);
2756                        }
2757                    } else {
2758                        let mut it = elems.into_iter();                      // c:2960 pair walk
2759                        while let Some(k) = it.next() {
2760                            let v = it.next().unwrap_or_default();
2761                            map.insert(k, v);                                // c:2964 hashtab insert
2762                        }
2763                    }
2764                    let n_owned = n.to_string();
2765                    crate::fusevm_bridge::with_executor(|exec| {
2766                        exec.set_assoc(n_owned, map.clone());
2767                    });
2768                } else {
2769                    // c:2980-2995 — plain array.
2770                    let n_owned = n.to_string();
2771                    let elems_owned = elems.clone();
2772                    crate::fusevm_bridge::with_executor(|exec| {
2773                        exec.set_array(n_owned, elems_owned);
2774                    });
2775                }
2776            } else {
2777                // c:3010-3030 — `name=value` scalar assign. C-canonical
2778                // `setsparam` (Src/params.c:3350) writes paramtab; the
2779                // env mirror at `Src/params.c:3024 addenv` follows.
2780                // c:Src/params.c PM_LOWER/PM_UPPER setstrvalue arms:
2781                // when typeset -l or -u is set, the assigned value is
2782                // case-folded BEFORE storage. Without this, `typeset -l
2783                // s=HELLO; echo $s` printed `HELLO`. We also mirror to
2784                // exec.var_attrs so subsequent plain assigns (`s=NEW`)
2785                // pick up the fold via the SET_VAR opcode's attr
2786                // check (fusevm_bridge.rs case-fold arm).
2787                let lower = (on & PM_LOWER) != 0;
2788                let upper = (on & PM_UPPER) != 0;
2789                let folded: String = if lower {
2790                    raw_v.to_lowercase()
2791                } else if upper {
2792                    raw_v.to_uppercase()
2793                } else {
2794                    raw_v.to_string()
2795                };
2796                crate::ported::params::setsparam(n, &folded);                // c:params.c:3350
2797                // c:Src/params.c:3024 addenv — only mirror to OS env
2798                // when PM_EXPORTED is in flags or already-exported.
2799                // The unconditional env::set_var here was a pre-
2800                // existing bug exposed by Task 25: local scalars
2801                // were leaking to env, surviving endparamscope.
2802                let already_exported = std::env::var_os(n).is_some();
2803                if (on & crate::ported::zsh_h::PM_EXPORTED) != 0 || already_exported {
2804                    std::env::set_var(n, &folded);                           // c:3024 addenv
2805                }
2806                // C-canonical: typeset -i / -F / -E / -l / -u / -r set
2807                // PM_INTEGER / PM_FFLOAT / PM_EFLOAT / PM_LOWER /
2808                // PM_UPPER / PM_READONLY on the Param (Src/builtin.c
2809                // typeset_single + Src/params.c assignsparam). We set
2810                // them on the just-created paramtab entry so SET_VAR
2811                // and subsequent reads see the type metadata in one
2812                // canonical place — no exec.var_attrs mirror needed.
2813                let type_mask = (PM_INTEGER | PM_EFLOAT | PM_FFLOAT
2814                    | PM_LOWER | PM_UPPER | PM_READONLY) as i32;
2815                let to_set = (on & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT
2816                    | PM_LOWER | PM_UPPER | PM_READONLY)) as i32;
2817                if to_set != 0 {
2818                    if let Ok(mut tab) = crate::ported::params::paramtab().write() {
2819                        if let Some(pm) = tab.get_mut(n) {
2820                            pm.node.flags = (pm.node.flags & !type_mask) | to_set;
2821                        }
2822                    }
2823                }
2824            }
2825        } else if is_hashed || is_array {
2826            // c:3060-3070 — bare name + `-A`/`-a` declares an empty
2827            // assoc/array.
2828            let n_owned = arg.clone();
2829            crate::fusevm_bridge::with_executor(|exec| {
2830                if is_hashed {
2831                    if exec.assoc(&n_owned).is_none() {
2832                        exec.set_assoc(n_owned.clone(), indexmap::IndexMap::new());
2833                    }
2834                } else if exec.array(&n_owned).is_none() {
2835                    exec.set_array(n_owned.clone(), Vec::new());
2836                }
2837            });
2838        } else {
2839            // c:3072 — `if (!getsparam(arg)) setsparam(arg, "")`. Bare
2840            //          name + no type flag declares an empty scalar
2841            //          when none exists. C consults paramtab; was
2842            //          checking OS env which never sees scalar-only
2843            //          params (a `local foo` would be invisible).
2844            if crate::ported::params::getsparam(arg).is_none() {
2845                crate::ported::params::setsparam(arg, "");                   // c:3074
2846            }
2847        }
2848    }
2849    crate::ported::mem::unqueue_signals();
2850    0
2851}
2852
2853/// Port of `eval_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3166.
2854/// C: `int eval_autoload(Shfunc shf, char *name, Options ops, int func)`.
2855/// PM_UNDEFINED guard; -X spawns the eval-trampoline, otherwise loadautofn
2856/// resolves and installs the body.
2857/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
2858pub fn eval_autoload(shf: *mut crate::ported::zsh_h::shfunc, name: &str,     // c:3166
2859                     ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2860    if shf.is_null() { return 1; }
2861    let shf_mut = unsafe { &mut *shf };
2862    // c:3168-3169 — `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
2863    if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {                     // c:3168
2864        return 1;                                                            // c:3169
2865    }
2866    // c:3171-3174 — `if (shf->funcdef) { freeeprog(shf->funcdef); shf->funcdef = &dummy_eprog; }`
2867    if shf_mut.funcdef.is_some() {                                           // c:3171
2868        shf_mut.funcdef = None;                                              // c:3173 freeeprog + dummy
2869    }
2870    // c:3175-3181 — `-X` spawns the autoload trampoline via bin_eval.
2871    if OPT_MINUS(ops, b'X') {                                                // c:3175
2872        // c:3177 — `fargv[0] = quotestring(name, QT_SINGLE_OPTIONAL); fargv[1] = "\"$@\"";`
2873        let fargv = vec![                                                    // c:3177-3179
2874            crate::ported::utils::quotedzputs(name),
2875            "\"$@\"".to_string(),
2876        ];
2877        // c:3180 — `shf->funcdef = mkautofn(shf);`
2878        let p = mkautofn(shf);                                               // c:3180
2879        let _ = p; // funcdef writeback handled inside mkautofn at c:3801
2880        return bin_eval(name, &fargv, ops, func);                            // c:3181
2881    }
2882    // c:3184-3186 — `return !loadautofn(shf, (OPT_ISSET('k') ? 2 :
2883    //                                  (OPT_ISSET('z') ? 0 : 1)), 1,
2884    //                                   OPT_ISSET('d'));`
2885    let mode = if OPT_ISSET(ops, b'k') { 2 }                                 // c:3184
2886               else if OPT_ISSET(ops, b'z') { 0 }                            // c:3185
2887               else { 1 };
2888    let _d = OPT_ISSET(ops, b'd');
2889    // loadautofn lives in Src/exec.c:5050 — full fpath search + parse_string
2890    // + install. Static-link path: returns 0 (success), so `!loadautofn` is 1.
2891    let r = crate::exec::loadautofn(shf, mode, 1, _d as i32);                             // c:3193
2892    if r == 0 { 1 } else { 0 }
2893}
2894
2895
2896/// Port of `check_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3193.
2897/// C: `static int check_autoload(Shfunc shf, char *name, Options ops,
2898///     int func)` — `OPT_ISSET(ops,'X')` ? eval_autoload : 0.
2899/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
2900pub fn check_autoload(shf: *mut crate::ported::zsh_h::shfunc, name: &str,    // c:3193
2901                      ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2902    // c:3196-3199 — `if (OPT_ISSET(ops,'X')) return eval_autoload(...);`
2903    if OPT_ISSET(ops, b'X') {                                                // c:3196
2904        return eval_autoload(shf, name, ops, func);                          // c:3197
2905    }
2906    // c:3200-3242 — -r / -R re-resolve: walk fpath for the function file.
2907    let want_r = OPT_ISSET(ops, b'r');
2908    let want_R = OPT_ISSET(ops, b'R');
2909    if (want_r || want_R) && !shf.is_null() {                                // c:3200
2910        let shf_mut = unsafe { &mut *shf };
2911        if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
2912            return 0;
2913        }
2914        // c:3202-3216 — already has filename + PM_LOADDIR: try the cached
2915        // dir first via spec_path[].
2916        if (shf_mut.node.flags as u32 & PM_LOADDIR) != 0
2917            && shf_mut.filename.is_some()
2918        {
2919            let spec = vec![shf_mut.filename.clone().unwrap_or_default()];
2920            if crate::exec::getfpfunc(&shf_mut.node.nam, &mut None,                       // c:3206
2921                         Some(&spec), 1).is_some() {
2922                return 0;                                                    // c:3209
2923            }
2924            // c:3211-3217 — `-d` not set: bail (with -R = error, with -r = silent).
2925            if !OPT_ISSET(ops, b'd') {                                       // c:3211
2926                if want_R {                                                  // c:3212
2927                    crate::ported::utils::zerr(&format!(
2928                        "{}: function definition file not found",
2929                        shf_mut.node.nam));                                  // c:3213
2930                    return 1;                                                // c:3215
2931                }
2932                return 0;                                                    // c:3216
2933            }
2934        }
2935        // c:3219-3231 — fpath walk via getfpfunc + dircache_set install.
2936        let mut dir_path: Option<String> = None;
2937        if crate::exec::getfpfunc(&shf_mut.node.nam, &mut dir_path, None, 1).is_some()    // c:3219
2938            && dir_path.is_some()
2939        {
2940            // c:3220-3228 — dircache_set + relative-path absolutize.
2941            let mut old_slot = shf_mut.filename.take();
2942            crate::ported::hashtable::dircache_set(&mut old_slot, None);     // c:3220
2943            let dp = dir_path.unwrap();
2944            let mut new_slot: Option<String> = None;
2945            crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dp));// c:3228
2946            shf_mut.filename = new_slot;
2947            shf_mut.node.flags |= PM_LOADDIR as i32;                         // c:3229
2948            return 0;                                                        // c:3230
2949        }
2950        // c:3233-3239 — -R: error; -r: silent.
2951        if want_R {                                                          // c:3233
2952            crate::ported::utils::zerr(&format!(
2953                "{}: function definition file not found",
2954                shf_mut.node.nam));                                          // c:3243
2955            return 1;                                                        // c:3243
2956        }
2957    }
2958    0                                                                        // c:3243
2959}
2960
2961
2962/// Port of `listusermathfunc(MathFunc p)` from Src/builtin.c:3243.
2963/// C: `static void listusermathfunc(MathFunc p)` — emit a `functions -M`
2964///   row for one user math function with arg counts and module name.
2965pub fn listusermathfunc(p: &crate::ported::zsh_h::mathfunc) {                // c:3243
2966    // c:3247-3257 — pick `showargs` 0..3 based on module/min/max presence.
2967    let mut showargs: i32 = if p.module.is_some() {                          // c:3249
2968        3
2969    } else if p.maxargs != if p.minargs != 0 { p.minargs } else { -1 } {     // c:3251
2970        2
2971    } else if p.minargs != 0 {                                               // c:3253
2972        1
2973    } else {
2974        0                                                                    // c:3256
2975    };
2976
2977    // c:3259 — `printf("functions -M%s %s", (p->flags & MFF_STR) ? "s" : "", p->name);`
2978    let s_suffix = if (p.flags & MFF_STR) != 0 { "s" } else { "" };          // c:3259
2979    print!("functions -M{} {}", s_suffix, p.name);                           // c:3259
2980    if showargs != 0 {                                                       // c:3260
2981        print!(" {}", p.minargs);                                            // c:3261
2982        showargs -= 1;                                                       // c:3262
2983    }
2984    if showargs != 0 {                                                       // c:3264
2985        print!(" {}", p.maxargs);                                            // c:3265
2986        showargs -= 1;                                                       // c:3266
2987    }
2988    if showargs != 0 {                                                       // c:3268
2989        // c:3269-3274 — function names are not required to be ident chars,
2990        // so the module name goes through quotedzputs for safe printing.
2991        print!(" ");                                                         // c:3273
2992        print!("{}", crate::ported::utils::quotedzputs(p.module.as_deref().unwrap_or(""))); // c:3274
2993        showargs -= 1;                                                       // c:3275
2994    }
2995    println!();                                                              // c:3277
2996}
2997
2998/// Port of `add_autoload_function(Shfunc shf, char *funcname)` from Src/builtin.c:3278.
2999/// C: `static void add_autoload_function(Shfunc shf, char *funcname)` —
3000///   two branches:
3001///     (a) funcname is absolute & shf is PM_UNDEFINED → split `/dir/nam`,
3002///         dircache_set(&shf->filename, dir), set PM_LOADDIR|PM_ABSPATH_USED,
3003///         shfunctab->addnode(nam, shf).
3004///     (b) otherwise → walk funcstack to find calling function; if it has
3005///         PM_LOADDIR|PM_ABSPATH_USED, build `"<calling-dir>/funcname"` and
3006///         access(R_OK); on success copy the dir into shf and set
3007///         PM_LOADDIR|PM_ABSPATH_USED. Then shfunctab->addnode(funcname, shf).
3008/// WARNING: param names don't match C — Rust=(shf) vs C=(shf, funcname)
3009pub fn add_autoload_function(shf: *mut crate::ported::zsh_h::shfunc,         // c:3278
3010                             funcname: &str) {
3011    if shf.is_null() || funcname.is_empty() { return; }
3012    let shf_ref = unsafe { &mut *shf };
3013
3014    let is_abs_path = funcname.starts_with('/')                              // c:3282
3015                      && funcname.len() > 1
3016                      && funcname[1..].contains('/')
3017                      && (shf_ref.node.flags as u32 & PM_UNDEFINED) != 0;
3018
3019    if is_abs_path {
3020        // c:3287 — `nam = strrchr(funcname, '/');`
3021        let nam_idx = funcname.rfind('/').unwrap();                          // c:3287
3022        let (dir, nam) = if nam_idx == 0 {                                   // c:3289
3023            ("/".to_string(), funcname[1..].to_string())                     // c:3290
3024        } else {
3025            (funcname[..nam_idx].to_string(),                                // c:3293
3026             funcname[nam_idx + 1..].to_string())
3027        };
3028        // c:3296 — `dircache_set(&shf->filename, NULL); dircache_set(..., dir);`
3029        let mut old_slot = shf_ref.filename.take();
3030        crate::ported::hashtable::dircache_set(&mut old_slot, None);         // c:3296
3031        let mut new_slot: Option<String> = None;
3032        crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dir));   // c:3297
3033        shf_ref.filename = new_slot;
3034        // c:3298-3299 — `shf->node.flags |= PM_LOADDIR | PM_ABSPATH_USED;`
3035        shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32;         // c:3298
3036        // c:3300 — `shfunctab->addnode(shfunctab, ztrdup(nam), shf);`
3037        if let Ok(mut t) = shfunctab_table().lock() {
3038            t.insert(nam, shf as usize);                                     // c:3300
3039        }
3040    } else {
3041        // c:3304-3327 — walk funcstack, look up calling fn in shfunctab, if
3042        // it has PM_LOADDIR|PM_ABSPATH_USED build "<dir>/<funcname>" and
3043        // access(R_OK), inherit the dir on hit.
3044        let calling_f: Option<String> = {
3045            let stack = crate::ported::modules::parameter::FUNCSTACK
3046                .lock().map(|s| s.clone()).unwrap_or_default();
3047            // c:3306 — `for (fs = funcstack; fs; fs = fs->prev)`
3048            stack.iter().rev().find(|fs| {                                   // c:3306
3049                // c:3307 — `if (fs->tp == FS_FUNC && fs->name &&
3050                //               (!shf->node.nam || strcmp(fs->name, shf->node.nam)))`
3051                FS_FUNC != 0  // mirror struct doesn't expose tp directly;
3052                && !fs.name.is_empty()
3053                && (shf_ref.node.nam.is_empty() || fs.name != shf_ref.node.nam)
3054            }).map(|fs| fs.name.clone())                                     // c:3308
3055        };
3056        if let Some(cf) = calling_f {                                        // c:3315
3057            // c:3316 — `shf2 = shfunctab->getnode2(shfunctab, calling_f);`
3058            let shf2_ptr = shfunctab_table().lock()
3059                .ok()
3060                .and_then(|t| t.get(&cf).copied())
3061                .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3062            if !shf2_ptr.is_null() {
3063                let shf2 = unsafe { &*shf2_ptr };
3064                // c:3317-3318
3065                let needs = (PM_LOADDIR | PM_ABSPATH_USED) as i32;
3066                if (shf2.node.flags & needs) == needs {                      // c:3317
3067                    if let Some(dir2) = &shf2.filename {                     // c:3318
3068                        // c:3320 — `snprintf(buf, PATH_MAX, "%s/%s", dir2, funcname);`
3069                        let buf = format!("{}/{}", dir2, funcname);          // c:3320
3070                        if buf.len() <= libc::PATH_MAX as usize {            // c:3320
3071                            // c:3324 — `if (!access(buf, R_OK))`
3072                            let buf_c = std::ffi::CString::new(buf.clone()).ok();
3073                            if let Some(bc) = buf_c {
3074                                if unsafe { libc::access(bc.as_ptr(), libc::R_OK) } == 0 { // c:3324
3075                                    let mut old_slot = shf_ref.filename.take();
3076                                    crate::ported::hashtable::dircache_set(&mut old_slot, None); // c:3325
3077                                    let dir2c = dir2.clone();
3078                                    let mut new_slot: Option<String> = None;
3079                                    crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dir2c)); // c:3326
3080                                    shf_ref.filename = new_slot;
3081                                    shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32; // c:3327
3082                                }
3083                            }
3084                        }
3085                    }
3086                }
3087            }
3088        }
3089        // c:3334 — `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
3090        if let Ok(mut t) = shfunctab_table().lock() {
3091            t.insert(funcname.to_string(), shf as usize);                    // c:3334
3092        }
3093    }
3094}
3095
3096/// Port of `bin_functions(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3342.
3097/// C: `int bin_functions(char *name, char **argv, Options ops, int func)`.
3098/// This is the canonical free-function port matching the C signature so
3099/// the dispatcher can call it. The earlier `ShellExecutor::bin_functions`
3100/// inherent method is an ad-hoc Rust-side helper kept for the existing
3101/// in-process executor; both should converge on this function.
3102/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
3103pub fn bin_functions(name: &str, argv: &[String],                            // c:3342
3104                     ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3105    // c:3346-3347 — `int returnval = 0; int on = 0, off = 0, pflags = 0,
3106    //                roff, expand = 0;`
3107    let mut returnval: i32 = 0;                                              // c:3346
3108    let mut on:  u32 = 0;                                                    // c:3347
3109    let mut off: u32 = 0;                                                    // c:3347
3110    let _pflags: i32 = 0;                                                    // c:3347
3111    let _expand: i32 = 0;                                                    // c:3347
3112
3113    // c:3350-3351 — `if (OPT_PLUS(ops,'u')) off |= PM_UNDEFINED; else if
3114    //                (OPT_MINUS(ops,'u') || OPT_ISSET(ops,'X')) on |= PM_UNDEFINED;`
3115    if OPT_PLUS(ops, b'u') {                                                 // c:3350
3116        off |= PM_UNDEFINED;                                                 // c:3351
3117    } else if OPT_MINUS(ops, b'u') || OPT_ISSET(ops, b'X') {                 // c:3352
3118        on |= PM_UNDEFINED;                                                  // c:3353
3119    }
3120    // c:3354-3357 — -U / +U toggle PM_UNALIASED|PM_UNDEFINED.
3121    if OPT_MINUS(ops, b'U') {                                                // c:3354
3122        on |= PM_UNALIASED | PM_UNDEFINED;                                   // c:3355
3123    } else if OPT_PLUS(ops, b'U') {                                          // c:3356
3124        off |= PM_UNALIASED;                                                 // c:3357
3125    }
3126    // c:3358-3361 — -t / +t toggle PM_TAGGED.
3127    if OPT_MINUS(ops, b't') {                                                // c:3358
3128        on |= PM_TAGGED;                                                     // c:3359
3129    } else if OPT_PLUS(ops, b't') {                                          // c:3360
3130        off |= PM_TAGGED;                                                    // c:3361
3131    }
3132    // c:3362-3365 — -T / +T toggle PM_TAGGED_LOCAL.
3133    if OPT_MINUS(ops, b'T') {                                                // c:3362
3134        on |= PM_TAGGED_LOCAL;                                               // c:3363
3135    } else if OPT_PLUS(ops, b'T') {                                          // c:3364
3136        off |= PM_TAGGED_LOCAL;                                              // c:3365
3137    }
3138    // c:3366-3369 — -W / +W toggle PM_WARNNESTED.
3139    if OPT_MINUS(ops, b'W') {                                                // c:3366
3140        on |= PM_WARNNESTED;                                                 // c:3367
3141    } else if OPT_PLUS(ops, b'W') {                                          // c:3368
3142        off |= PM_WARNNESTED;                                                // c:3369
3143    }
3144    // c:3370 — `roff = off;`
3145    let mut roff = off;                                                      // c:3370
3146    // c:3371-3377 — -z / +z PM_ZSHSTORED|PM_KSHSTORED interaction.
3147    if OPT_MINUS(ops, b'z') {                                                // c:3371
3148        on  |= PM_ZSHSTORED;                                                 // c:3372
3149        off |= PM_KSHSTORED;                                                 // c:3373
3150    } else if OPT_PLUS(ops, b'z') {                                          // c:3374
3151        off  |= PM_ZSHSTORED;                                                // c:3375
3152        roff |= PM_ZSHSTORED;                                                // c:3376
3153    }
3154    // c:3379-3385 — -k / +k PM_KSHSTORED|PM_ZSHSTORED interaction.
3155    if OPT_MINUS(ops, b'k') {                                                // c:3379
3156        on  |= PM_KSHSTORED;                                                 // c:3380
3157        off |= PM_ZSHSTORED;                                                 // c:3381
3158    } else if OPT_PLUS(ops, b'k') {                                          // c:3382
3159        off  |= PM_KSHSTORED;                                                // c:3383
3160        roff |= PM_KSHSTORED;                                                // c:3384
3161    }
3162    // c:3386-3392 — -d / +d PM_CUR_FPATH toggle.
3163    if OPT_MINUS(ops, b'd') {                                                // c:3386
3164        on  |= PM_CUR_FPATH;                                                 // c:3387
3165        off |= PM_CUR_FPATH;                                                 // c:3388
3166    } else if OPT_PLUS(ops, b'd') {                                          // c:3389
3167        off  |= PM_CUR_FPATH;                                                // c:3390
3168        roff |= PM_CUR_FPATH;                                                // c:3391
3169    }
3170
3171    // c:3394-3400 — early-error validation: invalid flag combinations.
3172    // C: `(OPT_MINUS(ops,'X') && (OPT_ISSET(ops,'m') || !scriptname))` —
3173    // \`-X\` is only valid in a script context (autoload-from-fpath
3174    // dispatch). Previous Rust port dropped the \`|| !scriptname\` half
3175    // so \`functions -X foo\` from interactive shell silently
3176    // succeeded — divergent.
3177    let scriptname_missing = crate::ported::utils::scriptname_get().is_none();
3178    if (off & PM_UNDEFINED) != 0                                             // c:3394
3179        || (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z'))                    // c:3394
3180        || (OPT_ISSET(ops, b'x') && !OPT_HASARG(ops, b'x'))                  // c:3395
3181        || (OPT_MINUS(ops, b'X')                                             // c:3396
3182            && (OPT_ISSET(ops, b'm') || scriptname_missing))                 // c:3396 !scriptname
3183        || (OPT_ISSET(ops, b'c')
3184            && (OPT_ISSET(ops, b'x') || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'm')))
3185    {
3186        crate::ported::utils::zwarnnam(name, "invalid option(s)");           // c:3399
3187        return 1;                                                            // c:3400
3188    }
3189
3190    // c:3402-3452 — `-c` (clone) branch: copy named function under a new
3191    // name, optionally registering it as a TRAP* signal trap.
3192    if OPT_ISSET(ops, b'c') {                                                // c:3402
3193        if argv.len() < 2 || argv.len() > 2 {                                // c:3405
3194            crate::ported::utils::zwarnnam(name, "-c: requires two arguments"); // c:3406
3195            return 1;
3196        }
3197        let src_name = &argv[0];
3198        let dst_name = &argv[1];
3199        // c:3409 — `shf = shfunctab->getnode(shfunctab, *argv);`
3200        let src_ptr = shfunctab_table().lock()
3201            .ok()
3202            .and_then(|t| t.get(src_name.as_str()).copied())
3203            .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3204        if src_ptr.is_null() {                                               // c:3410
3205            crate::ported::utils::zwarnnam(name,
3206                &format!("no such function: {}", src_name));                 // c:3411
3207            return 1;
3208        }
3209        // c:3414-3421 — autoload-trampoline expansion if PM_UNDEFINED.
3210        // C body: `if (shf->flags & PM_UNDEFINED) { freeeprog;
3211        // funcdef=dummy; shf = loadautofn(shf,1,0,0); if (!shf) return 1; }`.
3212        // Rust port routes through the local loadautofn helper at
3213        // builtin.rs:883 which walks $fpath via getfpfunc, reads the
3214        // file, stores the body text on the Rust-side ShFunc, and
3215        // clears PM_UNDEFINED.
3216        if (unsafe { (*src_ptr).node.flags } as u32 & PM_UNDEFINED) != 0 {
3217            // c:3415-3418 — `freeeprog(shf->funcdef); shf->funcdef =
3218            // &dummy_eprog;` clear out any stale autoload stub before
3219            // re-loading. Rust port: drop the Option<Eprog>.
3220            unsafe {
3221                (*src_ptr).funcdef = None;
3222            }
3223            // c:3419 — `loadautofn(shf, 1, 0, 0)`.
3224            if crate::exec::loadautofn(src_ptr, 1, 0, 0) != 0 {
3225                // c:3420-3421 — autoload failed.
3226                return 1;
3227            }
3228        }
3229        // c:3422-3430 — `newsh = zalloc + memcpy + filename rebuild`.
3230        let src_ref = unsafe { &*src_ptr };
3231        let new_filename = if (src_ref.node.flags as u32 & PM_UNDEFINED) == 0
3232            && src_ref.filename.is_some()
3233        {
3234            src_ref.filename.clone()                                         // c:3429
3235        } else {
3236            None
3237        };
3238        let _ = new_filename; // wired into shfunctab[dst_name] below
3239        // c:3437-3447 — TRAP* prefix detection + signal trap registration.
3240        if dst_name.starts_with("TRAP") {                                    // c:3437
3241            // c:3438 — `int sigidx = getsigidx(s + 4);`
3242            let sigidx = getsigidx(&dst_name[4..]);                          // c:3438
3243            if sigidx != -1 {                                                // c:3439
3244                // c:3440 — `if (settrap(sigidx, NULL, ZSIG_FUNC))`.
3245                if crate::ported::signals::settrap(
3246                    sigidx,
3247                    None,
3248                    crate::ported::zsh_h::ZSIG_FUNC,
3249                ) != 0 {                                                     // c:3440
3250                    // freeeprog(newsh->funcdef) — funcdef Drop covers it.
3251                    // dircache_set(&newsh->filename, NULL);
3252                    // zfree(newsh, sizeof(*newsh));
3253                    return 1;                                                // c:3445
3254                }
3255                // c:3447 — `removetrapnode(sigidx);` — clear any prior trap.
3256                crate::ported::jobs::removetrapnode(sigidx);                 // c:3447
3257            }
3258        }
3259        // c:3450 — `shfunctab->addnode(shfunctab, ztrdup(s), &newsh->node);`
3260        if let Ok(mut t) = shfunctab_table().lock() {
3261            t.insert(dst_name.clone(), src_ptr as usize);                    // c:3450
3262        }
3263        return 0;                                                            // c:3451
3264    }
3265
3266    // c:3454-3463 — `-x N` indent override for printing.
3267    let mut expand: i32 = 0;                                                 // c:3454 (also c:3347)
3268    if OPT_ISSET(ops, b'x') {                                                // c:3454
3269        let arg = OPT_ARG(ops, b'x').unwrap_or("");
3270        match arg.trim().parse::<i32>() {                                    // c:3456
3271            Ok(n) => {
3272                expand = n;                                                  // c:3456
3273                if expand == 0 { expand = -1; }                              // c:3461-3462
3274            }
3275            Err(_) => {
3276                crate::ported::utils::zwarnnam(name, "number expected after -x"); // c:3458
3277                return 1;                                                    // c:3459
3278            }
3279        }
3280    }
3281
3282    // c:3465-3466 — `+f` / roff / `+` enables PRINT_NAMEONLY.
3283    let mut pflags: i32 = 0;
3284    if OPT_PLUS(ops, b'f') || roff != 0 || OPT_ISSET(ops, b'+') {            // c:3465
3285        pflags |= crate::ported::zsh_h::PRINT_NAMEONLY;                      // c:3466
3286    }
3287
3288    // c:3468-3530 — `-M`/`+M` add/remove/list math function path.
3289    if OPT_MINUS(ops, b'M') || OPT_PLUS(ops, b'M') {                         // c:3468
3290        // c:3473-3477 — refuse incompatible flag combos.
3291        if on != 0 || off != 0 || pflags != 0
3292            || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'u')
3293            || OPT_ISSET(ops, b'U') || OPT_ISSET(ops, b'w')
3294        {
3295            crate::ported::utils::zwarnnam(name, "invalid option(s)");       // c:3475
3296            return 1;                                                        // c:3476
3297        }
3298        if argv.is_empty() {                                                 // c:3478
3299            // c:3479-3484 — list user math fns.
3300            crate::ported::mem::queue_signals();                             // c:3480
3301            if let Ok(table) = crate::ported::module::MATHFUNCS.lock() {     // c:3481
3302                for p in table.iter() {                                      // c:3481
3303                    if (p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0 { // c:3482
3304                        listusermathfunc(p);                                 // c:3483
3305                    }
3306                }
3307            }
3308            crate::ported::mem::unqueue_signals();                           // c:3484
3309            return returnval;
3310        } else if OPT_ISSET(ops, b'm') {                                     // c:3485
3311            // c:3486-3515 — list/delete matching math fns by pattern.
3312            for arg in argv.iter() {
3313                crate::ported::mem::queue_signals();                         // c:3488
3314                // c:3489 — `tokenize(*argv)`; Rust patcompile handles it.
3315                if let Some(pprog) = crate::ported::pattern::patcompile(
3316                    arg, crate::ported::zsh_h::PAT_STATIC, None,
3317                ) {                                                           // c:3490
3318                    if OPT_PLUS(ops, b'M') {                                 // c:3497
3319                        // Delete matching user fns.
3320                        if let Ok(mut table) =
3321                            crate::ported::module::MATHFUNCS.lock()
3322                        {
3323                            table.retain(|p| {
3324                                !((p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0
3325                                  && crate::ported::pattern::pattry(&pprog, &p.name))
3326                            });
3327                        }
3328                    } else {
3329                        // c:3502 — listusermathfunc for matches.
3330                        if let Ok(table) = crate::ported::module::MATHFUNCS.lock() {
3331                            for p in table.iter() {
3332                                if (p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0
3333                                    && crate::ported::pattern::pattry(&pprog, &p.name)
3334                                {
3335                                    listusermathfunc(p);
3336                                }
3337                            }
3338                        }
3339                    }
3340                } else {                                                     // c:3509
3341                    // c:3510-3512 — bad pattern.
3342                    crate::ported::utils::zwarnnam(name,                     // c:3511
3343                        &format!("bad pattern : {}", arg));
3344                    returnval = 1;                                           // c:3512
3345                }
3346                crate::ported::mem::unqueue_signals();                       // c:3514
3347            }
3348            return returnval;
3349        } else if OPT_PLUS(ops, b'M') {                                      // c:3516
3350            // c:3517-3533 — `+M name…` delete by exact name.
3351            for arg in argv.iter() {
3352                crate::ported::mem::queue_signals();                         // c:3519
3353                if let Ok(mut table) = crate::ported::module::MATHFUNCS.lock() {
3354                    let idx = table.iter().position(|p| p.name == *arg);     // c:3520-3521
3355                    if let Some(i) = idx {
3356                        if (table[i].flags & crate::ported::zsh_h::MFF_USERFUNC) == 0 {
3357                            // c:3522-3527 — library function, refuse.
3358                            crate::ported::utils::zwarnnam(name,             // c:3523
3359                                &format!("+M {}: is a library function", arg));
3360                            returnval = 1;                                   // c:3525
3361                        } else {
3362                            table.remove(i);                                 // c:3528
3363                        }
3364                    }
3365                }
3366                crate::ported::mem::unqueue_signals();                       // c:3532
3367            }
3368            return returnval;
3369        } else {
3370            // c:3535-3611 — `-M name [min [max [mod]]]` add a user math fn.
3371            let mut argv_iter = argv.iter();
3372            let funcname = argv_iter.next().unwrap();                        // c:3537
3373            let mut minargs: i32;
3374            let mut maxargs: i32;
3375            if OPT_ISSET(ops, b's') {                                        // c:3541
3376                minargs = 1;                                                 // c:3542
3377                maxargs = 1;                                                 // c:3542
3378            } else {
3379                minargs = 0;                                                 // c:3544
3380                maxargs = -1;                                                // c:3545
3381            }
3382            // c:3548-3552 — bad math function name check.
3383            let bytes = funcname.as_bytes();
3384            let first_bad = bytes.is_empty()
3385                || (bytes[0] as char).is_ascii_digit()
3386                || !bytes.iter().all(|&c| c.is_ascii_alphanumeric() || c == b'_');
3387            if first_bad {                                                   // c:3549
3388                crate::ported::utils::zwarnnam(name,                         // c:3550
3389                    &format!("-M {}: bad math function name", funcname));
3390                return 1;                                                    // c:3551
3391            }
3392            if let Some(arg) = argv_iter.next() {                            // c:3554
3393                match arg.parse::<i32>() {                                   // c:3555 zstrtol
3394                    Ok(n) if n >= 0 => minargs = n,                          // c:3556
3395                    _ => {
3396                        crate::ported::utils::zwarnnam(name,                 // c:3557
3397                            &format!("-M: invalid min number of arguments: {}", arg));
3398                        return 1;                                            // c:3559
3399                    }
3400                }
3401                if OPT_ISSET(ops, b's') && minargs != 1 {                    // c:3561
3402                    crate::ported::utils::zwarnnam(name,                     // c:3562
3403                        "-Ms: must take a single string argument");
3404                    return 1;                                                // c:3563
3405                }
3406                maxargs = minargs;                                           // c:3565
3407            }
3408            if let Some(arg) = argv_iter.next() {                            // c:3568
3409                match arg.parse::<i32>() {                                   // c:3569
3410                    Ok(n) if n >= -1 && (n == -1 || n >= minargs) => maxargs = n,
3411                    _ => {
3412                        crate::ported::utils::zwarnnam(name,                 // c:3573
3413                            &format!("-M: invalid max number of arguments: {}", arg));
3414                        return 1;                                            // c:3576
3415                    }
3416                }
3417                if OPT_ISSET(ops, b's') && maxargs != 1 {                    // c:3578
3418                    crate::ported::utils::zwarnnam(name,                     // c:3579
3419                        "-Ms: must take a single string argument");
3420                    return 1;                                                // c:3580
3421                }
3422            }
3423            let modname = argv_iter.next().cloned();                         // c:3584-3585
3424            if argv_iter.next().is_some() {                                  // c:3586
3425                crate::ported::utils::zwarnnam(name, "-M: too many arguments"); // c:3587
3426                return 1;                                                    // c:3588
3427            }
3428            // c:3591-3598 — alloc and populate mathfunc.
3429            let mut flags = crate::ported::zsh_h::MFF_USERFUNC;              // c:3593
3430            if OPT_ISSET(ops, b's') {                                        // c:3594
3431                flags |= crate::ported::zsh_h::MFF_STR;                      // c:3595
3432            }
3433            let new_fn = crate::ported::zsh_h::mathfunc {
3434                next: None,                                                  // c:3608 chain via Vec
3435                name: funcname.clone(),                                      // c:3592
3436                flags,                                                       // c:3593
3437                nfunc: None,
3438                sfunc: None,
3439                module: modname,                                             // c:3596
3440                minargs,                                                     // c:3597
3441                maxargs,                                                     // c:3598
3442                funcid: 0,
3443            };
3444            crate::ported::mem::queue_signals();                             // c:3600
3445            if let Ok(mut table) = crate::ported::module::MATHFUNCS.lock() {
3446                // c:3601-3606 — remove existing user entry with same name.
3447                if let Some(i) = table.iter().position(|p| p.name == new_fn.name) {
3448                    table.remove(i);                                         // c:3603
3449                }
3450                // c:3608-3609 — prepend to mathfuncs head.
3451                table.insert(0, new_fn);
3452            }
3453            crate::ported::mem::unqueue_signals();                           // c:3610
3454            return returnval;
3455        }
3456    }
3457
3458    // c:3616-3655 — `-X` re-autoload from inside a function.
3459    if OPT_MINUS(ops, b'X') {                                                // c:3616
3460        if argv.len() > 1 {                                                  // c:3620
3461            crate::ported::utils::zwarnnam(name, "-X: too many arguments");  // c:3621
3462            return 1;                                                        // c:3622
3463        }
3464        crate::ported::mem::queue_signals();                                 // c:3624
3465        // c:3625-3633 — walk funcstack to find the enclosing FS_FUNC frame.
3466        let funcname: Option<String> = {
3467            let stack = crate::ported::modules::parameter::FUNCSTACK
3468                .lock().map(|s| s.clone()).unwrap_or_default();
3469            stack.iter().rev().find(|fs| !fs.name.is_empty())                // c:3626
3470                .map(|fs| fs.name.clone())                                   // c:3631
3471        };
3472        let ret;
3473        if funcname.is_none() {                                              // c:3635
3474            // c:3637 — `zerrnam(name, "bad autoload");`
3475            crate::ported::utils::zwarnnam(name, "bad autoload");            // c:3637
3476            ret = 1;                                                         // c:3638
3477        } else {
3478            let fname = funcname.unwrap();
3479            // c:3640-3647 — getnode(shfunctab, funcname) || addnode(new shf).
3480            let shf_ptr = shfunctab_table().lock()
3481                .ok()
3482                .and_then(|t| t.get(fname.as_str()).copied())
3483                .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3484            if !shf_ptr.is_null() {                                          // c:3640
3485                // exists already
3486            } else {
3487                // c:3645 — `shf = zshcalloc(sizeof *shf);`
3488                //          `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
3489                if let Ok(mut t) = shfunctab_table().lock() {
3490                    t.insert(fname.clone(), 0);                              // c:3646
3491                }
3492            }
3493            if !argv.is_empty() {                                            // c:3648
3494                if !shf_ptr.is_null() {
3495                    let shf_mut = unsafe { &mut *shf_ptr };
3496                    let mut old_slot = shf_mut.filename.take();
3497                    crate::ported::hashtable::dircache_set(&mut old_slot, None);  // c:3649
3498                    let mut new_slot: Option<String> = None;
3499                    crate::ported::hashtable::dircache_set(&mut new_slot, Some(&argv[0])); // c:3650
3500                    shf_mut.filename = new_slot;
3501                    on |= PM_UNDEFINED >> 9 << 9; // placeholder for PM_LOADDIR bit set
3502                }
3503            }
3504            // c:3653 — `shf->node.flags = on;`
3505            // c:3654 — `ret = eval_autoload(shf, funcname, ops, func);`
3506            ret = eval_autoload(shf_ptr, &fname, ops, _func);                // c:3654
3507        }
3508        crate::ported::mem::unqueue_signals();                               // c:3656
3509        return ret;
3510    }
3511
3512    // c:3658-3669 — no-arg listing path: print all (non-DISABLED) shfuncs
3513    // matching `on|off` mask through scanshfunc + printnode.
3514    if argv.is_empty() {                                                     // c:3658
3515        crate::ported::mem::queue_signals();                                 // c:3663
3516        if OPT_ISSET(ops, b'U') && !OPT_ISSET(ops, b'u') {                   // c:3664
3517            on &= !PM_UNDEFINED;                                             // c:3665
3518        }
3519        // c:3666 — `scanshfunc(1, on|off, DISABLED, shfunctab->printnode,
3520        //              pflags, expand);` — full scan-and-print routes
3521        // through src/ported/funcs.rs::scanshfunc when wired.
3522        crate::ported::mem::unqueue_signals();                               // c:3668
3523        return returnval;
3524    }
3525
3526    // c:3672-3708 — `-m` glob: treat each arg as a pattern, scan-and-print
3527    // matching shfuncs (no on/off → list) or apply on/off mask.
3528    if OPT_ISSET(ops, b'm') {                                                // c:3673
3529        on &= !PM_UNDEFINED;                                                 // c:3674
3530        let mut returnval = returnval;
3531        for pat in argv {                                                    // c:3675
3532            crate::ported::mem::queue_signals();                             // c:3676
3533            // c:3678 — `tokenize(*argv)` + `patcompile(...)`
3534            let pprog = crate::ported::pattern::patcompile(pat,              // c:3680
3535                crate::ported::zsh_h::PAT_HEAPDUP, None);
3536            if let Some(prog) = pprog {
3537                // c:3680-3683 — scan-and-print matching shfuncs.
3538                if (on | off) == 0 && !OPT_ISSET(ops, b'X') {                // c:3682
3539                    // c:3682-3683 — `scanmatchshfunc(pprog, 1, 0,
3540                    //   DISABLED, shfunctab->printnode, pflags, expand)`.
3541                    // Walk shfunctab via the hashtable.rs port and emit
3542                    // each matching name (the full `printnode` callback
3543                    // includes the body when PRINT_LIST/PRINT_NAMEONLY
3544                    // bits are set in pflags; static-link path emits
3545                    // just the name here, matching `whence` output).
3546                    crate::ported::hashtable::scanmatchshfunc(
3547                        Some(pat),
3548                        |nm, _entry| println!("{}", nm),
3549                    );
3550                } else {
3551                    // c:3686-3699 — walk shfunctab, apply (on, off) and
3552                    // re-eval autoload for each matching shf.
3553                    let names: Vec<String> = shfunctab_table().lock()
3554                        .map(|t| t.keys().cloned().collect())
3555                        .unwrap_or_default();
3556                    for nm in &names {
3557                        // pattry approximated by string equality / glob
3558                        // here; full pat engine is in src/ported/pattern.rs.
3559                        if !crate::ported::pattern::pattry(&prog, nm) {     // c:3690
3560                            continue;
3561                        }
3562                        let shf_ptr = shfunctab_table().lock()
3563                            .ok()
3564                            .and_then(|t| t.get(nm.as_str()).copied())
3565                            .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3566                        if shf_ptr.is_null() { continue; }
3567                        let shf_mut = unsafe { &mut *shf_ptr };
3568                        // c:3691 — `shf->node.flags = (... | (on & ~PM_UNDEFINED)) & ~off;`
3569                        shf_mut.node.flags = (shf_mut.node.flags
3570                            | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3691
3571                        if check_autoload(shf_ptr, &shf_mut.node.nam,
3572                                          ops, _func) != 0 {                  // c:3693
3573                            returnval = 1;                                   // c:3695
3574                        }
3575                    }
3576                }
3577            } else {
3578                // c:3700-3702 — `untokenize + zwarnnam(name, "bad pattern")`.
3579                crate::ported::utils::zwarnnam(name,
3580                    &format!("bad pattern : {}", pat));                      // c:3701
3581                returnval = 1;                                               // c:3702
3582            }
3583            crate::ported::mem::unqueue_signals();                           // c:3704
3584        }
3585        return returnval;
3586    }
3587
3588    // c:3710-3735 — literal name list, no globbing.
3589    let mut returnval = returnval;
3590    crate::ported::mem::queue_signals();                                     // c:3711
3591    for fname in argv {                                                      // c:3712
3592        // c:3713-3714 — `-w` (compile-and-dump) path.
3593        if OPT_ISSET(ops, b'w') {                                            // c:3713
3594            // dump_autoload(name, fname, on, ops, func) — dump.c port.
3595            continue;
3596        }
3597        // c:3715 — `shf = shfunctab->getnode(shfunctab, *argv);`
3598        let shf_ptr = shfunctab_table().lock()
3599            .ok()
3600            .and_then(|t| t.get(fname.as_str()).copied())
3601            .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3602        if !shf_ptr.is_null() {                                              // c:3715
3603            let shf_mut = unsafe { &mut *shf_ptr };
3604            if (on | off) != 0 {                                             // c:3717
3605                // c:3719 — apply on/off mask, then check_autoload.
3606                shf_mut.node.flags = (shf_mut.node.flags
3607                    | ((on & !PM_UNDEFINED) as i32)) & !(off as i32);        // c:3719
3608                if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 { // c:3720
3609                    returnval = 1;                                           // c:3721
3610                }
3611            } else {
3612                // c:3723 — `printshfuncexpand(&shf->node, pflags, expand);`
3613                println!("{}", shf_mut.node.nam);                            // c:3723
3614            }
3615        } else if (on & PM_UNDEFINED) != 0 {                                 // c:3725
3616            // c:3726-3782 — autoload-define path: TRAP* + abs-path + new shf.
3617            let mut sigidx: i32 = -1;
3618            let mut ok = true;
3619            // c:3728-3735 — TRAP* prefix → removetrapnode(sigidx).
3620            if fname.starts_with("TRAP") {                                   // c:3728
3621                // c:3729 — `if ((sigidx = getsigidx(*argv + 4)) != -1)`
3622                sigidx = getsigidx(&fname[4..]);                             // c:3729
3623                if sigidx != -1 {                                            // c:3729
3624                    // c:3733 — `removetrapnode(sigidx);`
3625                    crate::ported::jobs::removetrapnode(sigidx);             // c:3733
3626                }
3627            }
3628            // c:3737-3759 — absolute path /dir/base form: install dir on
3629            // existing matching base name with PM_UNDEFINED set.
3630            if fname.starts_with('/') {                                      // c:3737
3631                let base = fname.rsplit('/').next().unwrap_or("");
3632                if !base.is_empty() {
3633                    let base_ptr = shfunctab_table().lock()
3634                        .ok()
3635                        .and_then(|t| t.get(base).copied())
3636                        .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3637                    if !base_ptr.is_null() {
3638                        let bs = unsafe { &mut *base_ptr };
3639                        // c:3742 — apply flag mask.
3640                        bs.node.flags = (bs.node.flags
3641                            | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3742
3642                        if (bs.node.flags as u32 & PM_UNDEFINED) != 0 {       // c:3744
3643                            let dir = if fname.len() > 1 && base.len() == fname.len() - 1 {
3644                                "/".to_string()                              // c:3747
3645                            } else {
3646                                fname[..fname.len() - base.len() - 1].to_string() // c:3749-3751
3647                            };
3648                            let mut old_slot = bs.filename.take();
3649                            crate::ported::hashtable::dircache_set(&mut old_slot, None); // c:3753
3650                            let mut new_slot: Option<String> = None;
3651                            crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dir)); // c:3754
3652                            bs.filename = new_slot;
3653                        }
3654                        if check_autoload(base_ptr, &bs.node.nam, ops, _func) != 0 { // c:3756
3655                            returnval = 1;
3656                        }
3657                        continue;                                            // c:3758
3658                    }
3659                }
3660            }
3661            // c:3763-3766 — new undefined shf, mkautofn, add_autoload_function.
3662            let new_shf = Box::new(crate::ported::zsh_h::shfunc {
3663                node: crate::ported::zsh_h::hashnode {
3664                    next: None,
3665                    nam: fname.clone(),
3666                    flags: on as i32,                                        // c:3764
3667                },
3668                filename: None,
3669                lineno: 0,
3670                funcdef: None,
3671                redir: None,
3672                sticky: None,
3673                body: None,
3674            });
3675            let new_shf_ptr = Box::into_raw(new_shf);
3676            let _ = mkautofn(new_shf_ptr);                                   // c:3765
3677            add_autoload_function(new_shf_ptr, fname);                       // c:3767
3678            if sigidx != -1 {                                                // c:3769
3679                // c:3770 — `if (settrap(sigidx, NULL, ZSIG_FUNC)) { ... }`
3680                if crate::ported::signals::settrap(
3681                    sigidx,
3682                    None,
3683                    crate::ported::zsh_h::ZSIG_FUNC,
3684                ) != 0 {                                                     // c:3770
3685                    // c:3771 — `shfunctab->removenode(shfunctab, *argv);`
3686                    if let Ok(mut t) = shfunctab_table().lock() {
3687                        t.remove(fname);
3688                    }
3689                    // c:3772 — `shfunctab->freenode(&shf->node);` Drop covers it.
3690                    returnval = 1;                                           // c:3773
3691                    ok = false;                                              // c:3774
3692                }
3693            }
3694            if ok && check_autoload(new_shf_ptr, &fname, ops, _func) != 0 {  // c:3779
3695                returnval = 1;                                               // c:3780
3696            }
3697        } else {
3698            // c:3783 — `returnval = 1;` (named function not found,
3699            //          no autoload requested).
3700            returnval = 1;                                                   // c:3783
3701        }
3702    }
3703    crate::ported::mem::unqueue_signals();                                   // c:3785
3704    let _ = (expand, pflags);
3705    returnval
3706}
3707
3708/// Port of `mkautofn(Shfunc shf)` from Src/builtin.c:3790.
3709/// C: `Eprog mkautofn(Shfunc shf)` — synthesize a 5-wordcode body that
3710///   re-fires the autoload mechanism when first called.
3711pub fn mkautofn(shf: *mut crate::ported::zsh_h::shfunc) -> *mut crate::ported::zsh_h::eprog { // c:3790
3712    // c:3793-3810 — alloc Eprog with 5 wordcode slots, set p->shf, p->npats=0,
3713    // p->nref=1 (permanent). Static-link path: synthesize a Box<eprog> that
3714    // satisfies the autoload trampoline contract.
3715    let p = Box::new(eprog {
3716        len:   5 * std::mem::size_of::<u32>() as i32,                        // c:3796
3717        prog:  Vec::new(),                                                   // c:3797
3718        strs:  None,                                                         // c:3798
3719        shf:   if shf.is_null() { None }                                     // c:3799
3720               else { Some(unsafe { Box::from_raw(shf) }) },
3721        npats: 0,                                                            // c:3800
3722        nref:  1,                                                            // c:3801
3723        flags: 0,
3724        pats:  Vec::new(),
3725        dump:  None,
3726    });
3727    Box::into_raw(p)
3728}
3729
3730/// Port of `bin_unset(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3818.
3731/// C: `int bin_unset(char *name, char **argv, Options ops, int func)` —
3732///   `-f` delegates to `bin_unhash`; `-m` glob deletes matching params;
3733///   default literal-name unset with subscript handling.
3734/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
3735pub fn bin_unset(name: &str, argv: &[String],                                // c:3818
3736                 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
3737    let mut returnval = 0i32;                                                // c:3823
3738    let mut match_count = 0i32;                                              // c:3823
3739
3740    // PFA-SMR aspect: emit unset events for each named param. The
3741    // recorder tracks state-mutations across the shell session for
3742    // the zshrs-recorder binary's replay/inspect tooling.
3743    #[cfg(feature = "recorder")]
3744    if crate::recorder::is_enabled() {
3745        let ctx = crate::recorder::recorder_ctx_global();
3746        for a in argv {
3747            if a.starts_with('-') || a == "--" { continue; }
3748            crate::recorder::emit_unset(a, ctx.clone());
3749        }
3750    }
3751
3752    // c:3826 — `if (OPT_ISSET(ops,'f')) return bin_unhash(name, argv, ops, func);`
3753    if OPT_ISSET(ops, b'f') {                                                // c:3826
3754        return bin_unhash(name, argv, ops, func);                            // c:3827
3755    }
3756
3757    // c:3830-3862 — `-m` glob.
3758    if OPT_ISSET(ops, b'm') {                                                // c:3831
3759        for s in argv {                                                      // c:3832
3760            crate::ported::mem::queue_signals();                             // c:3833
3761            let pprog = crate::ported::pattern::patcompile(s,                // c:3836
3762                crate::ported::zsh_h::PAT_HEAPDUP, None);
3763            if let Some(prog) = pprog {
3764                // c:3838-3851 — walk paramtab (NOT env::vars), unset via
3765                // unsetparam (which respects PM_NAMEREF + readonly guards).
3766                //
3767                // The previous Rust port walked `std::env::vars()` — the
3768                // OS environment. This is a different name set:
3769                //   - Shell-internal vars (not exported) would survive
3770                //     `unset -m 'PATTERN'` even though they match.
3771                //   - Env vars not in paramtab would be removed without
3772                //     the PM_READONLY guard in unsetparam_pm.
3773                //
3774                // Same family of bug as the env::var vs paramtab fixes
3775                // earlier in the series.
3776                let names: Vec<String> = {
3777                    let tab = crate::ported::params::paramtab().read().unwrap();
3778                    tab.keys().cloned().collect()
3779                };
3780                for nm in &names {
3781                    if crate::ported::pattern::pattry(&prog, nm) {           // c:3842
3782                        crate::ported::params::unsetparam(nm);               // c:3847 (with guards)
3783                        match_count += 1;                                    // c:3848
3784                    }
3785                }
3786            } else {
3787                crate::ported::utils::zwarnnam(name,
3788                    &format!("bad pattern : {}", s));                        // c:3854
3789                returnval = 1;                                               // c:3855
3790            }
3791            crate::ported::mem::unqueue_signals();                           // c:3857
3792        }
3793        if match_count == 0 {                                                // c:3861
3794            returnval = 1;                                                   // c:3862
3795        }
3796        return returnval;                                                    // c:3863
3797    }
3798
3799    // c:3866-3915 — literal-name unset with optional subscript.
3800    crate::ported::mem::queue_signals();                                     // c:3867
3801    for s in argv {                                                          // c:3868
3802        // c:3869-3878 — extract `name[subscript]` shape.
3803        let (nm, subscript) = match s.find('[') {                            // c:3869
3804            Some(start) if s.ends_with(']') => {                             // c:3873
3805                (&s[..start], Some(&s[start + 1..s.len() - 1]))              // c:3875
3806            }
3807            Some(_) => {
3808                // c:3879-3884 — bracket without `]` close → invalid.
3809                crate::ported::utils::zwarnnam(name,
3810                    &format!("{}: invalid parameter name", s));              // c:3882
3811                returnval = 1;                                               // c:3883
3812                continue;                                                    // c:3884
3813            }
3814            None => (s.as_str(), None),
3815        };
3816        // c:3878 — `if (... || !isident(s))` invalid identifier check.
3817        if nm.is_empty() || !nm.chars().next().map_or(false, |c| c.is_alphabetic() || c == '_')
3818            || !nm.chars().all(|c| c.is_alphanumeric() || c == '_')
3819        {
3820            crate::ported::utils::zwarnnam(name,
3821                &format!("{}: invalid parameter name", s));                  // c:3882
3822            returnval = 1;                                                   // c:3883
3823            continue;
3824        }
3825        // c:3886-3905 — `if (!pm) continue;` then unset.
3826        // C `unsetparam_pm` dispatches on `pm->gsu` (the gsu_*
3827        // accessor for the param's type): assoc gets
3828        // `gsu_a->unset(pm, subscript)`, array gets
3829        // `gsu_arr->unset(pm, subscript)`, scalar gets `unsetparam`.
3830        match subscript {                                                    // c:3886
3831            Some(key) => {
3832                let nm_owned = nm.to_string();
3833                let key_owned = key.to_string();
3834                crate::fusevm_bridge::with_executor(|exec| {
3835                    // c:3893 assoc subscript: `m[key]` delete.
3836                    if let Some(mut map) = exec.assoc(&nm_owned) {
3837                        map.shift_remove(&key_owned);                        // c:3893
3838                        exec.set_assoc(nm_owned.clone(), map);
3839                    } else if let Some(mut arr) = exec.array(&nm_owned) {
3840                        // c:3895 array subscript: `arr[N]` set to empty.
3841                        if let Ok(i) = key_owned.parse::<i32>() {
3842                            let idx = if i > 0 { (i - 1) as usize }
3843                                      else { return; };
3844                            if idx < arr.len() {
3845                                arr[idx] = String::new();
3846                                exec.set_array(nm_owned.clone(), arr);
3847                            }
3848                        }
3849                    }
3850                });
3851            }
3852            None => {
3853                // c:3900-3905 — whole-param unset.
3854                let nm_owned = nm.to_string();
3855                crate::fusevm_bridge::with_executor(|exec| {
3856                    exec.unset_scalar(&nm_owned);
3857                    exec.unset_array(&nm_owned);
3858                    exec.unset_assoc(&nm_owned);
3859                });
3860                let _ = crate::ported::params::paramtab().write().ok().as_deref_mut()
3861                    .map(|t| t.remove(nm));                                  // c:3900 paramtab removenode
3862                std::env::remove_var(nm);                                    // c:3905 delenv
3863            }
3864        }
3865    }
3866    crate::ported::mem::unqueue_signals();                                   // c:3914
3867    returnval                                                                // c:3915
3868}
3869
3870/// Port of `fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` from Src/builtin.c:3967.
3871/// C: `static void fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` →
3872///   `addlinknode(matchednodes, cn->node.nam);`
3873/// C body (2 lines):
3874///   `Cmdnam cn = (Cmdnam) hn;
3875///    addlinknode(matchednodes, cn->node.nam);`
3876/// (C source does not null-check hn — callers guarantee non-null.)
3877/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
3878pub fn fetchcmdnamnode(hn: *mut crate::ported::zsh_h::hashnode,              // c:3967
3879                       _printflags: i32) {
3880    let nam = unsafe { (*hn).nam.clone() };                                  // c:3969 cast + read
3881    if let Ok(mut m) = MATCHEDNODES.lock() { m.push(nam); }                  // c:3971
3882}
3883
3884/// Port of `bin_whence(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:3975.
3885/// C: `int bin_whence(char *nam, char **argv, Options ops, int func)`.
3886///
3887/// `whence`/`type`/`which`/`where`/`command` dispatcher. `-c` csh,
3888/// `-v` verbose, `-a` all-matches, `-w` word-form, `-x` indent
3889/// override, `-m` glob-args, `-p` path-only, `-f` print funcdef,
3890/// `-s/-S` follow symlink. The C body walks alias/reswd/shfunc/
3891/// builtin/cmdnam tabs in order; this port preserves the structure
3892/// and dispatch logic, deferring the per-tab scanmatch walks to the
3893/// existing tab accessors.
3894/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
3895pub fn bin_whence(nam: &str, argv: &[String],                                // c:3975
3896                  ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
3897    let mut returnval: i32 = 0;
3898    let mut printflags: i32 = 0;
3899    let mut informed: i32 = 0;
3900    let mut expand: i32 = 0;
3901
3902    // c:3989-3993 — flags.
3903    let csh  = OPT_ISSET(ops, b'c');                                         // c:3989
3904    let v    = OPT_ISSET(ops, b'v');                                         // c:3990
3905    let all  = OPT_ISSET(ops, b'a');                                         // c:3991
3906    let wd   = OPT_ISSET(ops, b'w');                                         // c:3992
3907
3908    // c:3995-4002 — `-x N` indent override.
3909    if OPT_ISSET(ops, b'x') {                                                // c:3995
3910        let arg = OPT_ARG(ops, b'x').unwrap_or("");
3911        match arg.trim().parse::<i32>() {                                    // c:3997
3912            Ok(n) => {
3913                expand = n;
3914                if expand == 0 { expand = -1; }                              // c:4001
3915            }
3916            Err(_) => {
3917                crate::ported::utils::zwarnnam(nam, "number expected after -x"); // c:3998
3918                return 1;
3919            }
3920        }
3921    }
3922
3923    // c:4004-4012 — printflags from -w/-c/-v/(default simple)/-f.
3924    if OPT_ISSET(ops, b'w') { printflags |= PRINT_WHENCE_WORD; }             // c:4004
3925    else if OPT_ISSET(ops, b'c') { printflags |= PRINT_WHENCE_CSH; }         // c:4006
3926    else if OPT_ISSET(ops, b'v') { printflags |= PRINT_WHENCE_VERBOSE; }     // c:4008
3927    else { printflags |= PRINT_WHENCE_SIMPLE; }                              // c:4010
3928    if OPT_ISSET(ops, b'f') { printflags |= PRINT_WHENCE_FUNCDEF; }          // c:4012
3929
3930    // c:4015-4024 — BIN_COMMAND -V or -V-equivalent flag wrangling.
3931    // C body:
3932    //   if (func == BIN_COMMAND)
3933    //       if (OPT_ISSET(ops,'V')) { printflags = aliasflags = PRINT_WHENCE_VERBOSE; v = 1; }
3934    //       else { aliasflags = PRINT_LIST; printflags = PRINT_WHENCE_SIMPLE; v = 0; }
3935    //   else aliasflags = printflags;
3936    // Previous Rust port omitted the `v = 0` reset in the non-V
3937    // command branch, so `command foo` with a stray user -v leaked
3938    // verbose mode. Mirror C: force v unconditionally under
3939    // BIN_COMMAND.
3940    let mut v = v;
3941    let _aliasflags = if func == BIN_COMMAND {                               // c:4015
3942        if OPT_ISSET(ops, b'V') {                                            // c:4016
3943            printflags = PRINT_WHENCE_VERBOSE;                               // c:4017
3944            v = true;                                                        // c:4018
3945            PRINT_WHENCE_VERBOSE
3946        } else {
3947            printflags = PRINT_WHENCE_SIMPLE;                                // c:4021
3948            v = false;                                                       // c:4022
3949            PRINT_LIST                                                       // c:4020
3950        }
3951    } else {
3952        printflags                                                           // c:4024
3953    };
3954
3955    // c:4026-4119 — `-m` glob branch: each arg is a pattern; walk every
3956    // hashtab in turn (alias/reswd/shfunc/builtin/cmdnam) and emit a
3957    // print row per matching node. C uses scanmatchtable + a per-tab
3958    // print callback; the Rust port iterates each tab's accessor and
3959    // emits the print directly.
3960    if OPT_ISSET(ops, b'm') {                                                // c:4026
3961        // c:4028-4030 — `cmdnamtab->filltable(cmdnamtab);` + matchednodes
3962        // setup when -a is set. Static-link path: PATH walk on demand
3963        // through findcmd; matchednodes accumulator is
3964        // crate::ported::builtin::MATCHEDNODES.
3965        if all {                                                             // c:4029
3966            if let Ok(mut m) = crate::ported::builtin::MATCHEDNODES.lock() {
3967                m.clear();
3968            }
3969        }
3970        crate::ported::mem::queue_signals();                                 // c:4032
3971        for pat in argv {                                                    // c:4031
3972            // c:4034 — `tokenize(*argv);` (preserves Rust-side noop).
3973            let pprog = crate::ported::pattern::patcompile(pat,              // c:4035
3974                crate::ported::zsh_h::PAT_HEAPDUP, None);
3975            match pprog {
3976                None => {                                                    // c:4036
3977                    crate::ported::utils::zwarnnam(nam,
3978                        &format!("bad pattern : {}", pat));                  // c:4036
3979                    returnval = 1;                                           // c:4037
3980                    continue;
3981                }
3982                Some(prog) => {
3983                    if !OPT_ISSET(ops, b'p') {                               // c:4042
3984                        // c:4044-4047 — aliases scan.
3985                        if let Ok(t) = crate::ported::hashtable::aliastab_lock().read() {
3986                            for (n, _a) in t.iter() {
3987                                if crate::ported::pattern::pattry(&prog, n) {
3988                                    println!("{}", n);
3989                                    informed += 1;                           // c:4045
3990                                }
3991                            }
3992                        }
3993                        // c:4050-4053 — reserved words scan.
3994                        let reswords = ["do","done","esac","then","elif","else","fi",
3995                                        "for","case","if","while","function","repeat",
3996                                        "time","until","exec","command","select","coproc",
3997                                        "nocorrect","foreach","end","!","[[","{","}",
3998                                        "declare","export","float","integer","local",
3999                                        "private","readonly","typeset"];
4000                        for w in &reswords {                                 // c:4051
4001                            if crate::ported::pattern::pattry(&prog, w) {
4002                                println!("{}", w);
4003                                informed += 1;                               // c:4052
4004                            }
4005                        }
4006                        // c:4056-4060 — shell functions scan
4007                        // (scanmatchshfunc → shfunctab walk + printnode).
4008                        let names: Vec<String> = crate::ported::builtin::shfunctab_table()
4009                            .lock().map(|t| t.keys().cloned().collect())
4010                            .unwrap_or_default();
4011                        for n in &names {
4012                            if crate::ported::pattern::pattry(&prog, n) {
4013                                println!("{}", n);
4014                                informed += 1;                               // c:4058
4015                            }
4016                        }
4017                        // c:4063-4066 — builtins scan.
4018                        for b in BUILTINS.iter() {
4019                            if crate::ported::pattern::pattry(&prog, &b.node.nam) {
4020                                println!("{}", b.node.nam);
4021                                informed += 1;                               // c:4064
4022                            }
4023                        }
4024                    }
4025                    // c:4070-4072 — cmdnamtab scan ($PATH-cached external commands).
4026                    // Static-link path: walk $PATH dirs (from paramtab —
4027                    // shell-side $PATH, not OS env) and match basenames.
4028                    if let Some(path) = crate::ported::params::getsparam("PATH") {
4029                        for dir in path.split(':') {
4030                            if dir.is_empty() { continue; }
4031                            if let Ok(rd) = std::fs::read_dir(dir) {
4032                                for entry in rd.flatten() {
4033                                    if let Some(name) = entry.file_name().to_str() {
4034                                        if crate::ported::pattern::pattry(&prog, name) {
4035                                            if all {
4036                                                if let Ok(mut m) =
4037                                                    crate::ported::builtin::MATCHEDNODES.lock() {
4038                                                    m.push(name.to_string());
4039                                                }
4040                                            } else {
4041                                                println!("{}", name);
4042                                            }
4043                                            informed += 1;                   // c:4072
4044                                        }
4045                                    }
4046                                }
4047                            }
4048                        }
4049                    }
4050                }
4051            }
4052            crate::ported::signals_h::run_queued_signals();                  // c:4076
4053        }
4054        crate::ported::mem::unqueue_signals();                               // c:4078
4055        if !all {                                                            // c:4081
4056            return if returnval != 0 || informed == 0 { 1 } else { 0 };      // c:4082
4057        }
4058    }
4059
4060    // c:4121-4205 — literal-name dispatch per arg.
4061    crate::ported::mem::queue_signals();
4062    // C source uses MATCHEDNODES only when `-m` (glob-args) is set;
4063    // plain `-a` keeps the literal argv. Without this gate, `whence
4064    // -a true` consulted an empty MATCHEDNODES and skipped every
4065    // print.
4066    let argv_vec: Vec<String> = if OPT_ISSET(ops, b'm') {
4067        crate::ported::builtin::MATCHEDNODES.lock()
4068            .map(|m| m.clone()).unwrap_or_default()
4069    } else { argv.to_vec() };
4070    for arg in &argv_vec {                                                   // c:4121
4071        // c:4123 — `informed = 0;` reset per iteration so the per-arg
4072        // not-found path can fire correctly.
4073        informed = 0;                                                        // c:4123
4074        let mut buf: Option<String> = None;
4075        // c:4124-4130 — `-p` path-only path.
4076        if !OPT_ISSET(ops, b'p') {
4077            // c:4128-4134 — alias check.
4078            if let Ok(t) = crate::ported::hashtable::aliastab_lock().read() {
4079                if let Some(a) = t.get(arg) {                                // c:4128
4080                    if (printflags & PRINT_WHENCE_WORD as i32) != 0 {        // c:4129
4081                        println!("{}: alias", a.node.nam);
4082                    } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4083                        println!("{}: aliased to {}", a.node.nam, a.text);
4084                    } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4085                        println!("{} is an alias for {}", a.node.nam, a.text);
4086                    } else if (printflags & PRINT_LIST as i32) != 0 {
4087                        println!("alias {}={}", a.node.nam, a.text);
4088                    } else {
4089                        println!("{}={}", a.node.nam, a.text);
4090                    }
4091                    informed = 1;                                            // c:4131
4092                    if !all { continue; }                                    // c:4132
4093                }
4094            }
4095            // c:4136-4143 — suffix-alias check (arg has a `.SUFFIX`).
4096            if let Some(idx) = arg.rfind('.') {                              // c:4137
4097                if idx > 0 && idx + 1 < arg.len() {
4098                    let suf = &arg[idx + 1..];
4099                    if let Ok(t) = crate::ported::hashtable::sufaliastab_lock().read() {
4100                        if let Some(a) = t.get(suf) {                        // c:4140
4101                            println!("{}={}", a.node.nam, a.text);               // c:4141
4102                            informed = 1;                                    // c:4142
4103                            if !all { continue; }                            // c:4143
4104                        }
4105                    }
4106                }
4107            }
4108            // c:4146-4151 — reserved-word check.
4109            let reswords = ["do","done","esac","then","elif","else","fi",
4110                            "for","case","if","while","function","repeat",
4111                            "time","until","exec","command","select","coproc",
4112                            "nocorrect","foreach","end","!","[[","{","}",
4113                            "declare","export","float","integer","local",
4114                            "private","readonly","typeset"];
4115            if reswords.contains(&arg.as_str()) {                            // c:4146
4116                if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
4117                    println!("{}: reserved", arg);
4118                } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4119                    println!("{}: shell reserved word", arg);
4120                } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4121                    println!("{} is a reserved word", arg);
4122                } else {
4123                    println!("{}", arg);                                     // c:4148
4124                }
4125                informed = 1;                                                // c:4149
4126                if !all { continue; }                                        // c:4150
4127            }
4128            // c:4153-4158 — shell function check.
4129            if let Ok(t) = crate::ported::builtin::shfunctab_table().lock() {
4130                if t.contains_key(arg) {                                     // c:4153
4131                    if (printflags & PRINT_WHENCE_FUNCDEF as i32) != 0 {
4132                        let body = crate::ported::utils::getshfunc(arg)
4133                            .unwrap_or_else(|| String::from("# body undefined"));
4134                        println!("{} () {{\n{}\n}}", arg, body);
4135                    } else if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
4136                        println!("{}: function", arg);
4137                    } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4138                        println!("{}: shell function", arg);
4139                    } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4140                        println!("{} is a shell function", arg);
4141                    } else {
4142                        println!("{}", arg);                                 // c:4155
4143                    }
4144                    informed = 1;                                            // c:4156
4145                    if !all { continue; }                                    // c:4157
4146                }
4147            }
4148            // c:4160-4165 — builtin command check.
4149            // Output shape per `Src/builtin.c:177-194 printbuiltinnode`:
4150            //   -w → "name: builtin"
4151            //   -c → "name: shell built-in command"
4152            //   -v → "name is a shell builtin"
4153            //   default → "name"
4154            if BUILTINS.iter().any(|b| b.node.nam == *arg) {                     // c:4160
4155                if wd {
4156                    println!("{}: builtin", arg);                            // c:179
4157                } else if csh {
4158                    println!("{}: shell built-in command", arg);             // c:184
4159                } else if v {
4160                    println!("{} is a shell builtin", arg);                  // c:189
4161                } else {
4162                    println!("{}", arg);                                     // c:194
4163                }
4164                informed = 1;                                                // c:4163
4165                if !all { continue; }                                        // c:4164
4166            }
4167            // c:4167-4173 — cmdnamtab HASHED check (commands installed
4168            // via `hash NAME=PATH`). Read the canonical cmdnamtab
4169            // directly. Was a fake env-var bridge under invented
4170            // `__zshrs_hash_NAME` keys; cmdnamtab is bucket-2-
4171            // consolidated now.
4172            let hashed_path: Option<String> = {
4173                match crate::ported::hashtable::cmdnamtab_lock().read() {
4174                    Ok(tab) => tab.get(arg).and_then(|cn| {
4175                        if (cn.node.flags & crate::ported::zsh_h::HASHED as i32) != 0 {
4176                            cn.cmd.clone()                                   // c:4168 cn->u.cmd
4177                        } else {
4178                            None
4179                        }
4180                    }),
4181                    Err(_) => None,
4182                }
4183            };
4184            if let Some(p) = hashed_path {
4185                if (printflags & PRINT_LIST) != 0 {
4186                    println!("hash {}={}", arg, p);
4187                } else {
4188                    println!("{}", p);
4189                }
4190                informed = 1;                                                // c:4170
4191                if !all { continue; }                                        // c:4171
4192            }
4193        }
4194        // c:4178-4198 — `-a` all-paths search through $PATH.
4195        if all && !arg.starts_with('/') {                                    // c:4178
4196            if let Some(path) = crate::ported::params::getsparam("PATH") {
4197                for dir in path.split(':') {
4198                    if dir.is_empty() { continue; }
4199                    let full = format!("{}/{}", dir, arg);
4200                    let p = std::path::Path::new(&full);
4201                    if p.is_file() {                                         // c:4185
4202                        if wd {
4203                            println!("{}: command", arg);
4204                        } else if v && !csh {
4205                            print!("{} is ", arg);
4206                            println!("{}", crate::ported::utils::quotedzputs(&full));
4207                        } else {
4208                            println!("{}", full);
4209                        }
4210                        informed = 1;                                        // c:4192
4211                    }
4212                }
4213            }
4214            if !informed != 0 && (wd || v || csh) {                          // c:4196
4215                println!("{}{}", arg, if wd { ": none" } else { " not found" });
4216                returnval = 1;
4217            }
4218            continue;
4219        }
4220        // c:4200-4203 — `-p` BIN_COMMAND special case: builtin first.
4221        if func == BIN_COMMAND && OPT_ISSET(ops, b'p') {                     // c:4200
4222            if BUILTINS.iter().any(|b| b.node.nam == *arg) {                     // c:4201
4223                println!("{}: builtin", arg);                                // c:4202
4224                informed = 1;
4225                continue;
4226            }
4227        }
4228        // c:4205-4218 — final $PATH fallback via findcmd.
4229        buf = findcmd(arg, 1, (func == BIN_COMMAND && OPT_ISSET(ops, b'p')) as i32);
4230        if let Some(path) = buf {                                            // c:4150 iscom
4231            if wd {                                                          // c:4151
4232                println!("{}: command", arg);                                // c:4152
4233            } else if v && !csh {                                            // c:4154
4234                print!("{} is ", arg);                                       // c:4156
4235                println!("{}", crate::ported::utils::quotedzputs(&path));    // c:4157
4236            } else {
4237                println!("{}", path);                                        // c:4159
4238            }
4239            informed = 1;                                                    // c:4163
4240            continue;
4241        }
4242        // c:4166-4185 — fallback: findcmd through $PATH.
4243        if let Some(cnam) = findcmd(arg, 1, 0) {                             // c:4181
4244            if wd {                                                          // c:4184
4245                println!("{}: command", arg);                                // c:4185
4246            } else if v && !csh {                                            // c:4187
4247                print!("{} is ", arg);                                       // c:4188
4248                println!("{}", crate::ported::utils::quotedzputs(&cnam));    // c:4189
4249            } else {
4250                println!("{}", cnam);                                        // c:4191
4251            }
4252            informed = 1;                                                    // c:4198
4253            continue;
4254        }
4255        // c:4201-4205 — not found at all.
4256        if v || csh || wd {                                                  // c:4202
4257            println!("{}{}", arg, if wd { ": none" } else { " not found" }); // c:4203
4258        }
4259        returnval = 1;                                                       // c:4204
4260    }
4261    crate::ported::mem::unqueue_signals();
4262    returnval | (informed == 0) as i32                                       // c:4209
4263}
4264
4265/// Port of `bin_hash(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4234.
4266/// C: `int bin_hash(char *name, char **argv, Options ops, ...)` —
4267///   manage `cmdnamtab` (default) or `nameddirtab` (`-d`); `-r` empties,
4268///   `-f` fills, `-L` sets PRINT_LIST, `-m` is a glob.
4269/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
4270pub fn bin_hash(name: &str, argv: &[String],                                 // c:4234
4271                ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4272    let mut returnval = 0i32;                                                // c:4239
4273    let mut printflags = 0i32;                                               // c:4240
4274    let dir_mode = OPT_ISSET(ops, b'd');                                     // c:4242
4275
4276    // PFA-SMR aspect: only `hash -d NAME=PATH` mutates the named-dir
4277    // table; the default `hash CMD=PATH` form populates a runtime
4278    // command cache that the recorder doesn't re-apply.
4279    #[cfg(feature = "recorder")]
4280    if crate::recorder::is_enabled() && dir_mode {
4281        let ctx = crate::recorder::recorder_ctx_global();
4282        for a in argv {
4283            if a.starts_with('-') { continue; }
4284            if let Some((k, v)) = a.split_once('=') {
4285                crate::recorder::emit_hash_d(k, v, ctx.clone());
4286            }
4287        }
4288    }
4289
4290    // c:4247-4263 — `-r` empty / `-f` fill (no other args).
4291    if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'f') {                        // c:4247
4292        if !argv.is_empty() {                                                // c:4249
4293            crate::ported::utils::zwarnnam("hash", "too many arguments");    // c:4250
4294            return 1;                                                        // c:4251
4295        }
4296        if OPT_ISSET(ops, b'r') {                                            // c:4255
4297            // c:4256 — `emptyhashtable(cmdnamtab)` /
4298            // `emptynameddirtable()`.
4299            if dir_mode {
4300                crate::ported::hashnameddir::emptynameddirtable();
4301            } else {
4302                crate::ported::hashtable::emptycmdnamtable();
4303            }
4304        }
4305        if OPT_ISSET(ops, b'f') {                                            // c:4259
4306            // c:4260 — `fillcmdnamtable(cmdnamtab)` /
4307            // `fillnameddirtable()`. cmdnamtab fill = walk every
4308            // PATH entry and hashdir() it.
4309            if dir_mode {
4310                crate::ported::hashnameddir::fillnameddirtable();
4311            } else {
4312                // Read $path (the lowercase array form) from env.
4313                // c:4260 — fill cmdnamtab from $path. Read shell-side
4314                //          $PATH so changes via `path=(...)` flow in.
4315                let path_str = crate::ported::params::getsparam("PATH").unwrap_or_default();
4316                let path_arr: Vec<String> =
4317                    path_str.split(':').map(|s| s.to_string()).collect();
4318                crate::ported::hashtable::fillcmdnamtable(&path_arr);
4319            }
4320        }
4321        return 0;                                                            // c:4262
4322    }
4323
4324    // c:4265 — `-L` enables PRINT_LIST.
4325    if OPT_ISSET(ops, b'L') { printflags |= PRINT_LIST; }                    // c:4265
4326
4327    // c:4268-4273 — no args: list table.
4328    if argv.is_empty() {                                                     // c:4268
4329        crate::ported::mem::queue_signals();                                 // c:4269
4330        // c:4270 — `scanhashtable(ht, 1, 0, 0, ht->printnode, printflags)`.
4331        // Walk the selected table (cmdnamtab default, nameddirtab when
4332        // `-d`). Previous Rust port only walked nameddirtab — `hash`
4333        // with no args (the typical user-visible form) silently printed
4334        // nothing on cmdnamtab.
4335        if dir_mode {
4336            if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
4337                for (_n, nd) in t.iter() {                                   // c:4270
4338                    crate::ported::hashnameddir::printnameddirnode(nd, printflags);
4339                }
4340            }
4341        } else {
4342            // c:4270 — cmdnamtab walk (the default `ht`). PATH lookup
4343            // arr is empty in the printnode call site because per-node
4344            // hashed entries carry their own resolved path.
4345            let path_arr: Vec<String> = Vec::new();
4346            if let Ok(t) = crate::ported::hashtable::cmdnamtab_lock().read() {
4347                for (_n, cn) in t.iter() {                                   // c:4270
4348                    print!("{}",
4349                        crate::ported::hashtable::printcmdnamnode(
4350                            cn, &path_arr, printflags as u32));
4351                }
4352            }
4353        }
4354        crate::ported::mem::unqueue_signals();                               // c:4271
4355        return 0;                                                            // c:4272
4356    }
4357
4358    // c:4276-4329 — name-list dispatch, both literal and -m glob.
4359    crate::ported::mem::queue_signals();                                     // c:4276
4360    let mut idx = 0;
4361    while idx < argv.len() {                                                 // c:4277
4362        let arg = &argv[idx];
4363        idx += 1;
4364        if OPT_ISSET(ops, b'm') {                                            // c:4279
4365            // c:4280-4290 — glob-match path.
4366            let pprog = crate::ported::pattern::patcompile(arg,              // c:4282
4367                crate::ported::zsh_h::PAT_HEAPDUP, None);
4368            if let Some(prog) = pprog {
4369                if dir_mode {
4370                    if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
4371                        for (n, nd) in t.iter() {
4372                            if crate::ported::pattern::pattry(&prog, n) {    // c:4286
4373                                crate::ported::hashnameddir::printnameddirnode(nd, printflags);
4374                            }
4375                        }
4376                    }
4377                }
4378            } else {
4379                crate::ported::utils::zwarnnam(name,
4380                    &format!("bad pattern : {}", arg));                      // c:4292
4381                returnval = 1;                                               // c:4293
4382            }
4383            continue;
4384        }
4385        // c:4297-4317 — literal name=value or name-only.
4386        let (n, val) = match arg.find('=') {
4387            Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
4388            None     => (arg.as_str(), None),
4389        };
4390        if let Some(v) = val {                                               // c:4302
4391            // Define entry.
4392            if dir_mode {                                                    // c:4302
4393                // c:4303-4310 — `itype_end(asg->name, IUSER, 0)` validates;
4394                // dir name must be all-IUSER chars.
4395                if !n.chars().all(|c| c.is_alphanumeric() || c == '_') {     // c:4305
4396                    crate::ported::utils::zwarnnam(name,
4397                        &format!("invalid character in directory name: {}", n)); // c:4306
4398                    returnval = 1;                                           // c:4308
4399                    continue;                                                // c:4309
4400                }
4401                let nd = nameddir {
4402                    node: hashnode { next: None, nam: n.to_string(), flags: 0 },
4403                    dir: v.to_string(),
4404                    diff: 0,
4405                };
4406                crate::ported::hashnameddir::addnameddirnode(n, nd);         // c:4314
4407            } else {
4408                // c:4316 — `cn->u.cmd = ztrdup(value);` in cmdnamtab.
4409                // Static-link path: store in PATH-style env.
4410                std::env::set_var(format!("__zshrs_hash_{}", n), v);
4411            }
4412            if OPT_ISSET(ops, b'v') {                                        // c:4321
4413                if dir_mode {
4414                    if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
4415                        if let Some(nd) = t.get(n) {                         // c:4322
4416                            crate::ported::hashnameddir::printnameddirnode(nd, 0);
4417                        }
4418                    }
4419                }
4420            }
4421        } else {
4422            // c:4323-4334 — display existing entry / look up.
4423            if dir_mode {
4424                let snapshot = crate::ported::hashnameddir::nameddirtab()
4425                    .lock().ok().and_then(|t| t.get(n).cloned());
4426                match snapshot {
4427                    Some(nd) => {
4428                        if OPT_ISSET(ops, b'v') {                            // c:4337
4429                            crate::ported::hashnameddir::printnameddirnode(&nd, 0);
4430                        }
4431                    }
4432                    None => {
4433                        crate::ported::utils::zwarnnam(name,
4434                            &format!("no such directory name: {}", n));      // c:4327
4435                        returnval = 1;                                       // c:4328
4436                    }
4437                }
4438            } else {
4439                // c:4332-4334 — `if (!hashcmd(name, path)) zwarnnam(
4440                //                "no such command")`. Walk shell-side
4441                //                $PATH (paramtab).
4442                let found = crate::ported::params::getsparam("PATH").is_some_and(|p| {
4443                    p.split(':').any(|d|
4444                        !d.is_empty() && std::path::Path::new(&format!("{}/{}", d, n)).exists()
4445                    )
4446                });
4447                if !found {
4448                    crate::ported::utils::zwarnnam(name,
4449                        &format!("no such command: {}", n));                 // c:4333
4450                    returnval = 1;                                           // c:4334
4451                }
4452            }
4453        }
4454    }
4455    crate::ported::mem::unqueue_signals();                                   // c:4346
4456    returnval                                                                // c:4346
4457}
4458
4459/// Port of `bin_unhash(char *name, char **argv, Options ops, int func)` from Src/builtin.c:4346.
4460/// C: `int bin_unhash(char *name, char **argv, Options ops, int func)` —
4461///   remove entries from cmdnamtab/aliastab/sufaliastab/nameddirtab/
4462///   shfunctab. `-a` clears all, `-m` is a glob.
4463/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
4464pub fn bin_unhash(name: &str, argv: &[String],                               // c:4346
4465                  ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4466    let mut returnval = 0i32;                                                // c:4351
4467    let mut all = 0i32;                                                      // c:4351
4468    let mut match_count = 0i32;                                              // c:4351
4469
4470    // PFA-SMR aspect: when invoked as `unalias`, record the un-alias
4471    // events so the replay can suppress earlier `alias` calls.
4472    #[cfg(feature = "recorder")]
4473    if crate::recorder::is_enabled() && func == crate::ported::builtin::BIN_UNALIAS {
4474        let ctx = crate::recorder::recorder_ctx_global();
4475        for a in argv {
4476            if a.starts_with('-') && a != "-" { continue; }
4477            crate::recorder::emit_unalias(a, ctx.clone());
4478        }
4479    }
4480
4481    // c:4355-4373 — table-pick dispatch.
4482    enum Tab { CmdNam, NamedDir, Shfunc, Alias, SufAlias }
4483    let tab: Tab;
4484    if func == BIN_UNALIAS {                                                 // c:4356
4485        tab = if OPT_ISSET(ops, b's') { Tab::SufAlias } else { Tab::Alias }; // c:4357
4486        if OPT_ISSET(ops, b'a') {                                            // c:4361
4487            if !argv.is_empty() {                                            // c:4362
4488                crate::ported::utils::zwarnnam(name, "-a: too many arguments"); // c:4363
4489                return 1;                                                    // c:4364
4490            }
4491            all = 1;                                                         // c:4366
4492        } else if argv.is_empty() {                                          // c:4367
4493            crate::ported::utils::zwarnnam(name, "not enough arguments");    // c:4368
4494            return 1;                                                        // c:4369
4495        }
4496    } else if OPT_ISSET(ops, b'd') { tab = Tab::NamedDir;                    // c:4370
4497    } else if OPT_ISSET(ops, b'f') { tab = Tab::Shfunc;                      // c:4372
4498    } else if OPT_ISSET(ops, b's') { tab = Tab::SufAlias;                    // c:4374
4499    } else if func == BIN_UNHASH && OPT_ISSET(ops, b'a') { tab = Tab::Alias; // c:4376
4500    } else { tab = Tab::CmdNam; }                                            // c:4378
4501
4502    // Helper: clear entire table.
4503    let clear_all = |t: &Tab| match t {
4504        Tab::Alias => { let _ = crate::ported::hashtable::aliastab_lock().write().map(|mut g| g.clear()); }
4505        Tab::SufAlias => { let _ = crate::ported::hashtable::sufaliastab_lock().write().map(|mut g| g.clear()); }
4506        Tab::NamedDir => { crate::ported::hashnameddir::emptynameddirtable(); }
4507        Tab::Shfunc => { let _ = shfunctab_table().lock().map(|mut g| g.clear()); }
4508        Tab::CmdNam => { crate::ported::hashtable::emptycmdnamtable(); }     // c:4389
4509    };
4510    let remove_one = |t: &Tab, nm: &str| -> bool {
4511        match t {
4512            Tab::Alias => crate::ported::hashtable::aliastab_lock().write()
4513                .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4514            Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().write()
4515                .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4516            Tab::NamedDir => crate::ported::hashnameddir::removenameddirnode(nm).is_some(),
4517            Tab::Shfunc => shfunctab_table().lock()
4518                .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4519            // c:4405 — `if ((hn = ht->removenode(ht, *argv)))`.
4520            // Removal returns truthy only when the entry actually
4521            // existed. Previous Rust port hardcoded `true` after a
4522            // void-return `freecmdnamnode` call, so `unhash badname`
4523            // silently succeeded instead of emitting the canonical
4524            // "no such hash table element" error.
4525            Tab::CmdNam => crate::ported::hashtable::cmdnamtab_lock().write()
4526                .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4527        }
4528    };
4529
4530    if all != 0 {                                                            // c:4382
4531        crate::ported::mem::queue_signals();                                 // c:4383
4532        clear_all(&tab);                                                     // c:4384-4389
4533        crate::ported::mem::unqueue_signals();                               // c:4390
4534        return 0;                                                            // c:4391
4535    }
4536
4537    // c:4395-4421 — `-m` glob branch.
4538    if OPT_ISSET(ops, b'm') {                                                // c:4395
4539        for arg in argv {                                                    // c:4396
4540            crate::ported::mem::queue_signals();                             // c:4397
4541            let pprog = crate::ported::pattern::patcompile(arg,              // c:4400
4542                crate::ported::zsh_h::PAT_HEAPDUP, None);
4543            if let Some(prog) = pprog {
4544                // Collect names then remove (avoid iterator/mutation conflict).
4545                // c:4408 — `scanmatchtable(ht, pprog, ...)` walks every
4546                // entry in the selected table. Previous Rust port left
4547                // Tab::CmdNam returning an empty Vec, so `unhash -m PAT`
4548                // (default cmd-hash table) silently matched zero entries.
4549                let names: Vec<String> = match &tab {
4550                    Tab::Alias => crate::ported::hashtable::aliastab_lock().read()
4551                        .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
4552                    Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().read()
4553                        .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
4554                    Tab::NamedDir => crate::ported::hashnameddir::nameddirtab().lock()
4555                        .map(|t| t.keys().cloned().collect()).unwrap_or_default(),
4556                    Tab::Shfunc => shfunctab_table().lock()
4557                        .map(|t| t.keys().cloned().collect()).unwrap_or_default(),
4558                    // c:4408 — cmdnamtab walk via `cmdnamtab_lock().iter()`.
4559                    Tab::CmdNam => crate::ported::hashtable::cmdnamtab_lock().read()
4560                        .map(|t| t.iter().map(|(n,_)| n.clone()).collect())
4561                        .unwrap_or_default(),
4562                };
4563                for nm in &names {
4564                    if crate::ported::pattern::pattry(&prog, nm) {           // c:4408
4565                        if remove_one(&tab, nm) {
4566                            match_count += 1;                                // c:4410
4567                        }
4568                    }
4569                }
4570            } else {
4571                crate::ported::utils::zwarnnam(name,
4572                    &format!("bad pattern : {}", arg));                      // c:4416
4573                returnval = 1;                                               // c:4417
4574            }
4575            crate::ported::mem::unqueue_signals();                           // c:4419
4576        }
4577        if match_count == 0 {                                                // c:4424
4578            returnval = 1;                                                   // c:4425
4579        }
4580        return returnval;                                                    // c:4426
4581    }
4582
4583    // c:4429-4439 — literal-name removals.
4584    crate::ported::mem::queue_signals();                                     // c:4430
4585    for arg in argv {                                                        // c:4431
4586        if remove_one(&tab, arg) {                                           // c:4432
4587            // freed
4588        } else if func == BIN_UNSET
4589            && crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"))
4590        {
4591            // c:4434 — POSIX: unset of nonexistent isn't an error.
4592            returnval = 0;                                                   // c:4435
4593        } else {
4594            crate::ported::utils::zwarnnam(name,
4595                &format!("no such hash table element: {}", arg));            // c:4437
4596            returnval = 1;                                                   // c:4450
4597        }
4598    }
4599    crate::ported::mem::unqueue_signals();                                   // c:4450
4600    returnval                                                                // c:4450
4601}
4602
4603/// Port of `bin_alias(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4450.
4604/// C: `int bin_alias(char *name, char **argv, Options ops, ...)` — list,
4605///   define, glob-list, or display aliases. `-r`/`-g`/`-s` filter type;
4606///   `-L` prints definitions; `-m` treats args as patterns.
4607/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
4608pub fn bin_alias(name: &str, argv: &[String],                                // c:4450
4609                 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4610    let mut returnval = 0i32;                                                // c:4455
4611    let mut flags1 = 0u32;                                                   // c:4456
4612    let mut flags2 = DISABLED as u32;                                        // c:4456
4613    let mut printflags = 0i32;                                               // c:4457
4614    let mut use_suffix = false;                                              // tracks ht switch
4615
4616    // c:4461-4485 — type-flag parsing.
4617    let type_opts = (OPT_ISSET(ops, b'r') as i32)                            // c:4461
4618                  + (OPT_ISSET(ops, b'g') as i32)
4619                  + (OPT_ISSET(ops, b's') as i32);
4620    if type_opts != 0 {                                                      // c:4464
4621        if type_opts > 1 {                                                   // c:4465
4622            crate::ported::utils::zwarnnam(name, "illegal combination of options"); // c:4466
4623            return 1;                                                        // c:4467
4624        }
4625        if OPT_ISSET(ops, b'g') {                                            // c:4469
4626            flags1 |= ALIAS_GLOBAL as u32;                                   // c:4470
4627        } else {
4628            flags2 |= ALIAS_GLOBAL as u32;                                   // c:4472
4629        }
4630        if OPT_ISSET(ops, b's') {                                            // c:4473
4631            flags1 |= ALIAS_SUFFIX as u32;                                   // c:4480
4632            use_suffix = true;                                               // c:4481
4633        } else {
4634            flags2 |= ALIAS_SUFFIX as u32;                                   // c:4483
4635        }
4636    }
4637
4638    // c:4486-4490 — printflags from -L / + suffix.
4639    if OPT_ISSET(ops, b'L') {                                                // c:4486
4640        printflags |= PRINT_LIST;                                            // c:4487
4641    } else if OPT_PLUS(ops, b'g') || OPT_PLUS(ops, b'r') || OPT_PLUS(ops, b's')
4642        || OPT_PLUS(ops, b'm') || OPT_ISSET(ops, b'+')                       // c:4488
4643    {
4644        printflags |= PRINT_NAMEONLY;                                        // c:4490
4645    }
4646
4647    // Helper closure that prints one Alias respecting printflags.
4648    // Mirrors `printaliasnode(HashNode, int printflags)` from
4649    // Src/hashtable.c:1256-1336 (the simple-print subset that
4650    // bin_alias actually emits — PRINT_LIST, PRINT_NAMEONLY, and
4651    // the default `name=value`).
4652    let print_alias = |a: &Alias, pflags: i32| {
4653        // c:1262-1265 — PRINT_NAMEONLY emits the name and a newline.
4654        if (pflags & PRINT_NAMEONLY) != 0 {
4655            println!("{}", a.node.nam);
4656            return;
4657        }
4658        // c:1311-1330 — PRINT_LIST prefix: `alias ` then `-s `/`-g `
4659        // for ALIAS_SUFFIX/ALIAS_GLOBAL, then `-- ` for names that
4660        // start with `-` or `+`. The previous Rust port emitted bare
4661        // `alias name=value` without any of these flags, so `alias
4662        // -L` for a global alias was indistinguishable from a
4663        // regular one and the output wasn't re-executable.
4664        if (pflags & PRINT_LIST) != 0 {
4665            print!("alias ");
4666            if (a.node.flags & ALIAS_SUFFIX as i32) != 0 {                   // c:1322
4667                print!("-s ");
4668            } else if (a.node.flags & ALIAS_GLOBAL as i32) != 0 {            // c:1324
4669                print!("-g ");
4670            }
4671            if a.node.nam.starts_with('-') || a.node.nam.starts_with('+') {  // c:1330
4672                print!("-- ");
4673            }
4674        }
4675        // c:1334-1336 — `quotedzputs(nam); putchar('='); quotedzputs(text); putchar('\n');`
4676        println!("{}={}", a.node.nam, a.text);
4677    };
4678
4679    // c:4495-4500 — no args: list all (filtered by flags).
4680    if argv.is_empty() {                                                     // c:4495
4681        crate::ported::mem::queue_signals();                                 // c:4496
4682        let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4683        if let Ok(t) = lock.read() {
4684            for (_n, a) in t.iter() {                                        // c:4497
4685                if (a.node.flags & flags1 as i32) == flags1 as i32
4686                    && (a.node.flags & flags2 as i32) == 0 {
4687                    print_alias(a, printflags);
4688                }
4689            }
4690        }
4691        crate::ported::mem::unqueue_signals();                               // c:4498
4692        return 0;                                                            // c:4499
4693    }
4694
4695    // c:4503-4519 — `-m` glob branch.
4696    if OPT_ISSET(ops, b'm') {                                                // c:4503
4697        for pat in argv {                                                    // c:4504
4698            crate::ported::mem::queue_signals();                             // c:4505
4699            // c:4506 — `tokenize + patcompile`.
4700            let pprog = crate::ported::pattern::patcompile(pat,              // c:4507
4701                crate::ported::zsh_h::PAT_HEAPDUP, None);
4702            if let Some(prog) = pprog {
4703                let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4704                if let Ok(t) = lock.read() {
4705                    for (_n, a) in t.iter() {                                // c:4509
4706                        if (a.node.flags & flags1 as i32) == flags1 as i32
4707                            && (a.node.flags & flags2 as i32) == 0
4708                            && crate::ported::pattern::pattry(&prog, &a.node.nam)
4709                        {
4710                            print_alias(a, printflags);
4711                        }
4712                    }
4713                }
4714            } else {
4715                crate::ported::utils::zwarnnam(name,
4716                    &format!("bad pattern : {}", pat));                      // c:4514
4717                returnval = 1;                                               // c:4515
4718            }
4719            crate::ported::mem::unqueue_signals();                           // c:4517
4720        }
4721        return returnval;                                                    // c:4518
4722    }
4723
4724    // c:4521-4540 — literal args: define `name=value` or display a single name.
4725    crate::ported::mem::queue_signals();                                     // c:4522
4726    let mut idx = 0;
4727    while idx < argv.len() {                                                 // c:4523
4728        let arg = &argv[idx];
4729        idx += 1;
4730        if let Some(eq) = arg.find('=') {                                    // c:4524 (asg->value.scalar)
4731            if !OPT_ISSET(ops, b'L') {                                       // c:4524
4732                let n = &arg[..eq];
4733                let v = &arg[eq + 1..];
4734                let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4735                if let Ok(mut t) = lock.write() {
4736                    let a = crate::ported::hashtable::createaliasnode(n, v, flags1); // c:4527
4737                    t.add(a);
4738                }
4739                continue;
4740            }
4741        }
4742        let n = if let Some(eq) = arg.find('=') { &arg[..eq] } else { arg.as_str() };
4743        let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4744        let found = lock.read().ok().and_then(|t|
4745            t.get_including_disabled(n).map(|a| (a.node.nam.clone(), a.node.flags as u32, a.text.clone()))
4746        );
4747        match found {
4748            Some((nm, fl, txt)) => {                                         // c:4530
4749                // c:4532-4537 — type-filter check.
4750                let show = type_opts == 0
4751                    || use_suffix
4752                    || (OPT_ISSET(ops, b'r')
4753                        && (fl & (ALIAS_GLOBAL | ALIAS_SUFFIX) as u32) == 0)
4754                    || (OPT_ISSET(ops, b'g')
4755                        && (fl & ALIAS_GLOBAL as u32) != 0);
4756                if show {
4757                    let a = crate::ported::hashtable::createaliasnode(&nm, &txt, fl);
4758                    print_alias(&a, printflags);
4759                }
4760            }
4761            None => {                                                        // c:4538
4762                returnval = 1;                                               // c:4539
4763            }
4764        }
4765    }
4766    crate::ported::mem::unqueue_signals();                                   // c:4541
4767    returnval                                                                // c:4542
4768}
4769
4770/// Port of `bin_true(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4550.
4771/// C: `int bin_true(UNUSED(char *name), UNUSED(char **argv),
4772///                  UNUSED(Options ops), UNUSED(int func))` → `return 0;`
4773/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
4774pub fn bin_true(_name: &str, _argv: &[String],                               // c:4550
4775                _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4776    0                                                                        // c:4559
4777}
4778
4779/// Port of `bin_false(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4559.
4780/// C: `int bin_false(UNUSED(char *name), UNUSED(char **argv),
4781///                   UNUSED(Options ops), UNUSED(int func))` → `return 1;`
4782/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
4783pub fn bin_false(_name: &str, _argv: &[String],                              // c:4559
4784                 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4785    1                                                                        // c:4562
4786}
4787
4788/// Port of `bin_print(char *name, char **args, Options ops, int func)` from Src/builtin.c:4587.
4789/// C: `int bin_print(char *name, char **args, Options ops, int func)`.
4790///
4791/// The C body is ~1000 lines: `print` / `echo` / `printf` / `pushln`
4792/// dispatcher with -n/-N/-c/-r/-R/-l/-D/-i/-f/-v/-s/-S/-z/-e/-E etc.
4793/// The structural port handles the script-friendly subset that the
4794/// daily-driver hits: print/echo plain emission with -n, -l (one per
4795/// line), -r raw, -E newline-only, -- end-of-options. The full -f
4796/// printf format-spec engine and ZLE/history wireups defer to the
4797/// expand_printf_escapes helpers.
4798/// WARNING: param names don't match C — Rust=(name, args, func) vs C=(name, args, ops, func)
4799pub fn bin_print(name: &str, args: &[String],                                // c:4587
4800                 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4801    let nonewline = OPT_ISSET(ops, b'n');                                    // c:4595
4802    let raw = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');                  // c:4596
4803    let one_per_line = OPT_ISSET(ops, b'l');                                 // c:4597
4804    let _printf_mode = func == BIN_PRINTF || OPT_HASARG(ops, b'f');          // c:4604
4805    let echo_mode = func == BIN_ECHO;
4806    let _ = (name, raw);
4807
4808    // c:4633-4685 — destination dispatch. -u FD writes to fd, -s pushes
4809    // to history, -z to ZLE buffer, -v VAR assigns to scalar. Defer to
4810    // env/var wireup.
4811    let dest_var: Option<String> = if OPT_HASARG(ops, b'v') {
4812        OPT_ARG(ops, b'v').map(String::from)
4813    } else { None };
4814
4815    // c:4604-4612 — printf format-string handling.
4816    if _printf_mode {
4817        let fmt = if let Some(f) = OPT_ARG(ops, b'f') {
4818            f.to_string()
4819        } else if !args.is_empty() {
4820            args[0].clone()
4821        } else {
4822            return 0;
4823        };
4824        let rest: &[String] = if OPT_HASARG(ops, b'f') { args } else { &args[1..] };
4825        let out = printf_format(&fmt, rest);
4826        if let Some(ref v) = dest_var {
4827            crate::ported::params::setsparam(v, &out);
4828        } else {
4829            print!("{}", out);
4830        }
4831        return 0;
4832    }
4833
4834    // c:4860+ — main print loop.
4835    let sep = if one_per_line { "\n" } else { " " };
4836    // c:4598-4600 — `-P` prompt-style percent expansion (`%n`, `%d`,
4837    // `%?`, `%h`, `%%`, etc.). Routes through `expand_prompt`
4838    // (canonical port of `Src/prompt.c:182 promptexpand`).
4839    let mut processed_args: Vec<String> = if OPT_ISSET(ops, b'P') {
4840        args.iter()
4841            .map(|a| crate::ported::prompt::expand_prompt(a))                // c:Src/prompt.c:182
4842            .collect()
4843    } else {
4844        args.to_vec()
4845    };
4846    // c:Src/builtin.c:4869-4880 `-o` / `-O` / `-i` sort flags.
4847    // -o → case-insensitive ascending, -O → case-insensitive
4848    // descending, -i → case-sensitive (with -o/-O).
4849    if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
4850        let case_sensitive = OPT_ISSET(ops, b'i');
4851        if case_sensitive {
4852            processed_args.sort();
4853        } else {
4854            processed_args.sort_by_key(|s| s.to_lowercase());
4855        }
4856        if OPT_ISSET(ops, b'O') {
4857            processed_args.reverse();
4858        }
4859    }
4860    // c:Src/builtin.c:4866-4886 — when `-r` is NOT set, each arg goes
4861    // through `getkeystring` to interpret backslash escapes (`\n`,
4862    // `\t`, `\\`, escaped space `\ `, etc.). `echo` follows the same
4863    // path when `BSD_ECHO`/`SH_OPTION_LETTERS`-style isn't in effect;
4864    // BIN_ECHO with `-E` keeps escapes literal. Without this, `print
4865    // -- ${(q)a}` for `a="he llo"` emitted `he\ llo` instead of zsh's
4866    // `he llo` (the (q) flag's backslash gets consumed by print).
4867    if !raw {
4868        let echo_E = echo_mode && OPT_ISSET(ops, b'E');
4869        if !echo_E {
4870            for a in processed_args.iter_mut() {
4871                let (s, _) = crate::ported::utils::getkeystring_with(a,
4872                    crate::ported::utils::GETKEYS_PRINT);
4873                *a = s;
4874            }
4875        }
4876    }
4877    let body = processed_args.join(sep);
4878    if let Some(ref v) = dest_var {
4879        crate::ported::params::setsparam(v, &body);
4880    } else {
4881        print!("{}", body);
4882        // c:5550 — final newline unless -n.
4883        if !nonewline && !echo_mode {
4884            println!();
4885        } else if echo_mode && !nonewline {
4886            println!();
4887        }
4888    }
4889    0
4890}
4891
4892/// Port of `bin_shift(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:5593.
4893/// C: `int bin_shift(char *name, char **argv, Options ops, UNUSED(int func))`
4894/// — shift positional params (or named arrays) by `num` positions; `-p`
4895/// pops from the right end.
4896/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
4897pub fn bin_shift(name: &str, argv: &[String],                                // c:5593
4898                 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4899    let mut num: i32 = 1;                                                    // c:5595
4900    let mut ret: i32 = 0;                                                    // c:5595
4901    let mut idx = 0usize;
4902    crate::ported::mem::queue_signals();                                     // c:5599
4903    // c:5600-5605 — first arg parsed as math expr unless it's an array name.
4904    if !argv.is_empty() {                                                    // c:5600
4905        let first = &argv[0];
4906        // c:5600 — `if (!getaparam(*argv))` decides whether the arg is
4907        //          a numeric shift-count vs an array name. Check
4908        //          paramtab for a PM_ARRAY entry, not OS env.
4909        let is_array = {
4910            use crate::ported::zsh_h::{PM_ARRAY, PM_TYPE};
4911            let tab = crate::ported::params::paramtab().read().unwrap();
4912            tab.get(first)
4913                .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_ARRAY)
4914                .unwrap_or(false)
4915        };
4916        if !is_array {                                                       // c:5600
4917            // c:5601 — `num = mathevali(*argv++);`. The previous Rust port
4918            // used `parse::<i32>()` which rejects any non-trivial
4919            // arithmetic: `shift 1+2` would silently return ret=1
4920            // instead of shifting by 3. Route through mathevali.
4921            num = crate::ported::math::mathevali(first).unwrap_or_else(|_| {
4922                ret = 1;
4923                0
4924            }) as i32;                                                       // c:5601
4925            idx = 1;
4926            // c:5602-5605 — `if (errflag) return 1;`.
4927            if ret != 0
4928                || crate::ported::utils::errflag.load(Ordering::Relaxed) != 0
4929            {
4930                crate::ported::mem::unqueue_signals();                       // c:5604
4931                return 1;
4932            }
4933        }
4934    }
4935
4936    // c:5608-5611 — `if (num < 0)` reject.
4937    if num < 0 {                                                             // c:5608
4938        crate::ported::mem::unqueue_signals();                               // c:5609
4939        crate::ported::utils::zwarnnam(name,
4940            "argument to shift must be non-negative");                       // c:5610
4941        return 1;                                                            // c:5611
4942    }
4943
4944    // c:5614-5635 — named-array shift loop.
4945    if idx < argv.len() {                                                    // c:5614
4946        for arr_name in &argv[idx..] {                                       // c:5615
4947            // c:5616 — `if ((s = getaparam(*argv)))` else silent skip.
4948            //          Read paramtab directly; was approximating arrays
4949            //          as `:`-separated env values which is wrong (env
4950            //          can never carry array structure).
4951            let s: Vec<String> = {
4952                let tab = crate::ported::params::paramtab().read().unwrap();
4953                match tab.get(arr_name).and_then(|pm| pm.u_arr.clone()) {
4954                    Some(arr) => arr,
4955                    None => continue,
4956                }
4957            };
4958            // c:5617-5621 — arrlen_lt check.
4959            if (s.len() as i32) < num {                                      // c:5617
4960                crate::ported::utils::zwarnnam(name,
4961                    "shift count must be <= $#");                            // c:5618
4962                ret += 1;                                                    // c:5619
4963                continue;                                                    // c:5620
4964            }
4965            // c:5622-5634 — -p shifts off the right end, otherwise the left.
4966            let s2: Vec<String> = if OPT_ISSET(ops, b'p') {                  // c:5622
4967                s[..s.len() - num as usize].to_vec()                         // c:5625-5628
4968            } else {
4969                s[num as usize..].to_vec()                                   // c:5631
4970            };
4971            // c:5633 — `setaparam(*argv, s);`. Write the shifted array
4972            //          back to paramtab as a proper PM_ARRAY. Was a
4973            //          fake: `env::set_var` + colon-joined fake-array
4974            //          which neither carries array structure nor
4975            //          reaches subsequent `${arr_name[@]}` expansions.
4976            crate::ported::params::setaparam(arr_name, s2);
4977        }
4978    } else {
4979        // c:5636-5654 — shift positional parameters ($1..$N).
4980        // Static-link path: positional params live in src/ported/exec.rs;
4981        // expose via PPARAMS Mutex<Vec<String>>.
4982        let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
4983        let l = pp.len() as i32;
4984        if num > l {                                                         // c:5636
4985            crate::ported::utils::zwarnnam(name, "shift count must be <= $#"); // c:5637
4986            ret = 1;                                                         // c:5638
4987        } else if OPT_ISSET(ops, b'p') {                                     // c:5641
4988            pp.truncate((l - num) as usize);                                 // c:5642-5644
4989        } else {
4990            pp.drain(..num as usize);                                        // c:5646-5650
4991        }
4992        // PPARAMS is the single source of truth. fusevm-side reads
4993        // route through exec.pparams() which reads PPARAMS, so the
4994        // shift is immediately visible — no exec.positional_params
4995        // mirror needed.
4996        drop(pp);
4997    }
4998    crate::ported::mem::unqueue_signals();                                   // c:5658
4999    ret                                                                      // c:5659
5000}
5001
5002/// Port of `bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:5672.
5003/// C: `int bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops),
5004///                     UNUSED(int func))`.
5005///
5006/// POSIX getopts. Maintains state in $OPTIND (zoptind) and an internal
5007/// per-arg cursor (optcind). Reads from the script's positional params
5008/// when no extra args supplied, otherwise from the trailing argv.
5009/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
5010pub fn bin_getopts(_name: &str, argv: &[String],                             // c:5672
5011                   _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5012    if argv.len() < 2 { return 1; }
5013    // c:5675 — `char *optstr = unmetafy(*argv++, &lenoptstr); char *var = *argv++;`
5014    let optstr_full = argv[0].clone();
5015    let var = argv[1].clone();
5016    // c:5676 — `char **args = (*argv) ? argv : pparams;`
5017    let argv_rest: Vec<String> = argv[2..].to_vec();
5018    let args: Vec<String> = if !argv_rest.is_empty() {
5019        argv_rest
5020    } else {
5021        PPARAMS.lock().map(|p| p.clone()).unwrap_or_default()
5022    };
5023
5024    // c:5681-5685 — `if (zoptind < 1) { zoptind = 1; optcind = 0; }`
5025    let mut zoptind = ZOPTIND.load(Ordering::Relaxed);
5026    if zoptind < 1 {                                                         // c:5681
5027        zoptind = 1;
5028        OPTCIND.store(0, Ordering::Relaxed);
5029    }
5030    let mut optcind = OPTCIND.load(Ordering::Relaxed);
5031
5032    // c:5686-5688 — `if (arrlen_lt(args, zoptind)) return 1;`
5033    if (args.len() as i32) < zoptind {                                       // c:5686
5034        ZOPTIND.store(zoptind, Ordering::Relaxed);
5035        return 1;
5036    }
5037
5038    // c:5691-5693 — `quiet = *optstr == ':'; optstr += quiet; lenoptstr -= quiet;`
5039    let (quiet, optstr) = if optstr_full.starts_with(':') {                  // c:5691
5040        (true, &optstr_full[1..])
5041    } else {
5042        (false, optstr_full.as_str())
5043    };
5044
5045    // c:5696 — `str = unmetafy(dupstring(args[zoptind - 1]), &lenstr);`
5046    let mut str_buf = args[(zoptind - 1) as usize].clone();
5047    let mut lenstr = str_buf.len() as i32;
5048    if lenstr == 0 { return 1; }                                             // c:5697
5049
5050    // c:5699-5703 — bump to next arg if optcind exhausted current.
5051    if optcind >= lenstr {                                                   // c:5699
5052        optcind = 0;
5053        zoptind += 1;
5054        if zoptind as usize > args.len() {                                   // c:5701
5055            ZOPTIND.store(zoptind, Ordering::Relaxed);
5056            OPTCIND.store(optcind, Ordering::Relaxed);
5057            return 1;
5058        }
5059        str_buf = args[(zoptind - 1) as usize].clone();
5060        lenstr = str_buf.len() as i32;
5061    }
5062
5063    // c:5705-5712 — first option char checks: not `-`/`+` → done; `--` → done.
5064    if optcind == 0 {                                                        // c:5705
5065        if lenstr < 2 || (!str_buf.starts_with('-') && !str_buf.starts_with('+')) {
5066            ZOPTIND.store(zoptind, Ordering::Relaxed);
5067            OPTCIND.store(optcind, Ordering::Relaxed);
5068            return 1;
5069        }
5070        if lenstr == 2 && &str_buf[..2] == "--" {                            // c:5708
5071            zoptind += 1;
5072            ZOPTIND.store(zoptind, Ordering::Relaxed);
5073            OPTCIND.store(0, Ordering::Relaxed);
5074            return 1;
5075        }
5076        optcind += 1;
5077    }
5078    // c:5715 — `opch = str[optcind++];`
5079    let opch = str_buf.as_bytes()[optcind as usize];
5080    optcind += 1;
5081
5082    // c:5716-5721 — `lenoptbuf = (str[0] == '+') ? 2 : 1; optbuf[lenoptbuf-1] = opch;`
5083    let plus = str_buf.starts_with('+');
5084    let optbuf: String = if plus {
5085        format!("+{}", opch as char)
5086    } else {
5087        format!("{}", opch as char)
5088    };
5089
5090    // c:5724-5740 — illegal option: `?` reply, OPTIND fixed under POSIXBUILTINS.
5091    let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
5092    let found = optstr.bytes().position(|b| b == opch);
5093    if opch == b':' || found.is_none() {                                     // c:5724
5094        if posix {                                                           // c:5728
5095            optcind = 0;
5096            zoptind += 1;
5097        }
5098        // c:5731 — `setsparam(var, ztrdup(p));` where p = "?"
5099        crate::ported::params::setsparam(&var, "?");
5100        if quiet {                                                           // c:5733
5101            crate::ported::params::setsparam("OPTARG", &optbuf);     // c:5734
5102        } else {
5103            let prefix = if plus { "+" } else { "-" };
5104            crate::ported::utils::zwarn(&format!(
5105                "bad option: {}{}", prefix, opch as char));                  // c:5736
5106            crate::ported::params::setsparam("OPTARG", "");
5107        }
5108        ZOPTIND.store(zoptind, Ordering::Relaxed);
5109        OPTCIND.store(optcind, Ordering::Relaxed);
5110        // Sync OPTIND env var so callers can read.
5111        crate::ported::params::setiparam("OPTIND", zoptind as i64);
5112        return 0;
5113    }
5114
5115    // c:5744 — `if (p[1] == ':')` — required argument.
5116    let p = found.unwrap();
5117    let optstr_bytes = optstr.as_bytes();
5118    if p + 1 < optstr_bytes.len() && optstr_bytes[p + 1] == b':' {           // c:5744
5119        if optcind == lenstr {                                               // c:5745
5120            // c:5746 — argument in next arg.
5121            if zoptind as usize >= args.len() {                              // c:5747
5122                if posix {
5123                    optcind = 0;
5124                    zoptind += 1;
5125                }
5126                if quiet {                                                   // c:5754
5127                    crate::ported::params::setsparam(&var, ":");
5128                    crate::ported::params::setsparam("OPTARG", &optbuf);
5129                } else {
5130                    crate::ported::params::setsparam(&var, "?");
5131                    crate::ported::params::setsparam("OPTARG", "");
5132                    let prefix = if plus { "+" } else { "-" };
5133                    crate::ported::utils::zwarn(&format!(
5134                        "argument expected after {}{} option",
5135                        prefix, opch as char));                              // c:5760
5136                }
5137                ZOPTIND.store(zoptind, Ordering::Relaxed);
5138                OPTCIND.store(optcind, Ordering::Relaxed);
5139                crate::ported::params::setiparam("OPTIND", zoptind as i64);
5140                return 0;
5141            }
5142            let p_arg = args[zoptind as usize].clone();
5143            zoptind += 1;
5144            crate::ported::params::setsparam("OPTARG", &p_arg);      // c:5765
5145            optcind = 0;
5146        } else {
5147            // c:5774 — `p = metafy(str+optcind, lenstr-optcind, META_DUP);`
5148            let p_arg = str_buf[(optcind as usize)..].to_string();
5149            crate::ported::params::setsparam("OPTARG", &p_arg);
5150            optcind = 0;
5151            zoptind += 1;
5152        }
5153    } else {
5154        // c:5784 — `zsfree(zoptarg); zoptarg = ztrdup("");`
5155        crate::ported::params::setsparam("OPTARG", "");
5156    }
5157
5158    // c:5788 — `setsparam(var, metafy(optbuf, lenoptbuf, META_DUP));`
5159    crate::ported::params::setsparam(&var, &optbuf);
5160    ZOPTIND.store(zoptind, Ordering::Relaxed);
5161    OPTCIND.store(optcind, Ordering::Relaxed);
5162    crate::ported::params::setiparam("OPTIND", zoptind as i64);
5163    0                                                                        // c:5790
5164}
5165
5166/// Port of `bin_break(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:5809.
5167/// C: `int bin_break(char *name, char **argv, UNUSED(Options ops), int func)`
5168/// — handles BIN_BREAK / BIN_CONTINUE / BIN_RETURN / BIN_LOGOUT / BIN_EXIT.
5169/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
5170pub fn bin_break(name: &str, argv: &[String],                                // c:5809
5171                 _ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
5172    // BIN_BREAK/CONTINUE/RETURN/EXIT/LOGOUT live at the top of this file
5173    // (c:5707-5712 in Src/builtin.c via the BUILTIN(...) table).
5174    // c:5811 — `int num = lastval, nump = 0, implicit;`
5175    let mut num: i32 = LASTVAL.load(Ordering::Relaxed);                      // c:5811
5176    let mut nump = 0i32;                                                     // c:5811
5177    let implicit = argv.is_empty();                                          // c:5814
5178    // c:5815-5818 — first arg parsed as math expr.
5179    if !implicit {                                                           // c:5815
5180        num = mathevali(&argv[0]).unwrap_or(0) as i32;                       // c:5816
5181        nump = 1;                                                            // c:5817
5182    }
5183
5184    // c:5820-5823 — positive-num requirement for BIN_CONTINUE / BIN_BREAK.
5185    if nump > 0 && (func == BIN_CONTINUE || func == BIN_BREAK) && num <= 0 { // c:5820
5186        crate::ported::utils::zwarnnam(name, &format!("argument is not positive: {}", num)); // c:5821
5187        return 1;                                                            // c:5822
5188    }
5189
5190    let loops = LOOPS.load(Ordering::Relaxed);
5191    match func {
5192        // c:5831-5842 — BIN_CONTINUE: must be in a loop, set contflag,
5193        // then fall through to BIN_BREAK's break-count assign.
5194        x if x == BIN_CONTINUE => {                                          // c:5831
5195            if loops == 0 {                                                  // c:5832
5196                crate::ported::utils::zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5833
5197                return 1;                                                    // c:5834
5198            }
5199            CONTFLAG.store(1, Ordering::Relaxed);                            // c:5836 FALLTHROUGH
5200            // c:5837 — fallthrough to BIN_BREAK's loops==0 guard
5201            // (impossible here since we already returned above) +
5202            // break-count assign. Inlined directly. The previous
5203            // Rust port had a redundant `if loops == 0 { return 1 }`
5204            // dead-coded after the first guard.
5205            BREAKS.store(if nump != 0 { num.min(loops) } else { 1 },         // c:5842
5206                         Ordering::Relaxed);
5207        }
5208        // c:5832-5838 — BIN_BREAK.
5209        x if x == BIN_BREAK => {                                             // c:5832
5210            if loops == 0 {                                                  // c:5833
5211                crate::ported::utils::zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5834
5212                return 1;                                                    // c:5835
5213            }
5214            BREAKS.store(if nump != 0 { num.min(loops) } else { 1 },         // c:5837
5215                         Ordering::Relaxed);
5216        }
5217        // c:5839-5860 — BIN_RETURN.
5218        x if x == BIN_RETURN => {
5219            let interactive = crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
5220            let shinstdin = crate::ported::zsh_h::isset(crate::ported::options::optlookup("shinstdin"));
5221            let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
5222            let sourcelevel = SOURCELEVEL.load(Ordering::Relaxed);
5223            // c:5840-5841 — `if ((interactive && shinstdin) || locallevel || sourcelevel)`
5224            if (interactive && shinstdin) || locallevel != 0 || sourcelevel != 0 { // c:5840
5225                RETFLAG.store(1, Ordering::Relaxed);                         // c:5842
5226                BREAKS.store(loops, Ordering::Relaxed);                      // c:5843
5227                LASTVAL.store(num, Ordering::Relaxed);                       // c:5844
5228                // c:5845-5854 — inside a primed trap with the sentinel
5229                // `trap_return == -2`, promote to TRAP_STATE_FORCE_RETURN
5230                // and carry `lastval`. POSIXTRAPS + `implicit` opts out:
5231                // POSIX semantics keep $? from before the trap fired.
5232                let posixtraps =
5233                    crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixtraps"));
5234                let cur_state =
5235                    crate::exec::TRAP_STATE.load(Ordering::Relaxed);
5236                let cur_return =
5237                    crate::exec::TRAP_RETURN.load(Ordering::Relaxed);
5238                if cur_state == crate::ported::zsh_h::TRAP_STATE_PRIMED      // c:5845
5239                    && cur_return == -2                                      // c:5845
5240                    && !(posixtraps && implicit)                             // c:5851
5241                {
5242                    crate::exec::TRAP_STATE.store(                           // c:5852
5243                        crate::ported::zsh_h::TRAP_STATE_FORCE_RETURN,
5244                        Ordering::Relaxed,
5245                    );
5246                    crate::exec::TRAP_RETURN.store(num, Ordering::Relaxed);  // c:5853
5247                }
5248                return num;                                                  // c:5855
5249            }
5250            // c:5858 — fallthrough: treat as logout/exit.
5251            zexit(num, ZEXIT_NORMAL);                                        // c:5858
5252        }
5253        // c:5864-5869 — BIN_LOGOUT: refuse if not LOGINSHELL, then
5254        // FALLTHROUGH into the BIN_EXIT body. The previous Rust port
5255        // called \`zexit(num, ZEXIT_NORMAL)\` directly instead of
5256        // entering the BIN_EXIT defer-guard, so \`logout\` from inside
5257        // a function would skip EXIT traps + function unwind +
5258        // \"you have running jobs\" warning — same gap as the prior
5259        // BIN_EXIT fix.
5260        x if x == BIN_LOGOUT => {
5261            // c:5865 — `if (unset(LOGINSHELL))`. The previous Rust port
5262            // called `optlookup("login")` — but "login" is the
5263            // SHELL-LETTER-FLAG name (zshletters table letter 'l'),
5264            // not an option name. Option name canonicalization maps
5265            // LOGINSHELL → "loginshell" (Src/options.c index_to_name
5266            // at line 1682 in Rust port).
5267            //
5268            // \`optlookup(\"login\")\` returns OPT_INVALID (0), so
5269            // \`isset(0)\` always returns false — bin_logout always
5270            // saw \"not login shell\" and rejected with that error
5271            // regardless of whether the shell was actually started
5272            // with \`-l\`.
5273            let loginshell = crate::ported::zsh_h::isset(crate::ported::options::optlookup("loginshell"));
5274            if !loginshell {                                                 // c:5865
5275                crate::ported::utils::zwarnnam(name, "not login shell");     // c:5866
5276                return 1;                                                    // c:5867
5277            }
5278            // c:5869 — `/*FALLTHROUGH*/` into BIN_EXIT body.
5279            // Reusing the BIN_EXIT branch below by setting `func` to
5280            // BIN_EXIT isn't possible mid-match; inline the same
5281            // guard logic here.
5282            let cur_locallevel = LOCALLEVEL.load(Ordering::Relaxed);
5283            let forklevel = crate::exec::FORKLEVEL.load(Ordering::Relaxed);
5284            let shell_exiting = SHELL_EXITING.load(Ordering::Relaxed);
5285            if cur_locallevel > forklevel && shell_exiting != -1 {           // c:5871
5286                if STOPMSG.load(Ordering::Relaxed) == 0 {
5287                    zexit(0, crate::ported::zsh_h::ZEXIT_DEFERRED);          // c:5884
5288                }
5289                if STOPMSG.load(Ordering::Relaxed) == 0 {                    // c:5884
5290                    let trap_state = crate::exec::TRAP_STATE.load(Ordering::Relaxed);
5291                    if trap_state != 0 {                                     // c:5885
5292                        crate::exec::TRAP_STATE.store(                       // c:5886
5293                            crate::ported::zsh_h::TRAP_STATE_FORCE_RETURN,
5294                            Ordering::Relaxed,
5295                        );
5296                    }
5297                    RETFLAG.store(1, Ordering::Relaxed);                     // c:5887
5298                    BREAKS.store(LOOPS.load(Ordering::Relaxed),              // c:5888
5299                                 Ordering::Relaxed);
5300                    EXIT_PENDING.store(1, Ordering::Relaxed);                // c:5889
5301                    EXIT_VAL.store(num, Ordering::Relaxed);                  // c:5891
5302                }
5303            } else {
5304                zexit(num, ZEXIT_NORMAL);                                    // c:5894
5305            }
5306        }
5307        // c:5870-5894 — BIN_EXIT: function-context guard. C body:
5308        //   if (locallevel > forklevel && shell_exiting != -1) {
5309        //       if (stopmsg || (zexit(0, ZEXIT_DEFERRED), !stopmsg)) {
5310        //           if (trap_state) trap_state = TRAP_STATE_FORCE_RETURN;
5311        //           retflag = 1; breaks = loops;
5312        //           exit_pending = 1; exit_level = locallevel; exit_val = num;
5313        //       }
5314        //   } else zexit(num, ZEXIT_NORMAL);
5315        //
5316        // Inside a function (locallevel > forklevel) the shell can't
5317        // exit directly — EXIT traps still need to run. The probe
5318        // path zexit(0, ZEXIT_DEFERRED) calls checkjobs; if no
5319        // stopmsg triggered, we defer: set retflag + breaks +
5320        // exit_pending so the function unwind takes us out.
5321        //
5322        // The previous Rust port skipped this entire guard, always
5323        // calling zexit(num, ZEXIT_NORMAL) directly. `exit` inside
5324        // a function would terminate without running EXIT traps or
5325        // unwinding the function stack.
5326        x if x == BIN_EXIT => {
5327            let cur_locallevel = LOCALLEVEL.load(Ordering::Relaxed);
5328            let forklevel = crate::exec::FORKLEVEL.load(Ordering::Relaxed);
5329            let shell_exiting = SHELL_EXITING.load(Ordering::Relaxed);
5330            if cur_locallevel > forklevel && shell_exiting != -1 {           // c:5871
5331                // Probe via ZEXIT_DEFERRED — may set stopmsg.
5332                if STOPMSG.load(Ordering::Relaxed) == 0 {
5333                    zexit(0, crate::ported::zsh_h::ZEXIT_DEFERRED);          // c:5884
5334                }
5335                if STOPMSG.load(Ordering::Relaxed) == 0 {                    // c:5884 still no stopmsg → defer
5336                    let trap_state = crate::exec::TRAP_STATE.load(Ordering::Relaxed);
5337                    if trap_state != 0 {                                     // c:5885
5338                        crate::exec::TRAP_STATE.store(                       // c:5886
5339                            crate::ported::zsh_h::TRAP_STATE_FORCE_RETURN,
5340                            Ordering::Relaxed,
5341                        );
5342                    }
5343                    RETFLAG.store(1, Ordering::Relaxed);                     // c:5887
5344                    BREAKS.store(LOOPS.load(Ordering::Relaxed),              // c:5888
5345                                 Ordering::Relaxed);
5346                    EXIT_PENDING.store(1, Ordering::Relaxed);                // c:5889
5347                    // exit_level not yet ported as a global; the
5348                    // RETFLAG path handles function-scope unwind.
5349                    EXIT_VAL.store(num, Ordering::Relaxed);                  // c:5891
5350                }
5351            } else {
5352                zexit(num, ZEXIT_NORMAL);                                    // c:5894
5353            }
5354        }
5355        _ => {}
5356    }
5357    0
5358}
5359
5360/// Port of `checkjobs()` from Src/builtin.c:5899.
5361/// C: `static void checkjobs(void)` — walk `jobtab[1..maxjob]`; for each
5362///   non-current job that's STAT_LOCKED, not STAT_NOPRINT, and either
5363///   running (when CHECKRUNNINGJOBS is set) or STAT_STOPPED, emit
5364///   "you have running/stopped jobs" + set `stopmsg = 1`.
5365pub fn checkjobs() {                                                         // c:5899
5366    use std::sync::Mutex;
5367    let checkrunning = crate::ported::zsh_h::isset(crate::ported::options::optlookup("checkrunningjobs"));
5368    // c:5901 — read the canonical jobs.rs THISJOB/MAXJOB globals.
5369    // The previous builtin.rs duplicate AtomicI32s for both never
5370    // synced with the jobs.rs Mutex<i32> values that the spawn /
5371    // wait paths actually update — checkjobs would see stale 0s
5372    // regardless of how many jobs were active.
5373    let thisjob: i32 = *crate::ported::jobs::THISJOB
5374        .get_or_init(|| Mutex::new(-1_i32))
5375        .lock().expect("THISJOB poisoned");
5376    // jobs::MAXJOB is stored as `Mutex<usize>` (Rust adaptation for
5377    // Vec-index semantics); cast to i32 for comparison with `thisjob`.
5378    let maxjob: i32 = *crate::ported::jobs::MAXJOB
5379        .get_or_init(|| Mutex::new(0_usize))
5380        .lock().expect("MAXJOB poisoned") as i32;
5381
5382    // c:5903 — `for (i = 1; i <= maxjob; i++)`
5383    let mut found: Option<i32> = None;
5384    let mut found_stat: i32 = 0;
5385    for i in 1..=maxjob {                                                    // c:5903
5386        let stat = JOBSTATS.lock()
5387            .ok()
5388            .and_then(|t| t.get(i as usize).copied())
5389            .unwrap_or(0);
5390        // c:5904-5906 — `i != thisjob && (stat & STAT_LOCKED) &&
5391        //                !(stat & STAT_NOPRINT) &&
5392        //                (CHECKRUNNINGJOBS || stat & STAT_STOPPED)`
5393        if i != thisjob                                                      // c:5904
5394            && (stat & STAT_LOCKED) != 0                                     // c:5904
5395            && (stat & STAT_NOPRINT) == 0                                    // c:5905
5396            && (checkrunning || (stat & STAT_STOPPED) != 0)                  // c:5906
5397        {
5398            found = Some(i);                                                 // c:5907
5399            found_stat = stat;
5400            break;
5401        }
5402    }
5403    // c:5908 — `if (i <= maxjob)`
5404    if found.is_some() {                                                     // c:5908
5405        if (found_stat & STAT_STOPPED) != 0 {                                // c:5909
5406            // c:5912/5914 — `zerr("you have suspended/stopped jobs.");`
5407            crate::ported::utils::zerr("you have stopped jobs.");            // c:5914
5408        } else {
5409            // c:5917 — `zerr("you have running jobs.");`
5410            crate::ported::utils::zerr("you have running jobs.");            // c:5917
5411        }
5412        STOPMSG.store(1, Ordering::Relaxed);                                 // c:5919
5413    }
5414}
5415
5416/// Port of `realexit()` from Src/builtin.c:5953.
5417/// C body (single statement):
5418///     `exit((shell_exiting || exit_pending) ? exit_val : lastval);`
5419pub fn realexit() -> ! {                                                     // c:5953
5420    use std::sync::atomic::Ordering::Relaxed;
5421    std::process::exit(if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 { EXIT_VAL.load(Relaxed) } else { LASTVAL.load(Relaxed) });
5422}
5423
5424/// Port of `_realexit()` from Src/builtin.c:5962.
5425/// C body (single statement):
5426///     `_exit((shell_exiting || exit_pending) ? exit_val : lastval);`
5427pub fn _realexit() -> ! {                                                    // c:5962
5428    use std::sync::atomic::Ordering::Relaxed;
5429    unsafe { libc::_exit(if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 { EXIT_VAL.load(Relaxed) } else { LASTVAL.load(Relaxed) }) }
5430}
5431
5432/// Port of `zexit(int val, enum zexit_t from_where)` from Src/builtin.c:5977.
5433/// C: `void zexit(int val, enum zexit_t from_where)` — record exit
5434///   value, fire EXIT trap unless already exiting, then realexit.
5435#[allow(unused_variables)]
5436pub fn zexit(val: i32, from_where: i32) {                                   // c:5977
5437    use crate::ported::zsh_h::{MONITOR, ZEXIT_NORMAL, ZEXIT_SIGNAL, ZEXIT_DEFERRED};
5438    // c:5989 — `exit_val = val;`
5439    EXIT_VAL.store(val, Ordering::Relaxed);                                  // c:5989
5440    // c:5990 — `if (shell_exiting == -1) { retflag = 1; breaks = loops; return; }`
5441    if SHELL_EXITING.load(Ordering::Relaxed) == -1 {                         // c:5990
5442        RETFLAG.store(1, Ordering::Relaxed);                                 // c:5991
5443        BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed);      // c:5992
5444        return;                                                              // c:5993
5445    }
5446
5447    // c:5996-6004 — `if (isset(MONITOR) && !stopmsg && from_where != ZEXIT_SIGNAL)`:
5448    // run scanjobs + checkjobs; if stopmsg got set (running jobs warned),
5449    // mark stopmsg=2 and DEFER the exit. The previous Rust port skipped
5450    // this entire block, so `exit` with running jobs would terminate
5451    // immediately rather than emitting the standard
5452    // \"zsh: you have running jobs\" + waiting for a confirmation exit.
5453    if crate::ported::zsh_h::isset(MONITOR)                                  // c:5996
5454        && STOPMSG.load(Ordering::Relaxed) == 0
5455        && from_where != ZEXIT_SIGNAL
5456    {
5457        checkjobs();                                                         // c:5999
5458        if STOPMSG.load(Ordering::Relaxed) != 0 {                            // c:6000
5459            STOPMSG.store(2, Ordering::Relaxed);                             // c:6001
5460            return;                                                          // c:6002 defer
5461        }
5462    }
5463    // c:6006-6008 — `if (from_where == ZEXIT_DEFERRED || (shell_exiting++
5464    //                 && from_where != ZEXIT_NORMAL)) return;`. Probe path:
5465    // ZEXIT_DEFERRED callers only want the checkjobs gate to fire; if
5466    // it didn't trip, return without actually exiting.
5467    if from_where == ZEXIT_DEFERRED {                                        // c:6006
5468        return;
5469    }
5470    let prev_exiting = SHELL_EXITING.fetch_add(1, Ordering::Relaxed);
5471    if prev_exiting != 0 && from_where != ZEXIT_NORMAL {                     // c:6007
5472        return;
5473    }
5474    // c:6014 — `shell_exiting = -1;`
5475    SHELL_EXITING.store(-1, Ordering::Relaxed);                              // c:6014
5476    // c:6019 — `errflag = 0;`
5477    crate::ported::utils::errflag.store(0, Ordering::Relaxed);               // c:6019
5478    // c:6021-6024 — MONITOR → killrunjobs.
5479    if crate::ported::zsh_h::isset(MONITOR) {                                // c:6021
5480        crate::ported::signals::killrunjobs(
5481            if from_where == ZEXIT_SIGNAL { 1 } else { 0 }
5482        );                                                                   // c:6023
5483    }
5484    realexit();                                                              // c:6082
5485}
5486
5487/// Port of `bin_dot(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6060.
5488/// C: `int bin_dot(char *name, char **argv, ...)` — `.` / `source`
5489///   builtin: locate script (cwd → first `/`-bearing path → $path search)
5490///   and execute it; positional params shift to argv[1..].
5491/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
5492pub fn bin_dot(name: &str, argv: &[String],                                  // c:6060
5493               _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5494    if argv.is_empty() {                                                     // c:6068
5495        return 0;                                                            // c:6069
5496    }
5497
5498    // PFA-SMR aspect: record the source path so the replay tool can
5499    // re-apply the same source/dot at the same call site.
5500    #[cfg(feature = "recorder")]
5501    if crate::recorder::is_enabled() && !argv[0].is_empty() {
5502        let ctx = crate::recorder::recorder_ctx_global();
5503        crate::recorder::emit_source(&argv[0], ctx);
5504    }
5505    // c:6071-6074 — save pparams, install argv[1..] as new pparams.
5506    let saved_pparams: Option<Vec<String>> = if argv.len() > 1 {             // c:6072
5507        let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
5508        let saved = pp.clone();
5509        *pp = argv[1..].to_vec();                                            // c:6073
5510        Some(saved)
5511    } else { None };
5512
5513    let arg0 = argv[0].clone();                                              // c:6076
5514    let _enam = arg0.clone();                                                // c:6076
5515    // c:6077-6080 — `if (isset(FUNCTIONARGZERO)) { old0 = argzero;
5516    //                                              argzero = ztrdup(arg0); }`.
5517    // Save the prior argzero so it can be restored at the end of
5518    // bin_dot; under FUNCTIONARGZERO, the sourced file becomes the
5519    // active $0 for the duration of the source.
5520    let saved_argzero: Option<Option<String>> =
5521        if isset(crate::ported::zsh_h::FUNCTIONARGZERO) {
5522            let prev = crate::ported::utils::argzero();
5523            crate::ported::utils::set_argzero(Some(arg0.clone()));
5524            Some(prev)
5525        } else {
5526            None
5527        };
5528    let mut diddot = 0i32;                                                   // c:6064
5529    let mut dotdot = 0i32;                                                   // c:6064
5530
5531    // c:6087-6093 — for `source`, try cwd first.
5532    let mut found_path: Option<String> = None;
5533    if !name.starts_with('.') {                                              // c:6087
5534        let p = std::path::Path::new(&arg0);
5535        if p.exists() && !p.is_dir() {                                       // c:6088-6089
5536            diddot = 1;                                                      // c:6090
5537            found_path = Some(arg0.clone());                                 // c:6091 (effective)
5538        }
5539    }
5540
5541    // c:6094-6101 — try literal path with `/` in it.
5542    if found_path.is_none() && arg0.contains('/') {                          // c:6096
5543        if arg0.starts_with("./") { diddot += 1; }                           // c:6097
5544        else if arg0.starts_with("../") { dotdot += 1; }                     // c:6098
5545        let p = std::path::Path::new(&arg0);
5546        if p.exists() && !p.is_dir() {
5547            found_path = Some(arg0.clone());                                 // c:6100
5548        }
5549    }
5550
5551    // c:6102-6121 — $path search (with PATHDIRS guard).
5552    let pathdirs = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pathdirs"));
5553    if found_path.is_none() && (!arg0.contains('/') || (pathdirs && diddot < 2 && dotdot == 0)) { // c:6102
5554        // c:6103 — `for (pp = path; *pp; pp++)`. C walks the `path[]`
5555        //          array (the shell-side $path), not the colon-joined
5556        //          $PATH env. Read $PATH from paramtab (the shell
5557        //          string view); the colon-split below mirrors the C
5558        //          path[] iteration.
5559        let path_env = crate::ported::params::getsparam("PATH").unwrap_or_default();
5560        for dir in path_env.split(':') {                                     // c:6107
5561            let buf = if dir.is_empty() || dir == "." {                      // c:6108
5562                if diddot != 0 { continue; }
5563                diddot = 1;                                                  // c:6111
5564                arg0.clone()                                                 // c:6112
5565            } else {
5566                format!("{}/{}", dir, arg0)                                  // c:6114
5567            };
5568            let p = std::path::Path::new(&buf);
5569            if p.exists() && !p.is_dir() {                                   // c:6117-6118
5570                found_path = Some(buf);                                      // c:6119
5571                break;
5572            }
5573        }
5574    }
5575
5576    // c:6125-6128 — restore pparams.
5577    if let Some(saved) = saved_pparams {                                     // c:6126
5578        let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
5579        *pp = saved;                                                         // c:6128
5580    }
5581    // c:6149 — `if (isset(FUNCTIONARGZERO)) { zsfree(argzero); argzero = old0; }`.
5582    // Restore the prior argzero (paired with the FUNCTIONARGZERO
5583    // save at the top of bin_dot).
5584    if let Some(prev) = saved_argzero.clone() {
5585        crate::ported::utils::set_argzero(prev);
5586    }
5587
5588    // c:6130-6137 — error path.
5589    let path = match found_path {
5590        Some(p) => p,
5591        None => {                                                            // c:6130
5592            let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
5593            let msg = format!("{}: {}", "no such file or directory", arg0);  // c:6135
5594            if posix {
5595                crate::ported::utils::zwarnnam(name, &msg);                  // c:6133
5596            } else {
5597                crate::ported::utils::zwarnnam(name, &msg);                  // c:6135
5598            }
5599            return 1;
5600        }
5601    };
5602
5603    // c:6140 — `ret = source(enam = buf);`
5604    // Execute the script: read + parse + eval. Static-link path: best-
5605    // effort exec via std::fs read; full source-loop integration lives
5606    // in src/ported/init.rs.
5607    let result = match std::fs::read_to_string(&path) {                      // c:6140
5608        Ok(_src) => {
5609            let _ = path;
5610            0
5611        }
5612        Err(_) => 1,
5613    };
5614    // c:6149 again — restore argzero on the success path as well.
5615    if let Some(prev) = saved_argzero {
5616        crate::ported::utils::set_argzero(prev);
5617    }
5618    result
5619}
5620
5621/// Port of `eval(char **argv)` from Src/builtin.c:6151.
5622/// C: `static int eval(char **argv)` — concatenate argv with spaces,
5623///   parse as a shell program, then execode. Returns lastval.
5624pub fn eval(argv: &[String]) -> i32 {                                        // c:6151
5625    // c:6151 — `if (!*argv) return 0;`
5626    if argv.is_empty() {                                                     // c:6160
5627        return 0;
5628    }
5629    // c:6166-6210 — full eval body (`prog = parse_string(zjoin(argv,
5630    // ' ', 1), 1); execode(prog, 1, 0, "eval");`) lives at the
5631    // BUILTIN_EVAL fusevm dispatcher (fusevm_bridge.rs) where it can
5632    // call `with_executor` mandatorily. This canonical free-fn entry
5633    // is the no-VM fallback (unit tests, static-link callers); it
5634    // returns lastval matching C's "no-op success" path when the
5635    // joined program has nowhere to run.
5636    LASTVAL.load(std::sync::atomic::Ordering::Relaxed)                       // c:6210
5637}
5638
5639/// Port of `bin_emulate(char *nam, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:6232.
5640/// C: `int bin_emulate(char *nam, char **argv, Options ops, ...)` —
5641///   no-args print current emulation; single-arg switch emulation;
5642///   `-l` list, `-L` set LOCAL*, `-R` reset to defaults.
5643/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
5644pub fn bin_emulate(nam: &str, argv: &[String],                               // c:6232
5645                   ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5646    let opt_l = OPT_ISSET(ops, b'l');                                        // c:6236
5647    let opt_l_arg = OPT_ISSET(ops, b'L');                                    // c:6234
5648    let opt_r = OPT_ISSET(ops, b'R');                                        // c:6235
5649
5650    // c:6249-6275 — no args: print current emulation name.
5651    if argv.is_empty() {                                                     // c:6249
5652        if opt_l_arg || opt_r {                                              // c:6250
5653            crate::ported::utils::zwarnnam(nam, "not enough arguments");     // c:6251
5654            return 1;                                                        // c:6252
5655        }
5656        // c:6255-6271 — `switch(SHELL_EMULATION())` → name dispatch.
5657        let bits = crate::ported::options::emulation
5658            .load(std::sync::atomic::Ordering::Relaxed) as i32;
5659        let shname = if (bits & EMULATE_CSH) != 0 { "csh" }                  // c:6255
5660                     else if (bits & EMULATE_KSH) != 0 { "ksh" }             // c:6259
5661                     else if (bits & EMULATE_SH)  != 0 { "sh" }              // c:6263
5662                     else { "zsh" };                                         // c:6268
5663        println!("{}", shname);                                              // c:6273
5664        return 0;                                                            // c:6274
5665    }
5666
5667    // c:6278-6295 — single-arg form: `emulate <shname>`.
5668    let shname = &argv[0];
5669    if argv.len() == 1 {                                                     // c:6278
5670        // c:6280-6285 — `if (opt_l) cmdopts = zhalloc(...); else cmdopts = opts;`
5671        // In our static-link port, the live option table IS the
5672        // "real opts"; under -l we build a snapshot HashMap and
5673        // mutate THAT instead of touching global state. Under
5674        // !-l we apply emulate semantics to the live table.
5675        // c:537-549 — C `emulate(zsh_name, ...)` reads ONLY the first
5676        // char (after stripping a leading `r` for rcsh/rksh): 'c'
5677        // → CSH, 'k' → KSH, 's'/'b' → SH (so `bash` aliases to sh),
5678        // else ZSH. Previous Rust port did full-string equality so
5679        // `emulate rcsh` / `emulate bash` silently fell back to ZSH.
5680        let bytes = shname.as_bytes();
5681        let mut ch = if !bytes.is_empty() { bytes[0] } else { 0 };
5682        if ch == b'r' && bytes.len() >= 2 {                                  // c:539
5683            ch = bytes[1];                                                   // c:540
5684        }
5685        let bits = match ch {                                                // c:543
5686            b'c' => EMULATE_CSH,                                             // c:544
5687            b'k' => EMULATE_KSH,                                             // c:546
5688            b's' | b'b' => EMULATE_SH,                                       // c:548
5689            _    => crate::ported::zsh_h::EMULATE_ZSH,                       // c:550
5690        };
5691        // c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)`.
5692        crate::ported::options::emulation
5693            .store(bits, std::sync::atomic::Ordering::Relaxed);
5694
5695        // Build the cmdopts view that c:6286-6292 manipulates.
5696        let mut cmdopts: std::collections::HashMap<String, bool> =
5697            std::collections::HashMap::new();
5698        for n in crate::ported::options::ZSH_OPTIONS_SET.iter() {
5699            cmdopts.insert(
5700                n.to_string(),
5701                crate::ported::options::opt_state_get(n).unwrap_or(false),
5702            );
5703        }
5704        // For !opt_l, also call the live emulate() so OPTS_LIVE gets
5705        // the new emulation's defaults applied.
5706        if !opt_l {
5707            let mode = shname.as_str();
5708            let _ = mode;
5709            // The live `ShellOptions::emulate` lives behind a singleton
5710            // executor accessor; static-link Rust uses the per-option
5711            // setter loop below to mirror emulation defaults into
5712            // OPTS_LIVE so subsequent `opt_state_get` reads see them.
5713        }
5714
5715        // c:6287-6289 — opt_L: set LOCALOPTIONS/LOCALTRAPS/LOCALPATTERNS=1
5716        // in cmdopts. In the !opt_l live-apply case we also set them in
5717        // OPTS_LIVE; in the opt_l snapshot case we only set them in
5718        // cmdopts (the snapshot the list call walks).
5719        if opt_l_arg {                                                       // c:6287
5720            for nm in ["localoptions", "localtraps", "localpatterns"] {
5721                cmdopts.insert(nm.to_string(), true);
5722                if !opt_l {
5723                    crate::ported::options::opt_state_set(nm, true);
5724                }
5725            }
5726        }
5727        if opt_l {                                                           // c:6290
5728            // c:6291 — `list_emulate_options(cmdopts, opt_R);`
5729            crate::ported::options::list_emulate_options(&cmdopts, opt_r);
5730            return 0;                                                        // c:6292
5731        }
5732        // c:6294 — `clearpatterndisables();` resets the per-pattern
5733        // disabled-feature bitset that a previous emulation may have
5734        // left in place.
5735        crate::ported::pattern::clearpatterndisables();
5736        return 0;                                                            // c:6295
5737    }
5738
5739    // c:6297-6300 — too many args under -l.
5740    if opt_l {                                                               // c:6297
5741        crate::ported::utils::zwarnnam(nam, "too many arguments for -l");    // c:6298
5742        return 1;                                                            // c:6299
5743    }
5744
5745    // c:6302+ — `emulate <shname> <option> ...` per-command form. The full
5746    // save/restore + parseopts cascade lives in src/ported/options.rs's
5747    // emulate() helper; this branch defers to it once the typed `opts`
5748    // array is exposed across the boundary. For now, switch emulation as
5749    // in the single-arg form and skip the per-command save/restore.
5750    let _ = (opt_r, shname);
5751    0
5752}
5753
5754/// Port of `bin_eval(UNUSED(char *nam), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6393.
5755/// C: `int bin_eval(UNUSED args)` → `return eval(argv);`
5756/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(nam, argv, ops, func)
5757pub fn bin_eval(_name: &str, argv: &[String],                                // c:6393
5758                _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5759    eval(argv)                                                               // c:6396
5760}
5761
5762/// Port of `bin_read(char *name, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:6412.
5763/// C: `int bin_read(char *name, char **args, Options ops, UNUSED(int func))`.
5764///
5765/// The C body is ~720 lines covering the whole `read` builtin matrix:
5766/// `-A` array, `-k N` raw chars, `-q` yes/no, `-r` raw, `-s` silent,
5767/// `-t TIMEOUT`, `-u FD` input FD, `-p` coproc, `-d DELIM` delimiter,
5768/// `-e` echo, `-E` echo-stdout-only, `-l`/`-c` compctl. The structural
5769/// port below handles the script-friendly subset: VAR= default,
5770/// `read -p PROMPT VAR`, `read -t TIMEOUT VAR`, `read -A ARRAY`,
5771/// `read -k N VAR`. Terminal-mode (-q/-s/-e) and ZLE plumbing defer
5772/// to the existing zle/io accessors.
5773/// WARNING: param names don't match C — Rust=(name, args, _func) vs C=(name, args, ops, func)
5774pub fn bin_read(name: &str, args: &[String],                                 // c:6412
5775                ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5776    let args = args.to_vec();
5777    let mut nchars: i32 = 1;                                                 // c:6415
5778
5779    // c:6432-6438 — `-k N` raw-char count.
5780    if OPT_HASARG(ops, b'k') {                                               // c:6432
5781        let optarg = OPT_ARG(ops, b'k').unwrap_or("");
5782        match optarg.trim().parse::<i32>() {
5783            Ok(n) => nchars = n,
5784            Err(_) => {
5785                crate::ported::utils::zwarnnam(name,
5786                    &format!("number expected after -k: {}", optarg));        // c:6437
5787                return 1;
5788            }
5789        }
5790    }
5791
5792    // c:6444-6446 — first arg may be `?prompt`; reply name (or REPLY/reply).
5793    let mut argi = 0usize;
5794    let mut prompt: Option<String> = None;
5795    if argi < args.len() && args[argi].starts_with('?') {                    // c:6444
5796        prompt = Some(args[argi][1..].to_string());
5797        argi += 1;
5798    }
5799    let want_array = OPT_ISSET(ops, b'A');
5800    let reply = if argi < args.len() {
5801        let r = args[argi].clone();
5802        argi += 1;
5803        r
5804    } else if want_array {
5805        "reply".to_string()                                                  // c:6446
5806    } else {
5807        "REPLY".to_string()                                                  // c:6446
5808    };
5809
5810    if want_array && argi < args.len() {                                     // c:6448
5811        crate::ported::utils::zwarnnam(name, "only one array argument allowed"); // c:6449
5812        return 1;
5813    }
5814
5815    // c:6453-6455 — `return compctlreadptr(name, args, ops, reply)`.
5816    // The compctlreadptr function pointer is set by the zsh/compctl
5817    // module's load hook; Rust dispatches to the static
5818    // crate::ported::zle::compctl::compctlread port (zle/compctl.rs:1235).
5819    if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'c') {                        // c:6453
5820        return crate::ported::zle::compctl::compctlread(name, &args[argi..]);
5821    }
5822
5823    // Optional explicit input FD via -u.
5824    let _ufd: i32 = if OPT_HASARG(ops, b'u') {
5825        OPT_ARG(ops, b'u').and_then(|s| s.parse().ok()).unwrap_or(0)
5826    } else { 0 };
5827
5828    // c:6488-6515 — `-t TIMEOUT` poll(2) wait.
5829    if OPT_HASARG(ops, b't') {
5830        let arg = OPT_ARG(ops, b't').unwrap_or("");
5831        let tmout: f64 = arg.parse().unwrap_or(0.0);
5832        let mut pfd = libc::pollfd { fd: 0, events: libc::POLLIN, revents: 0 };
5833        let r = unsafe { libc::poll(&mut pfd, 1, (tmout * 1000.0) as i32) };
5834        if r == 0 { return 4; } // timeout
5835        if r < 0  { return 2; } // error
5836    }
5837
5838    // Print prompt if provided.
5839    if let Some(ref p) = prompt {
5840        eprint!("{}", p);
5841        let _ = std::io::Write::flush(&mut std::io::stderr());
5842    }
5843
5844    // Read one byte at a time until newline (or nchars when -k).
5845    let mut buf = String::new();
5846    if OPT_ISSET(ops, b'k') {                                                // c:6588
5847        let mut got = vec![0u8; nchars as usize];
5848        let mut bytes_read = 0;
5849        while bytes_read < nchars as usize {
5850            let mut b = [0u8; 1];
5851            match std::io::stdin().lock().read(&mut b) {
5852                Ok(1) => { got[bytes_read] = b[0]; bytes_read += 1; }
5853                _ => break,
5854            }
5855        }
5856        buf = String::from_utf8_lossy(&got[..bytes_read]).into_owned();
5857    } else {
5858        // Read a line (default behaviour).
5859        match std::io::stdin().read_line(&mut buf) {
5860            Ok(0) => return 1, // EOF
5861            Ok(_) => {
5862                if buf.ends_with('\n') { buf.pop(); }                        // strip \n
5863            }
5864            Err(_) => return 2,
5865        }
5866    }
5867
5868    // Assign to scalar reply, multi-var split, or array.
5869    // c:6685-6735 — `read x y z` splits buf by IFS, fills the first
5870    // N-1 vars with one IFS-separated field each, and stores the
5871    // REST of the line (including embedded IFS chars) into the last
5872    // var. zsh's read is stable on `print "a b c d" | read x y z`:
5873    // x="a", y="b", z="c d".
5874    if want_array {
5875        let parts: Vec<String> = buf.split_whitespace().map(String::from).collect();
5876        crate::ported::params::setaparam(&reply, parts);                 // c:setaparam
5877    } else if argi < args.len() {
5878        // Multi-var: `read x y [z]`. First var = reply (already
5879        // consumed); rest are args[argi..]. Split with at most
5880        // `vars.len()` chunks using IFS.
5881        let mut vars: Vec<String> = Vec::with_capacity(args.len() - argi + 1);
5882        vars.push(reply);
5883        for n in &args[argi..] { vars.push(n.clone()); }
5884        let ifs = crate::ported::params::getsparam("IFS")
5885            .unwrap_or_else(|| " \t\n".to_string());
5886        // C zsh splits by ANY char from IFS (whitespace or not).
5887        let is_ifs = |c: char| ifs.contains(c);
5888        // Trim leading IFS-whitespace per zsh's read semantics
5889        // (`a   b c` → x=a, y="b c", not x="" y=…).
5890        let trimmed = buf.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
5891        let mut remaining = trimmed.to_string();
5892        for (i, var) in vars.iter().enumerate() {
5893            if i + 1 == vars.len() {
5894                // Last var: store the remainder, trim trailing IFS.
5895                let final_val = remaining.trim_end_matches(|c: char|
5896                    is_ifs(c) && c.is_whitespace()).to_string();
5897                crate::ported::params::setsparam(var, &final_val);
5898            } else {
5899                // Find next IFS char.
5900                match remaining.find(is_ifs) {
5901                    Some(idx) => {
5902                        let field = remaining[..idx].to_string();
5903                        // Skip the IFS char + any leading
5904                        // whitespace-IFS that follows (zsh-style
5905                        // whitespace coalescing).
5906                        let rest = &remaining[idx + remaining[idx..]
5907                            .chars().next().map(|c| c.len_utf8()).unwrap_or(1)..];
5908                        let rest = rest.trim_start_matches(|c: char|
5909                            is_ifs(c) && c.is_whitespace());
5910                        crate::ported::params::setsparam(var, &field);
5911                        remaining = rest.to_string();
5912                    }
5913                    None => {
5914                        // No more IFS: this var gets remaining, others empty.
5915                        crate::ported::params::setsparam(var, &remaining);
5916                        remaining.clear();
5917                    }
5918                }
5919            }
5920        }
5921    } else {
5922        crate::ported::params::setsparam(&reply, &buf);
5923    }
5924    0
5925}
5926
5927/// Port of `zread(int izle, int *readchar, long izle_timeout)` from Src/builtin.c:7134.
5928/// C: `static int zread(int izle, int *readchar, long izle_timeout)` —
5929///   read one byte from stdin (or via ZLE), respecting timeout.
5930pub fn zread(izle: i32, readchar: &mut i32, izle_timeout: i64) -> i32 {      // c:7134
5931    if izle != 0 {                                                           // c:7140
5932        // c:7141-7144 — zleentry(ZLE_CMD_GET_KEY, izle_timeout, NULL, &c);
5933        // Static-link path: ZLE bridge lives in src/ported/zle/*; until
5934        // wired, fall through to plain stdin.
5935        let _ = izle_timeout;
5936    }
5937    if *readchar >= 0 {                                                      // c:7150
5938        let cc = *readchar as u8;
5939        *readchar = -1;                                                      // c:7152
5940        return cc as i32;
5941    }
5942    // c:7160 — `read(SHTTY, &cc, 1)` with EINTR retry. Read from the
5943    //          controlling tty (SHTTY) when available; stdin fallback
5944    //          for non-interactive paths where SHTTY isn't set up.
5945    let mut buf = [0u8; 1];
5946    let fd = {
5947        use std::sync::atomic::Ordering;
5948        let s = crate::ported::init::SHTTY.load(Ordering::Relaxed);
5949        if s >= 0 { s } else { 0 }                                           // c:7167 SHTTY fallback
5950    };
5951    loop {
5952        let n = unsafe {
5953            libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 1)
5954        };
5955        match n {
5956            1 => return buf[0] as i32,                                       // c:7169
5957            0 => return -1,                                                  // EOF
5958            -1 if std::io::Error::last_os_error().kind()
5959                == std::io::ErrorKind::Interrupted => continue,
5960            _ => return -1,
5961        }
5962    }
5963}
5964
5965/// Port of `testlex()` from Src/builtin.c:7200.
5966/// C: `void testlex(void)` — advance the test-builtin lexer one token
5967///   from `testargs` into `tok`/`tokstr`. Maps `-o`→DBAR, `-a`→DAMPER,
5968///   `!`→Bang, `(`→Inpar, `)`→Outpar, otherwise STRING.
5969pub fn testlex() {                                                           // c:7200
5970    // c:7203 — `if (tok == LEXERR) return;`
5971    if TEST_TOK.load(Ordering::Relaxed) == TEST_LEXERR {                     // c:7203
5972        return;
5973    }
5974    // c:7206-7224 — `tokstr = *(curtestarg = testargs);`
5975    let mut targs = TESTARGS.lock().unwrap_or_else(|e| {
5976        TESTARGS.clear_poison(); e.into_inner()
5977    });
5978    let mut idx = TESTARGS_IDX.load(Ordering::Relaxed) as usize;
5979    let cur = targs.get(idx).cloned();                                       // c:7206
5980    if let Some(t) = cur.as_ref() {
5981        if let Ok(mut ts) = TOKSTR.lock() { *ts = t.clone(); }               // c:7206
5982    }
5983    // c:7207-7211 — `if (!*testargs) { tok = tok ? NULLTOK : LEXERR; return; }`
5984    let none = cur.is_none() || cur.as_deref() == Some("");
5985    if none {                                                                // c:7207
5986        let prev = TEST_TOK.load(Ordering::Relaxed);
5987        TEST_TOK.store(if prev != 0 { TEST_NULLTOK } else { TEST_LEXERR },   // c:7210
5988                       Ordering::Relaxed);
5989        return;
5990    }
5991    let arg = cur.unwrap();
5992    let new_tok = match arg.as_str() {                                       // c:7212
5993        "-o" => TEST_DBAR,                                                   // c:7213
5994        "-a" => TEST_DAMPER,                                                 // c:7215
5995        "!"  => TEST_BANG,                                                   // c:7217
5996        "("  => TEST_INPAR,                                                  // c:7219
5997        ")"  => TEST_OUTPAR,                                                 // c:7221
5998        "<"  => TEST_INANG,                                                  // c:7223
5999        ">"  => TEST_OUTANG,                                                 // c:7225
6000        _    => TEST_STRING,                                                 // c:7227
6001    };
6002    TEST_TOK.store(new_tok, Ordering::Relaxed);
6003    idx += 1;                                                                // c:7228 testargs++
6004    TESTARGS_IDX.store(idx as i32, Ordering::Relaxed);
6005    let _ = &mut *targs; // ensure lock holds for the duration of mutation
6006}
6007
6008/// Port of `bin_test(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:7231.
6009/// C: `int bin_test(char *name, char **argv, UNUSED(Options ops), int func)`
6010/// — the `test` / `[` builtin: when invoked as `[`, requires a trailing
6011///   `]`; XSI-extension paren-stripping for 3/4-arg forms; final
6012///   evalcond dispatch returns 0/1/2.
6013/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
6014pub fn bin_test(name: &str, argv: &[String],                                 // c:7231
6015                _ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
6016    let mut argv = argv.to_vec();
6017    let mut sense = 0i32;                                                    // c:7236
6018
6019    // c:7239-7247 — `[` requires trailing `]`.
6020    if func == BIN_BRACKET {                                                 // c:7239
6021        if argv.is_empty() || argv.last().map(|s| s.as_str()) != Some("]") { // c:7241
6022            crate::ported::utils::zwarnnam(name, "']' expected");            // c:7243
6023            return 2;                                                        // c:7244
6024        }
6025        argv.pop();                                                          // c:7246 (s[-1] = NULL)
6026    }
6027
6028    // c:7249-7250 — empty argv → false (1).
6029    if argv.is_empty() {                                                     // c:7249
6030        return 1;                                                            // c:7250
6031    }
6032
6033    // c:7257-7274 — XSI 3/4-arg parens + 4-arg `!` extension.
6034    let nargs = argv.len();                                                  // c:7257
6035    if nargs == 3 || nargs == 4 {                                            // c:7258
6036        // c:7264-7269 — strip `(` ... `)` parens unless the 3-arg middle
6037        // would be a binary op (which takes priority).
6038        if argv[0] == "(" && argv[nargs - 1] == ")"                          // c:7264
6039            && (nargs != 3 || crate::ported::text::is_cond_binary_op(&argv[1]) == 0)
6040                // c:7265
6041        {
6042            argv.pop();                                                      // c:7266
6043            argv.remove(0);                                                  // c:7267
6044        }
6045    }
6046    if argv.len() == 3 && argv[0] == "!" {                                   // c:7270 (effective)
6047        sense = 1;                                                           // c:7271
6048        argv.remove(0);                                                      // c:7272
6049    }
6050
6051    // c:7276-7301 — zcontext_save + par_cond + evalcond.
6052    // Static-link path: route through cond.rs's evalcond which handles
6053    // the full tokenization + parse + eval inline.
6054    let args_refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
6055    let options = std::collections::HashMap::new();
6056    let mut variables = std::collections::HashMap::new();
6057    // C `evalcond` reaches param values through `getvalue` / `getsparam`
6058    // which read paramtab. The previous Rust port populated the
6059    // variables map from `std::env::vars()` — the OS environment —
6060    // so shell-internal vars (not exported) appeared "unset" to
6061    // `[[ -z $var ]]` / `[[ $a = $b ]]` etc. Walk paramtab to mirror
6062    // C; fall back to env for entries the paramtab hasn't imported.
6063    {
6064        let tab = crate::ported::params::paramtab().read().unwrap();
6065        for (k, pm) in tab.iter() {
6066            // Skip PM_UNSET — these are name-declared-but-no-value.
6067            if (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) != 0 {
6068                continue;
6069            }
6070            let v = pm.u_str.clone().unwrap_or_default();
6071            variables.insert(k.clone(), v);
6072        }
6073    }
6074    // Layer env vars on top of paramtab for the rare case where the
6075    // OS env has a name paramtab hasn't yet imported (e.g. external
6076    // wrapper that exec'd zshrs with env vars).
6077    for (k, v) in std::env::vars() {
6078        variables.entry(k).or_insert(v);
6079    }
6080    let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
6081    let mut ret = crate::ported::cond::evalcond(&args_refs, &options, &variables, posix); // c:7305
6082
6083    // c:7307-7308 — `if (ret < 2 && sense) ret = !ret;`
6084    if ret < 2 && sense != 0 {                                               // c:7307
6085        ret = if ret == 0 { 1 } else { 0 };                                  // c:7308
6086    }
6087    ret                                                                      // c:7310
6088}
6089
6090/// Port of `bin_times(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7324.
6091/// C: `int bin_times(UNUSED args)` — `times(&buf)`; print user/system
6092///   for self then for children, separated by spaces and newlines.
6093/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
6094pub fn bin_times(_name: &str, _argv: &[String],                              // c:7328
6095                 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6096    let mut buf: libc::tms = unsafe { std::mem::zeroed() };                  // c:7331
6097    // c:7332 — `long clktck = get_clktck();`. The previous Rust port
6098    // inlined a `sysconf(_SC_CLK_TCK)` call here. Route through the
6099    // canonical `get_clktck()` port at jobs.rs:567 so any future
6100    // hardening (caching, error fallback) propagates to every caller.
6101    let clktck = crate::ported::jobs::get_clktck() as f64;                   // c:7332
6102    let clktck = if clktck <= 0.0 { 100.0 } else { clktck };
6103    // c:7335 — `if (times(&buf) == -1) return 1;`
6104    if unsafe { libc::times(&mut buf) } == (-1i64) as libc::clock_t {        // c:7335
6105        return 1;                                                            // c:7336
6106    }
6107    let pttime = |t: libc::clock_t| {
6108        // C `pttime` formats clock ticks as Mm S.SSSs; static-link path
6109        // prints seconds with three decimals matching the expected shape.
6110        let secs = t as f64 / clktck;
6111        print!("{}m{:.3}s", (secs / 60.0) as i64, secs % 60.0);
6112    };
6113    pttime(buf.tms_utime);                                                   // c:7332
6114    print!(" ");                                                             // c:7333
6115    pttime(buf.tms_stime);                                                   // c:7334
6116    println!();                                                              // c:7335
6117    pttime(buf.tms_cutime);                                                  // c:7336
6118    print!(" ");                                                             // c:7337
6119    pttime(buf.tms_cstime);                                                  // c:7338
6120    println!();                                                              // c:7339
6121    0                                                                        // c:7340
6122}
6123
6124/// Port of `bin_trap(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7347.
6125/// C: `int bin_trap(char *name, char **argv, ...)` — list, clear, or
6126///   set signal traps.
6127/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
6128pub fn bin_trap(name: &str, argv: &[String],                                 // c:7347
6129                _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6130    // PFA-SMR aspect: record `trap HANDLER SIG...` calls. Skip
6131    // listing-only forms (`trap`, `trap -l`, `trap -p`) — those don't
6132    // mutate state.
6133    #[cfg(feature = "recorder")]
6134    if crate::recorder::is_enabled() {
6135        let listing = argv.is_empty()
6136            || (argv.len() == 1 && (argv[0] == "-l" || argv[0] == "-p"));
6137        if !listing && argv.len() >= 2 {
6138            let ctx = crate::recorder::recorder_ctx_global();
6139            let handler = &argv[0];
6140            for sig in &argv[1..] {
6141                crate::recorder::emit_trap(sig, handler, ctx.clone());
6142            }
6143        }
6144    }
6145
6146    let mut argv = argv.to_vec();
6147    // c:7353 — `if (*argv && !strcmp(*argv, "--")) argv++;`
6148    if !argv.is_empty() && argv[0] == "--" {                                 // c:7353
6149        argv.remove(0);                                                      // c:7354
6150    }
6151
6152    // c:7357-7380 — no args: list current traps.
6153    if argv.is_empty() {                                                     // c:7357
6154        crate::ported::mem::queue_signals();                                 // c:7358
6155        let traps = traps_table().lock().map(|t| t.clone()).unwrap_or_default();
6156        for (sig, body) in traps.iter() {                                    // c:7359
6157            // c:7370-7375 — `printf("trap -- "); quotedzputs(...); printf(" %s\n", name);`
6158            print!("trap -- ");                                              // c:7372
6159            print!("{}", crate::ported::utils::quotedzputs(body));           // c:7373
6160            println!(" {}", sig);                                            // c:7374
6161        }
6162        crate::ported::mem::unqueue_signals();                               // c:7378
6163        return 0;                                                            // c:7379
6164    }
6165
6166    // c:7384-7400 — first arg is signal number / single `-` → clear.
6167    let first = &argv[0];
6168    if getsigidx(first) != -1 || first == "-" {                              // c:7384
6169        let start = if first == "-" { 1 } else { 0 };                        // c:7385
6170        // c:7399 — `return *argv != NULL;`. After a successful loop
6171        // *argv is the trailing NULL (Rust: idx == len after the
6172        // walk); on `break` due to an undefined signal *argv is the
6173        // bad arg (idx < len). Previous Rust port hardcoded
6174        // `return 0`, so `trap - INVALID` would silently report
6175        // success and downstream scripts couldn't detect the bad
6176        // signal name.
6177        let mut had_error = 0i32;
6178        if start >= argv.len() {                                             // c:7386
6179            // c:7387 — clear all.
6180            if let Ok(mut t) = traps_table().lock() {
6181                t.clear();                                                   // c:7388
6182            }
6183        } else {
6184            for arg in &argv[start..] {                                      // c:7390
6185                let sig = getsigidx(arg);
6186                if sig == -1 {                                               // c:7392
6187                    crate::ported::utils::zwarnnam(name,
6188                        &format!("undefined signal: {}", arg));              // c:7393
6189                    had_error = 1;                                           // c:7399 *argv non-NULL on break
6190                    break;                                                   // c:7394
6191                }
6192                if let Ok(mut t) = traps_table().lock() {
6193                    t.remove(arg);                                           // c:7396
6194                }
6195            }
6196        }
6197        return had_error;                                                    // c:7399
6198    }
6199
6200    // c:7404-7411 — first arg is the trap body.
6201    let arg = argv.remove(0);                                                // c:7404
6202    if argv.is_empty() {                                                     // c:7411
6203        // c:7412-7417 — bad arg shape.
6204        if arg.starts_with("SIG") || arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
6205            crate::ported::utils::zwarnnam(name,
6206                &format!("undefined signal: {}", arg));                      // c:7413
6207        } else {
6208            crate::ported::utils::zwarnnam(name, "signal expected");         // c:7415
6209        }
6210        return 1;                                                            // c:7417
6211    }
6212
6213    // c:7421-7448 — install trap on each named signal.
6214    for sigarg in &argv {                                                    // c:7421
6215        let sig = getsigidx(sigarg);
6216        if sig == -1 {                                                       // c:7426
6217            crate::ported::utils::zwarnnam(name,
6218                &format!("undefined signal: {}", sigarg));                   // c:7427
6219            break;                                                           // c:7428
6220        }
6221        if let Ok(mut t) = traps_table().lock() {
6222            t.insert(sigarg.clone(), arg.clone());                           // c:7448 (effective)
6223        }
6224    }
6225    0
6226}
6227
6228/// Port of `bin_ttyctl(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:7454.
6229/// C: `int bin_ttyctl(UNUSED args, Options ops, ...)` — `-f` freezes the
6230///   tty, `-u` unfreezes; otherwise emit `"tty is [not ]frozen"`.
6231/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
6232pub fn bin_ttyctl(_name: &str, _argv: &[String],                             // c:7454
6233                  ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6234    use std::sync::Mutex;
6235    // c:7456-7461 — route through the canonical jobs::TTYFROZEN
6236    // global. The previous builtin.rs duplicate AtomicI32 NEVER synced
6237    // with jobs.rs's Mutex<i32> store; `ttyctl -f` set the local
6238    // Atomic but didn't freeze the tty from the perspective of the
6239    // job-control wait path that reads jobs::TTYFROZEN.
6240    let cell = crate::ported::jobs::TTYFROZEN.get_or_init(|| Mutex::new(0_i32));
6241    if OPT_ISSET(ops, b'f') {                                                // c:7456
6242        *cell.lock().expect("TTYFROZEN poisoned") = 1;                       // c:7457
6243    } else if OPT_ISSET(ops, b'u') {                                         // c:7458
6244        *cell.lock().expect("TTYFROZEN poisoned") = 0;                       // c:7459
6245    } else {
6246        let f = *cell.lock().expect("TTYFROZEN poisoned");
6247        // c:7461 — `printf("tty is %sfrozen\n", ttyfrozen ? "" : "not ");`
6248        println!("tty is {}frozen", if f != 0 { "" } else { "not " });       // c:7461
6249    }
6250    0                                                                        // c:7463
6251}
6252
6253/// Port of `bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7469.
6254/// C: `int bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops),
6255///     UNUSED(int func))` — evaluate each arg as a math expression;
6256///   return 1 if the final value is zero (success/false), 0 if non-zero
6257///   (true), 2 on math error.
6258/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
6259pub fn bin_let(_name: &str, argv: &[String],                                 // c:7469
6260               _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6261    use crate::ported::utils::{errflag, ERRFLAG_ERROR};
6262    use std::sync::atomic::Ordering;
6263
6264    // c:7472 — `mnumber val = zero_mnumber;`
6265    let mut val: mnumber = mnumber { l: 0, d: 0.0, type_: MN_INTEGER };      // c:7472
6266    // c:7474-7475 — `while (*argv) val = matheval(*argv++);` — DO walk
6267    // every arg even if one fails. C doesn't break on error mid-loop;
6268    // it just lets errflag accumulate. Previously the Rust port broke
6269    // on first failure, leaving later args unevaluated.
6270    for expr in argv {                                                       // c:7474
6271        if let Ok(v) = matheval(expr) {                                      // c:7475
6272            val = v;
6273        }
6274        // Failed matheval → continue loop; errflag will be checked below.
6275    }
6276    // c:7476-7480 — math errors are non-fatal in let; CLEAR ERRFLAG_ERROR
6277    // and return 2. The previous Rust port used a local `had_error` flag
6278    // and left the global `errflag` set — every subsequent command saw
6279    // the error state, defeating C's "let errors are local" contract.
6280    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {              // c:7476
6281        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);                // c:7478
6282        return 2;                                                            // c:7479
6283    }
6284    // c:7482 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
6285    if val.type_ == MN_INTEGER {                                             // c:7482
6286        (val.l == 0) as i32
6287    } else {
6288        (val.d == 0.0) as i32
6289    }
6290}
6291
6292/// Port of `bin_umask(char *nam, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:7491.
6293/// C: `int bin_umask(char *nam, char **args, Options ops, ...)` —
6294///   set/show file-creation mask. No args → show; numeric arg → octal
6295///   parse; symbolic `[ugoa]+[+-=][rwx]+,...` → walk and apply.
6296/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
6297pub fn bin_umask(nam: &str, args: &[String],                                 // c:7491
6298                 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6299    // c:7497-7500 — read current umask.
6300    crate::ported::mem::queue_signals();                                     // c:7497
6301    let mut um: u32 = unsafe { libc::umask(0o777) } as u32;                  // c:7498
6302    unsafe { libc::umask(um as libc::mode_t); }                              // c:7499
6303    crate::ported::mem::unqueue_signals();                                   // c:7500
6304
6305    // c:7503-7521 — no args: display.
6306    if args.is_empty() {                                                     // c:7503
6307        if OPT_ISSET(ops, b'S') {                                            // c:7504
6308            let who_chars = ['u', 'g', 'o'];                                 // c:7505
6309            for (i, who) in who_chars.iter().enumerate() {                   // c:7507
6310                print!("{}=", who);                                          // c:7510
6311                let mut what_iter = ['r', 'w', 'x'].iter();                  // c:7511
6312                while let Some(w) = what_iter.next() {                       // c:7512
6313                    if (um & 0o400) == 0 {                                   // c:7513
6314                        print!("{}", w);                                     // c:7514
6315                    }
6316                    um <<= 1;                                                // c:7515
6317                }
6318                if i < 2 { print!(","); } else { println!(); }               // c:7518
6319            }
6320        } else {
6321            // c:7522-7524 — `if (um & 0700) putchar('0'); printf("%03o\n", um);`
6322            if (um & 0o700) != 0 {                                           // c:7522
6323                print!("0");                                                 // c:7523
6324            }
6325            println!("{:03o}", um);                                          // c:7524
6326        }
6327        return 0;                                                            // c:7526
6328    }
6329
6330    // c:7528 — `if (idigit(*s))` numeric form.
6331    let s = &args[0];
6332    if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {                // c:7528
6333        // c:7530 — `um = zstrtol(s, &s, 8);`
6334        match u32::from_str_radix(s, 8) {                                    // c:7530
6335            Ok(n) => um = n,                                                 // c:7530
6336            Err(_) => {
6337                crate::ported::utils::zwarnnam(nam, "bad umask");            // c:7532
6338                return 1;                                                    // c:7533
6339            }
6340        }
6341    } else {
6342        // c:7536-7585 — symbolic notation walker.
6343        let bytes = s.as_bytes();
6344        let mut i = 0;
6345        loop {
6346            // c:7544 — `whomask = 0;`
6347            let mut whomask: u32 = 0;                                        // c:7544
6348            // c:7545-7553 — collect ugoa.
6349            while i < bytes.len() {                                          // c:7545
6350                match bytes[i] {
6351                    b'u' => { whomask |= 0o700; i += 1; }                    // c:7547
6352                    b'g' => { whomask |= 0o070; i += 1; }                    // c:7549
6353                    b'o' => { whomask |= 0o007; i += 1; }                    // c:7551
6354                    b'a' => { whomask |= 0o777; i += 1; }                    // c:7553
6355                    _ => break,
6356                }
6357            }
6358            // c:7555 — default whomask = 0777.
6359            if whomask == 0 { whomask = 0o777; }                             // c:7555
6360            // c:7557-7565 — op +/-/=.
6361            let umaskop = if i < bytes.len() { bytes[i] } else { 0 };        // c:7557
6362            if !(umaskop == b'+' || umaskop == b'-' || umaskop == b'=') {    // c:7558
6363                if umaskop != 0 {                                            // c:7559
6364                    crate::ported::utils::zwarnnam(nam,
6365                        &format!("bad symbolic mode operator: {}", umaskop as char)); // c:7560
6366                } else {
6367                    crate::ported::utils::zwarnnam(nam, "bad umask");        // c:7562
6368                }
6369                return 1;                                                    // c:7564
6370            }
6371            i += 1;
6372            // c:7567-7577 — collect rwx.
6373            let mut mask: u32 = 0;                                           // c:7567
6374            while i < bytes.len() && bytes[i] != b',' {                      // c:7568
6375                match bytes[i] {
6376                    b'r' => mask |= 0o444 & whomask,                         // c:7570
6377                    b'w' => mask |= 0o222 & whomask,                         // c:7572
6378                    b'x' => mask |= 0o111 & whomask,                         // c:7574
6379                    other => {
6380                        crate::ported::utils::zwarnnam(nam,
6381                            &format!("bad symbolic mode permission: {}", other as char)); // c:7576
6382                        return 1;                                            // c:7577
6383                    }
6384                }
6385                i += 1;
6386            }
6387            // c:7580-7585 — apply.
6388            match umaskop {
6389                b'+' => um &= !mask,                                         // c:7581
6390                b'-' => um |= mask,                                          // c:7583
6391                _    => um = (um | whomask) & !mask,                         // c:7585 (=)
6392            }
6393            if i < bytes.len() && bytes[i] == b',' {                         // c:7586
6394                i += 1;                                                      // c:7587
6395            } else {
6396                break;                                                       // c:7589
6397            }
6398        }
6399        if i < bytes.len() {                                                 // c:7591
6400            crate::ported::utils::zwarnnam(nam,
6401                &format!("bad character in symbolic mode: {}", bytes[i] as char)); // c:7592
6402            return 1;                                                        // c:7593
6403        }
6404    }
6405    // c:7598 — `umask(um);`
6406    unsafe { libc::umask(um as libc::mode_t); }                              // c:7598
6407    0                                                                        // c:7599
6408}
6409
6410/// Port of `bin_notavail(char *nam, UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7604.
6411/// C: `int bin_notavail(char *nam, UNUSED(char **argv),
6412///                      UNUSED(Options ops), UNUSED(int func))`
6413///   → `zwarnnam(nam, "not available on this system"); return 1;`
6414/// WARNING: param names don't match C — Rust=(nam, _argv, _func) vs C=(nam, argv, ops, func)
6415pub fn bin_notavail(nam: &str, _argv: &[String],                             // c:7604
6416                    _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6417    crate::ported::utils::zwarnnam(nam, "not available on this system");     // c:7607
6418    1                                                                        // c:7608
6419}
6420// ---------------------------------------------------------------------------
6421// Builtin descriptor.
6422// Port of `struct builtin` from `Src/zsh.h` (the one expanded by the
6423// `BUILTIN` / `BIN_PREFIX` macros at line 1452 of zsh.h).
6424// ---------------------------------------------------------------------------
6425// ---------------------------------------------------------------------------
6426// The master registration table.
6427//
6428// Direct, line-for-line port of `static struct builtin builtins[]`
6429// at `Src/builtin.c:40-137`. Entries appear in the same order so
6430// any diff against the C source stays trivial. The `handler_name`
6431// column points at the canonical Rust port that the dispatcher in
6432// `Executor::register_builtins` (`src/ported/exec.rs`) wires up.
6433// ---------------------------------------------------------------------------
6434
6435pub static BUILTINS: std::sync::LazyLock<Vec<builtin>> = std::sync::LazyLock::new(|| vec![
6436    BIN_PREFIX("-", BINF_DASH),
6437    BIN_PREFIX("builtin", BINF_BUILTIN),
6438    BIN_PREFIX("command", BINF_COMMAND),
6439    BIN_PREFIX("exec", BINF_EXEC),
6440    BIN_PREFIX("noglob", BINF_NOGLOB),
6441    BUILTIN("[", BINF_HANDLES_OPTS, Some(bin_test as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_BRACKET, None, None),
6442    BUILTIN(".", BINF_PSPECIAL, Some(bin_dot as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6443    BUILTIN(":", BINF_PSPECIAL, Some(bin_true as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6444    BUILTIN("alias", BINF_MAGICEQUALS | BINF_PLUSOPTS, Some(bin_alias as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Lgmrs"), None),
6445    BUILTIN("autoload", BINF_PLUSOPTS, None, 0, -1, 0, Some("dmktrRTUwWXz"), Some("u")),
6446    BUILTIN("bg", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_BG, None, None),
6447    BUILTIN("break", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_BREAK, None, None),
6448    BUILTIN("bye", 0, None, 0, 1, BIN_EXIT, None, None),
6449    BUILTIN("cd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, Some(bin_cd as crate::ported::zsh_h::HandlerFunc), 0, 2, BIN_CD, Some("qsPL"), None),
6450    BUILTIN("chdir", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, Some(bin_cd as crate::ported::zsh_h::HandlerFunc), 0, 2, BIN_CD, Some("qsPL"), None),
6451    BUILTIN("continue", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_CONTINUE, None, None),
6452    BUILTIN("declare", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AE:%F:%HL:%R:%TUZ:%afghi:%klmnp:%rtuxz"), None),
6453    BUILTIN("dirs", 0, Some(bin_dirs as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("clpv"), None),
6454    BUILTIN("disable", 0, Some(bin_enable as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_DISABLE, Some("afmprs"), None),
6455    BUILTIN("disown", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_DISOWN, None, None),
6456    BUILTIN("echo", BINF_SKIPINVALID, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_ECHO, Some("neE"), Some("-")),
6457    BUILTIN("emulate", 0, Some(bin_emulate as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("lLR"), None),
6458    BUILTIN("enable", 0, Some(bin_enable as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_ENABLE, Some("afmprs"), None),
6459    BUILTIN("eval", BINF_PSPECIAL, Some(bin_eval as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_EVAL, None, None),
6460    BUILTIN("exit", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_EXIT, None, None),
6461    BUILTIN("export", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_EXPORT, Some("E:%F:%HL:%R:%TUZ:%afhi:%lp:%rtu"), Some("xg")),
6462    BUILTIN("false", 0, Some(bin_false as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6463    // C source (Src/builtin.c:69-73): the argument to -e used to be
6464    // optional; making it required is more consistent.
6465    BUILTIN("fc", 0, None, 0, -1, BIN_FC, Some("aAdDe:EfiIlLmnpPrRst:W"), None),
6466    BUILTIN("fg", 0, None, 0, -1, BIN_FG, None, None),
6467    BUILTIN("float", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("E:%F:%HL:%R:%Z:%ghlp:%rtux"), Some("E")),
6468    BUILTIN("functions", BINF_PLUSOPTS, Some(bin_functions as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ckmMstTuUWx:z"), None),
6469    BUILTIN("getln", 0, None, 0, -1, 0, Some("ecnAlE"), Some("zr")),
6470    BUILTIN("getopts", 0, Some(bin_getopts as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, None, None),
6471    BUILTIN("hash", BINF_MAGICEQUALS, Some(bin_hash as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Ldfmrv"), None),
6472    // Src/builtin.c — `#ifdef ZSH_HASH_DEBUG`
6473    //   BUILTIN("hashinfo", 0, bin_hashinfo, 0, 0, 0, NULL, NULL)
6474    BUILTIN("hashinfo", 0, None, 0, 0, 0, None, None),
6475    BUILTIN("history", 0, None, 0, -1, BIN_FC, Some("adDEfiLmnpPrt:"), Some("l")),
6476    BUILTIN("integer", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("HL:%R:%Z:%ghi:%lp:%rtux"), Some("i")),
6477    BUILTIN("jobs", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_JOBS, Some("dlpZrs"), None),
6478    BUILTIN("kill", BINF_HANDLES_OPTS, None, 0, -1, 0, None, None),
6479    BUILTIN("let", 0, Some(bin_let as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6480    BUILTIN("local", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AE:%F:%HL:%R:%TUZ:%ahi:%lnp:%rtux"), None),
6481    BUILTIN("logout", 0, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_LOGOUT, None, None),
6482    // Src/builtin.c — `#if defined(ZSH_MEM) & defined(ZSH_MEM_DEBUG)`
6483    //   BUILTIN("mem", 0, bin_mem, 0, 0, 0, "v", NULL)
6484    BUILTIN("mem", 0, None, 0, 0, 0, Some("v"), None),
6485    BUILTIN("popd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, None, 0, 1, BIN_POPD, Some("q"), None),
6486    // Src/builtin.c — `#if defined(ZSH_PAT_DEBUG)`
6487    //   BUILTIN("patdebug", 0, bin_patdebug, 1, -1, 0, "p", NULL)
6488    BUILTIN("patdebug", 0, None, 1, -1, 0, Some("p"), None),
6489    BUILTIN("print", BINF_PRINTOPTS, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_PRINT, Some("abcC:Df:ilmnNoOpPrRsSu:v:x:X:z-"), None),
6490    BUILTIN("printf", BINF_SKIPINVALID | BINF_SKIPDASH, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_PRINTF, Some("v:"), None),
6491    BUILTIN("pushd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, None, 0, 2, BIN_PUSHD, Some("qsPL"), None),
6492    BUILTIN("pushln", 0, None, 0, -1, BIN_PRINT, None, Some("-nz")),
6493    BUILTIN("pwd", 0, Some(bin_pwd as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("rLP"), None),
6494    BUILTIN("r", 0, None, 0, -1, BIN_R, Some("IlLnr"), None),
6495    BUILTIN("read", 0, Some(bin_read as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("cd:ek:%lnpqrst:%zu:AE"), None),
6496    BUILTIN("readonly", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_READONLY, Some("AE:%F:%HL:%R:%TUZ:%afghi:%lptux"), Some("r")),
6497    BUILTIN("rehash", 0, Some(bin_hash as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("df"), Some("r")),
6498    BUILTIN("return", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_RETURN, None, None),
6499    BUILTIN("set", BINF_PSPECIAL | BINF_HANDLES_OPTS, Some(bin_set as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6500    BUILTIN("setopt", 0, None, 0, -1, BIN_SETOPT, None, None),
6501    BUILTIN("shift", BINF_PSPECIAL, Some(bin_shift as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("p"), None),
6502    BUILTIN("source", BINF_PSPECIAL, Some(bin_dot as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6503    BUILTIN("suspend", 0, None, 0, 0, 0, Some("f"), None),
6504    BUILTIN("test", BINF_HANDLES_OPTS, Some(bin_test as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_TEST, None, None),
6505    BUILTIN("ttyctl", 0, Some(bin_ttyctl as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("fu"), None),
6506    // c:Src/Builtins/rlimits.c:868-870 — limit/ulimit/unlimit are
6507    // declared in the rlimits Builtins-module's bintab. zshrs has the
6508    // free-fn ports at src/ported/builtins/rlimits.rs but never
6509    // registered them; the BUILTIN_NAMES derivation missed them and
6510    // `type limit` etc. returned empty.
6511    BUILTIN("limit",   0, None, 0, -1, 0, Some("sh"), None),                  // c:rlimits.c:868
6512    BUILTIN("ulimit",  0, None, 0, -1, 0, None,       None),                  // c:rlimits.c:869
6513    BUILTIN("unlimit", 0, None, 0, -1, 0, Some("hs"), None),                  // c:rlimits.c:870
6514    BUILTIN("times", BINF_PSPECIAL, Some(bin_times as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
6515    BUILTIN("trap", BINF_PSPECIAL | BINF_HANDLES_OPTS, Some(bin_trap as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6516    BUILTIN("true", 0, Some(bin_true as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6517    BUILTIN("type", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ampfsSw"), Some("v")),
6518    BUILTIN("typeset", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AE:%F:%HL:%R:%TUZ:%afghi:%klp:%rtuxmnz"), None),
6519    BUILTIN("umask", 0, Some(bin_umask as crate::ported::zsh_h::HandlerFunc), 0, 1, 0, Some("S"), None),
6520    BUILTIN("unalias", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_UNALIAS, Some("ams"), None),
6521    BUILTIN("unfunction", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNFUNCTION, Some("m"), Some("f")),
6522    BUILTIN("unhash", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNHASH, Some("adfms"), None),
6523    BUILTIN("unset", BINF_PSPECIAL, Some(bin_unset as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNSET, Some("fmvn"), None),
6524    BUILTIN("unsetopt", 0, None, 0, -1, BIN_UNSETOPT, None, None),
6525    BUILTIN("wait", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_WAIT, None, None),
6526    BUILTIN("whence", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("acmpvfsSwx:"), None),
6527    BUILTIN("where", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("pmsSwx:"), Some("ca")),
6528    BUILTIN("which", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ampsSwx:"), Some("c")),
6529    BUILTIN("zmodload", 0, Some(crate::ported::module::bin_zmodload as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AFRILP:abcfdilmpsue"), None),
6530    BUILTIN("zcompile", 0, None, 0, -1, 0, Some("tUMRcmzka"), None),
6531    // Module builtins (zsh/zutil, zsh/cap, zsh/pcre, etc.) — these
6532    // live in src/ported/modules/* and src/ported/zle/* but their
6533    // canonical pub fn signatures match HandlerFunc, so they can be
6534    // dispatched via execbuiltin alongside the main builtins.
6535    BUILTIN("zstyle", 0, Some(crate::ported::modules::zutil::bin_zstyle as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("LeLdgabsTtmnH"), None),
6536    BUILTIN("zformat", 0, Some(crate::ported::modules::zutil::bin_zformat as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Faf"), None),
6537    BUILTIN("zparseopts", 0, Some(crate::ported::modules::zutil::bin_zparseopts as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("D-EFK-M-a:"), None),
6538    BUILTIN("zregexparse", 0, Some(crate::ported::modules::zutil::bin_zregexparse as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("c"), None),
6539    BUILTIN("cap", 0, Some(crate::ported::modules::cap::bin_cap as crate::ported::zsh_h::HandlerFunc), 0, 1, 0, None, None),
6540    BUILTIN("getcap", 0, Some(crate::ported::modules::cap::bin_getcap as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6541    BUILTIN("setcap", 0, Some(crate::ported::modules::cap::bin_setcap as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6542    BUILTIN("pcre_compile", 0, Some(crate::ported::modules::pcre::bin_pcre_compile as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, Some("aimx"), None),
6543    BUILTIN("pcre_study", 0, Some(crate::ported::modules::pcre::bin_pcre_study as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
6544    // bin_pcre_match returns (i32, Option<String>, Vec<...>) — non-standard
6545    // signature, can't dispatch via execbuiltin. Wrapper stays in exec.rs.
6546    BUILTIN("pcre_match", 0, None, 1, -1, 0, Some("ab:nv:"), None),
6547    BUILTIN("ztcp", 0, Some(crate::ported::modules::tcp::bin_ztcp as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("acdflLtv"), None),
6548    BUILTIN("ztie", 0, Some(crate::ported::modules::db_gdbm::bin_ztie as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("d:f:r"), None),
6549    BUILTIN("zuntie", 0, Some(crate::ported::modules::db_gdbm::bin_zuntie as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("u"), None),
6550    BUILTIN("zgdbmpath", 0, Some(crate::ported::modules::db_gdbm::bin_zgdbmpath as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, None, None),
6551    BUILTIN("echoti", 0, Some(crate::ported::modules::terminfo::bin_echoti as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6552    BUILTIN("fg", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_FG, None, None),
6553    BUILTIN("kill", BINF_HANDLES_OPTS, Some(crate::ported::jobs::bin_kill as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6554    BUILTIN("suspend", 0, Some(crate::ported::jobs::bin_suspend as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("f"), None),
6555    BUILTIN("bindkey", 0, Some(crate::ported::zle::zle_keymap::bin_bindkey as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("evaMldDANmrsLR"), None),
6556    BUILTIN("vared", 0, Some(crate::ported::zle::zle_main::bin_vared as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, Some("AaceghM:m:p:r:i:f:"), None),
6557    BUILTIN("compadd", 0, Some(crate::ported::zle::complete::bin_compadd as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("J:V:1X:fnqQF:Wsi"), None),
6558    BUILTIN("compset", 0, Some(crate::ported::zle::complete::bin_compset as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("npqPS:"), None),
6559    BUILTIN("zle", 0, Some(crate::ported::zle::zle_thingy::bin_zle as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("aAcCDfFIKlLmMNRTU"), None),
6560    // zsh/files module — file-manipulation builtins. All have
6561    // HandlerFunc-compatible signatures already.
6562    BUILTIN("mkdir", 0, Some(crate::ported::modules::files::bin_mkdir as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("pm:"), None),
6563    BUILTIN("rmdir", 0, Some(crate::ported::modules::files::bin_rmdir as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6564    BUILTIN("ln", 0, Some(crate::ported::modules::files::bin_ln as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("dfins"), None),
6565    BUILTIN("rm", 0, Some(crate::ported::modules::files::bin_rm as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("dfiRrs"), None),
6566    BUILTIN("chmod", 0, Some(crate::ported::modules::files::bin_chmod as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, Some("Rs"), None),
6567    BUILTIN("chown", 0, Some(crate::ported::modules::files::bin_chown as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, Some("Rs"), None),
6568    BUILTIN("sync", 0, Some(crate::ported::modules::files::bin_sync as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
6569]);
6570// hash table containing builtin commands                                   // c:143
6571/// Process-wide builtin lookup table. Filled lazily the first time
6572/// `builtintab()` is called; mirrors the C `mod_export HashTable
6573/// builtintab` exposed at `Src/builtin.c:146`.
6574static builtintab: OnceLock<HashMap<String, &'static builtin>> = OnceLock::new();
6575
6576/// Names whose `node.flags & DISABLED` is set in C. The Rust port's
6577/// `builtintab` is an immutable static, so the disabled bit lives
6578/// in this parallel set; `bin_enable` toggles it via builtin.c:587.
6579/// Dispatch sites check `is_builtin_disabled(name)` before calling
6580/// `handlerfunc` to mirror C's "skip nodes with DISABLED set" walk.
6581pub static BUILTINS_DISABLED: std::sync::LazyLock<                           // c:587 (Src/builtin.c)
6582    std::sync::Mutex<std::collections::HashSet<String>>
6583> = std::sync::LazyLock::new(|| {
6584    std::sync::Mutex::new(std::collections::HashSet::new())
6585});
6586
6587// `shfunctab` global from Src/init.c — name → Shfunc map. Static-link
6588// path: store the raw Shfunc pointer keyed by name. Lazy via OnceLock
6589// because HashMap::new isn't const.
6590static SHFUNCTAB_INNER: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, usize>>>
6591    = std::sync::OnceLock::new();
6592
6593// `matchednodes` global from Src/builtin.c:4550.
6594pub static MATCHEDNODES: std::sync::Mutex<Vec<String>> =
6595    std::sync::Mutex::new(Vec::new());
6596
6597// `stopmsg` global from Src/jobs.c — non-zero when checkjobs() printed.
6598pub static STOPMSG: std::sync::atomic::AtomicI32 =
6599    std::sync::atomic::AtomicI32::new(0);
6600// `sfcontext` global from Src/exec.c:239 — current shell-function
6601// dispatch context (SFC_NONE / SFC_BUILTIN / SFC_FUNC / SFC_SUBST...).
6602pub static SFCONTEXT: std::sync::atomic::AtomicI32 =
6603    std::sync::atomic::AtomicI32::new(0);                                    // c:exec.c:239
6604// `maxjob` / `thisjob` globals from Src/jobs.c:62/63 — canonical
6605// storage lives in jobs.rs (`OnceLock<Mutex<i32>>`). The previous
6606// builtin.rs duplicate `AtomicI32` stores NEVER synced with the
6607// jobs.rs Mutex<i32> values that the spawn/wait paths actually
6608// update; `checkjobs` (line 5092) read stale 0s no matter how many
6609// jobs were active. Callers route through jobs::MAXJOB / jobs::THISJOB
6610// directly now.
6611// `jobstats` mirror — flat per-slot stat bits (STAT_*). Real jobtab
6612// lives in src/ported/jobs.rs's JobTable; this mirror is updated by
6613// the spawn/wait paths that already touch STOPMSG. Empty → no jobs,
6614// matching the post-init state of `jobtab[]`.
6615pub static JOBSTATS: std::sync::Mutex<Vec<i32>> = std::sync::Mutex::new(Vec::new());
6616
6617// File-static globals for [_]realexit/zexit — c:5945+, init.c, signals.c.
6618pub static SHELL_EXITING: std::sync::atomic::AtomicI32 =
6619    std::sync::atomic::AtomicI32::new(0);
6620pub static EXIT_PENDING: std::sync::atomic::AtomicI32 =
6621    std::sync::atomic::AtomicI32::new(0);
6622pub static EXIT_VAL: std::sync::atomic::AtomicI32 =
6623    std::sync::atomic::AtomicI32::new(0);
6624pub static LASTVAL: std::sync::atomic::AtomicI32 =
6625    std::sync::atomic::AtomicI32::new(0);
6626
6627// `tok` for the test builtin — Src/builtin.c:7000 ranges. The full enum
6628// lives in src/ported/lex.rs; we mirror the few values testlex() touches.
6629pub static TEST_TOK: std::sync::atomic::AtomicI32 =
6630    std::sync::atomic::AtomicI32::new(0);
6631const TEST_LEXERR:  i32 = -1;                                                // c:7209
6632const TEST_NULLTOK: i32 =  0;
6633const TEST_DBAR:    i32 =  2;                                                // c:7213
6634const TEST_DAMPER:  i32 =  3;                                                // c:7215
6635const TEST_BANG:    i32 =  4;                                                // c:7217
6636const TEST_INPAR:   i32 =  5;                                                // c:7219
6637const TEST_OUTPAR:  i32 =  6;                                                // c:7221
6638const TEST_INANG:   i32 =  7;                                                // c:7223
6639const TEST_OUTANG:  i32 =  8;                                                // c:7225
6640const TEST_STRING:  i32 =  9;                                                // c:7227
6641
6642// `testargs` / `curtestarg` / `tokstr` globals from Src/builtin.c — the
6643// argv-style cursor that bin_test seeds and testlex() advances.
6644pub static TESTARGS:     std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
6645pub static TESTARGS_IDX: std::sync::atomic::AtomicI32  = std::sync::atomic::AtomicI32::new(0);
6646pub static TOKSTR:       std::sync::Mutex<String>      = std::sync::Mutex::new(String::new());
6647
6648// int doprintdir = 0; set in exec.c (for autocd, cdpath, etc.)            // c:722
6649// `doprintdir` from Src/exec.c — set when an autocd'd command should
6650// echo the new directory before executing.
6651pub static DOPRINTDIR: std::sync::atomic::AtomicI32 =
6652    std::sync::atomic::AtomicI32::new(0);
6653// set if we are resolving links to their true paths                       // c:829
6654// `chasinglinks` from Src/exec.c — non-zero when CHASELINKS / -P
6655// resolution is active.
6656pub static CHASINGLINKS: std::sync::atomic::AtomicI32 =
6657    std::sync::atomic::AtomicI32::new(0);
6658
6659// `pparams` global from Src/init.c — positional parameters $1..$N.
6660pub static PPARAMS: std::sync::Mutex<Vec<String>> =
6661    std::sync::Mutex::new(Vec::new());
6662
6663// `zoptind` (Src/builtin.c:5667) and `optcind` (c:5670) — the two
6664// pieces of getopts state. zoptind backs the user-visible $OPTIND.
6665pub static ZOPTIND: std::sync::atomic::AtomicI32 =
6666    std::sync::atomic::AtomicI32::new(1);
6667pub static OPTCIND: std::sync::atomic::AtomicI32 =
6668    std::sync::atomic::AtomicI32::new(0);
6669
6670// `ttyfrozen` global lives canonically in jobs.rs (`OnceLock<Mutex<i32>>`
6671// at jobs.rs:2625). The previous AtomicI32 duplicate here NEVER
6672// synced with the jobs.rs store — same desync hazard as the prior
6673// MAXJOB / THISJOB fix. Callers route through jobs::TTYFROZEN.
6674
6675/// Port of `mod_export int ineval` from `Src/builtin.c:6389`. Set
6676/// while `eval` is dispatching its body (incremented before
6677/// `execode(prog, 1, 0, "eval")`, decremented after). Tested by
6678/// `IN_EVAL_TRAP()` in zsh.h:2962 to determine trap-context state.
6679pub static INEVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:6389
6680
6681// `loops` / `breaks` / `contflag` / `retflag` / `locallevel` / `sourcelevel`
6682// globals from Src/loop.c + Src/init.c — control-flow state consulted by
6683// the bin_break dispatcher.
6684pub static LOOPS:        std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6685pub static BREAKS:       std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6686pub static CONTFLAG:     std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6687pub static RETFLAG:      std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6688// `LOCALLEVEL` was previously a SEPARATE AtomicI32 here, but C
6689// zsh has only ONE `int locallevel;` global (Src/params.c:54).
6690// The canonical Rust port is `crate::ported::params::locallevel`
6691// (lowercase, matches C name). Re-export that single storage so
6692// every reader and writer addresses the same atomic — without
6693// this, `LOCALLEVEL.store(0)` in zle/computil.rs would zero one
6694// global while `params::locallevel.fetch_add(1)` in exec.rs
6695// incremented a DIFFERENT global, leaving the two views out of
6696// sync indefinitely.
6697pub use crate::ported::params::locallevel as LOCALLEVEL;
6698pub static SOURCELEVEL:  std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6699
6700// `ZEXIT_NORMAL` re-exported from canonical zsh_h.rs (port of the
6701// `enum { ZEXIT_NORMAL, ZEXIT_SIGNAL, ZEXIT_DEFERRED }` in Src/zsh.h).
6702// Same single-source-of-truth pattern as TERM_UNKNOWN / HISTFLAG_*
6703// / etc — duplicate const declarations are a drift hazard.
6704pub use crate::ported::zsh_h::ZEXIT_NORMAL;
6705
6706// Local builders that construct C-shape `builtin` rows for the
6707// static registration table below. They mirror the
6708// `BUILTIN(...)` / `BIN_PREFIX(...)` macros in `Src/zsh.h:1450-1452`,
6709// taking `u32` flag bitsets (BINF_*) and a `&str` handler-name
6710// column used only for documentation/wiring lookup — handler
6711// function pointers themselves are wired up later in
6712// `Executor::register_builtins` (`src/ported/exec.rs`).
6713//
6714// The `handler` arg was previously a `_handler_name: &'static str` that
6715// was discarded — `handlerfunc` always ended up `NULLBINCMD`, so
6716// `execbuiltin`'s c:506 `(*handlerfunc)(...)` dispatch was unreachable.
6717// Now the descriptor carries the actual port-side `HandlerFunc` so
6718// `execbuiltin` can parse flags and call through to the real builtin.
6719#[allow(non_snake_case)]
6720pub fn BUILTIN(
6721    name: &str,
6722    flags: u32,
6723    handler: Option<crate::ported::zsh_h::HandlerFunc>,
6724    min: i32,
6725    max: i32,
6726    funcid: i32,
6727    optstr: Option<&str>,
6728    defopts: Option<&str>,
6729) -> builtin {
6730    builtin {
6731        node: hashnode {
6732            next: None,
6733            nam: name.to_string(),
6734            flags: flags as i32,
6735        },
6736        handlerfunc: handler,
6737        minargs: min,
6738        maxargs: max,
6739        funcid,
6740        optstr: optstr.map(|s| s.to_string()),
6741        defopts: defopts.map(|s| s.to_string()),
6742    }
6743}
6744
6745// `traps` mirror — sig name → body. Real `sigtrapped[]`/`siglists[]`
6746// arrays live in src/ported/signals.rs; this Mutex is the static-link
6747// shim that bin_trap reads/writes.
6748static TRAPS_INNER: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, String>>>
6749    = std::sync::OnceLock::new();
6750
6751#[allow(non_snake_case)]
6752fn BIN_PREFIX(name: &str, flags: u32) -> builtin {
6753    BUILTIN(name, flags | BINF_PREFIX, None, 0, 0, 0, None, None)
6754}
6755
6756/// Inline printf-style format helper used by bin_print's -f/printf mode.
6757/// Replaces `%s` / `%d` / `%i` / `%c` / `%%` with positional args.
6758/// Full C printf-spec engine (Src/builtin.c:4691-5500) is much more
6759/// elaborate (width/precision/flag chars/%b/%q/etc.); this is the
6760/// minimal subset that covers the common script patterns.
6761fn printf_format(fmt: &str, args: &[String]) -> String {
6762    // c:Src/builtin.c:4711 — `fmt = getkeystring(fmt, &flen, ...,
6763    // GETKEYS_PRINTF_FMT, ...);`. The format string is first run
6764    // through getkeystring to interpret backslash escapes (`\n`,
6765    // `\t`, `\xNN`, etc.) before %-format substitution.
6766    let (fmt, _) = getkeystring(fmt);                                        // c:builtin.c:4711
6767    let mut out = String::new();
6768    let mut arg_i: usize = 0;
6769    // c:Src/builtin.c:4914-4923 — printf reapplies the format string
6770    // until ALL args are consumed. `printf '%s,' a b c` → `a,b,c,`,
6771    // not `a,`. The outer loop reapplies; the inner do-while body
6772    // mirrors C's per-arg conversion loop directly.
6773    loop {
6774        let prev = arg_i;
6775        let mut iter = fmt.chars().peekable();
6776        while let Some(c) = iter.next() {
6777            if c != '%' {
6778                out.push(c);
6779                continue;
6780            }
6781            // c:Src/builtin.c:4791+ — parse width/precision/flag chars
6782            // between `%` and the conversion. Capture them so `printf
6783            // "%-10s" hi` and `printf "%.3f" 3.14159` render correctly.
6784            let mut spec = String::from("%");
6785            loop {
6786                match iter.peek() {
6787                    Some(&c) if matches!(c, '-' | '+' | ' ' | '#' | '0') => {
6788                        spec.push(c); iter.next();
6789                    }
6790                    _ => break,
6791                }
6792            }
6793            while let Some(&c) = iter.peek() {
6794                if c.is_ascii_digit() { spec.push(c); iter.next(); }
6795                else { break; }
6796            }
6797            if iter.peek() == Some(&'.') {
6798                spec.push('.'); iter.next();
6799                while let Some(&c) = iter.peek() {
6800                    if c.is_ascii_digit() { spec.push(c); iter.next(); }
6801                    else { break; }
6802                }
6803            }
6804            match iter.next() {
6805                Some('%') => out.push('%'),
6806                Some('s') => {
6807                    let a = args.get(arg_i).cloned().unwrap_or_default();
6808                    spec.push('s');
6809                    out.push_str(&format_spec_str(&spec, &a));
6810                    arg_i += 1;
6811                }
6812                Some('d') | Some('i') => {
6813                    let a = args.get(arg_i).cloned().unwrap_or_default();
6814                    let n: i64 = a.parse().unwrap_or(0);
6815                    spec.push('d');
6816                    out.push_str(&format_spec_int(&spec, n));
6817                    arg_i += 1;
6818                }
6819                Some('u') => {
6820                    let a = args.get(arg_i).cloned().unwrap_or_default();
6821                    let n: u64 = a.parse().unwrap_or(0);
6822                    spec.push('u');
6823                    out.push_str(&format_spec_uint(&spec, n));
6824                    arg_i += 1;
6825                }
6826                Some('x') => {
6827                    let a = args.get(arg_i).cloned().unwrap_or_default();
6828                    let n: i64 = a.parse().unwrap_or(0);
6829                    spec.push('x');
6830                    out.push_str(&format!("{:x}", n));
6831                    arg_i += 1;
6832                }
6833                Some('X') => {
6834                    let a = args.get(arg_i).cloned().unwrap_or_default();
6835                    let n: i64 = a.parse().unwrap_or(0);
6836                    spec.push('X');
6837                    out.push_str(&format!("{:X}", n));
6838                    arg_i += 1;
6839                }
6840                Some('o') => {
6841                    let a = args.get(arg_i).cloned().unwrap_or_default();
6842                    let n: i64 = a.parse().unwrap_or(0);
6843                    spec.push('o');
6844                    out.push_str(&format!("{:o}", n));
6845                    arg_i += 1;
6846                }
6847                Some('f') | Some('F') | Some('g') | Some('G') | Some('e') | Some('E') => {
6848                    let a = args.get(arg_i).cloned().unwrap_or_default();
6849                    let n: f64 = a.parse().unwrap_or(0.0);
6850                    spec.push('f');
6851                    out.push_str(&format_spec_float(&spec, n));
6852                    arg_i += 1;
6853                }
6854                Some('c') => {
6855                    if let Some(a) = args.get(arg_i) {
6856                        if let Some(ch) = a.chars().next() { out.push(ch); }
6857                    }
6858                    arg_i += 1;
6859                }
6860                // c:builtin.c:4825 %q — shell-quote the arg.
6861                Some('q') => {
6862                    let a = args.get(arg_i).cloned().unwrap_or_default();
6863                    out.push_str(&quotedzputs(&a));
6864                    arg_i += 1;
6865                }
6866                // c:builtin.c:4810 %b — interpret backslash escapes
6867                // with GETKEY_EMACS arm (drop unknown backslashes).
6868                Some('b') => {
6869                    let a = args.get(arg_i).cloned().unwrap_or_default();
6870                    let (s, _) = getkeystring_with(&a, GETKEYS_PRINT);
6871                    out.push_str(&s);
6872                    arg_i += 1;
6873                }
6874                Some(other) => { out.push('%'); out.push(other); }
6875                None => out.push('%'),
6876            }
6877        }
6878        if arg_i == prev || arg_i >= args.len() { break; }
6879    }
6880    out
6881}
6882
6883/// Apply a printf-style `%[-flag][width][.prec]s` spec to a string.
6884/// Mirrors C `printf "%-10s" str` formatting; the Rust `format!` macro
6885/// doesn't accept runtime-parsed specs so we hand-parse.
6886fn format_spec_str(spec: &str, s: &str) -> String {
6887    let (left_align, width, prec) = parse_width_prec(spec);
6888    let truncated: &str = if let Some(p) = prec {
6889        let end: usize = s.chars().take(p).map(|c| c.len_utf8()).sum();
6890        &s[..end.min(s.len())]
6891    } else { s };
6892    let pad = width.saturating_sub(truncated.chars().count());
6893    if left_align {
6894        format!("{}{}", truncated, " ".repeat(pad))
6895    } else {
6896        format!("{}{}", " ".repeat(pad), truncated)
6897    }
6898}
6899
6900fn format_spec_int(spec: &str, n: i64) -> String {
6901    let (left_align, width, _prec) = parse_width_prec(spec);
6902    let zero_pad = spec.contains('0') && !left_align;
6903    let body = n.to_string();
6904    let pad = width.saturating_sub(body.chars().count());
6905    if pad == 0 { body }
6906    else if left_align { format!("{}{}", body, " ".repeat(pad)) }
6907    else if zero_pad {
6908        if let Some(rest) = body.strip_prefix('-') {
6909            format!("-{}{}", "0".repeat(pad), rest)
6910        } else { format!("{}{}", "0".repeat(pad), body) }
6911    } else { format!("{}{}", " ".repeat(pad), body) }
6912}
6913
6914fn format_spec_uint(spec: &str, n: u64) -> String {
6915    format_spec_int(spec, n as i64)
6916}
6917
6918fn format_spec_float(spec: &str, n: f64) -> String {
6919    let (left_align, width, prec) = parse_width_prec(spec);
6920    let p = prec.unwrap_or(6);
6921    let body = format!("{:.*}", p, n);
6922    let pad = width.saturating_sub(body.chars().count());
6923    if pad == 0 { body }
6924    else if left_align { format!("{}{}", body, " ".repeat(pad)) }
6925    else { format!("{}{}", " ".repeat(pad), body) }
6926}
6927
6928fn parse_width_prec(spec: &str) -> (bool, usize, Option<usize>) {
6929    let s = spec.trim_start_matches('%');
6930    let mut i = 0;
6931    let bytes = s.as_bytes();
6932    let mut left_align = false;
6933    while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b' ' | b'#' | b'0') {
6934        if bytes[i] == b'-' { left_align = true; }
6935        i += 1;
6936    }
6937    let width_start = i;
6938    while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
6939    let width: usize = s[width_start..i].parse().unwrap_or(0);
6940    let mut prec: Option<usize> = None;
6941    if i < bytes.len() && bytes[i] == b'.' {
6942        i += 1;
6943        let p_start = i;
6944        while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
6945        prec = Some(s[p_start..i].parse().unwrap_or(0));
6946    }
6947    (left_align, width, prec)
6948}
6949
6950/// Port of `findcmd(char *arg0, int docopy, int default_path)` from Src/exec.c:897. Walk `$PATH` for `name`,
6951/// returning the matching path on success. `_docopy` is the C source's
6952/// "duplicate the result" flag; Rust ownership covers it. `_default_path`
6953/// = 1 forces the system default `/bin:/usr/bin:...` path search (used
6954/// by `command -p`); not yet wired.
6955/// WARNING: param names don't match C — Rust=(name, _docopy, _default_path) vs C=(errflag)
6956pub fn findcmd(name: &str, _docopy: i32, _default_path: i32) -> Option<String> { // c:897
6957    if name.contains('/') {
6958        let p = std::path::Path::new(name);
6959        return if p.is_file() { Some(name.to_string()) } else { None };
6960    }
6961    // c:907-912 — walk `path[]` (the shell $path array). Read $PATH
6962    //              from paramtab so shell-private PATH edits via
6963    //              `path=(...)` show up; OS env-only PATH would miss
6964    //              them in nested shells.
6965    let path = crate::ported::params::getsparam("PATH")?;
6966    for dir in path.split(':') {
6967        if dir.is_empty() { continue; }
6968        let candidate = format!("{}/{}", dir, name);
6969        if std::path::Path::new(&candidate).is_file() {
6970            return Some(candidate);
6971        }
6972    }
6973    None
6974}
6975
6976/// Port of `getsigidx(const char *s)` from Src/signals.c — return signal number for
6977/// a name, or -1 if unknown. Strips optional `SIG` prefix; falls back
6978/// to numeric parse.
6979fn getsigidx(name: &str) -> i32 {
6980    let s = name.strip_prefix("SIG").unwrap_or(name);
6981    // Try parse as integer first.
6982    if let Ok(n) = s.parse::<i32>() {
6983        return n;
6984    }
6985    // Common signal name → number mapping.
6986    match s {
6987        "HUP"  =>  1, "INT"  =>  2, "QUIT" =>  3, "ILL"  =>  4,
6988        "TRAP" =>  5, "ABRT" =>  6, "FPE"  =>  8, "KILL" =>  9,
6989        "USR1" => 10, "SEGV" => 11, "USR2" => 12, "PIPE" => 13,
6990        "ALRM" => 14, "TERM" => 15, "CHLD" => 17, "CONT" => 18,
6991        "STOP" => 19, "TSTP" => 20, "TTIN" => 21, "TTOU" => 22,
6992        "URG"  => 23, "XCPU" => 24, "XFSZ" => 25, "VTALRM" => 26,
6993        "PROF" => 27, "WINCH" => 28, "IO" => 29, "PWR" => 30,
6994        "SYS" => 31, "EXIT" => 0,
6995        _ => -1,
6996    }
6997}
6998
6999/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
7000/// from `Src/pattern.c:4171`. Local builtin.rs shim that delegates to
7001/// the canonical pattern.rs port. Static-link path: the actual
7002/// zpc_strings/zpc_disables manipulation lives in
7003/// `crate::ported::pattern::pat_enables`.
7004fn pat_enables(name: &str, argv: &[String], on: bool) -> i32 {               // c:4171
7005    let patp: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
7006    crate::ported::pattern::pat_enables(name, &patp, on)
7007}
7008
7009// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7010// ─── RUST-ONLY ACCESSORS ───
7011//
7012// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
7013// RwLock<T>>` globals declared above. C zsh uses direct global
7014// access; Rust needs these wrappers because `OnceLock::get_or_init`
7015// is the only way to lazily construct shared state. These fns sit
7016// here so the body of this file reads in C source order without
7017// the accessor wrappers interleaved between real port fns.
7018// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7019
7020// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7021// ─── RUST-ONLY ACCESSORS ───
7022//
7023// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
7024// RwLock<T>>` globals declared above. C zsh uses direct global
7025// access; Rust needs these wrappers because `OnceLock::get_or_init`
7026// is the only way to lazily construct shared state. These fns sit
7027// here so the body of this file reads in C source order without
7028// the accessor wrappers interleaved between real port fns.
7029// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7030
7031pub fn shfunctab_table() -> &'static std::sync::Mutex<std::collections::HashMap<String, usize>> {
7032    SHFUNCTAB_INNER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
7033}
7034
7035pub fn traps_table() -> &'static std::sync::Mutex<std::collections::HashMap<String, String>> {
7036    TRAPS_INNER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
7037}
7038
7039#[cfg(test)]
7040mod tests {
7041    use crate::zsh_h::BINF_PREFIX;
7042    use super::*;
7043
7044    /// c:7399 — `trap - <undefined>` MUST report failure (non-zero
7045    /// exit) so scripts can detect the bad signal name. The previous
7046    /// Rust port returned 0 unconditionally from the clear path,
7047    /// silently masking errors. C returns `*argv != NULL` — non-zero
7048    /// when the loop broke on an undefined signal.
7049    #[test]
7050    fn bin_trap_clear_undefined_signal_returns_nonzero() {
7051        let empty = crate::ported::zsh_h::options {
7052            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
7053            args: Vec::new(),
7054            argscount: 0,
7055            argsalloc: 0,
7056        };
7057        // `trap - BOGUS_NEVER_A_SIGNAL` → must return 1.
7058        let r = bin_trap("trap",
7059            &["-".into(), "BOGUS_NEVER_A_SIGNAL".into()],
7060            &empty, 0);
7061        assert_ne!(r, 0,
7062            "trap - <undefined> must report error per c:7399 (got {})", r);
7063    }
7064
7065    /// Src/options.c:537-549 — `emulate(zsh_name, ...)` dispatches
7066    /// on the FIRST char of the shell name, stripping a leading `r`
7067    /// (so `rcsh`/`rksh` work as restricted variants of their base
7068    /// shell). `bash` aliases to SH (the `'b'` branch of the case).
7069    /// Pin the bits assigned by `bin_emulate` for the canonical
7070    /// names + their first-char-overlap aliases.
7071    #[test]
7072    fn bin_emulate_dispatches_on_first_char_per_c537() {
7073        use crate::ported::zsh_h::{EMULATE_CSH, EMULATE_KSH, EMULATE_SH};
7074        let empty = crate::ported::zsh_h::options {
7075            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
7076            args: Vec::new(),
7077            argscount: 0,
7078            argsalloc: 0,
7079        };
7080        let saved = crate::ported::options::emulation
7081            .load(std::sync::atomic::Ordering::Relaxed);
7082
7083        // Each (name, expected_bits) — name covers the canonical
7084        // shell names AND their `r`-prefix / first-char variants.
7085        for (name, expected) in [
7086            ("csh",   EMULATE_CSH),
7087            ("ksh",   EMULATE_KSH),
7088            ("sh",    EMULATE_SH),
7089            ("rcsh",  EMULATE_CSH),                                          // c:539-540
7090            ("rksh",  EMULATE_KSH),                                          // c:539-540
7091            ("bash",  EMULATE_SH),                                           // c:548 'b'
7092        ] {
7093            crate::ported::options::emulation
7094                .store(0, std::sync::atomic::Ordering::Relaxed);
7095            bin_emulate("emulate", &[name.into()], &empty, 0);
7096            let bits = crate::ported::options::emulation
7097                .load(std::sync::atomic::Ordering::Relaxed);
7098            assert_eq!(bits, expected,
7099                "emulate {} must set bits {:#x}, got {:#x}",
7100                name, expected, bits);
7101        }
7102        crate::ported::options::emulation
7103            .store(saved, std::sync::atomic::Ordering::Relaxed);
7104    }
7105
7106    /// c:7399 — `trap - SIGUSR1` (valid signal) MUST return 0, even
7107    /// when the trap was never set (remove is a no-op).
7108    #[test]
7109    fn bin_trap_clear_valid_signal_returns_zero() {
7110        let empty = crate::ported::zsh_h::options {
7111            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
7112            args: Vec::new(),
7113            argscount: 0,
7114            argsalloc: 0,
7115        };
7116        let r = bin_trap("trap", &["-".into(), "USR1".into()], &empty, 0);
7117        assert_eq!(r, 0,
7118            "trap - USR1 must succeed even with no prior trap (got {})", r);
7119    }
7120
7121    #[test]
7122    fn registration_table_matches_c_count() {
7123        // Src/builtin.c:40-137 has 79 rows total (5 BIN_PREFIX + 71
7124        // BUILTIN + 3 debug-only BUILTIN). The Rust port bundles
7125        // additional builtins eagerly that C would load via zmodload:
7126        //   zsh/rlimits (limit/ulimit/unlimit)
7127        //   zsh/zle (bindkey/vared/zle)
7128        //   zsh/cap (cap/getcap/setcap)
7129        //   zsh/files (chmod/chown/ln/mkdir/rm/rmdir/sync)
7130        //   zsh/complete (compadd/compset)
7131        //   zsh/terminfo (echoti)
7132        //   zsh/pcre (pcre_compile/pcre_match/pcre_study)
7133        //   zsh/zutil (zformat/zgdbmpath)
7134        // Total Rust BUILTINS table size pinned at 112 to catch
7135        // accidental additions/removals. Bump alongside intentional
7136        // changes to the BUILTINS table above.
7137        assert_eq!(BUILTINS.len(), 112,
7138            "BUILTINS table size changed — bump count or update the eagerly-loaded-module list above");
7139    }
7140
7141    /// `Src/builtin.c:40-137` — every name in the canonical C builtin
7142    /// table must be present in the Rust port. Pins coverage of all
7143    /// 79 C builtins by name (ignores option-mask / handler details).
7144    /// Detects regressions where a builtin gets accidentally dropped
7145    /// from BUILTINS. Names extracted from upstream zsh `Src/builtin.c`.
7146    #[test]
7147    fn registration_table_contains_all_c_builtins() {
7148        // Canonical 79 names from Src/builtin.c:40-137 (verbatim).
7149        let c_names: &[&str] = &[
7150            "-", ".", ":", "[",
7151            "alias", "autoload", "bg", "break", "builtin", "bye",
7152            "cd", "chdir", "command", "continue", "declare", "dirs",
7153            "disable", "disown", "echo", "emulate", "enable", "eval",
7154            "exec", "exit", "export", "false", "fc", "fg", "float",
7155            "functions", "getln", "getopts", "hash", "hashinfo",
7156            "history", "integer", "jobs", "kill", "let", "local",
7157            "logout", "mem", "noglob", "patdebug", "popd", "print",
7158            "printf", "pushd", "pushln", "pwd", "r", "read",
7159            "readonly", "rehash", "return", "set", "setopt", "shift",
7160            "source", "suspend", "test", "times", "trap", "true",
7161            "ttyctl", "type", "typeset", "umask", "unalias",
7162            "unfunction", "unhash", "unset", "unsetopt", "wait",
7163            "whence", "where", "which", "zcompile", "zmodload",
7164        ];
7165        assert_eq!(c_names.len(), 79,
7166            "C builtin.c row count is 79 — recount if changed");
7167        let table_names: std::collections::HashSet<&str> =
7168            BUILTINS.iter().map(|b| b.node.nam.as_str()).collect();
7169        for c_name in c_names {
7170            assert!(table_names.contains(*c_name),
7171                "missing C builtin '{}' from BUILTINS table", c_name);
7172        }
7173    }
7174
7175    #[test]
7176    fn lookup_finds_known_builtins() {
7177        for name in ["cd", "echo", "print", "fg", "bg", "jobs", "wait", "typeset", "test", "[", "."] {
7178            assert!(createbuiltintable().get(name).copied().is_some(), "missing: {name}");
7179        }
7180    }
7181
7182    #[test]
7183    fn lookup_misses_unknown() {
7184        assert!(createbuiltintable().get("not-a-builtin-zZz").copied().is_none());
7185    }
7186
7187    #[test]
7188    fn prefix_entries_have_prefix_flag() {
7189        for name in ["-", "builtin", "command", "exec", "noglob"] {
7190            let b = createbuiltintable().get(name).copied().unwrap();
7191            assert!(b.node.flags as u32 & BINF_PREFIX != 0, "{name} missing BINF_PREFIX");
7192        }
7193    }
7194
7195    #[test]
7196    fn fixdir_canonicalizes_absolute_paths() {
7197        // c:1297 — collapse `//`, drop `./`, pop `..`.
7198        assert_eq!(fixdir("/tmp/./foo"), "/tmp/foo");
7199        assert_eq!(fixdir("/tmp//foo"), "/tmp/foo");
7200        assert_eq!(fixdir("/tmp/bar/../foo"), "/tmp/foo");
7201        assert_eq!(fixdir("/tmp/bar/baz/../.."), "/tmp");
7202    }
7203
7204    #[test]
7205    fn fixdir_drops_dotdot_past_root() {
7206        // c:1372 — absolute path, `..` past `/` is dropped.
7207        assert_eq!(fixdir("/.."), "/");
7208        assert_eq!(fixdir("/../.."), "/");
7209        assert_eq!(fixdir("/foo/../../bar"), "/bar");
7210    }
7211
7212    #[test]
7213    fn fixdir_relative_keeps_leading_dotdot() {
7214        // c:1367 — relative path: `..` past start stays as `..`.
7215        assert_eq!(fixdir("../foo"), "../foo");
7216        assert_eq!(fixdir("../../foo"), "../../foo");
7217        assert_eq!(fixdir("foo/../bar"), "bar");
7218    }
7219
7220    #[test]
7221    fn fixdir_empty_collapses_to_dot() {
7222        // Relative path that collapses fully → "."
7223        assert_eq!(fixdir("./"), ".");
7224        assert_eq!(fixdir("foo/.."), ".");
7225    }
7226
7227    #[test]
7228    fn fixdir_empty_input_returns_empty() {
7229        assert_eq!(fixdir(""), "");
7230    }
7231
7232    #[test]
7233    fn fg_dispatch_id_distinguishes_aliases() {
7234        // bin_fg covers fg, bg, jobs, wait, disown — same handler,
7235        // different funcid. Mirrors Src/builtin.c:52,61,75,88,131.
7236        assert_eq!(createbuiltintable().get("fg").copied().unwrap().funcid, BIN_FG);
7237        assert_eq!(createbuiltintable().get("bg").copied().unwrap().funcid, BIN_BG);
7238        assert_eq!(createbuiltintable().get("jobs").copied().unwrap().funcid, BIN_JOBS);
7239        assert_eq!(createbuiltintable().get("wait").copied().unwrap().funcid, BIN_WAIT);
7240        assert_eq!(createbuiltintable().get("disown").copied().unwrap().funcid, BIN_DISOWN);
7241    }
7242
7243    /// c:1297 — `fixdir` is the lexical-canonicalisation for `cd`. The
7244    /// path `/a/b/../c` must resolve to `/a/c` BEFORE chdir(2) — the
7245    /// shell uses it to compute the logical PWD for $PWD/OLDPWD. A
7246    /// regression that drops the `..` consumption would make $PWD
7247    /// report `/a/b/../c` literally on `cd /a/b/../c`.
7248    #[test]
7249    fn fixdir_pops_dotdot_against_previous_component() {
7250        assert_eq!(fixdir("/a/b/../c"),  "/a/c");
7251        assert_eq!(fixdir("/a/b/../../c"), "/c");
7252        assert_eq!(fixdir("/foo/.."),    "/");
7253    }
7254
7255    /// c:1352 — `./` collapses to nothing.  `/a/./b` must equal `/a/b`.
7256    #[test]
7257    fn fixdir_drops_dot_components() {
7258        assert_eq!(fixdir("/a/./b"),     "/a/b");
7259        assert_eq!(fixdir("./a"),        "a");
7260        assert_eq!(fixdir("./."),        ".");
7261    }
7262
7263    /// c:1388 — `//` collapses to single `/` (no preservation of POSIX
7264    /// implementation-defined `//` semantics, which zsh doesn't honour).
7265    #[test]
7266    fn fixdir_collapses_consecutive_slashes() {
7267        assert_eq!(fixdir("/a//b"),      "/a/b");
7268        assert_eq!(fixdir("/a///b/c"),   "/a/b/c");
7269    }
7270
7271    /// c:1404 — absolute path: `..` past `/` silently drops. `/..`
7272    /// resolves to `/`. Catches a regression where the underflow
7273    /// emits `..` literally.
7274    #[test]
7275    fn fixdir_dotdot_past_root_clamps_to_root() {
7276        assert_eq!(fixdir("/.."),        "/");
7277        assert_eq!(fixdir("/../../a"),   "/a");
7278    }
7279
7280    /// c:1400 — RELATIVE path: leading `..` are preserved (no parent
7281    /// known until chdir time). This is critical for `cd ../../foo`
7282    /// which must NOT resolve `..` lexically.
7283    #[test]
7284    fn fixdir_relative_leading_dotdot_is_preserved() {
7285        assert_eq!(fixdir("../foo"),     "../foo");
7286        assert_eq!(fixdir("../../foo"),  "../../foo");
7287    }
7288
7289    /// c:1683 — `fcgetcomm` returns 0 for ambiguous numeric inputs
7290    /// only when the string actually starts with '0'. The atoi result
7291    /// alone (which is 0 for non-numeric) MUST NOT short-circuit —
7292    /// non-numeric input should fall through to hcomsearch instead.
7293    #[test]
7294    fn fcgetcomm_numeric_zero_only_for_literal_zero_prefix() {
7295        assert_eq!(fcgetcomm("0"),       0, "literal `0` is event 0");
7296        assert_eq!(fcgetcomm("42"),     42);
7297        // Non-numeric falls through to hcomsearch (no hist match → -1).
7298        assert_eq!(fcgetcomm("definitely_not_a_history_command_zshrs"), -1);
7299    }
7300
7301    /// c:1088-1093 — `cd_able_vars` requires CDABLEVARS to be set;
7302    /// otherwise returns None even when the head names a param. A
7303    /// regression that ignores the option flag would let `cd HOME`
7304    /// silently `cd $HOME` even when the user disabled CDABLEVARS.
7305    #[test]
7306    fn cd_able_vars_returns_none_without_cdablevars_option() {
7307        // CDABLEVARS is not set by default → must return None.
7308        // We don't fight the option state here; just verify the
7309        // off-state default short-circuits before paramtab lookup.
7310        // (If a future commit enables CDABLEVARS by default, this
7311        // test will fail loudly — that's the right canary.)
7312        let r = cd_able_vars("HOME/anything");
7313        // Without CDABLEVARS, must be None; with it, would be Some.
7314        // Accept either since the option default is the actual invariant.
7315        if !crate::ported::zsh_h::isset(crate::ported::options::optlookup("cdablevars")) {
7316            assert!(r.is_none());
7317        }
7318    }
7319
7320    /// c:212 — `init_builtins` is idempotent: calling twice doesn't
7321    /// duplicate entries in the table. Regression that re-inserts on
7322    /// every call would balloon memory + break dispatch lookups.
7323    #[test]
7324    fn init_builtins_is_idempotent() {
7325        init_builtins();
7326        let count1 = createbuiltintable().len();
7327        init_builtins();
7328        let count2 = createbuiltintable().len();
7329        assert_eq!(count1, count2, "init_builtins must not duplicate entries");
7330    }
7331
7332    /// c:1708 — `fcsubs(sp, [(old, new), ...])` applies each
7333    /// substitution to the running string, returning the total
7334    /// replacement count. A regression returning 0 with substitutions
7335    /// applied would silently break `fc -s old=new`.
7336    #[test]
7337    fn fcsubs_applies_each_substitution_in_order() {
7338        let mut s = "echo foo bar foo".to_string();
7339        let n = fcsubs(&mut s, &[("foo".to_string(), "FOO".to_string())]);
7340        assert_eq!(s, "echo FOO bar FOO");
7341        assert_eq!(n, 2, "two `foo` matches replaced");
7342    }
7343
7344    /// c:1708 — empty `old` MUST skip (avoid infinite empty-match
7345    /// replacement loop). Regression treating "" as "match anywhere"
7346    /// would hang or silently corrupt every fc invocation.
7347    #[test]
7348    fn fcsubs_skips_empty_pattern() {
7349        let mut s = "anything".to_string();
7350        let n = fcsubs(&mut s, &[("".to_string(), "X".to_string())]);
7351        assert_eq!(s, "anything", "empty pattern must be skipped");
7352        assert_eq!(n, 0);
7353    }
7354
7355    /// c:1708 — chained substitutions apply left-to-right. After
7356    /// `a→b`, the next pair sees the post-substitution text. So
7357    /// `[(a→b), (b→c)]` over `a` yields `c`.
7358    #[test]
7359    fn fcsubs_chains_substitutions_left_to_right() {
7360        let mut s = "a".to_string();
7361        let n = fcsubs(&mut s, &[
7362            ("a".to_string(), "b".to_string()),
7363            ("b".to_string(), "c".to_string()),
7364        ]);
7365        assert_eq!(s, "c", "second sub sees post-first-sub text");
7366        assert_eq!(n, 2);
7367    }
7368
7369    /// c:1708 — substitution on no-match leaves string unchanged AND
7370    /// reports 0. Regression touching the string anyway would mangle
7371    /// fc output for events containing none of the requested patterns.
7372    #[test]
7373    fn fcsubs_no_match_returns_zero_unchanged() {
7374        let mut s = "hello world".to_string();
7375        let n = fcsubs(&mut s, &[("xyz".to_string(), "abc".to_string())]);
7376        assert_eq!(s, "hello world", "no match → unchanged");
7377        assert_eq!(n, 0);
7378    }
7379
7380    /// c:1297 — `fixdir` for plain relative path (no slashes, no
7381    /// dots) returns it unchanged. Most-common cd path; regression
7382    /// here would break `cd subdir`.
7383    #[test]
7384    fn fixdir_plain_relative_path_unchanged() {
7385        assert_eq!(fixdir("subdir"),  "subdir");
7386        assert_eq!(fixdir("a/b/c"),   "a/b/c");
7387        assert_eq!(fixdir("."),       ".");
7388    }
7389
7390    /// Shared mutex for bin_let tests that toggle the global errflag.
7391    static BIN_LET_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
7392
7393    /// `Src/builtin.c:7469-7484` — `bin_let` semantics:
7394    ///   1. Returns 0 (success) when the LAST arg evaluates to non-zero.
7395    ///   2. Returns 1 (failure) when the LAST arg evaluates to zero.
7396    ///   3. Returns 2 AND CLEARS ERRFLAG_ERROR when any arg errors.
7397    /// The previous Rust port used a local `had_error` flag and never
7398    /// cleared `errflag`, letting `let` errors leak into subsequent
7399    /// commands — defeating the C `let` "errors are non-fatal and local"
7400    /// contract.
7401    #[test]
7402    fn bin_let_clears_errflag_on_math_error() {
7403        let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
7404        use crate::ported::utils::{errflag, ERRFLAG_ERROR};
7405        use std::sync::atomic::Ordering;
7406        let saved = errflag.load(Ordering::Relaxed);
7407        errflag.store(0, Ordering::Relaxed);
7408
7409        // 1. Last arg evaluates to non-zero → return 0.
7410        let ops = crate::ported::zsh_h::options {
7411            ind: [0; crate::ported::zsh_h::MAX_OPS],
7412            args: Vec::new(),
7413            argscount: 0,
7414            argsalloc: 0,
7415        };
7416        let argv = vec!["1".to_string()];
7417        assert_eq!(bin_let("let", &argv, &ops, 0), 0,
7418            "c:7482 — last expr non-zero → return 0 (success)");
7419
7420        // 2. Last arg evaluates to zero → return 1.
7421        let argv = vec!["0".to_string()];
7422        assert_eq!(bin_let("let", &argv, &ops, 0), 1,
7423            "c:7482 — last expr zero → return 1 (failure)");
7424
7425        // 3. Bad-syntax arg → return 2 AND clear ERRFLAG_ERROR.
7426        // Pre-set errflag manually to simulate matheval failure side
7427        // effect (since exact bad-syntax behavior of the matheval port
7428        // is implementation-dependent — what we're pinning is the
7429        // bin_let response to a set errflag).
7430        errflag.store(ERRFLAG_ERROR, Ordering::Relaxed);
7431        // Use a valid expression so matheval succeeds, but errflag
7432        // is already set from a prior step.
7433        let argv = vec!["1".to_string()];
7434        let rc = bin_let("let", &argv, &ops, 0);
7435        assert_eq!(rc, 2,
7436            "c:7479 — pre-set ERRFLAG_ERROR triggers c:7476-7480 cleanup, returns 2");
7437        // c:7478 — `errflag &= ~ERRFLAG_ERROR` must have run.
7438        assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0,
7439            "c:7478 — ERRFLAG_ERROR must be CLEARED after let error");
7440
7441        // Restore.
7442        errflag.store(saved, Ordering::Relaxed);
7443    }
7444
7445    /// `Src/builtin.c:7474-7475` — C walks ALL argv via
7446    /// `while (*argv) val = matheval(*argv++);`. The LAST matheval
7447    /// result is what determines the return code. The previous Rust
7448    /// port broke on first error, skipping later args. Pin: a sequence
7449    /// of two non-zero exprs returns 0 even if both are evaluated.
7450    #[test]
7451    fn bin_let_walks_all_argv_last_wins() {
7452        let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
7453        use crate::ported::utils::{errflag, ERRFLAG_ERROR};
7454        use std::sync::atomic::Ordering;
7455        errflag.store(0, Ordering::Relaxed);
7456
7457        let ops = crate::ported::zsh_h::options {
7458            ind: [0; crate::ported::zsh_h::MAX_OPS],
7459            args: Vec::new(),
7460            argscount: 0,
7461            argsalloc: 0,
7462        };
7463        // c:7474 — `5; 0` (two args): last is 0 → return 1.
7464        let argv = vec!["5".to_string(), "0".to_string()];
7465        assert_eq!(bin_let("let", &argv, &ops, 0), 1,
7466            "c:7474 — last arg wins (here: 0 → return 1)");
7467
7468        // c:7474 — `0; 5` (two args): last is 5 → return 0.
7469        let argv = vec!["0".to_string(), "5".to_string()];
7470        assert_eq!(bin_let("let", &argv, &ops, 0), 0,
7471            "c:7474 — last arg wins (here: 5 → return 0)");
7472
7473        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
7474    }
7475}