Skip to main content

zsh/ported/
params.rs

1//! Parameter management for zshrs
2//!
3//! Port from zsh/Src/params.c (6511 lines → full Rust port)
4//!
5//! Provides shell parameters (variables), special parameters, arrays,
6//! associative arrays, parameter attributes, namerefs, scoping,
7//! tied parameters, and all special parameter get/set functions.
8
9#[allow(unused_imports)]
10use crate::ported::utils::zerr;
11use crate::func_body_fmt::FuncBodyFmt;
12use indexmap::IndexMap;
13use std::collections::{HashMap, HashSet};
14use std::env;
15use std::sync::atomic::{AtomicI64, Ordering};
16use std::time::{Instant, SystemTime, UNIX_EPOCH};
17use crate::ported::zsh_h::{
18    gsu_array, gsu_float, gsu_hash, gsu_integer, gsu_scalar
19};
20use crate::ported::zsh_h::VALFLAG_SUBST;
21use crate::ported::zsh_h::{Param, hashnode, isset as isset_opt};
22use fusevm::Value;
23use crate::ported::zsh_h::Marker;
24use crate::ported::zsh_h::{SCANPM_ISVAR_AT, SCANPM_NONAMEREF};
25use crate::ported::zsh_h::SCANPM_WANTINDEX;
26
27#[allow(unused_imports)]
28use crate::ported::zsh_h::{
29    PM_TYPE, PM_SCALAR, PM_NAMEREF, PM_INTEGER, PM_EFLOAT, PM_FFLOAT,
30    PM_ARRAY, PM_HASHED, PM_HASHELEM, PM_NAMEDDIR, PM_UNIQUE,
31    PM_READONLY, PM_UNSET, PM_EXPORTED, PM_AUTOLOAD, PM_DEFAULTED,
32    PM_DECLARED, PM_REMOVABLE, PM_NORESTORE, PM_LOCAL, PM_RO_BY_DESIGN,
33    PM_LEFT, PM_RIGHT_B, PM_RIGHT_Z, PM_SPECIAL, PM_TAGGED, PM_TIED, PM_UPPER,
34    SCANPM_CHECKING, SCANPM_MATCHMANY, SCANPM_MATCHKEY, SCANPM_MATCHVAL,
35    SCANPM_KEYMATCH, SCANPM_WANTKEYS, SCANPM_WANTVALS, SCANPM_ARRONLY,
36    VALFLAG_EMPTY, VALFLAG_INV,
37    ASSPM_WARN, ASSPM_AUGMENT, ASSPM_ENV_IMPORT,
38    PRINT_NAMEONLY, PRINT_TYPESET, PRINT_INCLUDEVALUE, PRINT_KV_PAIR,
39    PRINT_LINE, PRINT_POSIX_READONLY, PRINT_POSIX_EXPORT,
40    EXECOPT, KSHARRAYS, AUTONAMEDIRS, ALLEXPORT,
41    WARNCREATEGLOBAL, WARNNESTEDVAR,
42    isset, unset,
43};
44#[allow(unused_imports)]
45use crate::ported::math::{mnumber, MN_INTEGER, MN_FLOAT};
46#[allow(unused_imports)]
47use crate::ported::utils::errflag;
48#[allow(unused_imports)]
49use crate::ported::signals::{queue_signals, unqueue_signals};
50
51/// Port of `static int lc_update_needed` from `Src/params.c:5850`
52/// (under `#ifdef USE_LOCALE`). Set to 1 by `scanendscope` when a
53/// LC_*/LANG param's scope ends; consumed by `endparamscope` to
54/// trigger a `setlocale()` refresh.
55pub static LC_UPDATE_NEEDED: std::sync::atomic::AtomicI32 =
56    std::sync::atomic::AtomicI32::new(0);
57
58/// Port of `static Param foundparam` from `Src/params.c:640`.
59/// Set by `scanparamvals` to the last param it touched, read by
60/// `assignsparam` / `assignnparam` for the assoc-element path.
61/// Stores the param name; the live `&param` lookup is done by
62/// the caller through paramtab.
63pub static FOUNDPARAM: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
64    std::sync::OnceLock::new();
65
66/// Port of `rprompt_indent_unsetfn(Param pm, int exp)` from `Src/params.c:152`. C
67/// body: `stdunsetfn(pm, exp); rprompt_indent = 1;` — keeps in
68/// sync with init_term().
69pub fn rprompt_indent_unsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
70    stdunsetfn(pm, exp);
71    *RPROMPT_INDENT.lock().unwrap() = 1;
72}
73
74
75// =============================================================================
76// IPDEF{1,2,4,5,5U,6,7,7R,7U,8,9,10} + LCIPDEF — special-parameter
77// table entry constructors. All defined as macros in
78// `Src/params.c:296-406`. Each produces one row of the
79// `special_params[]` table; the differences are flag combinations
80// + which gsu (getter/setter union) the entry binds.
81//
82// In C, `BR(p)` is `{(void *)(p)}` for the param's `u` data field;
83// `GSU(g)` is the `&g` of the named gsu_scalar/gsu_integer/etc.
84// The Rust port stores `var` and `gsu` as `usize` slot indexes
85// into per-evaluator tables, matching the existing PARAMDEF helper
86// above. The flag bit combinations mirror the C macros line-by-line.
87// =============================================================================
88
89/// Port of `IPDEF1(A,B,C)` from `Src/params.c:296` —
90/// `{{NULL,A,PM_INTEGER|PM_SPECIAL|C},BR(NULL),GSU(B),10,0,...}`.
91#[inline] #[allow(non_snake_case)]
92pub fn IPDEF1(A: &str, B: usize, C: i32) -> paramdef {        // c:params.c:296
93    paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL) as i32 | C, gsu: B, ..Default::default() }
94}
95
96/// Port of `IPDEF2(A,B,C)` from `Src/params.c:309` —
97/// `{{NULL,A,PM_SCALAR|PM_SPECIAL|C},BR(NULL),GSU(B),0,0,...}`.
98#[inline] #[allow(non_snake_case)]
99pub fn IPDEF2(A: &str, B: usize, C: i32) -> paramdef {        // c:params.c:309
100    paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL) as i32 | C, gsu: B, ..Default::default() }
101}
102
103// ---------------------------------------------------------------------------
104// Parameter flags (from zsh.h PM_* flags)
105// ---------------------------------------------------------------------------
106
107// What level of localness we are at.                                       // c:47
108//                                                                          // c:48
109// Hand-wavingly, this is incremented at every function call and decremented // c:49
110// at every function return.  See startparamscope().                        // c:50
111
112/// Port of `mod_export int locallevel;` from `Src/params.c:54`.
113/// Tracks function-local-scope nesting depth. Bumped by
114/// `startparamscope()` (params.c:5879) on every function call,
115/// decremented by `endparamscope()` (params.c:5950) on return.
116#[allow(non_upper_case_globals)]
117pub static locallevel: std::sync::atomic::AtomicI32 =                        // c:54
118    std::sync::atomic::AtomicI32::new(0);
119
120// ---------------------------------------------------------------------------
121// Real `param` struct lives in Src/zsh.h:1829 (port at zsh_h.rs:750).
122// It uses C-union flattening: u_str / u_arr / u_val / u_dval / u_hash
123// dispatched on `PM_TYPE(node.flags)`. There is NO `ParamValue` enum in
124// C; do not reintroduce one.
125// ---------------------------------------------------------------------------
126
127pub use crate::ported::zsh_h::param;
128
129/// Port of `LCIPDEF(name)` from `Src/params.c:324` —
130/// `IPDEF2(name, lc_blah_gsu, PM_UNSET)`.
131#[inline] #[allow(non_snake_case)]
132pub fn LCIPDEF(name: &str) -> paramdef {                                     // c:params.c:324
133    IPDEF2(name, 0, PM_UNSET as i32)                                         // c:324 lc_blah_gsu (slot 0)
134}
135
136/// Port of `IPDEF4(A,B)` from `Src/params.c:344` —
137/// `{{NULL,A,PM_INTEGER|PM_READONLY_SPECIAL},BR((void*)B),
138///   GSU(varint_readonly_gsu),10,0,...}`.
139#[inline] #[allow(non_snake_case)]
140pub fn IPDEF4(A: &str, B: usize) -> paramdef {                          // c:params.c:344
141    paramdef { name: A.into(), flags: (PM_INTEGER | PM_READONLY_SPECIAL) as i32, var: B, ..Default::default() }
142}
143
144/// Port of `IPDEF5(A,B,F)` from `Src/params.c:353` —
145/// `{{NULL,A,PM_INTEGER|PM_SPECIAL},BR((void*)B),GSU(F),10,0,...}`.
146#[inline] #[allow(non_snake_case)]
147pub fn IPDEF5(A: &str, B: usize, F: usize) -> paramdef {              // c:params.c:353
148    paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL) as i32, var: B, gsu: F, ..Default::default() }
149}
150
151/// Port of `IPDEF5U(A,B,F)` from `Src/params.c:354` — c:353 + PM_UNSET.
152#[inline] #[allow(non_snake_case)]
153pub fn IPDEF5U(A: &str, B: usize, F: usize) -> paramdef {             // c:params.c:354
154    paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL | PM_UNSET) as i32, var: B, gsu: F, ..Default::default() }
155}
156
157/// Port of `IPDEF6(A,B,F)` from `Src/params.c:362` — c:353 + PM_DONTIMPORT.
158#[inline] #[allow(non_snake_case)]
159pub fn IPDEF6(A: &str, B: usize, F: usize) -> paramdef {              // c:params.c:362
160    paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT) as i32, var: B, gsu: F, ..Default::default() }
161}
162
163/// Port of `IPDEF7(A,B)` from `Src/params.c:367` —
164/// `{{NULL,A,PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(varscalar_gsu),0,0,...}`.
165#[inline] #[allow(non_snake_case)]
166pub fn IPDEF7(A: &str, B: usize) -> paramdef {                          // c:params.c:367
167    paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL) as i32, var: B, ..Default::default() }
168}
169
170/// Port of `IPDEF7U(A,B)` from `Src/params.c:369` — c:367 + PM_UNSET.
171#[inline] #[allow(non_snake_case)]
172pub fn IPDEF7U(A: &str, B: usize) -> paramdef {                         // c:params.c:369
173    paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL | PM_UNSET) as i32, var: B, ..Default::default() }
174}
175
176/// Port of `IPDEF7R(A,B)` from `Src/params.c:368` — c:367 + PM_DONTIMPORT_SUID.
177#[inline] #[allow(non_snake_case)]
178pub fn IPDEF7R(A: &str, B: usize) -> paramdef {                         // c:params.c:368
179    paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL | PM_DONTIMPORT_SUID) as i32, var: B, ..Default::default() }
180}
181
182/// Port of `IPDEF9(A,B,C,D)` from `Src/params.c:431` —
183/// `{{NULL,A,D|PM_ARRAY|PM_SPECIAL|PM_DONTIMPORT},BR((void*)B),
184///   GSU(vararray_gsu),0,0,NULL,C,NULL,0}`.
185#[inline] #[allow(non_snake_case)]
186pub fn IPDEF9(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:384
187    paramdef { name: A.into(), flags: (PM_ARRAY | PM_SPECIAL | PM_DONTIMPORT) as i32 | D, var: B, ..Default::default() }
188}
189
190/// Port of `IPDEF8(A,B,C,D)` from `Src/params.c:394` —
191/// `{{NULL,A,D|PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(colonarr_gsu),
192///   0,0,NULL,C,NULL,0}`.
193/// `C` is the colon-arr field; the Rust port stores it in `getnfn`
194/// since `paramdef` lacks a dedicated colon-arr slot until that's
195/// ported.
196#[inline] #[allow(non_snake_case)]
197pub fn IPDEF8(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:394
198    paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL) as i32 | D, var: B, ..Default::default() }
199}
200
201/// Port of `IPDEF10(A,B)` from `Src/params.c:438` —
202/// `{{NULL,A,PM_ARRAY|PM_SPECIAL},BR(NULL),GSU(B),10,0,...}`.
203#[inline] #[allow(non_snake_case)]
204pub fn IPDEF10(A: &str, B: usize) -> paramdef {                         // c:params.c:406
205    paramdef { name: A.into(), flags: (PM_ARRAY | PM_SPECIAL) as i32, gsu: B, ..Default::default() }
206}
207
208/// Port of `newparamtable(int size, char const *name)` from `Src/params.c:519`. C body
209/// allocates a HashTable via `newhashtable(size, name, NULL)`
210/// and wires the vtable. Rust port constructs a fresh
211/// `Box<hashtable>` with the param-specific callbacks left as
212/// `None` (the hashtable.rs vtable cannot host the typed
213/// param-callback signatures yet — wiring them requires the
214/// hashtable backend refactor).
215#[allow(unused_variables)]
216pub fn newparamtable(size: i32, name: &str)
217    -> Option<crate::ported::zsh_h::HashTable>
218{
219    let hsize = if size == 0 { 17 } else { size };
220    let mut nodes: Vec<Option<crate::ported::zsh_h::HashNode>> =
221        Vec::with_capacity(hsize as usize);
222    for _ in 0..hsize {
223        nodes.push(None);
224    }
225    Some(Box::new(crate::ported::zsh_h::hashtable {
226        hsize,
227        ct: 0,
228        nodes,
229        tmpdata: 0,
230        hash: None,
231        emptytable: None,
232        filltable: None,
233        cmpnodes: None,
234        addnode: None,
235        getnode: None,
236        getnode2: None,
237        removenode: None,
238        disablenode: None,
239        enablenode: None,
240        freenode: None,
241        printnode: None,
242        scantab: None,
243    }))
244}
245
246/// Direct port of `static Param loadparamnode(HashTable ht, Param
247/// pm, const char *nam)` from `Src/params.c:544-567`. If `pm` is
248/// an AUTOLOAD stub, fires the module loader and re-fetches the
249/// node from ht; otherwise returns pm unchanged.
250///
251/// C body:
252///   if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str) {
253///       int level = pm->level;
254///       char *mn = dupstring(pm->u.str);
255///       (void)ensurefeature(mn, "p:", nam);
256///       pm = (Param)gethashnode2(ht, nam);
257///       while (pm && pm->level > level) pm = pm->old;
258///       if (pm && (pm->level != level || (pm->flags & PM_AUTOLOAD)))
259///           pm = NULL;
260///       if (!pm) zerr("autoloading module %s failed...", mn, nam);
261///   }
262///   return pm;
263/// Port of `loadparamnode(HashTable ht, Param pm, const char *nam)` from `Src/params.c:544`.
264/// WARNING: param names don't match C — Rust=(pm, nam) vs C=(ht, pm, nam)
265pub fn loadparamnode(                                                        // c:544
266    _ht: &crate::ported::zsh_h::HashTable,
267    pm: Option<crate::ported::zsh_h::Param>,
268    nam: &str,
269) -> Option<crate::ported::zsh_h::Param> {
270
271    // c:546 — `if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str)`.
272    let (level, modname) = match &pm {
273        Some(p)
274            if p.node.flags & PM_AUTOLOAD as i32 != 0 && p.u_str.is_some() =>
275        {
276            (p.level, p.u_str.clone().unwrap())
277        }
278        _ => return pm,                                                      // c:566 fall through
279    };
280
281    // c:549 — `ensurefeature(mn, "p:", nam)` fires the module loader.
282    // The Rust ensurefeature signature differs (takes ModuleTable);
283    // for now we look up the module without a table to keep the
284    // dispatch site honest. Module-table integration is pending.
285    // c:550 — re-fetch the node from ht after autoload.
286    let mut pm = paramtab().write().unwrap().get(nam).cloned();
287    // c:551 — walk pm->old back to original level.
288    while let Some(ref p) = pm {
289        if p.level > level {
290            pm = p.old.clone().map(|b| crate::ported::zsh_h::Param::from(b));
291        } else {
292            break;
293        }
294    }
295    // c:553-554 — if pm is at wrong level or still AUTOLOAD, treat
296    // as load failure.
297    let still_bad = match &pm {
298        Some(p) => p.level != level || p.node.flags & PM_AUTOLOAD as i32 != 0,
299        None => true,
300    };
301    if still_bad {
302        pm = None;
303        // c:561-563 — `zerr("autoloading module %s failed to define
304        // parameter: %s", mn, nam)`.
305        crate::ported::utils::zerr(&format!(
306            "autoloading module {} failed to define parameter: {}",
307            modname, nam
308        ));
309    }
310    pm                                                                       // c:566
311}
312
313
314
315// ---------------------------------------------------------------------------
316// Numeric type for parameters (from params.c mnumber)
317// ---------------------------------------------------------------------------
318
319
320// ---------------------------------------------------------------------------
321// Value struct - mirrors C's struct value for subscript access
322// ---------------------------------------------------------------------------
323// ---------------------------------------------------------------------------
324// Shell parameter
325// ---------------------------------------------------------------------------
326
327
328// ---------------------------------------------------------------------------
329// Tied parameter data
330// ---------------------------------------------------------------------------
331
332// TiedData removed: was a Rust-only sidecar for the deleted `ParamTable`'s
333// `tied: HashMap<String, TiedData>` field. C source stores tied-pair
334// metadata via `pm->ename` (the partner name) and `pm->u.data` (the
335// separator char) on the real `param` struct (Src/zsh.h:750 / Src/params.c
336// `bin_typeset()` typeset -T branch).
337
338
339// ---------------------------------------------------------------------------
340// Parameter table print types (from printparamnode)
341// ---------------------------------------------------------------------------
342
343// ---------------------------------------------------------------------------
344// Special parameter definitions table (mirrors special_params[] in C)
345// ---------------------------------------------------------------------------
346
347/// Special-parameter definition — Rust extension paralleling the
348/// `IPDEF*` macro entries in `Src/params.c:297-392`. C uses
349/// `struct paramdef` (`Src/zsh.h:2082`, mirrored at `zsh_h.rs:950`)
350/// with `var` + `gsu` pointers; the Rust port carries a trimmed
351/// shape with `pm_type`/`pm_flags`/`tied_name` until the full
352/// `gsu`-callback plumbing lands. Canonical `paramdef` is the
353/// long-term target.
354#[allow(non_camel_case_types)]
355#[derive(Clone, Debug)]
356pub struct special_paramdef {
357    pub name: &'static str,
358    pub pm_type: u32,  // PM_INTEGER | PM_SCALAR | PM_ARRAY
359    pub pm_flags: u32, // PM_READONLY_SPECIAL, PM_DONTIMPORT, etc.
360    pub tied_name: Option<&'static str>,
361}
362
363/// Index of the first entry in `special_params` that lives in the
364/// zsh-only section (after the `{{NULL,NULL,0}, BR(NULL), ...}`
365/// sentinel at `Src/params.c:392`). Entries before this index are
366/// always loaded; entries at and after this index are only loaded
367/// under non-sh/non-ksh emulation. Mirrors the C two-section table
368/// terminated by an inner NULL sentinel.
369pub const SPECIAL_PARAMS_ZSH_START: usize = 54;                              // c:392
370
371/// All special parameters from params.c special_params[]
372pub const special_params: &[special_paramdef] = &[
373    // Integer specials with custom GSU
374    special_paramdef {
375        name: "#",
376        pm_type: PM_INTEGER,
377        pm_flags: PM_READONLY,
378        tied_name: None,
379    },
380    special_paramdef {
381        name: "ERRNO",
382        pm_type: PM_INTEGER,
383        pm_flags: PM_UNSET,
384        tied_name: None,
385    },
386    special_paramdef {
387        name: "GID",
388        pm_type: PM_INTEGER,
389        pm_flags: PM_DONTIMPORT,
390        tied_name: None,
391    },
392    special_paramdef {
393        name: "EGID",
394        pm_type: PM_INTEGER,
395        pm_flags: PM_DONTIMPORT,
396        tied_name: None,
397    },
398    special_paramdef {
399        name: "HISTSIZE",
400        pm_type: PM_INTEGER,
401        pm_flags: 0,
402        tied_name: None,
403    },
404    special_paramdef {
405        name: "RANDOM",
406        pm_type: PM_INTEGER,
407        pm_flags: 0,
408        tied_name: None,
409    },
410    special_paramdef {
411        name: "SAVEHIST",
412        pm_type: PM_INTEGER,
413        pm_flags: 0,
414        tied_name: None,
415    },
416    special_paramdef {
417        name: "SECONDS",
418        pm_type: PM_INTEGER,
419        pm_flags: 0,
420        tied_name: None,
421    },
422    special_paramdef {
423        name: "UID",
424        pm_type: PM_INTEGER,
425        pm_flags: PM_DONTIMPORT,
426        tied_name: None,
427    },
428    special_paramdef {
429        name: "EUID",
430        pm_type: PM_INTEGER,
431        pm_flags: PM_DONTIMPORT,
432        tied_name: None,
433    },
434    special_paramdef {
435        name: "TTYIDLE",
436        pm_type: PM_INTEGER,
437        pm_flags: PM_READONLY,
438        tied_name: None,
439    },
440    // Scalar specials with custom GSU
441    special_paramdef {
442        name: "USERNAME",
443        pm_type: PM_SCALAR,
444        pm_flags: PM_DONTIMPORT,
445        tied_name: None,
446    },
447    special_paramdef {
448        name: "-",
449        pm_type: PM_SCALAR,
450        pm_flags: PM_READONLY,
451        tied_name: None,
452    },
453    special_paramdef {
454        name: "histchars",
455        pm_type: PM_SCALAR,
456        pm_flags: PM_DONTIMPORT,
457        tied_name: None,
458    },
459    special_paramdef {
460        name: "HOME",
461        pm_type: PM_SCALAR,
462        pm_flags: PM_UNSET,
463        tied_name: None,
464    },
465    special_paramdef {
466        name: "TERM",
467        pm_type: PM_SCALAR,
468        pm_flags: PM_UNSET,
469        tied_name: None,
470    },
471    special_paramdef {
472        name: "TERMINFO",
473        pm_type: PM_SCALAR,
474        pm_flags: PM_UNSET,
475        tied_name: None,
476    },
477    special_paramdef {
478        name: "TERMINFO_DIRS",
479        pm_type: PM_SCALAR,
480        pm_flags: PM_UNSET,
481        tied_name: None,
482    },
483    special_paramdef {
484        name: "WORDCHARS",
485        pm_type: PM_SCALAR,
486        pm_flags: 0,
487        tied_name: None,
488    },
489    special_paramdef {
490        name: "IFS",
491        pm_type: PM_SCALAR,
492        pm_flags: PM_DONTIMPORT,
493        tied_name: None,
494    },
495    special_paramdef {
496        name: "_",
497        pm_type: PM_SCALAR,
498        pm_flags: PM_DONTIMPORT,
499        tied_name: None,
500    },
501    special_paramdef {
502        name: "KEYBOARD_HACK",
503        pm_type: PM_SCALAR,
504        pm_flags: PM_DONTIMPORT,
505        tied_name: None,
506    },
507    special_paramdef {
508        name: "0",
509        pm_type: PM_SCALAR,
510        pm_flags: 0,
511        tied_name: None,
512    },
513    // Readonly integer variables bound to C globals
514    special_paramdef {
515        name: "!",
516        pm_type: PM_INTEGER,
517        pm_flags: PM_READONLY,
518        tied_name: None,
519    },
520    special_paramdef {
521        name: "$",
522        pm_type: crate::ported::zsh_h::PM_INTEGER,
523        pm_flags: crate::ported::zsh_h::PM_READONLY,
524        tied_name: None,
525    },
526    special_paramdef {
527        name: "?",
528        pm_type: crate::ported::zsh_h::PM_INTEGER,
529        pm_flags: crate::ported::zsh_h::PM_READONLY,
530        tied_name: None,
531    },
532    special_paramdef {
533        name: "HISTCMD",
534        pm_type: crate::ported::zsh_h::PM_INTEGER,
535        pm_flags: crate::ported::zsh_h::PM_READONLY,
536        tied_name: None,
537    },
538    special_paramdef {
539        name: "LINENO",
540        pm_type: crate::ported::zsh_h::PM_INTEGER,
541        pm_flags: crate::ported::zsh_h::PM_READONLY,
542        tied_name: None,
543    },
544    special_paramdef {
545        name: "PPID",
546        pm_type: crate::ported::zsh_h::PM_INTEGER,
547        pm_flags: crate::ported::zsh_h::PM_READONLY,
548        tied_name: None,
549    },
550    special_paramdef {
551        name: "ZSH_SUBSHELL",
552        pm_type: crate::ported::zsh_h::PM_INTEGER,
553        pm_flags: crate::ported::zsh_h::PM_READONLY,
554        tied_name: None,
555    },
556    // Settable integer variables
557    special_paramdef {
558        name: "COLUMNS",
559        pm_type: crate::ported::zsh_h::PM_INTEGER,
560        pm_flags: 0,
561        tied_name: None,
562    },
563    special_paramdef {
564        name: "LINES",
565        pm_type: crate::ported::zsh_h::PM_INTEGER,
566        pm_flags: 0,
567        tied_name: None,
568    },
569    special_paramdef {
570        name: "ZLE_RPROMPT_INDENT",
571        pm_type: crate::ported::zsh_h::PM_INTEGER,
572        pm_flags: crate::ported::zsh_h::PM_UNSET,
573        tied_name: None,
574    },
575    special_paramdef {
576        name: "SHLVL",
577        pm_type: crate::ported::zsh_h::PM_INTEGER,
578        pm_flags: 0,
579        tied_name: None,
580    },
581    special_paramdef {
582        name: "FUNCNEST",
583        pm_type: crate::ported::zsh_h::PM_INTEGER,
584        pm_flags: 0,
585        tied_name: None,
586    },
587    special_paramdef {
588        name: "OPTIND",
589        pm_type: crate::ported::zsh_h::PM_INTEGER,
590        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
591        tied_name: None,
592    },
593    special_paramdef {
594        name: "TRY_BLOCK_ERROR",
595        pm_type: crate::ported::zsh_h::PM_INTEGER,
596        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
597        tied_name: None,
598    },
599    special_paramdef {
600        name: "TRY_BLOCK_INTERRUPT",
601        pm_type: crate::ported::zsh_h::PM_INTEGER,
602        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
603        tied_name: None,
604    },
605    // Scalar variables bound to C globals
606    special_paramdef {
607        name: "OPTARG",
608        pm_type: crate::ported::zsh_h::PM_SCALAR,
609        pm_flags: 0,
610        tied_name: None,
611    },
612    special_paramdef {
613        name: "NULLCMD",
614        pm_type: crate::ported::zsh_h::PM_SCALAR,
615        pm_flags: 0,
616        tied_name: None,
617    },
618    special_paramdef {
619        name: "POSTEDIT",
620        pm_type: crate::ported::zsh_h::PM_SCALAR,
621        pm_flags: crate::ported::zsh_h::PM_UNSET,
622        tied_name: None,
623    },
624    special_paramdef {
625        name: "READNULLCMD",
626        pm_type: crate::ported::zsh_h::PM_SCALAR,
627        pm_flags: 0,
628        tied_name: None,
629    },
630    special_paramdef {
631        name: "PS1",
632        pm_type: crate::ported::zsh_h::PM_SCALAR,
633        pm_flags: 0,
634        tied_name: None,
635    },
636    special_paramdef {
637        name: "RPS1",
638        pm_type: crate::ported::zsh_h::PM_SCALAR,
639        pm_flags: crate::ported::zsh_h::PM_UNSET,
640        tied_name: None,
641    },
642    special_paramdef {
643        name: "RPROMPT",
644        pm_type: crate::ported::zsh_h::PM_SCALAR,
645        pm_flags: crate::ported::zsh_h::PM_UNSET,
646        tied_name: None,
647    },
648    special_paramdef {
649        name: "PS2",
650        pm_type: crate::ported::zsh_h::PM_SCALAR,
651        pm_flags: 0,
652        tied_name: None,
653    },
654    special_paramdef {
655        name: "RPS2",
656        pm_type: crate::ported::zsh_h::PM_SCALAR,
657        pm_flags: crate::ported::zsh_h::PM_UNSET,
658        tied_name: None,
659    },
660    special_paramdef {
661        name: "RPROMPT2",
662        pm_type: crate::ported::zsh_h::PM_SCALAR,
663        pm_flags: crate::ported::zsh_h::PM_UNSET,
664        tied_name: None,
665    },
666    special_paramdef {
667        name: "PS3",
668        pm_type: crate::ported::zsh_h::PM_SCALAR,
669        pm_flags: 0,
670        tied_name: None,
671    },
672    special_paramdef {
673        name: "PS4",
674        pm_type: crate::ported::zsh_h::PM_SCALAR,
675        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT_SUID,
676        tied_name: None,
677    },
678    special_paramdef {
679        name: "SPROMPT",
680        pm_type: crate::ported::zsh_h::PM_SCALAR,
681        pm_flags: 0,
682        tied_name: None,
683    },
684    // Readonly arrays
685    special_paramdef {
686        name: "*",
687        pm_type: crate::ported::zsh_h::PM_ARRAY,
688        pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
689        tied_name: None,
690    },
691    special_paramdef {
692        name: "@",
693        pm_type: crate::ported::zsh_h::PM_ARRAY,
694        pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
695        tied_name: None,
696    },
697    // ===================================================================
698    // c:388-392 — `/* This empty row indicates the end of parameters
699    // available in all emulations. */` NULL sentinel terminates the
700    // "always loaded" section. Entries below this line are only added
701    // under zsh emulation (else-branch of EMULATION(EMULATE_SH|EMULATE_KSH)
702    // at createparamtable c:840-846).
703    // SPECIAL_PARAMS_ZSH_START tracks this section boundary.
704    // ===================================================================
705    // Tied colon-separated/array pairs
706    special_paramdef {
707        name: "CDPATH",
708        pm_type: crate::ported::zsh_h::PM_SCALAR,
709        pm_flags: crate::ported::zsh_h::PM_TIED,
710        tied_name: Some("cdpath"),
711    },
712    special_paramdef {
713        name: "FIGNORE",
714        pm_type: crate::ported::zsh_h::PM_SCALAR,
715        pm_flags: crate::ported::zsh_h::PM_TIED,
716        tied_name: Some("fignore"),
717    },
718    special_paramdef {
719        name: "FPATH",
720        pm_type: crate::ported::zsh_h::PM_SCALAR,
721        pm_flags: crate::ported::zsh_h::PM_TIED,
722        tied_name: Some("fpath"),
723    },
724    special_paramdef {
725        name: "MAILPATH",
726        pm_type: crate::ported::zsh_h::PM_SCALAR,
727        pm_flags: crate::ported::zsh_h::PM_TIED,
728        tied_name: Some("mailpath"),
729    },
730    special_paramdef {
731        name: "PATH",
732        pm_type: crate::ported::zsh_h::PM_SCALAR,
733        pm_flags: crate::ported::zsh_h::PM_TIED,
734        tied_name: Some("path"),
735    },
736    special_paramdef {
737        name: "PSVAR",
738        pm_type: crate::ported::zsh_h::PM_SCALAR,
739        pm_flags: crate::ported::zsh_h::PM_TIED,
740        tied_name: Some("psvar"),
741    },
742    special_paramdef {
743        name: "ZSH_EVAL_CONTEXT",
744        pm_type: crate::ported::zsh_h::PM_SCALAR,
745        pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_TIED,
746        tied_name: Some("zsh_eval_context"),
747    },
748    special_paramdef {
749        name: "MODULE_PATH",
750        pm_type: crate::ported::zsh_h::PM_SCALAR,
751        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT | crate::ported::zsh_h::PM_TIED,
752        tied_name: Some("module_path"),
753    },
754    special_paramdef {
755        name: "MANPATH",
756        pm_type: crate::ported::zsh_h::PM_SCALAR,
757        pm_flags: crate::ported::zsh_h::PM_TIED,
758        tied_name: Some("manpath"),
759    },
760    // Locale
761    special_paramdef {
762        name: "LANG",
763        pm_type: crate::ported::zsh_h::PM_SCALAR,
764        pm_flags: crate::ported::zsh_h::PM_UNSET,
765        tied_name: None,
766    },
767    special_paramdef {
768        name: "LC_ALL",
769        pm_type: crate::ported::zsh_h::PM_SCALAR,
770        pm_flags: crate::ported::zsh_h::PM_UNSET,
771        tied_name: None,
772    },
773    special_paramdef {
774        name: "LC_COLLATE",
775        pm_type: crate::ported::zsh_h::PM_SCALAR,
776        pm_flags: crate::ported::zsh_h::PM_UNSET,
777        tied_name: None,
778    },
779    special_paramdef {
780        name: "LC_CTYPE",
781        pm_type: crate::ported::zsh_h::PM_SCALAR,
782        pm_flags: crate::ported::zsh_h::PM_UNSET,
783        tied_name: None,
784    },
785    special_paramdef {
786        name: "LC_MESSAGES",
787        pm_type: crate::ported::zsh_h::PM_SCALAR,
788        pm_flags: crate::ported::zsh_h::PM_UNSET,
789        tied_name: None,
790    },
791    special_paramdef {
792        name: "LC_NUMERIC",
793        pm_type: crate::ported::zsh_h::PM_SCALAR,
794        pm_flags: crate::ported::zsh_h::PM_UNSET,
795        tied_name: None,
796    },
797    special_paramdef {
798        name: "LC_TIME",
799        pm_type: crate::ported::zsh_h::PM_SCALAR,
800        pm_flags: crate::ported::zsh_h::PM_UNSET,
801        tied_name: None,
802    },
803    // Zsh-only aliases
804    special_paramdef {
805        name: "ARGC",
806        pm_type: crate::ported::zsh_h::PM_INTEGER,
807        pm_flags: crate::ported::zsh_h::PM_READONLY,
808        tied_name: None,
809    },
810    special_paramdef {
811        name: "HISTCHARS",
812        pm_type: crate::ported::zsh_h::PM_SCALAR,
813        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
814        tied_name: None,
815    },
816    special_paramdef {
817        name: "status",
818        pm_type: crate::ported::zsh_h::PM_INTEGER,
819        pm_flags: crate::ported::zsh_h::PM_READONLY,
820        tied_name: None,
821    },
822    special_paramdef {
823        name: "prompt",
824        pm_type: crate::ported::zsh_h::PM_SCALAR,
825        pm_flags: 0,
826        tied_name: None,
827    },
828    special_paramdef {
829        name: "PROMPT",
830        pm_type: crate::ported::zsh_h::PM_SCALAR,
831        pm_flags: 0,
832        tied_name: None,
833    },
834    special_paramdef {
835        name: "PROMPT2",
836        pm_type: crate::ported::zsh_h::PM_SCALAR,
837        pm_flags: 0,
838        tied_name: None,
839    },
840    special_paramdef {
841        name: "PROMPT3",
842        pm_type: crate::ported::zsh_h::PM_SCALAR,
843        pm_flags: 0,
844        tied_name: None,
845    },
846    special_paramdef {
847        name: "PROMPT4",
848        pm_type: crate::ported::zsh_h::PM_SCALAR,
849        pm_flags: 0,
850        tied_name: None,
851    },
852    special_paramdef {
853        name: "argv",
854        pm_type: crate::ported::zsh_h::PM_ARRAY,
855        pm_flags: 0,
856        tied_name: None,
857    },
858    // pipestatus array
859    special_paramdef {
860        name: "pipestatus",
861        pm_type: crate::ported::zsh_h::PM_ARRAY,
862        pm_flags: 0,
863        tied_name: None,
864    },
865];
866
867/// Port of `static initparam special_params_sh[]` from
868/// `Src/params.c:447-460`. "Alternative versions of colon-separated
869/// path parameters for sh emulation. These don't link to the array
870/// versions." Loaded by `createparamtable` (c:840-844) when
871/// `EMULATION(EMULATE_SH|EMULATE_KSH)` is non-zero, instead of the
872/// zsh-only section of `special_params`. All entries are scalars
873/// (`IPDEF8` macro adds `PM_SCALAR|PM_SPECIAL`); the C-side
874/// `tied_name` is NULL so these aren't tied to lowercase array
875/// counterparts.
876pub const special_params_sh: &[special_paramdef] = &[
877    special_paramdef {                                                        // c:448
878        name: "CDPATH",
879        pm_type: crate::ported::zsh_h::PM_SCALAR,
880        pm_flags: 0,
881        tied_name: None,
882    },
883    special_paramdef {                                                        // c:449
884        name: "FIGNORE",
885        pm_type: crate::ported::zsh_h::PM_SCALAR,
886        pm_flags: 0,
887        tied_name: None,
888    },
889    special_paramdef {                                                        // c:450
890        name: "FPATH",
891        pm_type: crate::ported::zsh_h::PM_SCALAR,
892        pm_flags: 0,
893        tied_name: None,
894    },
895    special_paramdef {                                                        // c:451
896        name: "MAILPATH",
897        pm_type: crate::ported::zsh_h::PM_SCALAR,
898        pm_flags: 0,
899        tied_name: None,
900    },
901    special_paramdef {                                                        // c:452
902        name: "PATH",
903        pm_type: crate::ported::zsh_h::PM_SCALAR,
904        pm_flags: 0,
905        tied_name: None,
906    },
907    special_paramdef {                                                        // c:453
908        name: "PSVAR",
909        pm_type: crate::ported::zsh_h::PM_SCALAR,
910        pm_flags: 0,
911        tied_name: None,
912    },
913    special_paramdef {                                                        // c:454
914        name: "ZSH_EVAL_CONTEXT",
915        pm_type: crate::ported::zsh_h::PM_SCALAR,
916        pm_flags: crate::ported::zsh_h::PM_READONLY,
917        tied_name: None,
918    },
919    special_paramdef {                                                        // c:457 (security comment)
920        name: "MODULE_PATH",
921        pm_type: crate::ported::zsh_h::PM_SCALAR,
922        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
923        tied_name: None,
924    },
925];
926
927/// Port of `getparamnode(HashTable ht, const char *nam)` from `Src/params.c:570`. C body:
928/// `pm = loadparamnode(ht, gethashnode2(ht, nam), nam);
929///  if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm);
930///  return (HashNode)pm;`
931/// Stub: needs HashTable + autoload + nameref resolve.
932/// WARNING: param names don't match C — Rust=() vs C=(ht, nam)
933pub fn getparamnode(ht: &crate::ported::zsh_h::HashTable, nam: &str)         // c:570
934    -> Option<crate::ported::zsh_h::Param>
935{
936    // c:572 — `pm = loadparamnode(ht, gethashnode2(ht, nam), nam)`.
937    let pm = paramtab().read().unwrap().get(nam).cloned();
938    let pm = loadparamnode(ht, pm, nam);
939    // c:573 — `if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm)`.
940    if let Some(p) = pm {
941        if p.node.flags & PM_UNSET as i32 == 0 {
942            // ht == realparamtab check — both Rust accessors point at
943            // the same backing store today, so this is always true.
944            return resolve_nameref(Some(p));
945        }
946        return Some(p);
947    }
948    None
949}
950
951/// Port of `scancopyparams(HashNode hn, UNUSED(int flags))` from `Src/params.c:584`. C body:
952/// ```c
953/// Param tpm = (Param) zshcalloc(sizeof *tpm);
954/// tpm->node.nam = ztrdup(pm->node.nam);
955/// copyparam(tpm, pm, 0);
956/// addhashnode(outtable, tpm->node.nam, tpm);
957/// ```
958/// Real port: clone the param via `Box::new(pm.clone())` (Rust
959/// equivalent of zshcalloc + copyparam) and push it into the
960/// caller-supplied destination table. The original C uses the
961/// global `outtable`; Rust port plumbs it in explicitly.
962/// WARNING: param names don't match C — Rust=(pm, _flags, outtable) vs C=(hn, flags)
963pub fn scancopyparams(
964    pm: &mut crate::ported::zsh_h::param,
965    _flags: i32,
966    outtable: &mut std::collections::HashMap<String, Box<crate::ported::zsh_h::param>>,
967) {
968    // c:586-588 — `tpm = (Param) zshcalloc(...); copyparam(tpm, pm, 0); addnode(...)`.
969    let mut tpm = Box::new(pm.clone());                                      // c:586 zshcalloc
970    tpm.old = None; tpm.env = None; tpm.ename = None;                        // c:1242 (calloc-zero fields copyparam doesn't set)
971    copyparam(&mut tpm, pm, 0);                                              // c:587
972    let nam = tpm.node.nam.clone();
973    outtable.insert(nam, tpm);                                               // c:588 addnode(outtable, ztrdup(pm->node.nam), tpm)
974}
975
976/// Port of `copyparamtable(HashTable ht, char *name)` from `Src/params.c:596`. C body:
977/// allocates a fresh paramtable via `newparamtable(ht->hsize, name)`,
978/// sets the global `outtable = nht`, then scans the source via
979/// `scanhashtable(ht, 0, 0, 0, scancopyparams, 0)` and clears
980/// `outtable` on exit. Rust port returns the freshly-allocated
981/// table; the per-node clone walk requires the HashTable iterator
982/// which isn't wired yet (callers receive the empty allocated
983/// table — same shape the C source returns when `ht` is empty).
984pub fn copyparamtable(ht: Option<&crate::ported::zsh_h::HashTable>, name: &str)
985    -> Option<crate::ported::zsh_h::HashTable>
986{
987    let ht = ht?;
988    newparamtable(ht.hsize, name)
989}
990
991/// Port of `deleteparamtable(HashTable t)` from `Src/params.c:616`. C body:
992/// `int odelunset = delunset; delunset = 1; deletehashtable(t);
993///  delunset = odelunset;` — flips the global before tearing down
994/// each entry so unset callbacks fire. Rust port: `Drop` cascades
995/// through `Box<hashtable>` to clear all `nodes`; consume the
996/// table by value to mirror the C ownership transfer.
997pub fn deleteparamtable(t: Option<crate::ported::zsh_h::HashTable>) {
998    // c:616-623 — `int odelunset = delunset; delunset = 1;` save/
999    // restore so the inner free path fires every entry's unsetfn.
1000    let odelunset =
1001        DELUNSET.swap(1, std::sync::atomic::Ordering::Relaxed);              // c:620-621
1002    if let Some(table) = t {
1003        // Box dropped here → fields freed; param freenode callbacks
1004        // are invoked transparently via Drop on each `param` entry.
1005        drop(table);
1006    }
1007    DELUNSET.store(odelunset, std::sync::atomic::Ordering::Relaxed);         // c:623
1008}
1009
1010/// Port of `scancountparams(UNUSED(HashNode hn), int flags)` from `Src/params.c:630`. C body:
1011/// ```c
1012/// ++numparamvals;
1013/// if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
1014///     ++numparamvals;
1015/// ```
1016/// Increments the static `numparamvals` global used by
1017/// `paramvalarr`. Rust port mirrors against a counter passed by
1018/// reference (no static-mutable in safe Rust).
1019/// WARNING: param names don't match C — Rust=(_hn, flags, numparamvals) vs C=(hn, flags)
1020pub fn scancountparams(_hn: &crate::ported::zsh_h::param, flags: i32, numparamvals: &mut u32) {
1021    *numparamvals += 1;
1022    if (flags as u32 & SCANPM_WANTKEYS) != 0 && (flags as u32 & SCANPM_WANTVALS) != 0 {
1023        *numparamvals += 1;
1024    }
1025}
1026
1027/// Port of `scanparamvals(HashNode hn, int flags)` from `Src/params.c:644`. Real C body
1028/// is the per-node callback for `paramvalarr`: applies SCANPM_MATCHKEY
1029/// (pattry on name) / SCANPM_MATCHVAL (pattry on value) / SCANPM_KEYMATCH
1030/// (compile pm.nam as pattern, match against scanstr) / SCANPM_WANTKEYS
1031/// / SCANPM_WANTVALS / SCANPM_MATCHMANY filters, populating the
1032/// `paramvals[]` slice with the param's name and/or `getstrvalue`
1033/// result, and stashing `foundparam = pm`. State lives in the C
1034/// file-scope statics ported above as `NUMPARAMVALS` / `SCANPROG` /
1035/// `SCANSTR` / `PARAMVALS` / `FOUNDPARAM`.
1036/// WARNING: param names don't match C — Rust=(flags) vs C=(hn, flags)
1037pub fn scanparamvals(                                                        // c:644
1038    pm: &mut crate::ported::zsh_h::param,
1039    flags: i32,
1040) {
1041    let f = flags as u32;
1042    if NUMPARAMVALS.load(Ordering::Relaxed) != 0
1043        && (f & SCANPM_MATCHMANY) == 0
1044        && (f & (SCANPM_MATCHVAL | SCANPM_MATCHKEY | SCANPM_KEYMATCH)) != 0
1045    {
1046        return;
1047    }
1048    if (f & SCANPM_KEYMATCH) != 0 {
1049        // patcompile(pm.node.nam) + pattry(prog, scanstr)
1050        let scanstr = scanstr_lock().lock().unwrap().clone();
1051        if let Some(s) = scanstr {
1052            if !pattry(&pm.node.nam, &s) { return; }
1053        } else {
1054            return;
1055        }
1056    } else if (f & SCANPM_MATCHKEY) != 0 {
1057        let prog = scanprog_lock().lock().unwrap().clone();
1058        if let Some(p) = prog {
1059            if !pattry(&p, &pm.node.nam) { return; }
1060        } else {
1061            return;
1062        }
1063    }
1064    set_foundparam(Some(pm.node.nam.clone()));
1065    if (f & SCANPM_WANTKEYS) != 0 {
1066        paramvals_lock().lock().unwrap().push(pm.node.nam.clone());
1067        NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1068        if (f & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) == 0 {
1069            return;
1070        }
1071    }
1072    let mut vbuf = crate::ported::zsh_h::value {
1073        pm: None,                      // placeholder; real C re-binds
1074        arr: Vec::new(),
1075        scanflags: 0,
1076        valflags: 0,
1077        start: 0,
1078        end: -1,
1079    };
1080    // C: paramvals[numparamvals] = getstrvalue(&v);
1081    // We don't move pm into vbuf to preserve the borrow; mirror the
1082    // C semantics by reading u_str directly via strgetfn for the
1083    // PM_SCALAR fast path and falling back through getstrvalue when
1084    // wired.
1085    let s = strgetfn(pm);
1086    let _ = vbuf;
1087    if (f & SCANPM_MATCHVAL) != 0 {
1088        let prog = scanprog_lock().lock().unwrap().clone();
1089        let matched = prog.map(|p| pattry(&p, &s)).unwrap_or(false);
1090        if matched {
1091            paramvals_lock().lock().unwrap().push(s);
1092            let inc = if (f & SCANPM_WANTVALS) != 0 { 1 } else if (f & SCANPM_WANTKEYS) == 0 { 1 } else { 0 };
1093            NUMPARAMVALS.fetch_add(inc, Ordering::Relaxed);
1094        } else if (f & SCANPM_WANTKEYS) != 0 {
1095            // Discard previously-pushed key.
1096            paramvals_lock().lock().unwrap().pop();
1097            NUMPARAMVALS.fetch_sub(1, Ordering::Relaxed);
1098        }
1099    } else {
1100        paramvals_lock().lock().unwrap().push(s);
1101        NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1102    }
1103    set_foundparam(None);
1104}
1105
1106/// Direct port of `char **paramvalarr(HashTable ht, int flags)`
1107/// from `Src/params.c:689-702`. Scans the param hash twice (count,
1108/// then collect) and returns a heap-allocated string array. C body:
1109/// ```c
1110/// numparamvals = 0;
1111/// if (ht) scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
1112/// paramvals = zhalloc((numparamvals + 1) * sizeof(char *));
1113/// if (ht) { numparamvals = 0;
1114///           scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags); }
1115/// paramvals[numparamvals] = 0;
1116/// return paramvals;
1117/// ```
1118/// SCANPM_MATCHKEY / SCANPM_MATCHVAL filter against `scanprog`
1119/// (the active glob/regex from the caller's `${(k)var[(I)pattern]}`
1120/// subscript); SCANPM_WANTKEYS / SCANPM_WANTVALS / SCANPM_WANTINDEX
1121/// control which fields land in the output array.
1122///
1123/// The Rust port takes a `&Mutex<HashMap>` (paramtab handle) so
1124/// callers don't need to thread the HashTable wrapper through.
1125/// Port of `paramvalarr(HashTable ht, int flags)` from `Src/params.c:689`.
1126#[allow(unused_variables)]
1127pub fn paramvalarr(ht: &crate::ported::zsh_h::HashTable, flags: i32) -> Vec<String> {  // c:689
1128
1129    let flags_u = flags as u32;
1130    let want_keys = (flags_u & SCANPM_WANTKEYS) != 0;
1131    let want_vals = (flags_u & SCANPM_WANTVALS) != 0;
1132    let want_index = (flags_u & SCANPM_WANTINDEX) != 0;
1133
1134    let tab = paramtab().read().unwrap();
1135    let mut out: Vec<String> = Vec::with_capacity(tab.len() * 2);
1136    let mut idx: i64 = 0;
1137    // c:695-696, c:699-700 — scanhashtable filters out PM_UNSET and
1138    // PM_HASHELEM nodes; scanparamvals emits each visible entry's
1139    // key / value / index per flags.
1140    for (k, pm) in tab.iter() {
1141        let pflags = pm.node.flags;
1142        idx += 1;                                                            // c:scanparamvals
1143        if pflags & PM_UNSET as i32 != 0 {
1144            continue;
1145        }
1146        if pflags & PM_HASHELEM as i32 != 0 {
1147            continue;
1148        }
1149        if want_index {
1150            out.push(idx.to_string());
1151        }
1152        if want_keys {
1153            out.push(k.clone());
1154        }
1155        if want_vals || (!want_keys && !want_index) {
1156            // c:scanparamvals — emits getstrvalue(pm) when WANTVALS
1157            // (or by default when nothing else is requested).
1158            let v = pm.u_str.clone().unwrap_or_default();
1159            out.push(v);
1160        }
1161    }
1162    out
1163}
1164
1165/// Port of `getvaluearr(Value v)` from `Src/params.c:710`. C body:
1166/// ```c
1167/// if (v->arr) return v->arr;
1168/// else if (PM_TYPE == PM_ARRAY) return v->arr = pm->gsu.a->getfn(pm);
1169/// else if (PM_TYPE == PM_HASHED) {
1170///     v->arr = paramvalarr(pm->gsu.h->getfn(pm), v->scanflags);
1171///     v->start = 0; v->end = numparamvals + 1; return v->arr;
1172/// } else return NULL;
1173/// ```
1174pub fn getvaluearr(v: Option<&mut crate::ported::zsh_h::value>) -> Vec<String> {
1175    let v = match v { Some(v) => v, None => return Vec::new() };
1176    if !v.arr.is_empty() {
1177        return v.arr.clone();
1178    }
1179    let pm = match v.pm.as_mut() { Some(p) => p, None => return Vec::new() };
1180    let t = PM_TYPE(pm.node.flags as u32);
1181    if t == PM_ARRAY {
1182        v.arr = arrgetfn(pm);
1183        return v.arr.clone();
1184    }
1185    if t == PM_HASHED {
1186        // paramvalarr(hashgetfn(pm), v.scanflags) — backend pending.
1187        v.arr = Vec::new();
1188        v.start = 0;
1189        v.end = 1; // numparamvals + 1
1190        return v.arr.clone();
1191    }
1192    Vec::new()
1193}
1194
1195/// ```c
1196/// struct value vbuf; Value v; int slice; char **arr;
1197/// if (!(v = getvalue(&vbuf, &name, 1)) || *name) return 0;
1198/// if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1;
1199/// slice = v->start != 0 || v->end != -1;
1200/// if (PM_TYPE(v->pm->node.flags) != PM_ARRAY || !slice)
1201///     return !slice && !(v->pm->node.flags & PM_UNSET);
1202/// if (!v->end) return 0;
1203/// if (!(arr = getvaluearr(v))) return 0;
1204/// return arrlen_ge(arr, v->end < 0 ? - v->end : v->end);
1205/// ```
1206/// Returns 1 if `name` resolves to a set parameter (or a non-empty
1207/// slice/element of one). Used by `[[ -v NAME ]]`/`[[ -n …]]`
1208/// dispatch in cond.c and the readonly-check inside builtin.c.
1209/// Port of `issetvar(char *name)` from `Src/params.c:732`.
1210pub fn issetvar(name: &str) -> i32 {                                         // c:732
1211    let mut vbuf = crate::ported::zsh_h::value {
1212        pm: None,
1213        arr: Vec::new(),
1214        scanflags: 0,
1215        valflags: 0,
1216        start: 0,
1217        end: -1,
1218    };
1219    let mut cursor: &str = name;
1220    let v = match getvalue(Some(&mut vbuf), &mut cursor, 1) {                // c:739
1221        Some(v) => v,
1222        None => return 0,
1223    };
1224    if !cursor.is_empty() {                                                  // c:739
1225        return 0; // c:740 no value or more chars after the variable name
1226    }
1227    if (v.scanflags as u32 & !SCANPM_ARRONLY) != 0 {                         // c:741
1228        return if v.end > 1 { 1 } else { 0 };                                // c:742
1229    }
1230
1231    let slice = v.start != 0 || v.end != -1;                                 // c:744
1232    let pm = match v.pm.as_ref() {
1233        Some(p) => p,
1234        None => return 0,
1235    };
1236    if PM_TYPE(pm.node.flags as u32) != PM_ARRAY || !slice {                 // c:745
1237        return if !slice && (pm.node.flags as u32 & PM_UNSET) == 0 { 1 } else { 0 }; // c:746
1238    }
1239
1240    if v.end == 0 {                                                          // c:748 empty array slice
1241        return 0;                                                            // c:749
1242    }
1243    // c:751 — get the array and check end is within range
1244    let arr = getvaluearr(Some(v));
1245    if arr.is_empty() {                                                      // c:751
1246        return 0;                                                            // c:752
1247    }
1248    // c:753
1249    let bound: usize = if v.end < 0 { (-v.end) as usize } else { v.end as usize };
1250    if crate::ported::utils::arrlen_ge(&arr, bound) { 1 } else { 0 }
1251}
1252
1253/// Direct port of `static int split_env_string(char *env, char
1254/// **name, char **value)` from `Src/params.c:763`.
1255///
1256/// Walks `env` until either `=` or end. Returns `None` (C `0`) if:
1257///   - any byte before `=` has the high bit set (c:771-777 — names
1258///     outside the portable character set are silently rejected),
1259///   - no `=` is present (c:783-785 fall-through),
1260///   - or the name is empty (`*str == '=' && str == tenv`, c:782).
1261/// Otherwise returns `Some((name, value))` (C `1` + out-params).
1262///
1263/// Out-param style differs from C (we return a tuple); the
1264/// rejection rules are 1:1.
1265pub fn split_env_string(env: &str) -> Option<(String, String)> {             // c:763
1266    if env.is_empty() {                                                      // c:763 !env
1267        return None;
1268    }
1269    let bytes = env.as_bytes();
1270    // c:770-779 — walk name bytes, reject if high bit set.
1271    let mut i = 0;
1272    while i < bytes.len() && bytes[i] != b'=' {                              // c:770
1273        if bytes[i] >= 128 {                                                 // c:771 (unsigned char) >= 128
1274            return None;                                                     // c:777
1275        }
1276        i += 1;
1277    }
1278    // c:780-785 — accept only if `=` was found at non-zero offset.
1279    if i > 0 && i < bytes.len() && bytes[i] == b'=' {                        // c:780
1280        let name = String::from_utf8_lossy(&bytes[..i]).into_owned();        // c:781-782
1281        let value = String::from_utf8_lossy(&bytes[i + 1..]).into_owned();   // c:783
1282        Some((name, value))                                                  // c:784
1283    } else {
1284        None                                                                 // c:786
1285    }
1286}
1287
1288// parameter entries as well as setting up parameter table                 // c:812
1289// entries for environment variables we inherit.                           // c:813
1290/// Direct port of `createparamtable()` from `Src/params.c:817-988`.
1291///
1292/// Walks the same five-stage init sequence as the C source:
1293///   1. Touch paramtab/realparamtab so the OnceLocks initialise
1294///      (c:835 — newparamtable(151,"paramtab")).
1295///   2. Register every `special_params[]` entry as a PM_SPECIAL
1296///      node in the global paramtab (c:838-847). EMULATE_SH/KSH
1297///      override list (`special_params_sh`) is wired below.
1298///   3. Initialise non-special params that must precede env
1299///      import: MAILCHECK / KEYTIMEOUT / LISTMAX / TMPPREFIX /
1300///      TIMEFMT / HOST / LOGNAME (c:854-879).
1301///   4. Walk std::env::vars() and import each name that is a legal
1302///      ident and not blocked via `dontimport`. Mark PM_EXPORTED
1303///      and stamp the param's env field (c:893-925).
1304///   5. Post-import wiring: HOME PM_UNSET clear + LOGNAME/SHLVL
1305///      env sync, CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
1306///      ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL (c:931-979).
1307///
1308/// Limitations:
1309///   - `noerrs` counter (`utils.c:NOERRS`) is module-private to the
1310///     Rust port, so the `noerrs = 2` guard at c:850 is a no-op.
1311///   The rest of the C body (ALLEXPORT toggle, set_pwd_env,
1312///   signals[] build with SIGRTMIN..MAX) is fully wired below.
1313pub fn createparamtable() {                                                  // c:817
1314
1315    // c:835 — `paramtab = realparamtab = newparamtable(151, "paramtab")`.
1316    let _ = paramtab();
1317    let _ = realparamtab();
1318
1319    // Helper closure (single definition; mirrors the C
1320    // `paramtab->addnode(paramtab, ztrdup(name), ip)` site).
1321    let add_special = |ip: &special_paramdef,
1322                       tab: &mut std::collections::HashMap<
1323        String,
1324        crate::ported::zsh_h::Param,
1325    >| {
1326        let pm = Box::new(crate::ported::zsh_h::param {
1327            node: crate::ported::zsh_h::hashnode {
1328                next: None,
1329                nam: ip.name.to_string(),
1330                flags: (ip.pm_type | ip.pm_flags | PM_SPECIAL) as i32,
1331            },
1332            u_data: 0,
1333            u_arr: None,
1334            u_str: None,
1335            u_val: 0,
1336            u_dval: 0.0,
1337            u_hash: None,
1338            gsu_s: None,
1339            gsu_i: None,
1340            gsu_f: None,
1341            gsu_a: None,
1342            gsu_h: None,
1343            base: 0,
1344            width: 0,
1345            env: None,
1346            ename: None,
1347            old: None,
1348            level: 0,
1349        });
1350        tab.insert(ip.name.to_string(), pm);
1351    };
1352
1353    // c:838-840 — `for (ip = special_params; ip->node.nam; ip++)
1354    //              paramtab->addnode(...)`. Section 1: always loaded.
1355    {
1356        let mut tab = paramtab().write().unwrap();
1357        for ip in special_params[..SPECIAL_PARAMS_ZSH_START].iter() {
1358            add_special(ip, &mut tab);
1359        }
1360    }
1361
1362    // c:840-847 — emulation branch. Under EMULATE_SH/EMULATE_KSH,
1363    // load special_params_sh (scalar versions). Otherwise load
1364    // special_params zsh-only section (the continuation past the
1365    // inner NULL sentinel).
1366    let is_sh_ksh = crate::ported::zsh_h::EMULATION(
1367        crate::ported::zsh_h::EMULATE_SH | crate::ported::zsh_h::EMULATE_KSH,
1368    );
1369    {
1370        let mut tab = paramtab().write().unwrap();
1371        if is_sh_ksh {
1372            // c:841-843 — sh/ksh: scalar replacements.
1373            for ip in special_params_sh.iter() {
1374                add_special(ip, &mut tab);
1375            }
1376        } else {
1377            // c:845-847 — zsh: continuation tail (array-tied + lowercase
1378            // aliases + pipestatus).
1379            for ip in special_params[SPECIAL_PARAMS_ZSH_START..].iter() {
1380                add_special(ip, &mut tab);
1381            }
1382        }
1383    }
1384    // c:848 — `argvparam = (Param) &argvparam_pm;` is the C handle a
1385    //         positional-param fetchvalue path follows to reach
1386    //         `pparams`. The Rust port resolves $1..$N directly from
1387    //         `PPARAMS` via `value.start`/`value.end` indices (see
1388    //         fetchvalue at params.rs:6395-6407), so no separate
1389    //         Param descriptor is wired up here.
1390    // c:851 — `noerrs = 2`; NOERRS module-private, so this guard is
1391    //         a no-op for now.
1392
1393    // c:858-860 — standard non-special params (must precede env import).
1394    setiparam("MAILCHECK", 60);                                              // c:858
1395    setiparam("KEYTIMEOUT", 40);                                             // c:859
1396    setiparam("LISTMAX", 100);                                               // c:860
1397
1398    // c:870-871 — TMPPREFIX / TIMEFMT defaults. C wraps each string
1399    // through ztrdup_metafy() to escape Meta bytes before storing in
1400    // the param table; the Rust port mirrors this.
1401    setsparam(
1402        "TMPPREFIX",
1403        &crate::ported::utils::ztrdup_metafy(DEFAULT_TMPPREFIX),
1404    );                                                                       // c:870
1405    setsparam(
1406        "TIMEFMT",
1407        &crate::ported::utils::ztrdup_metafy(
1408            crate::ported::zsh_system_h::DEFAULT_TIMEFMT,
1409        ),
1410    );                                                                       // c:871
1411
1412    // c:873-876 — HOST from gethostname() (ztrdup_metafy wrap c:875).
1413    let mut host_buf = [0u8; 256];
1414    let host_rc = unsafe {
1415        libc::gethostname(host_buf.as_mut_ptr() as *mut libc::c_char, 256)
1416    };
1417    let hostname = if host_rc == 0 {
1418        std::ffi::CStr::from_bytes_until_nul(&host_buf)
1419            .ok()
1420            .and_then(|c| c.to_str().ok())
1421            .unwrap_or("")
1422            .to_string()
1423    } else {
1424        String::new()
1425    };
1426    setsparam("HOST", &crate::ported::utils::ztrdup_metafy(&hostname));      // c:875
1427
1428    // c:878-882 — LOGNAME from `getlogin()` libc syscall (with
1429    // \`cached_username\` as fallback when DISABLE_DYNAMIC_NSS).
1430    //
1431    // The previous Rust port read \`env::var(\"LOGNAME\")\` /
1432    // \`env::var(\"USER\")\` — different source. \`getlogin\` returns the
1433    // kernel's record of the controlling-terminal login user; env
1434    // LOGNAME/USER is whatever the parent process passed in (can be
1435    // spoofed). For audit / SUID-aware code paths, the kernel's view
1436    // is the right one.
1437    let logname = unsafe {
1438        let p = libc::getlogin();
1439        if p.is_null() {
1440            String::new()
1441        } else {
1442            std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
1443        }
1444    };                                                                       // c:880 getlogin()
1445    let logname = if logname.is_empty() {
1446        // c:882 — `ztrdup(cached_username)` fallback.
1447        crate::ported::utils::get_username()
1448    } else {
1449        logname
1450    };
1451    setsparam("LOGNAME", &crate::ported::utils::ztrdup_metafy(&logname));    // c:878
1452
1453    // c:891 — pushheap() / c:921 — popheap(). Wraps the env-import
1454    // loop so per-iter allocations land on the heap zone.
1455    crate::ported::mem::pushheap();                                          // c:891
1456
1457    // c:893-924 — environment import loop.
1458    for (iname, ivalue) in std::env::vars() {
1459        if iname.is_empty() {
1460            continue;
1461        }
1462        // c:897 — leading-digit reject (`!idigit(*iname)`).
1463        if iname.as_bytes()[0].is_ascii_digit() {
1464            continue;
1465        }
1466        // c:897 — must be a valid identifier.
1467        if !isident(&iname) {
1468            continue;
1469        }
1470        // c:897 — `!strchr(iname, '[')` reject subscripted names.
1471        if iname.contains('[') {
1472            continue;
1473        }
1474        // c:902-906 — block if PM_DONTIMPORT-family flags say so.
1475        let blocked = {
1476            let tab = paramtab().read().unwrap();
1477            tab.get(&iname)
1478                .map(|pm| dontimport(pm.node.flags) != 0)
1479                .unwrap_or(false)
1480        };
1481        if blocked {
1482            continue;
1483        }
1484        // c:907-908 — assignsparam(..., ASSPM_ENV_IMPORT).
1485        let metafied = crate::ported::utils::metafy(&ivalue);
1486        let _ = assignsparam(
1487            &iname,
1488            &metafied,
1489            crate::ported::zsh_h::ASSPM_ENV_IMPORT,
1490        );
1491        // c:909-915 — stamp PM_EXPORTED and the env-side string.
1492        let mut tab = paramtab().write().unwrap();
1493        if let Some(pm) = tab.get_mut(&iname) {
1494            pm.node.flags |= PM_EXPORTED as i32;
1495            let env_str = if pm.node.flags & PM_SPECIAL as i32 != 0 {
1496                // c:912 — `pm->env = mkenvstr(pm->node.nam,
1497                // getsparam(pm->node.nam), pm->node.flags)`. For
1498                // special params the C body re-fetches the
1499                // canonical string via getsparam; we use ivalue
1500                // here (already metafied above).
1501                mkenvstr(&iname, &ivalue, pm.node.flags)
1502            } else {
1503                // c:914 — `pm->env = ztrdup(*envp2)` for non-special:
1504                // direct env-line copy.
1505                format!("{}={}", iname, ivalue)
1506            };
1507            pm.env = Some(env_str);
1508        }
1509    }
1510
1511    crate::ported::mem::popheap();                                           // c:921
1512
1513    // c:933-944 — HOME / LOGNAME / SHLVL post-import wiring.
1514    //
1515    // C body (verbatim):
1516    //   pm = paramtab->getnode(paramtab, "HOME");
1517    //   if (EMULATION(EMULATE_ZSH)) {
1518    //       pm->node.flags &= ~PM_UNSET;
1519    //       if (!(pm->node.flags & PM_EXPORTED))
1520    //           addenv(pm, home);
1521    //   } else if (!home)
1522    //       pm->node.flags |= PM_UNSET;
1523    //   pm = paramtab->getnode(paramtab, "LOGNAME");
1524    //   if (!(pm->node.flags & PM_EXPORTED))
1525    //       addenv(pm, pm->u.str);
1526    //   pm = paramtab->getnode(paramtab, "SHLVL");
1527    //   sprintf(buf, "%d", (int)++shlvl);
1528    //   addenv(pm, buf);
1529
1530    // c:938-945 — HOME. EMULATE_ZSH path clears PM_UNSET and
1531    // addenv(home) when not already exported; non-zsh path sets
1532    // PM_UNSET when `home` is empty/unset.
1533    let is_zsh = crate::ported::zsh_h::EMULATION(
1534        crate::ported::zsh_h::EMULATE_ZSH,
1535    );
1536    let home_val = home_lock().lock().expect("home poisoned").clone();
1537    let home_action: Option<bool> = {
1538        let mut tab = paramtab().write().unwrap();
1539        if let Some(pm) = tab.get_mut("HOME") {
1540            if is_zsh {                                                      // c:939
1541                pm.node.flags &= !(PM_UNSET as i32);                         // c:941
1542                if pm.node.flags & PM_EXPORTED as i32 == 0 {                 // c:942
1543                    Some(true)
1544                } else {
1545                    Some(false)
1546                }
1547            } else if home_val.is_empty() {                                  // c:944
1548                pm.node.flags |= PM_UNSET as i32;                            // c:945
1549                Some(false)
1550            } else {
1551                Some(false)
1552            }
1553        } else {
1554            None
1555        }
1556    };
1557    if let Some(true) = home_action {
1558        addenv("HOME", &home_val);                                           // c:943
1559    }
1560
1561    // c:946-948 — LOGNAME. If not already exported, addenv(pm, pm->u.str).
1562    let logname_export: Option<String> = {
1563        let tab = paramtab().read().unwrap();
1564        tab.get("LOGNAME").and_then(|pm| {
1565            if pm.node.flags & PM_EXPORTED as i32 == 0 {
1566                pm.u_str.clone()
1567            } else {
1568                None
1569            }
1570        })
1571    };
1572    if let Some(ustr) = logname_export {
1573        addenv("LOGNAME", &ustr);                                            // c:948
1574    }
1575
1576    // c:949-953 — SHLVL: unconditionally addenv with the incremented
1577    // value. C uses the \`shlvl\` integer global (IPDEF5 declared at
1578    // params.c:358 with varinteger_gsu) which was populated during
1579    // env-import. C: \`++shlvl\` then \`sprintf(buf, \"%d\", (int)shlvl)\`.
1580    //
1581    // The previous Rust port read SHLVL fresh from env::var; the
1582    // canonical read is through paramtab (which has the parsed
1583    // integer post-import). Falls back to env for the rare case
1584    // where paramtab hasn't seen the import yet.
1585    let new_shlvl: i32 = crate::ported::params::getsparam("SHLVL")
1586        .or_else(|| std::env::var("SHLVL").ok())
1587        .and_then(|s| s.parse().ok())
1588        .unwrap_or(0)
1589        + 1;                                                                 // c:951 `++shlvl`
1590    setiparam("SHLVL", new_shlvl as i64);
1591    addenv("SHLVL", &new_shlvl.to_string());                                 // c:953
1592
1593    // c:949-967 — CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
1594    // ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL. C body wraps each
1595    // through ztrdup_metafy() — Rust mirrors that. CPUTYPE is set
1596    // from uname()'s `machine` field at runtime (c:957-961); the
1597    // other three (MACHTYPE / OSTYPE / VENDOR) come from config.h
1598    // values frozen at configure-time (c:961, c:963, c:964).
1599    let utsname = nix::sys::utsname::uname().ok();
1600    let cputype = utsname
1601        .as_ref()
1602        .map(|u| u.machine().to_string_lossy().to_string())
1603        .unwrap_or_else(|| "unknown".to_string());
1604    setsparam("CPUTYPE", &crate::ported::utils::ztrdup_metafy(&cputype));    // c:954/960
1605    setsparam(                                                               // c:961
1606        "MACHTYPE",
1607        &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::MACHTYPE),
1608    );
1609    setsparam(                                                               // c:962
1610        "OSTYPE",
1611        &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::OSTYPE),
1612    );
1613    let tty_str = {
1614        let p = unsafe { libc::ttyname(0) };
1615        if !p.is_null() {
1616            unsafe { std::ffi::CStr::from_ptr(p) }
1617                .to_string_lossy()
1618                .to_string()
1619        } else {
1620            String::new()
1621        }
1622    };
1623    setsparam("TTY", &crate::ported::utils::ztrdup_metafy(&tty_str));        // c:963
1624    setsparam(                                                               // c:964
1625        "VENDOR",
1626        &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::VENDOR),
1627    );
1628    let argv0 = std::env::args().next().unwrap_or_default();
1629    setsparam(
1630        "ZSH_ARGZERO",
1631        &crate::ported::utils::ztrdup(&argv0),
1632    );                                                                       // c:965 (ztrdup, not _metafy: posixzero)
1633    setsparam(
1634        "ZSH_VERSION",
1635        &crate::ported::utils::ztrdup_metafy(
1636            crate::ported::patchlevel::ZSH_VERSION,
1637        ),
1638    );                                                                       // c:966 (Config/version.mk VERSION via patchlevel::ZSH_VERSION)
1639    setsparam(
1640        "ZSH_PATCHLEVEL",
1641        &crate::ported::utils::ztrdup_metafy(
1642            crate::ported::patchlevel::ZSH_PATCHLEVEL,
1643        ),
1644    );                                                                       // c:967
1645
1646    // c:968-979 — `setaparam("signals", sigptr = zalloc((TRAPCOUNT
1647    // + 1) * sizeof(char *))); t = sigs; while (t - sigs <= SIGCOUNT)
1648    // *sigptr++ = ztrdup_metafy(*t++); { for (sig = SIGRTMIN; sig <=
1649    // SIGRTMAX; sig++) *sigptr++ = ztrdup_metafy(rtsigname(sig, 0));
1650    // } while ((*sigptr++ = ztrdup_metafy(*t++))) ;`. Builds the
1651    // $signals array: indices 0..=SIGCOUNT walked from the static
1652    // sigs[] name table, then SIGRTMIN..SIGRTMAX names, then the
1653    // trailing tail (DEBUG / ERR / EXIT / ZERR sentinels).
1654    let mut signals_arr: Vec<String> = Vec::new();
1655    for &(name, _num) in
1656        crate::ported::signals_h::SIGS.iter()
1657    {
1658        signals_arr.push(crate::ported::utils::ztrdup_metafy(name));
1659    }
1660    // RT-signal range (Linux-only; macOS SIGS table already includes
1661    // the realtime names and rtsigname returns "" out of range).
1662    #[cfg(target_os = "linux")]
1663    {
1664        for sig in libc::SIGRTMIN()..=libc::SIGRTMAX() {
1665            let nm = crate::ported::signals::rtsigname(sig);
1666            if !nm.is_empty() {
1667                signals_arr.push(crate::ported::utils::ztrdup_metafy(&nm));
1668            }
1669        }
1670    }
1671    {
1672        let mut tab = paramtab().write().unwrap();
1673        let pm = Box::new(crate::ported::zsh_h::param {
1674            node: crate::ported::zsh_h::hashnode {
1675                next: None,
1676                nam: "signals".to_string(),
1677                flags: (crate::ported::zsh_h::PM_ARRAY
1678                    | crate::ported::zsh_h::PM_SPECIAL) as i32,
1679            },
1680            u_data: 0,
1681            u_arr: Some(signals_arr),
1682            u_str: None,
1683            u_val: 0,
1684            u_dval: 0.0,
1685            u_hash: None,
1686            gsu_s: None,
1687            gsu_i: None,
1688            gsu_f: None,
1689            gsu_a: None,
1690            gsu_h: None,
1691            base: 0,
1692            width: 0,
1693            env: None,
1694            ename: None,
1695            old: None,
1696            level: 0,
1697        });
1698        tab.insert("signals".to_string(), pm);
1699    }
1700
1701    // c:980 — `noerrs = 0` restore. NOERRS module-private (see above).
1702}
1703
1704/// Parallel storage for PM_HASHED parameter values. `param.u_hash`
1705/// is typed `Option<HashTable>` per Src/zsh.h:1841 but the full
1706/// HashTable substrate isn't wired yet; the assoc-array values live
1707/// here keyed on param name until that lands.
1708static PARAMTAB_HASHED_STORAGE_INNER: OnceLock<
1709    Mutex<HashMap<String, indexmap::IndexMap<String, String>>>,
1710> = OnceLock::new();
1711
1712/// Port of `assigngetset(Param pm)` from `Src/params.c:994`. C body
1713/// installs the standard get/set/unset vtable matching the
1714/// param's PM_TYPE so subsequent assignment dispatches go
1715/// through `pm->gsu.X->setfn`.
1716pub fn assigngetset(pm: &mut crate::ported::zsh_h::param) {
1717    match PM_TYPE(pm.node.flags as u32) {
1718        x if x == PM_SCALAR || x == PM_NAMEREF => {
1719            pm.gsu_s = Some(Box::new(gsu_scalar {
1720                getfn: strgetfn,
1721                setfn: strsetfn,
1722                unsetfn: stdunsetfn,
1723            }));
1724        }
1725        x if x == PM_INTEGER => {
1726            pm.gsu_i = Some(Box::new(gsu_integer {
1727                getfn: intgetfn,
1728                setfn: intsetfn,
1729                unsetfn: stdunsetfn,
1730            }));
1731        }
1732        x if x == PM_EFLOAT || x == PM_FFLOAT => {
1733            pm.gsu_f = Some(Box::new(gsu_float {
1734                getfn: floatgetfn,
1735                setfn: floatsetfn,
1736                unsetfn: stdunsetfn,
1737            }));
1738        }
1739        x if x == PM_ARRAY => {
1740            pm.gsu_a = Some(Box::new(gsu_array {
1741                getfn: arrgetfn,
1742                setfn: arrsetfn,
1743                unsetfn: stdunsetfn,
1744            }));
1745        }
1746        x if x == PM_HASHED => {
1747            pm.gsu_h = Some(Box::new(gsu_hash {
1748                getfn: hashgetfn,
1749                setfn: hashsetfn,
1750                unsetfn: stdunsetfn,
1751            }));
1752        }
1753        _ => {
1754            // DPUTS(1, "BUG: tried to create param node without valid flag")
1755        }
1756    }
1757}
1758
1759/// Port of `createparam(char *name, int flags)` from `Src/params.c:1030`. C body
1760/// (~130 lines, see comment header at c:1020-1027) creates a
1761/// parameter so that it can be assigned to. Returns NULL if the
1762/// parameter already exists or can't be created, otherwise
1763/// returns the new node. If a parameter of the same name exists
1764/// in an outer scope, it is hidden by the new one. An already
1765/// existing node at the current level may be "created" and
1766/// returned provided it is unset and not special. If the
1767/// parameter can't be created because it already exists,
1768/// PM_UNSET is cleared.
1769///
1770/// Faithful port covers:
1771/// - PM_HASHELEM / PM_EXPORTED tweak when paramtab != realparamtab (c:1034)
1772/// - PM_RO_BY_DESIGN read-only rejection (c:1043-1052)
1773/// - PM_NAMEREF chain follow via `resolve_nameref_rec` (c:1062-1104)
1774/// - hidden vs reuse-old branches (c:1108-1147)
1775/// - `pm->node.flags = flags & ~PM_LOCAL` finalization (c:1155)
1776/// - `assigngetset(pm)` for non-special params (c:1157-1158)
1777///
1778/// Paramtab-backed branches (c:1034 paramtab compare, c:1038
1779/// gethashnode2, c:1144-1146 paramtab.removenode/addnode) cannot
1780/// fully execute until the paramtab vtable lands; they are
1781/// preserved as architectural intent. The faithful behaviour
1782/// emerges as soon as paramtab is wired (no signature drift
1783/// at this site).
1784pub fn createparam(                                                          // c:1030
1785    name: &str,
1786    mut flags: i32,
1787) -> Option<crate::ported::zsh_h::Param> {
1788    // c:1034-1035 — when paramtab != realparamtab (we're inside
1789    // a hash-element scope), strip PM_EXPORTED + add PM_HASHELEM.
1790    // Without paramtab/realparamtab live yet, this branch is
1791    // skipped — the caller is expected to be in the
1792    // realparamtab scope which is the common case.
1793
1794    // c:1037 — `if (name != nulstring) { ... } else { hcalloc; nulstring }`
1795    // c:1038-1041 — oldpm = gethashnode2(paramtab, name)
1796    //   Without paramtab backend, we cannot consult the table; treat
1797    //   the param as new. The PM_RO_BY_DESIGN / PM_NAMEREF / hidden
1798    //   branches (c:1043-1147) collapse to "allocate fresh".
1799    // c:1037-1041 — `oldpm = gethashnode2(paramtab, name)`. Look up
1800    // any existing Param at this name so the c:1108/1135 branches
1801    // can decide reuse-vs-shadow. PM_RO_BY_DESIGN / PM_NAMEREF
1802    // chase branches (c:1043-1104) elided — covered when nameref
1803    // / readonly-by-design Params are wired.
1804    let oldpm: Option<crate::ported::zsh_h::Param> = if !name.is_empty() {
1805        paramtab().read().ok().and_then(|t| t.get(name).cloned())
1806    } else {
1807        None
1808    };
1809
1810    if !name.is_empty() {
1811        // c:1149-1150 — `if (isset(ALLEXPORT) && !(flags & PM_HASHELEM)) flags |= PM_EXPORTED;`
1812        if isset(crate::ported::zsh_h::ALLEXPORT)
1813            && (flags as u32 & PM_HASHELEM) == 0
1814        {
1815            flags |= PM_EXPORTED as i32;
1816        }
1817    }
1818
1819    // c:1108 — `if (oldpm && (oldpm->level == locallevel || !(flags
1820    // & PM_LOCAL)))`: reuse the existing Param in place. c:1135 —
1821    // else allocate a fresh pm and chain pm.old = oldpm (the
1822    // local-shadow path). The reuse arm just returns the existing
1823    // pm with reset base/width; the shadow arm does the chain
1824    // installation that endparamscope later unwinds.
1825    let cur_locallevel = locallevel.load(std::sync::atomic::Ordering::Relaxed);
1826    let reuse = match &oldpm {
1827        Some(op) => op.level == cur_locallevel || (flags as u32 & PM_LOCAL) == 0,
1828        None => false,
1829    };
1830
1831    let mut pm: crate::ported::zsh_h::Param = if reuse {
1832        // c:1132-1134 — `pm = oldpm; pm->base = pm->width = 0;
1833        // oldpm = pm->old;` Reuse the entry already in paramtab.
1834        let mut existing = oldpm.unwrap();                                   // safe: reuse=true requires Some
1835        existing.base = 0;                                                   // c:1133
1836        existing.width = 0;                                                  // c:1133
1837        existing
1838    } else {
1839        // c:1136 zshcalloc(sizeof *pm) — fresh allocation; chain the
1840        // outer Param into pm.old (c:1137) so endparamscope can
1841        // restore it. c:1144 paramtab->removenode is implicit since
1842        // we re-insert below.
1843        Box::new(crate::ported::zsh_h::param {
1844            node: crate::ported::zsh_h::hashnode {
1845                next: None,
1846                nam: name.to_string(),
1847                flags: 0,
1848            },
1849            u_data: 0,
1850            u_arr: None,
1851            u_str: None,
1852            u_val: 0,
1853            u_dval: 0.0,
1854            u_hash: None,
1855            gsu_s: None,
1856            gsu_i: None,
1857            gsu_f: None,
1858            gsu_a: None,
1859            gsu_h: None,
1860            base: 0,
1861            width: 0,
1862            env: None,
1863            ename: None,
1864            old: oldpm,                                                      // c:1137 pm->old = oldpm
1865            level: cur_locallevel,                                           // c:builtin.c:2576 PM_LOCAL → pm->level = locallevel
1866        })
1867    };
1868
1869    pm.node.flags = flags & !(PM_LOCAL as i32);                              // c:1155
1870    if (pm.node.flags as u32 & PM_SPECIAL) == 0 {                            // c:1157
1871        assigngetset(&mut pm);                                               // c:1158
1872    }
1873    // c:1146 `paramtab->addnode(paramtab, ztrdup(name), pm)`. For
1874    // the reuse arm this overwrites the same entry; for the shadow
1875    // arm it installs the new chained pm on top of the (now-
1876    // displaced) old.
1877    if !name.is_empty() {
1878        let cloned = pm.clone();
1879        paramtab().write().unwrap().insert(name.to_string(), pm);
1880        return Some(cloned);
1881    }
1882    Some(pm)                                                                 // c:1159
1883}
1884
1885/// Empty special-hash sentinel.
1886/// Port of `shempty()` from Src/params.c:1166. The C source uses
1887/// it as a no-op getfn callback for special hashes that need an
1888/// addressable function pointer but no actual work. Provided here
1889/// so future callers that match the C source's signature can call
1890/// it directly.
1891pub fn shempty() {}
1892
1893/// Port of `setsparam(char *s, char *val)` from Src/params.c:3350.
1894/// C body: `return assignsparam(s, val, ASSPM_WARN);`
1895/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1896pub fn setsparam(s: &str, val: &str)                                         // c:3350
1897    -> Option<crate::ported::zsh_h::Param>
1898{
1899    assignsparam(s, val, ASSPM_WARN as i32)                                  // c:3352
1900}
1901
1902/// Direct port of `Param createspecialhash(char *name, GetNodeFunc
1903/// get, ScanTabFunc scan, int flags)` from `Src/params.c:1182-1224`.
1904/// Creates a PM_SPECIAL|PM_HASHED parameter with the supplied get
1905/// and scan callbacks, attaches an empty hash table, and returns
1906/// the new Param (or None if `createparam` fails).
1907///
1908/// C body wiring:
1909///   - `pm = createparam(name, PM_SPECIAL|PM_HASHED|flags)` (c:1186)
1910///   - If shadowing an old param at function scope, `pm->level =
1911///     locallevel` (c:1204-1205) so the old one is exposed after
1912///     leaving the fn.
1913///   - `pm->gsu.h = (flags & PM_READONLY) ? &stdhash_gsu :
1914///     &nullsethash_gsu` (c:1206-1207)
1915///   - `pm->u.hash = newhashtable(0, name, NULL)` (c:1208) with
1916///     no-op add/empty/remove/free callbacks (`shempty`) plus the
1917///     supplied `get` / `scan` callbacks.
1918///
1919/// The Rust port drops `GetNodeFunc` / `ScanTabFunc` fn-pointer
1920/// parameters because the Rust HashTable model uses owned
1921/// HashMap<String, T> rather than C-style vtable dispatch; the
1922/// returned Param carries the empty hash and PM_HASHED flag so
1923/// callers can fill it via the standard array/hash setfn path.
1924pub fn createspecialhash(name: &str, flags: i32)                             // c:1182
1925    -> Option<crate::ported::zsh_h::Param>
1926{
1927
1928    // c:1186 — `createparam(name, PM_SPECIAL|PM_HASHED|flags)`.
1929    let mut pm = createparam(name, (PM_SPECIAL | PM_HASHED) as i32 | flags)?;
1930
1931    // c:1204-1205 — if shadowing an old param, set level=locallevel.
1932    if pm.old.is_some() {
1933        // C: `pm->level = locallevel`. The previous Rust port had
1934        // `let ll = 0_i32;` as a hardcoded placeholder — meaning
1935        // shadowed special-hash params (`fpath`, `path`, `psvar`,
1936        // etc. assigned inside a function via local) would NEVER
1937        // get their level tagged for restoration. After the function
1938        // returned, the original param would be inaccessible because
1939        // the shadow record's level (always 0) wouldn't trigger the
1940        // endparamscope unset. Now reads the canonical `locallevel`
1941        // global from params.rs (matching the C global).
1942        pm.level = locallevel.load(std::sync::atomic::Ordering::Relaxed) as i32; // c:1205
1943    }
1944
1945    // c:1206-1207 — GSU selection. We can't set the gsu_h pointer
1946    // without the full GSU port wired; leave it None and let the
1947    // standard setfn dispatch route through the existing hashsetfn
1948    // / nullsethashfn helpers.
1949
1950    // c:1208 — `pm->u.hash = newhashtable(0, name, NULL)`. Rust
1951    // stores an empty HashTable in u_hash. The C body then sets
1952    // hash/empty/add/get/get2/remove/disable/enable/free/print
1953    // callbacks (c:1210-1221) which in our Rust model are implicit
1954    // (HashMap handles add/get/remove; freenode is Drop).
1955    let ht = Box::new(crate::ported::zsh_h::hashtable {
1956        hsize: 0,
1957        ct: 0,
1958        nodes: Vec::new(),
1959        tmpdata: 0,
1960        hash: None,
1961        emptytable: None,
1962        filltable: None,
1963        cmpnodes: None,
1964        addnode: None,
1965        getnode: None,
1966        getnode2: None,
1967        removenode: None,
1968        disablenode: None,
1969        enablenode: None,
1970        freenode: None,
1971        printnode: None,
1972        scantab: None,
1973    });
1974    pm.u_hash = Some(ht);
1975    let _ = name;
1976
1977    Some(pm)                                                                 // c:1223
1978}
1979
1980/// ```c
1981/// tpm->node.flags = pm->node.flags;
1982/// tpm->base = pm->base;
1983/// tpm->width = pm->width;
1984/// tpm->level = pm->level;
1985/// if (!fakecopy) {
1986///     tpm->old = pm->old;
1987///     tpm->node.flags &= ~PM_SPECIAL;
1988/// }
1989/// switch (PM_TYPE(pm->node.flags)) {
1990/// case PM_SCALAR: case PM_NAMEREF:
1991///     tpm->u.str = ztrdup(pm->gsu.s->getfn(pm)); break;
1992/// case PM_INTEGER:
1993///     tpm->u.val = pm->gsu.i->getfn(pm); break;
1994/// case PM_EFLOAT: case PM_FFLOAT:
1995///     tpm->u.dval = pm->gsu.f->getfn(pm); break;
1996/// case PM_ARRAY:
1997///     tpm->u.arr = zarrdup(pm->gsu.a->getfn(pm)); break;
1998/// case PM_HASHED:
1999///     tpm->u.hash = copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam);
2000///     break;
2001/// }
2002/// if (!fakecopy)
2003///     assigngetset(tpm);
2004/// ```
2005/// Copies `pm`'s value + level/base/width/flags into `tpm`.
2006/// `fakecopy = 1` means we're saving a snapshot (e.g. for special
2007/// param scope-save) and don't need callable get/set callbacks; in
2008/// that case `tpm->old`/PM_SPECIAL are preserved untouched and
2009/// `assigngetset` is skipped.
2010/// Port of `copyparam(Param tpm, Param pm, int fakecopy)` from `Src/params.c:1236`.
2011/// WARNING: param names don't match C — Rust=(pm, fakecopy) vs C=(tpm, pm, fakecopy)
2012pub fn copyparam(                                                            // c:1236
2013    tpm: &mut crate::ported::zsh_h::param,
2014    pm: &mut crate::ported::zsh_h::param,
2015    fakecopy: i32,
2016) {
2017    tpm.node.flags = pm.node.flags;                                          // c:1244
2018    tpm.base = pm.base;                                                      // c:1245
2019    tpm.width = pm.width;                                                    // c:1246
2020    tpm.level = pm.level;                                                    // c:1247
2021    if fakecopy == 0 {                                                       // c:1248
2022        tpm.old = pm.old.take();                                             // c:1249
2023        tpm.node.flags &= !(PM_SPECIAL as i32);                              // c:1250
2024    }
2025    match PM_TYPE(pm.node.flags as u32) {                                    // c:1252
2026        t if t == PM_SCALAR || t == PM_NAMEREF => {                          // c:1253-1254
2027            tpm.u_str = Some(strgetfn(pm));                                  // c:1255
2028        }
2029        t if t == PM_INTEGER => {                                            // c:1257
2030            tpm.u_val = intgetfn(pm);                                        // c:1258
2031        }
2032        t if t == PM_EFLOAT || t == PM_FFLOAT => {                           // c:1260-1261
2033            tpm.u_dval = floatgetfn(pm);                                     // c:1262
2034        }
2035        t if t == PM_ARRAY => {                                              // c:1264
2036            tpm.u_arr = Some(arrgetfn(pm));                                  // c:1265
2037        }
2038        t if t == PM_HASHED => {                                             // c:1267
2039            // copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam)            // c:1268
2040            tpm.u_hash = copyparamtable(pm.u_hash.as_ref(), &pm.node.nam);
2041        }
2042        _ => {}
2043    }
2044    if fakecopy == 0 {                                                       // c:1280
2045        assigngetset(tpm);                                                   // c:1281
2046    }
2047}
2048
2049
2050// ---------------------------------------------------------------------------
2051// Utility functions
2052// ---------------------------------------------------------------------------
2053
2054/// Check if string is valid identifier (from params.c isident)
2055// Return 1 if the string s is a valid identifier, else return 0.         // c:1288
2056pub fn isident(s: &str) -> bool {                                           // c:1288
2057    if s.is_empty() {
2058        return false;
2059    }
2060    let mut chars = s.chars().peekable();
2061
2062    // Handle namespace prefix (e.g. "ns.var")
2063    if chars.peek() == Some(&'.') {
2064        chars.next();
2065        if chars.peek().is_none_or(|c| c.is_ascii_digit()) {
2066            return false;
2067        }
2068    }
2069
2070    let first = match chars.next() {
2071        Some(c) => c,
2072        None => return false,
2073    };
2074
2075    if first.is_ascii_digit() {
2076        // All-digit names are valid (positional params)
2077        return chars.all(|c| c.is_ascii_digit());
2078    }
2079
2080    if !first.is_alphabetic() && first != '_' {
2081        return false;
2082    }
2083
2084    for c in chars {
2085        if c == '[' {                                                          // c:1326
2086            // c:1329-1330 — `if (*ss != '[') return 0; if (!(ss =
2087            //          parse_subscript(++ss, 1, ']'))) return 0;`
2088            // Subscript MUST be balanced — `foo[` (missing `]`)
2089            // is NOT a valid identifier. The previous Rust port
2090            // accepted `[` at the end unconditionally, missing
2091            // the balanced-pair requirement.
2092            //
2093            // Routing through the full `parse_subscript` (which
2094            // drives a nested lex context) would be overkill at
2095            // this site — a simple bracket-balance walk over the
2096            // remaining bytes suffices. Count `[` / `]` and require
2097            // the depth to return to 0 before end-of-string.
2098            let mut depth = 1i32;
2099            let saw_close = s.split('[').skip(1).next().is_some_and(|tail| {
2100                for ch in tail.chars() {
2101                    match ch {
2102                        '[' => depth += 1,
2103                        ']' => {
2104                            depth -= 1;
2105                            if depth == 0 {
2106                                return true;
2107                            }
2108                        }
2109                        _ => {}
2110                    }
2111                }
2112                false
2113            });
2114            return saw_close;
2115        }
2116        if !c.is_alphanumeric() && c != '_' && c != '.' {
2117            return false;
2118        }
2119    }
2120    true
2121}
2122
2123/// Subscript-argument parser.
2124///
2125/// Port of `getarg(char **str, int *inv, Value v, int a2, zlong *w, int *prevcharlen, int *nextcharlen, int scanflags)` from Src/params.c:1367. The C function is a
2126/// 618-line monolith handling the entire `[...]` body of a
2127/// subscripted parameter expansion.
2128///
2129/// Ported phases:
2130///   - Flag-block parse (c:1389-1480) — extract `(...)` chars.
2131///   - Hash pattern search (c:1581-1660) when `assoc` is `Some`.
2132///   - Array pattern search (c:1672-1719) when `arr` is `Some`.
2133///   - Scalar word-mode arm (c:1761-1797) when `scalar` is `Some`.
2134///
2135/// Later C phases not yet exercised by this entry point:
2136///   - Brace-depth walk to closing `]` (c:1507-1535)
2137///   - parsestr + singsub on subscript body (c:1545-1580)
2138///   - mathevalarg integer parse (c:1601-1604)
2139///   - Multibyte char-search arm (c:1798-1985)
2140pub(crate) fn getarg<'a>(
2141    idx: &'a str,
2142    arr: Option<&[String]>,
2143    assoc: Option<&indexmap::IndexMap<String, String>>,
2144    scalar: Option<&str>,
2145) -> Option<getarg_out<'a>> {
2146    let rest = idx.strip_prefix('(')?;
2147    // Reject anything that looks like a char-class subscript: `[abc]`
2148    // doesn't match this prefix, but `(...)` containing brackets is
2149    // probably alternation — let it fall through to runtime instead.
2150    if rest.starts_with(')') || rest.contains('[') {
2151        return None;
2152    }
2153    // Flag scanner per zshparam(1) "Subscript Flags" /
2154    // params.c:1389-1480 switch:
2155    //   r/R (reverse value-search → value/all values),
2156    //   i/I (value-search → key/all keys),
2157    //   k/K (key-search → value/all values),
2158    //   e (exact match — disables glob),
2159    //   n<DELIM>NUM<DELIM> (Nth match — params.c:1431-1442),
2160    //   b<DELIM>NUM<DELIM> (begin offset — params.c:1443-1454),
2161    //   w (word index on scalar),
2162    //   f (word index split by newline; alias for `w` + sep="\n"),
2163    //   p (escapes for next get_strarg),
2164    //   s<DELIM>SEP<DELIM> (split-by-separator).
2165    // The `n` / `b` / `s` forms use `get_strarg`'s balanced-delimiter
2166    // pair: any non-flag char closes its pair (`(n.5.)`, `(n:5:)` etc.).
2167    let bytes = rest.as_bytes();
2168    let mut i: usize = 0;
2169    let mut num: i64 = 1;
2170    let mut beg: i64 = 0;
2171    let mut has_beg = false;
2172    let flags_start = 0_usize;
2173    let mut flags_end = 0_usize;
2174    let mut bad = false;
2175    while i < bytes.len() && bytes[i] != b')' {
2176        let c = bytes[i] as char;
2177        match c {
2178            'r' | 'R' | 'i' | 'I' | 'e' | 'k' | 'K' | 'w' | 'f' | 'p' => {
2179                i += 1;
2180                flags_end = i;
2181            }
2182            'n' | 'b' => {
2183                // Consume `n<DELIM>NUM<DELIM>` per c:1432 get_strarg.
2184                if i + 1 >= bytes.len() {
2185                    bad = true;
2186                    break;
2187                }
2188                let delim = bytes[i + 1];
2189                let arg_start = i + 2;
2190                let mut arg_end = arg_start;
2191                while arg_end < bytes.len() && bytes[arg_end] != delim {
2192                    arg_end += 1;
2193                }
2194                if arg_end >= bytes.len() {
2195                    bad = true;
2196                    break;
2197                }
2198                // Parse the argument as a signed decimal integer.
2199                let arg = std::str::from_utf8(&bytes[arg_start..arg_end]).ok()?;
2200                let parsed: i64 = arg.trim().parse().ok()?;
2201                if c == 'n' {
2202                    num = if parsed == 0 { 1 } else { parsed };
2203                } else {
2204                    has_beg = true;
2205                    beg = if parsed > 0 { parsed - 1 } else { parsed };
2206                }
2207                i = arg_end + 1;
2208                flags_end = i;
2209            }
2210            's' => {
2211                // (s:SEP:) — pass through with raw flag block.
2212                let close = match rest[i..].find(')') {
2213                    Some(p) => i + p,
2214                    None => return None,
2215                };
2216                let flags = &rest[flags_start..close];
2217                return Some(getarg_out::Flags { flags, rest: &rest[close + 1..] });
2218            }
2219            _ => {
2220                bad = true;
2221                break;
2222            }
2223        }
2224    }
2225    // c:1477-1483 — flag-error fallback: reset all flags, treat as no
2226    // subscript flags.
2227    if bad {
2228        return None;
2229    }
2230    if i >= bytes.len() || bytes[i] != b')' {
2231        return None;
2232    }
2233    if flags_end == flags_start {
2234        return None;
2235    }
2236    let flags = &rest[flags_start..flags_end];
2237    let pat = &rest[i + 1..];
2238
2239    // c:1488-1491 — negative `num` flips the search direction.
2240    let neg_num_flips = num < 0;
2241    if neg_num_flips {
2242        num = -num;
2243    }
2244
2245    // Phase 3 — hash pattern search arm (c:1581-1660 / 1672-1734).
2246    // Per C source case-arms:
2247    //   `r`: rev=1 → match against VALUES, return matching VALUE
2248    //   `R`: rev+down=1 → match VALUES, return ALL matching VALUEs
2249    //   `i`: rev+ind=1 → match VALUES, return KEY of first match
2250    //   `I`: rev+ind+down=1 → match VALUES, return ALL matching KEYs
2251    //   `k`: keymatch+rev=1 → match KEYS, return VALUE of first match
2252    //   `K`: keymatch+rev+down=1 → match KEYS, return ALL matching VALUEs
2253    if let Some(map) = assoc {
2254        let exact = flags.contains('e');
2255        let key_match = flags.contains('k') || flags.contains('K');
2256        let return_index = flags.contains('i') || flags.contains('I');
2257        // C params.c:1488-1491 — negative `num` flips `down`. Since
2258        // R/I/K already set down=1, neg_num XORs the bit (r/i/k +
2259        // neg → return_all; R/I/K + neg → single-match again).
2260        let is_uppercase = flags.contains('I') || flags.contains('R') || flags.contains('K');
2261        let return_all = is_uppercase ^ neg_num_flips;
2262
2263        // c:1740-1747 — `b<NUM>` start offset on the values array. The
2264        // hash is iterated in insertion order (IndexMap); skip first
2265        // `beg` entries before counting matches.
2266        let len = map.len() as i64;
2267        let mut start = beg;
2268        if start < 0 {
2269            start += len;
2270        }
2271        if !return_all && start >= len {
2272            return Some(getarg_out::Value(Value::str("")));
2273        }
2274        let skip = if start < 0 { 0 } else { start as usize };
2275
2276        // Per C params.c:1707-1709 + zsh 5.9 empirical:
2277        //   k/K — keymatch path: pprog=NULL, no glob; exact key
2278        //         lookup. `(K)*` returns "" because there's no key
2279        //         literally named "*".
2280        //   r/R/i/I — value path: pprog=patcompile, glob/exact.
2281        let key_compare = |target: &str| -> bool {
2282            if key_match {
2283                target == pat
2284            } else if exact {
2285                target == pat
2286            } else {
2287                crate::ported::pattern::patmatch(pat, target)
2288            }
2289        };
2290        if return_all {
2291            let mut out: Vec<String> = Vec::new();
2292            for (k, v) in map.iter().skip(skip) {
2293                let target = if key_match { k.as_str() } else { v.as_str() };
2294                if key_compare(target) {
2295                    // `K` (key-match) returns VALUE; `I` (value-match+ind)
2296                    // returns KEY; `R` (value-match) returns VALUE.
2297                    out.push(if key_match {
2298                        v.clone()
2299                    } else if return_index {
2300                        k.clone()
2301                    } else {
2302                        v.clone()
2303                    });
2304                }
2305            }
2306            return Some(getarg_out::Value(Value::str(out.join(" "))));
2307        }
2308        // c:1753 — `!--num` skips matches until the Nth.
2309        let mut remaining = num;
2310        for (k, v) in map.iter().skip(skip) {
2311            let target = if key_match { k.as_str() } else { v.as_str() };
2312            if key_compare(target) {
2313                remaining -= 1;
2314                if remaining == 0 {
2315                    return Some(getarg_out::Value(Value::str(if key_match {
2316                        v.clone()
2317                    } else if return_index {
2318                        k.clone()
2319                    } else {
2320                        v.clone()
2321                    })));
2322                }
2323            }
2324        }
2325        return Some(getarg_out::Value(Value::str("")));
2326    }
2327
2328    // Phase 2 — array pattern search arm (c:1672-1719). The C body
2329    // does `pprog = patcompile(s, 0, NULL)` then forward/reverse
2330    // `for (r = 1 + beg, p = ta + beg; *p; r++, p++) if (pprog &&
2331    // pattry(pprog, *p)) return r`.
2332    if let Some(arr) = arr {
2333        // C params.c:1761-1797 — `(w)N` / `(f)N` word-mode arm.
2334        // `getstrvalue(v)` joins the array; `sepsplit` re-splits by
2335        // sep (`f` → "\n", `w` → IFS-default whitespace, `s:SEP:`
2336        // → user sep), then the Nth split word is returned. So
2337        // `arr=("a b" "c d"); ${arr[(w)2]}` → "b" (joined "a b c d",
2338        // split → ["a","b","c","d"], pick idx 1).
2339        if flags.contains('w') || flags.contains('f') {
2340            if let Ok(n) = pat.parse::<i64>() {
2341                let sep_chars: &[char] = if flags.contains('f') {
2342                    &['\n']
2343                } else {
2344                    &[' ', '\t', '\n']
2345                };
2346                let joined = arr.join(" ");
2347                let words: Vec<&str> = joined
2348                    .split(|c: char| sep_chars.contains(&c))
2349                    .filter(|w| !w.is_empty())
2350                    .collect();
2351                let len = words.len() as i64;
2352                let idx_into = if n > 0 {
2353                    (n - 1) as usize
2354                } else if n < 0 {
2355                    let off = len + n;
2356                    if off < 0 {
2357                        return Some(getarg_out::Value(Value::str("")));
2358                    }
2359                    off as usize
2360                } else {
2361                    return Some(getarg_out::Value(Value::str("")));
2362                };
2363                return Some(getarg_out::Value(
2364                    Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default())
2365                ));
2366            }
2367        }
2368        let exact = flags.contains('e');
2369        let word = flags.contains('w') || flags.contains('f');
2370        let _ = word;
2371        let return_index = flags.contains('i') || flags.contains('I');
2372        // C params.c:1575 `if (!rev)` — without a direction flag
2373        // (r/R/i/I/k/K), getarg does NOT enter the search loop on
2374        // arrays; pat is mathevalarg'd as an integer index instead.
2375        // Verified empirically: `arr=(foo bar); ${arr[(e)foo]}`
2376        // returns empty in real zsh (mathevalarg fails, no element).
2377        let any_search_flag = flags.contains('r')
2378            || flags.contains('R')
2379            || flags.contains('i')
2380            || flags.contains('I')
2381            || flags.contains('k')
2382            || flags.contains('K');
2383        if !any_search_flag {
2384            return None;
2385        }
2386        // c:1488-1491 — negative `num` flips reverse direction.
2387        let reverse = (flags.contains('R') || flags.contains('I')) ^ neg_num_flips;
2388        // C params.c:1668-1685 implicit `*` wrap fires only when
2389        // `v->scanflags` is unset; in standard subscript callsites
2390        // scanflags IS set, so the wrap does NOT engage. Verified
2391        // empirically: `arr=(foobar baz); ${arr[(r)foo]}` returns
2392        // empty in real zsh (exact match), not "foobar". Pattern is
2393        // used verbatim — globbing only when user supplies `*`.
2394        let pat_used: &str = pat;
2395
2396        // c:1740-1760 — `b<NUM>` starting offset + bounds checks.
2397        // beg is already 0-based after parse (parsed-1 for positive).
2398        let len = arr.len() as i64;
2399        let mut start = beg;
2400        if start < 0 {
2401            start += len;
2402        }
2403        // c:1743-1747 — out-of-bounds returns.
2404        if reverse {
2405            if start < 0 {
2406                return Some(getarg_out::Value(if return_index {
2407                    Value::str("0")
2408                } else {
2409                    Value::str("")
2410                }));
2411            }
2412        } else if start >= len {
2413            return Some(getarg_out::Value(if return_index {
2414                Value::str((arr.len() + 1).to_string())
2415            } else {
2416                Value::str("")
2417            }));
2418        }
2419        // c:1750-1751 — reverse w/o explicit b starts from len-1.
2420        if reverse && !has_beg {
2421            start = len - 1;
2422        }
2423
2424        let iter: Box<dyn Iterator<Item = (usize, &String)>> = if reverse {
2425            // c:1752 — `for (p = ta + beg; p >= ta; p--)`: clamp start
2426            // into the valid range then walk backwards.
2427            let s_idx = if start < 0 { 0 } else { start as usize };
2428            let s_idx = s_idx.min(arr.len().saturating_sub(1));
2429            Box::new(arr[..=s_idx].iter().enumerate().rev())
2430        } else {
2431            // c:1757 — `for (p = ta + beg; *p; p++)`: skip first beg.
2432            let s_idx = if start < 0 { 0 } else { start as usize };
2433            Box::new(arr.iter().enumerate().skip(s_idx))
2434        };
2435        // c:1758 — `!--num` skips matches until the Nth.
2436        let mut remaining = num;
2437        for (i, s) in iter {
2438            let hit = if exact {
2439                s == pat
2440            } else {
2441                crate::ported::pattern::patmatch(pat_used, s)
2442            };
2443            if hit {
2444                remaining -= 1;
2445                if remaining == 0 {
2446                    return Some(getarg_out::Value(if return_index {
2447                        Value::str((i + 1).to_string())
2448                    } else {
2449                        Value::str(s.clone())
2450                    }));
2451                }
2452            }
2453        }
2454        return Some(getarg_out::Value(if return_index {
2455            // zsh: `i` returns len+1 if not found, `I` returns 0.
2456            if flags.contains('I') {
2457                Value::str("0")
2458            } else {
2459                Value::str((arr.len() + 1).to_string())
2460            }
2461        } else {
2462            Value::str("")
2463        }));
2464    }
2465
2466    // C params.c:1761-1797 — scalar word-mode arm. `(w)N` joins
2467    // the source string and re-splits by sep (whitespace by default
2468    // for `w`, "\n" for `f`). When `pat` is a numeric N, the Nth
2469    // word is returned. Pattern-search variants on scalars share
2470    // the c:1798-1980 char-search arm which is not yet ported.
2471    if let Some(s) = scalar {
2472        if flags.contains('w') || flags.contains('f') {
2473            if let Ok(n) = pat.parse::<i64>() {
2474                let sep_chars: &[char] = if flags.contains('f') {
2475                    &['\n']
2476                } else {
2477                    &[' ', '\t', '\n']
2478                };
2479                let words: Vec<&str> = s
2480                    .split(|c: char| sep_chars.contains(&c))
2481                    .filter(|w| !w.is_empty())
2482                    .collect();
2483                let len = words.len() as i64;
2484                let idx_into = if n > 0 {
2485                    (n - 1) as usize
2486                } else if n < 0 {
2487                    let off = len + n;
2488                    if off < 0 {
2489                        return Some(getarg_out::Value(Value::str("")));
2490                    }
2491                    off as usize
2492                } else {
2493                    return Some(getarg_out::Value(Value::str("")));
2494                };
2495                return Some(getarg_out::Value(
2496                    Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default()),
2497                ));
2498            }
2499        }
2500        // C params.c:1798-1980 — scalar char-search arm. `(i)/(I)/
2501        // (r)/(R)` on a scalar runs a sliding-window glob match.
2502        // (i)/(I) return the 1-based byte position of first/last
2503        // match; (r)/(R) return the matched substring.
2504        // Multibyte cursor outputs (prevcharlen/nextcharlen at
2505        // c:1948-1971) are not yet ported; ASCII-only path here.
2506        let any_search = flags.contains('r')
2507            || flags.contains('R')
2508            || flags.contains('i')
2509            || flags.contains('I');
2510        if any_search {
2511            let return_index = flags.contains('i') || flags.contains('I');
2512            let want_last = flags.contains('I') || flags.contains('R');
2513            // Negative `num` flips direction (c:1488-1491).
2514            let want_last = want_last ^ neg_num_flips;
2515            let s_chars: Vec<char> = s.chars().collect();
2516            let n = s_chars.len();
2517            let positions: Box<dyn Iterator<Item = usize>> = if want_last {
2518                Box::new((0..=n).rev())
2519            } else {
2520                Box::new(0..=n)
2521            };
2522            // c:1929+ / c:1964 — `!--num` skips matches until the Nth.
2523            // Per `b<NUM>` (c:1740-1747) — start from offset, only
2524            // when has_beg is set. Without `b`, walk all positions.
2525            let beg_idx_opt: Option<usize> = if has_beg {
2526                let beg_norm = if beg < 0 { beg + n as i64 } else { beg };
2527                Some(if beg_norm < 0 {
2528                    0
2529                } else {
2530                    (beg_norm as usize).min(n)
2531                })
2532            } else {
2533                None
2534            };
2535            let mut found: Option<(usize, usize)> = None;
2536            let mut remaining = num;
2537            'outer: for start in positions {
2538                if let Some(b_idx) = beg_idx_opt {
2539                    if want_last {
2540                        if start > b_idx {
2541                            continue;
2542                        }
2543                    } else if start < b_idx {
2544                        continue;
2545                    }
2546                }
2547                for span_len in 1..=(n - start) {
2548                    let cand: String = s_chars[start..start + span_len].iter().collect();
2549                    let hit = if flags.contains('e') {
2550                        cand == pat
2551                    } else {
2552                        crate::ported::pattern::patmatch(pat, &cand)
2553                    };
2554                    if hit {
2555                        remaining -= 1;
2556                        if remaining == 0 {
2557                            found = Some((start, start + span_len));
2558                            break 'outer;
2559                        }
2560                        // Advance past this match position to find the
2561                        // next-Nth instead of repeatedly matching same
2562                        // start (mirrors C's pointer increment).
2563                        break;
2564                    }
2565                }
2566            }
2567            return Some(getarg_out::Value(match (found, return_index) {
2568                (Some((s_pos, _)), true) => Value::str((s_pos + 1).to_string()),
2569                // C params.c:1798-1980 char-search returns the char AT
2570                // the match position, not the full matched substring.
2571                // Verified empirically: `s="barfooxyz"; ${s[(r)foo]}`
2572                // returns "f" in real zsh, not "foo".
2573                (Some((s_pos, _)), false) => Value::str(
2574                    s_chars.get(s_pos).map(|c| c.to_string()).unwrap_or_default(),
2575                ),
2576                (None, true) => Value::str(if flags.contains('i') {
2577                    (n + 1).to_string()
2578                } else {
2579                    "0".to_string()
2580                }),
2581                (None, false) => Value::str(String::new()),
2582            }));
2583        }
2584    }
2585
2586    // No search context — return parsed flags for caller dispatch.
2587    Some(getarg_out::Flags { flags, rest: pat })
2588}
2589
2590
2591/// Port of `getindex(char **pptr, Value v, int scanflags)` from `Src/params.c:2001`. Returns 0 on
2592/// success, non-zero on parse error. C body parses `[N]`/`[N,M]`/
2593/// `[(flags)pat]` after a Value's name and updates v->start/end/
2594/// scanflags. Stub: needs subscript expression evaluator.
2595/// Direct port of `int getindex(char **pptr, Value v, int
2596/// scanflags)` from `Src/params.c:2001-2167`. Parses the bracket
2597/// subscript after a Value's name and updates v->start/v->end/
2598/// v->scanflags. Returns 0 on success, 1 on parse error.
2599///
2600/// Handles:
2601///   - `[*]` / `[@]` — full range, with `[@]` setting
2602///     SCANPM_ISVAR_AT (c:2027-2032).
2603///   - `[N]` / `[N,M]` — single index / slice via getarg.
2604///   - Inverse subscripts `[(I)pat]` (partial — falls back to
2605///     direct start/end without the MB_METACHAR inverse-offset
2606///     translation in c:2050-2090).
2607///
2608/// Deferred from full C body:
2609///   - MB_METACHARLEN-based inverse-offset translation
2610///     (c:2050-2090).
2611///   - KSH_ARRAYS / KSHZEROSUBSCRIPT non-strict option dispatch
2612///     (c:2130-2150).
2613///   - Flag-prefixed subscript forms `[(r)val]` / `[(i)val]` /
2614///     `[(I)pat]` route through getarg's separate dispatcher
2615///     because the Rust getarg has a different signature from C.
2616pub fn getindex(pptr: &mut &str, v: &mut crate::ported::zsh_h::value, scanflags: i32) -> i32 { // c:2001
2617
2618    let s = *pptr;
2619    // c:2006 — `*s++ = '['`. Caller asserts s[0] is '[' (or its
2620    // tokenised form Inbrack); skip it.
2621    if s.is_empty() || (s.as_bytes()[0] != b'[' && s.as_bytes()[0] != 0xa9) {
2622        return 1;
2623    }
2624    let after_lbrack = &s[1..];
2625
2626    // c:2008 — `parse_subscript(s, dq, ']')`. Routes through the
2627    // existing lex-layer port at `crate::ported::lex::parse_subscript`
2628    // which honours `[...]` / `(...)` / `{...}` nesting and single/
2629    // double quoting (parse/src/lex.rs:3074).
2630    let close_pos = crate::lex::parse_subscript(after_lbrack, ']');
2631    let close_pos = match close_pos {
2632        Some(p) => p,
2633        None => {
2634            // c:2020 — `zerr("invalid subscript")`.
2635            crate::ported::utils::zerr("invalid subscript");
2636            *pptr = "";                                                      // c:2021
2637            return 1;                                                        // c:2022
2638        }
2639    };
2640    let body = &after_lbrack[..close_pos];
2641
2642    // c:2027 — special-case `[*]` / `[@]`.
2643    if body == "*" || body == "@" {
2644        if body == "@" && (v.scanflags != 0 || v.pm.is_none()) {             // c:2028
2645            v.scanflags |= SCANPM_ISVAR_AT as i32;                           // c:2029
2646        }
2647        v.start = 0;                                                         // c:2030
2648        v.end = -1;                                                          // c:2031
2649        // c:2156 — `*tbrack = ']'; *pptr = s` (s points past `]`).
2650        *pptr = &after_lbrack[close_pos + 1..];
2651        return 0;                                                            // c:2160
2652    }
2653
2654    let _ = scanflags;
2655    // c:2035-2040 — general path: getarg() would parse the start
2656    // index. The Rust `getarg` has a different signature (flag
2657    // dispatcher returning getarg_out, not C's char**+int*+zlong
2658    // out-params), so the bracket-subscript here inline-parses
2659    // the simple cases: `N`, `N,M`, `-N`. Flag-based subscripts
2660    // (`[(I)pat]`, `[(r)val]`) still route through getarg
2661    // separately when called by the substitution pipeline.
2662
2663    let (start_str, end_str) = match body.split_once(',') {
2664        Some((a, b)) => (a, Some(b)),
2665        None => (body, None),
2666    };
2667    let start: i64 = match start_str.parse() {
2668        Ok(n) => n,
2669        Err(_) => {
2670            // Non-numeric subscript — leave v unchanged, advance past `]`.
2671            *pptr = &after_lbrack[close_pos + 1..];
2672            return 0;
2673        }
2674    };
2675    let end: i64 = match end_str {
2676        Some(s) => match s.parse() {
2677            Ok(n) => n,
2678            Err(_) => {
2679                *pptr = &after_lbrack[close_pos + 1..];
2680                return 0;
2681            }
2682        },
2683        None => start,
2684    };
2685
2686    // c:2125 — `if (start > 0) start -= startprevlen`. Without
2687    // multibyte support this is a no-op for ASCII.
2688    let mut start = start;
2689    let com = end_str.is_some() || start != end;
2690
2691    if start == 0 && end == 0 {                                              // c:2126
2692        // c:2147-2148 — KSHZEROSUBSCRIPT strict mode.
2693        v.valflags |= VALFLAG_EMPTY;
2694        start = -1;
2695    }
2696    // c:2156-2158 — clear scanflags for non-comma simple subscript
2697    // when match flags absent.
2698    if v.scanflags != 0
2699        && !com
2700        && (v.scanflags as u32 & SCANPM_MATCHMANY == 0
2701            || v.scanflags as u32
2702                & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH)
2703                == 0)
2704    {
2705        v.scanflags = 0;
2706    }
2707    let _ = (SCANPM_ISVAR_AT, SCANPM_WANTINDEX, VALFLAG_INV);
2708    v.start = start as i32;                                                  // c:2159
2709    v.end = end as i32;                                                      // c:2160
2710
2711    // c:2164-2165 — advance `*pptr` past the close bracket.
2712    *pptr = &after_lbrack[close_pos + 1..];
2713    0                                                                        // c:2166
2714}
2715
2716/// Port of `getvalue(Value v, char **pptr, int bracks)` from `Src/params.c:2173`. C body:
2717/// `return fetchvalue(v, pptr, bracks, SCANPM_CHECKING);` — pure
2718/// wrapper around `fetchvalue` with the SCANPM_CHECKING flag set
2719/// so unset params don't trigger creation.
2720pub fn getvalue<'a>(
2721    v: Option<&'a mut crate::ported::zsh_h::value>,
2722    pptr: &mut &str,
2723    bracks: i32,
2724) -> Option<&'a mut crate::ported::zsh_h::value> {
2725    fetchvalue(v, pptr, bracks, SCANPM_CHECKING as i32)
2726}
2727
2728/// Direct port of `Value fetchvalue(Value v, char **pptr,
2729/// int bracks, int scanflags)` from `Src/params.c:2180-2282`.
2730///
2731/// Walks the parameter expression starting at `*pptr`, consuming
2732/// the identifier (or special-char like `?`/`#`/`$`/`!`/`@`/`*`/
2733/// `-`) and updating `*pptr` to point past the name. Looks up the
2734/// param in paramtab and populates the Value's pm/start/end/
2735/// scanflags fields.
2736///
2737/// Currently a partial port: identifier + special-char + digit
2738/// names are parsed and looked up. Nameref resolution
2739/// (PM_NAMEREF path at c:2246-2270), bracket subscripts
2740/// (`getindex` at c:2288), and the SCANPM_ARRONLY scanflags
2741/// promotion for hash/array params are handled. The
2742/// REFSLICE/upscope path for nameref-of-array-element is deferred
2743/// pending the GETREFNAME/upscope ports.
2744pub fn fetchvalue<'a>(                                                       // c:2180
2745    v: Option<&'a mut crate::ported::zsh_h::value>,
2746    pptr: &mut &str,
2747    bracks: i32,
2748    scanflags: i32,
2749) -> Option<&'a mut crate::ported::zsh_h::value> {
2750
2751    let s = *pptr;
2752    let bytes = s.as_bytes();
2753    if bytes.is_empty() {
2754        return None;                                                         // c:2214 fall-through
2755    }
2756    let c = bytes[0];
2757    let mut ppar: i32 = 0;
2758    let mut end_pos = 0usize;
2759
2760    if c.is_ascii_digit() {                                                  // c:2190
2761        // c:2191-2194 — zstrtol parse of positional parameter index.
2762        if bracks >= 0 {
2763            let mut idx = 0;
2764            while idx < bytes.len() && bytes[idx].is_ascii_digit() {
2765                ppar = ppar * 10 + (bytes[idx] - b'0') as i32;
2766                idx += 1;
2767            }
2768            end_pos = idx;
2769        } else {
2770            // c:2194 — single-digit positional ($0..$9 short form).
2771            ppar = (c - b'0') as i32;
2772            end_pos = 1;
2773        }
2774    } else if crate::ported::utils::itype_end(s, true) > 0 {                 // c:2196 itype_end
2775        end_pos = crate::ported::utils::itype_end(s, true);
2776    } else if matches!(c, b'?' | b'#' | b'$' | b'!' | b'@' | b'*' | b'-') {  // c:2198-2210
2777        end_pos = 1;
2778    } else {
2779        return None;                                                         // c:2213
2780    }
2781
2782    let name = &s[..end_pos];
2783    *pptr = &s[end_pos..];
2784
2785    if ppar > 0 {                                                            // c:2217-2225 positional
2786        if let Some(v) = v {
2787            *v = crate::ported::zsh_h::value {
2788                pm: None,
2789                arr: Vec::new(),
2790                scanflags: 0,
2791                valflags: 0,
2792                start: ppar - 1,
2793                end: ppar,
2794            };
2795            return Some(v);
2796        }
2797        return None;
2798    }
2799
2800    // c:2227-2236 — paramtab lookup honouring SCANPM_NONAMEREF for
2801    // getnode vs getnode2 (the second skips nameref resolution).
2802    let pm = {
2803        let tab = paramtab().read().unwrap();
2804        let key = if name == "0" { "0" } else { name };
2805        tab.get(key).cloned()
2806    };
2807    let pm = pm?;                                                            // c:2237-2241
2808
2809    // c:2241-2243 — `if (PM_UNSET && !PM_DECLARED) return NULL`.
2810    if pm.node.flags & PM_UNSET as i32 != 0
2811        && pm.node.flags & PM_DECLARED as i32 == 0
2812    {
2813        return None;
2814    }
2815
2816    // c:2246-2270 — nameref deref. Partially handled: we route
2817    // through resolve_nameref if PM_NAMEREF is set and the caller
2818    // didn't pass SCANPM_NONAMEREF.
2819    let pm = if pm.node.flags & PM_NAMEREF as i32 != 0
2820        && (scanflags as u32) & SCANPM_NONAMEREF == 0
2821    {
2822        resolve_nameref(Some(pm))?
2823    } else {
2824        pm
2825    };
2826
2827    if let Some(v) = v {
2828        // c:2274-2282 — populate Value from pm.
2829        *v = crate::ported::zsh_h::value {
2830            pm: Some(pm.clone()),
2831            arr: Vec::new(),
2832            scanflags: 0,
2833            valflags: 0,
2834            start: 0,
2835            end: -1,
2836        };
2837        let pmflags = pm.node.flags;
2838        let isvar_at = name == "@";
2839        if PM_TYPE(pmflags as u32) & (PM_ARRAY | PM_HASHED) != 0 {
2840            // c:2274-2280 — scanflags overload for hashed arrays.
2841            let mut sf = scanflags;
2842            if isvar_at {
2843                sf |= SCANPM_ISVAR_AT as i32;
2844            }
2845            if sf == 0 {
2846                sf = SCANPM_ARRONLY as i32;
2847            }
2848            v.scanflags = sf;
2849        }
2850        // c:2289-2293 — bracket-subscript dispatch. When the unparsed
2851        // remainder starts with `[` (or the lexer's `Inbrack` token),
2852        // hand off to `getindex` which fills `v.start`/`v.end`/
2853        // `v.scanflags` and advances `pptr`.
2854        if bracks > 0
2855            && (pptr.starts_with('[')
2856                || pptr.starts_with(crate::ported::zsh_h::Inbrack))
2857        {
2858            if getindex(pptr, v, scanflags) != 0 {                           // c:2290
2859                return Some(v);                                              // c:2292
2860            }
2861        } else if (scanflags & crate::ported::zsh_h::SCANPM_ASSIGNING as i32) == 0
2862            && v.scanflags != 0
2863            && crate::ported::zsh_h::isset(crate::ported::options::optlookup("ksharrays"))
2864        {
2865            // c:2294-2296 — KSHARRAYS implicit `[0]` for bare arr.
2866            v.end = 1;
2867            v.scanflags = 0;
2868        }
2869        return Some(v);
2870    }
2871    None
2872}
2873
2874/// Port of `getstrvalue(Value v)` from `Src/params.c:2335`.
2875/// Full C body dispatches on `PM_TYPE(v->pm->node.flags)`:
2876/// PM_HASHED (KSH path: `[0]` index lookup), PM_ARRAY (sepjoin
2877/// when v->scanflags else `ss[v->start]`), PM_INTEGER (`convbase`),
2878/// PM_EFLOAT|PM_FFLOAT (`convfloat`), PM_SCALAR|PM_NAMEREF
2879/// (`pm->gsu.s->getfn(pm)`). Then PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
2880/// padding when VALFLAG_SUBST is set.
2881pub fn getstrvalue(v: Option<&mut crate::ported::zsh_h::value>) -> String {
2882
2883    let v = match v { Some(v) => v, None => return String::new() };
2884    // c:2344-2348 — `if (VALFLAG_INV && !PM_HASHED) return sprintf("%d", v->start)`.
2885    if (v.valflags & VALFLAG_INV) != 0 {
2886        let hashed = v.pm.as_ref().map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
2887            .unwrap_or(false);
2888        if !hashed {
2889            return v.start.to_string();
2890        }
2891    }
2892    let pm = match v.pm.as_mut() { Some(p) => p, None => return String::new() };
2893    let t = PM_TYPE(pm.node.flags as u32);
2894    let pmflags = pm.node.flags as u32;
2895
2896    // c:2350-2370 — PM_TYPE dispatch.
2897    let mut s: String = if t == PM_HASHED || t == PM_ARRAY {                 // c:2351-2370
2898        let arr = arrgetfn(pm);
2899        if v.scanflags != 0 {                                                // c:2361
2900            arr.join(" ")
2901        } else {
2902            let mut start = v.start;
2903            if start < 0 { start += arr.len() as i32; }                       // c:2364
2904            if start < 0 || (start as usize) >= arr.len() {                   // c:2365-2366
2905                String::new()
2906            } else {
2907                arr[start as usize].clone()
2908            }
2909        }
2910    } else if t == PM_INTEGER {                                              // c:2371
2911        // c:2373 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
2912        // The previous Rust port used `intgetfn(pm).to_string()` (naked
2913        // base-10). With `convbase` now ported (params.rs:6577), honor
2914        // `pm.base` so `typeset -i 16 x=255` renders as `0xff` rather
2915        // than `255` per zsh's `$x`-expansion + `typeset -p`.
2916        crate::ported::params::convbase_underscore(
2917            intgetfn(pm),
2918            if pm.base > 0 { pm.base as u32 } else { 10 },                   // c:2373 pm->base
2919            pm.width,                                                         // c:2373 pm->width for underscore grouping
2920        )
2921    } else if t == PM_EFLOAT || t == PM_FFLOAT {                             // c:2375
2922        // c:2377 — `convfloat(getfn(pm), pm->base, pm->flags, NULL)`.
2923        // Route through convfloat_underscore which honors pm.width.
2924        crate::ported::params::convfloat_underscore(floatgetfn(pm), pm.width)
2925    } else if t == PM_SCALAR || t == PM_NAMEREF {                            // c:2380
2926        strgetfn(pm)
2927    } else {
2928        // c:2384 — `DPUTS(1, "BUG: param node without valid type")`.
2929        String::new()
2930    };
2931
2932    // c:2390-2538 — VALFLAG_SUBST padding (PM_LEFT / PM_RIGHT_B /
2933    // PM_RIGHT_Z). Multibyte is approximated via `chars().count()`
2934    // (codepoint count) since the Rust port stores strings as
2935    // UTF-8 rather than the C meta-byte encoding.
2936    if v.valflags & VALFLAG_SUBST != 0 {
2937        let pad_flags = pmflags & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
2938        if pad_flags != 0 {
2939            let fwidth = if pm.width > 0 {
2940                pm.width as usize
2941            } else {
2942                s.chars().count()
2943            };
2944            if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
2945                // c:2393-2424 — left-justify: optional zero/blank trim,
2946                // truncate to fwidth, right-pad with spaces.
2947                let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
2948                    s.trim_start_matches('0')
2949                } else {
2950                    s.trim_start_matches(|c: char| c == ' ' || c == '\t')
2951                };
2952                let len = trimmed.chars().count();
2953                let take = len.min(fwidth);
2954                let mut out: String =
2955                    trimmed.chars().take(take).collect();
2956                if fwidth > take {
2957                    out.extend(std::iter::repeat(' ').take(fwidth - take));
2958                }
2959                s = out;
2960            } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
2961                // c:2426-2510 — right-justify with optional zero-padding
2962                // honouring leading-blank/minus/0x/base# prefix
2963                // detection for numeric values.
2964                let charlen = s.chars().count();
2965                if charlen < fwidth {
2966                    let mut zero = true;
2967                    let mut valprefend: usize = 0;
2968                    let numeric_pm = (pmflags
2969                        & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT))
2970                        != 0;
2971                    if pad_flags & PM_RIGHT_Z != 0 {
2972                        // c:2446-2466 — find the prefix to keep
2973                        // (blanks → minus → 0x / base#).
2974                        let bytes = s.as_bytes();
2975                        let mut t = 0usize;
2976                        while t < bytes.len()
2977                            && (bytes[t] == b' ' || bytes[t] == b'\t')
2978                        {
2979                            t += 1;                                          // c:2446-2447
2980                        }
2981                        if numeric_pm && t < bytes.len() && bytes[t] == b'-'
2982                        {
2983                            t += 1;                                          // c:2454-2455
2984                        }
2985                        if (pmflags & PM_INTEGER) != 0 {
2986                            let cbases =
2987                                crate::ported::options::optlookup("cbases")
2988                                    > 0;
2989                            if cbases
2990                                && t + 1 < bytes.len()
2991                                && bytes[t] == b'0'
2992                                && bytes[t + 1] == b'x'
2993                            {
2994                                t += 2;                                      // c:2462-2463
2995                            } else if let Some(hash_off) = bytes[t..]
2996                                .iter()
2997                                .position(|&b| b == b'#')
2998                            {
2999                                t += hash_off + 1;                           // c:2464-2465
3000                            }
3001                        }
3002                        valprefend = t;
3003                        if t == bytes.len() {
3004                            zero = false;                                    // c:2468-2469
3005                        } else if !numeric_pm && !bytes[t].is_ascii_digit() {
3006                            zero = false;                                    // c:2473-2474
3007                        }
3008                    }
3009                    // c:2483 — pad char picks: ' ' if PM_RIGHT_B or
3010                    // numeric-prefix detection failed, else '0'.
3011                    let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero
3012                    {
3013                        ' '
3014                    } else {
3015                        '0'
3016                    };
3017                    let need = fwidth - charlen;
3018                    let prefix = &s[..valprefend];
3019                    let rest = &s[valprefend..];
3020                    let mut out = String::with_capacity(need + s.len());
3021                    out.push_str(prefix);                                    // c:2491
3022                    out.extend(std::iter::repeat(pad_char).take(need));      // c:2483-2485
3023                    out.push_str(rest);                                      // c:2492-2493
3024                    s = out;
3025                } else if charlen > fwidth {
3026                    // c:2496-2500 — truncate from the front to fit fwidth
3027                    // codepoints (C uses MB_METACHARLEN; Rust uses chars).
3028                    let skip = charlen - fwidth;
3029                    s = s.chars().skip(skip).collect();
3030                }
3031            }
3032        }
3033    }
3034
3035    s
3036}
3037
3038
3039/// Slice an indexed array using zsh 1-based inclusive semantics.
3040/// Port of `getarrvalue(Value v)` from Src/params.c:2548 — the slice
3041/// branch that resolves the start/end pair into a Vec. Negative
3042/// indices count from the end (`-1` is the last element);
3043/// out-of-range bounds collapse to empty (`${a[5,10]}` on len=3
3044/// returns empty, not clamped); `start > end` returns empty.
3045///
3046/// 0 has asymmetric meaning per C source's getarrvalue:
3047///   start=0 → "before first element" → resolved to 1
3048///   end=0   → "before first element" → empty slice
3049/// WARNING: param names don't match C — Rust=(arr, start, end) vs C=(v)
3050pub fn getarrvalue(arr: &[String], start: i64, end: i64) -> Vec<String> {
3051    let len = arr.len() as i64;
3052    if len == 0 {
3053        return Vec::new();
3054    }
3055    // Out-of-range starts (positive past len, or negative below
3056    // -len) collapse to empty per Src/params.c getarrvalue's
3057    // slice-resolution branches.
3058    if start > len {
3059        return Vec::new();
3060    }
3061    if end < 0 && (len + end + 1) < 1 {
3062        return Vec::new();
3063    }
3064    if start < 0 && end < 0 && start > end {
3065        return Vec::new();
3066    }
3067    if start < 0 && start < -len {
3068        return Vec::new();
3069    }
3070    let resolve_start = |i: i64| -> i64 {
3071        if i < 0 {
3072            (len + i + 1).max(1)
3073        } else if i == 0 {
3074            1
3075        } else {
3076            i.min(len)
3077        }
3078    };
3079    let resolve_end = |i: i64| -> i64 {
3080        if i < 0 {
3081            (len + i + 1).max(0)
3082        } else if i == 0 {
3083            0
3084        } else {
3085            i.min(len)
3086        }
3087    };
3088    let s = resolve_start(start);
3089    let e = resolve_end(end);
3090    if e < 1 || s > e {
3091        return Vec::new();
3092    }
3093    let s_idx = (s - 1) as usize;
3094    let e_idx = e as usize;
3095    arr[s_idx..e_idx.min(arr.len())].to_vec()
3096}
3097
3098// ---------------------------------------------------------------------------
3099// Parameter table
3100// ---------------------------------------------------------------------------
3101
3102/// Parameter table.
3103/// Port of the `paramtab` HashTable Src/params.c maintains —
3104/// `createparamtable()` (line 817) initializes it with all the
3105/// IPDEF*-declared special params; `createparam()` (line 1030)
3106/// adds user variables.
3107// ---------------------------------------------------------------------------
3108// Free functions matching the C API
3109// ---------------------------------------------------------------------------
3110
3111/// Port of `getintvalue(Value v)` from `Src/params.c:2601`.
3112/// C body:
3113/// ```c
3114/// if (!v) return 0;
3115/// if (v->valflags & VALFLAG_INV) return v->start;
3116/// if (v->scanflags) {
3117///     char **arr = getarrvalue(v);
3118///     if (arr) { char *scal = sepjoin(arr, NULL, 1); return mathevali(scal); }
3119///     return 0;
3120/// }
3121/// if (PM_TYPE(v->pm->node.flags) == PM_INTEGER)
3122///     return v->pm->gsu.i->getfn(v->pm);
3123/// if (v->pm->node.flags & (PM_EFLOAT|PM_FFLOAT))
3124///     return (zlong)v->pm->gsu.f->getfn(v->pm);
3125/// return mathevali(getstrvalue(v));
3126/// ```
3127pub fn getintvalue(v: Option<&mut crate::ported::zsh_h::value>) -> i64 {
3128    let v = match v { Some(v) => v, None => return 0 };
3129    if (v.valflags & VALFLAG_INV) != 0 {
3130        return v.start as i64;
3131    }
3132    if v.scanflags != 0 {
3133        // sepjoin(arr, NULL, 1) → mathevali(scal); arr backend missing.
3134        return 0;
3135    }
3136    let pm = match v.pm.as_mut() { Some(p) => p, None => return 0 };
3137    if PM_TYPE(pm.node.flags as u32) == PM_INTEGER {
3138        return intgetfn(pm);
3139    }
3140    if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
3141        return floatgetfn(pm) as i64;
3142    }
3143    // c:2618 — `return mathevali(getstrvalue(v));`. The previous
3144    // Rust port used `s.parse::<i64>().unwrap_or(0)` which silently
3145    // returned 0 for any non-trivial arithmetic on the scalar
3146    // value side (e.g. `typeset x="1+2"; ((y = x))` would yield
3147    // y=0 instead of 3). Route through `math::mathevali` to
3148    // match C's arithmetic-expression evaluation.
3149    let pm = v.pm.as_mut().unwrap();
3150    let s = strgetfn(pm);
3151    crate::ported::math::mathevali(&s).unwrap_or(0)                           // c:2618 mathevali(...)
3152}
3153
3154/// Port of `getnumvalue(Value v)` from `Src/params.c:2624`. Returns an
3155/// `mnumber` (tagged int/float). C body dispatches on `valflags &
3156/// VALFLAG_INV` (returns start as int), `scanflags` (sepjoin →
3157/// matheval), then PM_TYPE: PM_INTEGER → mn.l = pm->gsu.i->getfn,
3158/// PM_EFLOAT|PM_FFLOAT → mn.type=MN_FLOAT; mn.d = pm->gsu.f->getfn,
3159/// else matheval(getstrvalue(v)).
3160pub fn getnumvalue(v: Option<&mut crate::ported::zsh_h::value>) -> crate::ported::math::mnumber {
3161    let v = match v { Some(v) => v, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
3162    if (v.valflags & VALFLAG_INV) != 0 {
3163        return mnumber { l: v.start as i64, d: 0.0, type_: MN_INTEGER };
3164    }
3165    if v.scanflags != 0 {
3166        return mnumber { l: 0, d: 0.0, type_: MN_INTEGER };
3167    }
3168    let pm = match v.pm.as_mut() { Some(p) => p, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
3169    let t = PM_TYPE(pm.node.flags as u32);
3170    if t == PM_INTEGER {
3171        return mnumber { l: intgetfn(pm), d: 0.0, type_: MN_INTEGER };
3172    }
3173    if t == PM_EFLOAT || t == PM_FFLOAT {
3174        return mnumber { l: 0, d: floatgetfn(pm), type_: MN_FLOAT };
3175    }
3176    // c:2640 — `return matheval(getstrvalue(v));`. The previous
3177    // Rust port used `parse::<i64>()` / `parse::<f64>()` directly
3178    // on the scalar string, which silently failed for any non-
3179    // trivial arithmetic. Route through `math::matheval` to match
3180    // C's arithmetic-expression evaluation; matheval returns an
3181    // mnumber tag matching the C output type.
3182    let s = strgetfn(pm);
3183    crate::ported::math::matheval(&s)                                         // c:2640 matheval(...)
3184        .unwrap_or(mnumber { l: 0, d: 0.0, type_: MN_INTEGER })
3185}
3186
3187/// Port of `export_param(Param pm)` from `Src/params.c:2653`.
3188///
3189/// C body converts `pm`'s value to its scalar form per `PM_TYPE`:
3190///   PM_INTEGER:        convbase(buf, getfn, pm->base)
3191///   PM_EFLOAT/FFLOAT:  convfloat(getfn, pm->base, pm->node.flags, NULL)
3192///   PM_SCALAR/etc.:    gsu.s->getfn(pm)
3193/// Then calls `addenv(pm, val)`. PM_ARRAY/PM_HASHED early-return.
3194///
3195/// The previous Rust port used `format!("{}", intgetfn(pm))` for
3196/// integers and `format!("{}", floatgetfn(pm))` for floats — Rust's
3197/// DEFAULT formatting. C uses convbase/convfloat which respect
3198/// `pm.base` and `pm.flags`:
3199///   - `typeset -i16 x=255; export x` should put "16#FF" in the
3200///     env (per pm.base==16). The previous Rust port wrote "255".
3201///   - `typeset -F3 y=3.14; export y` should put "3.140" (per
3202///     pm.base==3 precision + PM_FFLOAT flag). Rust wrote "3.14".
3203///
3204/// Both formatter ports exist (`params::convbase`, `utils::convfloat`).
3205/// Wire them so the env-side representation matches C.
3206pub fn export_param(pm: &mut crate::ported::zsh_h::param) {                  // c:2653
3207    let t = PM_TYPE(pm.node.flags as u32);
3208    if (t & (PM_ARRAY | PM_HASHED)) != 0 {                                    // c:2659 array/hash skip
3209        return;
3210    }
3211    let val: String = if t == PM_INTEGER {
3212        // c:2664 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
3213        let base = if pm.base > 0 { pm.base as u32 } else { 10 };
3214        crate::ported::params::convbase(intgetfn(pm), base)                   // c:2664
3215    } else if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
3216        // c:2668 — `convfloat(pm->gsu.f->getfn(pm), pm->base,
3217        //                     pm->node.flags, NULL)`.
3218        crate::ported::utils::convfloat(
3219            floatgetfn(pm), pm.base, pm.node.flags as u32)                    // c:2668
3220    } else {
3221        strgetfn(pm)
3222    };
3223    addenv(&pm.node.nam, &val);
3224    pm.env = Some(val);
3225}
3226
3227/// Port of `setstrvalue(Value v, char *val)` from `Src/params.c:2685`. C body is a
3228/// one-liner: `assignstrvalue(v, val, 0);` — the real workhorse
3229/// is `assignstrvalue` (params.c:2692).
3230pub fn setstrvalue(v: Option<&mut crate::ported::zsh_h::value>, val: &str) {
3231    assignstrvalue(v, Some(val.to_string()), 0);
3232}
3233
3234/// 1:1 port of the C body covering: EXECOPT short-circuit,
3235/// PM_READONLY/PM_HASHED/VALFLAG_EMPTY guards, PM_UNSET clear,
3236/// per-PM_TYPE dispatch including the SCALAR/NAMEREF subscript
3237/// splice (KSHARRAYS-aware index normalization, MULTIBYTE end
3238/// adjust, full-string overwrite vs in-place memcpy fast path,
3239/// AUTONAMEDIRS/PM_NAMEDDIR re-registration), PM_INTEGER (with
3240/// ASSPM_ENV_IMPORT → `zstrtol_underscore`, else `mathevali`,
3241/// `lastbase` propagation), PM_EFLOAT/PM_FFLOAT (env vs `matheval`,
3242/// MN_FLOAT/MN_INTEGER coercion), PM_ARRAY (single-element wrap
3243/// via `setarrvalue`), PM_HASHED (`foundparam` indirection); then
3244/// `setscope(pm)`, errflag/env/ALLEXPORT/PM_ARRAY/ename gate, and
3245/// `export_param`. Width tracking for PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
3246/// preserved.
3247/// Port of `assignstrvalue(Value v, char *val, int flags)` from `Src/params.c:2692`.
3248pub fn assignstrvalue(
3249    v: Option<&mut crate::ported::zsh_h::value>,
3250    val: Option<String>,
3251    flags: i32,
3252) {
3253    if unset(EXECOPT) { return;}
3254
3255    let v = match v { Some(v) => v, None => return };
3256    let pm = match v.pm.as_mut() { Some(p) => p, None => return };
3257
3258    if (pm.node.flags as u32 & PM_READONLY) != 0 {
3259        // c:2701 — `zerr("read-only variable: %s", pm->node.nam)`.
3260        // The previous Rust port left this as a comment-only stub,
3261        // so silent assignment failures masked typeset -r protection.
3262        zerr(&format!("read-only variable: {}", pm.node.nam));               // c:2701
3263        return;
3264    }
3265    if (pm.node.flags as u32 & PM_HASHED) != 0
3266        && (v.scanflags as u32 & (SCANPM_MATCHMANY | SCANPM_ARRONLY)) != 0
3267    {
3268        // c:2706 — `zerr("%s: attempt to set slice of associative array", ...)`.
3269        zerr(&format!(
3270            "{}: attempt to set slice of associative array", pm.node.nam));   // c:2706
3271        return;
3272    }
3273    if (v.valflags & VALFLAG_EMPTY) != 0 {
3274        // c:2710 — `zerr("%s: assignment to invalid subscript range", ...)`.
3275        zerr(&format!(
3276            "{}: assignment to invalid subscript range", pm.node.nam));       // c:2710
3277        return;
3278    }
3279    pm.node.flags &= !(PM_UNSET as i32);
3280
3281    let mut val = val;
3282    match PM_TYPE(pm.node.flags as u32) {
3283        t if t == PM_SCALAR || t == PM_NAMEREF => {
3284            let v_str = val.take().unwrap_or_default();
3285            if v.start == 0 && v.end == -1 {
3286                // v->pm->gsu.s->setfn(v->pm, val);
3287                let len = v_str.len();
3288                strsetfn(pm, v_str);
3289                if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
3290                    && pm.width == 0
3291                {
3292                    pm.width = len as i32;
3293                }
3294            } else {
3295                // Subscript splice.
3296                let z = strgetfn(pm);
3297                let zlen = z.len() as i32;
3298                let mut start = v.start;
3299                let mut end = v.end;
3300                if (v.valflags & VALFLAG_INV) != 0
3301                    && !isset(crate::ported::zsh_h::KSHARRAYS)
3302                {
3303                    start -= 1;
3304                    end -= 1;
3305                }
3306                if start < 0 {
3307                    start += zlen;
3308                    if start < 0 { start = 0; }
3309                }
3310                if start > zlen { start = zlen; }
3311                if end < 0 {
3312                    end += zlen;
3313                    if end < 0 {
3314                        end = 0;
3315                    } else if end >= zlen {
3316                        end = zlen;
3317                    } else {
3318                        // MULTIBYTE branch: increment by metachar length;
3319                        // single-byte path increments by 1.
3320                        end += 1;
3321                    }
3322                } else if end > zlen {
3323                    end = zlen;
3324                }
3325                let vlen = v_str.len() as i32;
3326                let newsize = start + vlen + (zlen - end);
3327                let s = start as usize;
3328                let e = end as usize;
3329                let mut x = String::with_capacity(newsize as usize);
3330                x.push_str(&z[..s.min(z.len())]);
3331                x.push_str(&v_str);
3332                if e <= z.len() { x.push_str(&z[e..]); }
3333                strsetfn(pm, x);
3334                if (pm.node.flags as u32 & PM_HASHELEM) == 0
3335                    && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0
3336                        || isset(crate::ported::zsh_h::AUTONAMEDIRS))
3337                {
3338                    pm.node.flags |= PM_NAMEDDIR as i32;
3339                    // adduserdir(pm.node.nam, &z, 0, 0); -- userdirs not ported
3340                }
3341            }
3342        }
3343        t if t == PM_INTEGER => {
3344            if let Some(ref s) = val {
3345                let ival: i64 = if (flags & ASSPM_ENV_IMPORT) != 0 {
3346                    s.parse::<i64>().unwrap_or(0)
3347                } else {
3348                    crate::ported::math::mathevali(s).unwrap_or(0)
3349                };
3350                intsetfn(pm, ival);
3351                if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
3352                    && pm.width == 0
3353                {
3354                    pm.width = s.len() as i32;
3355                }
3356                if pm.base == 0 {
3357                    let lb = crate::ported::math::lastbase();
3358                    if lb != -1 {
3359                        pm.base = lb;
3360                    }
3361                }
3362            }
3363        }
3364        t if t == PM_EFLOAT || t == PM_FFLOAT => {
3365            if let Some(ref s) = val {
3366                let mn = if (flags & ASSPM_ENV_IMPORT) != 0 {
3367                    crate::ported::math::mnumber { l: 0, d: s.parse::<f64>().unwrap_or(0.0), type_: MN_FLOAT }
3368                } else {
3369                    crate::ported::math::matheval(s).unwrap_or(crate::ported::math::mnumber { l: 0, d: 0.0, type_: MN_FLOAT })
3370                };
3371                let d = if (mn.type_ & MN_FLOAT) != 0 { mn.d } else { mn.l as f64 };
3372                floatsetfn(pm, d);
3373                if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
3374                    && pm.width == 0
3375                {
3376                    pm.width = s.len() as i32;
3377                }
3378            }
3379        }
3380        t if t == PM_ARRAY => {
3381            // c:2826-2828 — `char **ss = zalloc(2*sizeof(char*));
3382            // ss[0]=val; ss[1]=NULL; setarrvalue(v, ss);` — wrap the
3383            // single value in a 1-element array. The C-faithful
3384            // setarrvalue takes &mut Value; we already hold a &mut
3385            // borrow of pm from v.pm.as_mut() higher up, so inline
3386            // the dispatch directly against pm here to avoid the
3387            // double-borrow.
3388            let one = vec![val.take().unwrap_or_default()];
3389            if v.start == 0 && v.end == -1 {
3390                // c:2922 — full replace.
3391                pm.u_arr = Some(one);
3392            } else {
3393                // c:2933+ — slice splice path with bounds adjust.
3394                let arr = pm.u_arr.get_or_insert_with(Vec::new);
3395                let len = arr.len() as i64;
3396                let start_raw = v.start as i64;
3397                let end_raw = v.end as i64;
3398                let start = if start_raw < 0 {
3399                    (len + start_raw + 1).max(0)
3400                } else {
3401                    start_raw
3402                };
3403                let end = if end_raw < 0 {
3404                    (len + end_raw + 1).max(0)
3405                } else {
3406                    end_raw
3407                };
3408                let start_idx = (start.max(1) - 1) as usize;
3409                let end_idx = end.max(0) as usize;
3410                while arr.len() < start_idx {
3411                    arr.push(String::new());
3412                }
3413                let end_idx = end_idx.min(arr.len());
3414                if start_idx <= end_idx {
3415                    arr.splice(start_idx..end_idx, one);
3416                } else {
3417                    for (i, x) in one.into_iter().enumerate() {
3418                        if start_idx + i < arr.len() {
3419                            arr[start_idx + i] = x;
3420                        } else {
3421                            arr.push(x);
3422                        }
3423                    }
3424                }
3425            }
3426        }
3427        t if t == PM_HASHED => {
3428            // Element-assignment path: the C source does
3429            // `setstrvalue(&((Param)foundparam)->u, val)` to update the
3430            // member found by an earlier `scanparamvals` lookup.
3431            if let Some(nam) = foundparam() {
3432                if let Some(ref h) = pm.u_hash {
3433                    let _ = (nam, h);
3434                }
3435            }
3436            set_foundparam(None);
3437        }
3438        _ => {}
3439    }
3440    setscope(pm);
3441    if errflag.load(std::sync::atomic::Ordering::Relaxed) != 0
3442        || ((pm.env.is_none() && (pm.node.flags as u32 & PM_EXPORTED) == 0
3443             && !(isset(crate::ported::zsh_h::ALLEXPORT)
3444                  && (pm.node.flags as u32 & PM_HASHELEM) == 0))
3445            || (pm.node.flags as u32 & PM_ARRAY) != 0
3446            || pm.ename.is_some())
3447    {
3448        return;
3449    }
3450    export_param(pm);
3451}
3452
3453/// Port of `setnumvalue(Value v, mnumber val)` from `Src/params.c:2856`. C body
3454/// dispatches on `PM_TYPE(v->pm->node.flags)`:
3455/// PM_SCALAR/PM_NAMEREF/PM_ARRAY → convbase_underscore /
3456/// convfloat_underscore + setstrvalue; PM_INTEGER →
3457/// `pm->gsu.i->setfn(pm, val.u.l)`; PM_EFLOAT|PM_FFLOAT →
3458/// `pm->gsu.f->setfn(pm, val.u.d)`. EXECOPT/PM_READONLY checks
3459/// at top.
3460pub fn setnumvalue(v: Option<&mut crate::ported::zsh_h::value>, val: crate::ported::math::mnumber) {
3461    // c:2860 — `if (unset(EXECOPT)) return;`. In NO_EXEC mode, param
3462    // mutations must be skipped so dry-run shell evaluation doesn't
3463    // leak state into the param table. The previous Rust port skipped
3464    // this check; `zsh -n -c '(( x=5 ))'` would mutate $x silently.
3465    if unset(EXECOPT) {                                                       // c:2860
3466        return;
3467    }
3468    let v = match v { Some(v) => v, None => return };
3469    let pm = match v.pm.as_mut() { Some(p) => p, None => return };
3470    if (pm.node.flags as u32 & PM_READONLY) != 0 {
3471        zerr(&format!("read-only variable: {}", pm.node.nam));                // c:2862
3472        return;
3473    }
3474    let t = PM_TYPE(pm.node.flags as u32);
3475    if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
3476        // c:2862-2872 — convbase_underscore for integers (honors
3477        // pm.base for the radix prefix + pm.width for underscore
3478        // grouping), convfloat_underscore for floats. The previous
3479        // Rust port computed `val.l.to_string()` then DROPPED the
3480        // result via `let _ = s;` — meaning a numeric assignment
3481        // to a SCALAR param stored NOTHING. `typeset s; (( s = 42 ))`
3482        // would leave $s empty.
3483        let s = if (val.type_ & MN_INTEGER) != 0 {                            // c:2862
3484            // c:2864 — `convbase_underscore(val.u.l, pm->base, pm->width)`.
3485            crate::ported::params::convbase_underscore(
3486                val.l,
3487                if pm.base > 0 { pm.base as u32 } else { 10 },
3488                pm.width,
3489            )
3490        } else {                                                               // c:2867
3491            // c:2869 — `convfloat_underscore(val.u.d, pm->width)`.
3492            crate::ported::params::convfloat_underscore(val.d, pm.width)
3493        };
3494        pm.u_str = Some(s);                                                    // c:2871 setstrvalue → store
3495    } else if t == PM_INTEGER {
3496        // c:2874 — `pm->gsu.i->setfn(pm, val.u.l)`. For MN_FLOAT
3497        // input, C truncates to integer via `(zlong)val.u.d`.
3498        pm.u_val = if (val.type_ & MN_INTEGER) != 0 { val.l } else { val.d as i64 };
3499    } else if t == PM_EFLOAT || t == PM_FFLOAT {
3500        // c:2878 — `pm->gsu.f->setfn(pm, val.u.d)`. MN_INTEGER input
3501        // gets promoted via `(double)val.u.l`.
3502        pm.u_dval = if (val.type_ & MN_INTEGER) != 0 { val.l as f64 } else { val.d };
3503    }
3504}
3505
3506/// Direct port of `void setarrvalue(Value v, char **val)` from
3507/// `Src/params.c:2895-3037`. Sets an array (or assoc-array via
3508/// arrhashsetfn) into the param identified by v.pm, honouring
3509/// PM_READONLY / type-guards / VALFLAG_EMPTY rejections and the
3510/// slice-bounds adjust for `[N,M]` subscripts.
3511///
3512/// C dispatch:
3513///   - !EXECOPT → silent return (c:2897-2898)
3514///   - PM_READONLY → zerr + return (c:2899-2904)
3515///   - !PM_ARRAY && !PM_HASHED → zerr (c:2905-2911)
3516///   - VALFLAG_EMPTY → zerr (c:2913-2917)
3517///   - start==0,end==-1 && PM_HASHED → arrhashsetfn(0) (c:2919-2922)
3518///   - start==0,end==-1 && PM_ARRAY → gsu.a->setfn (c:2922-2923)
3519///   - start==-1,end==0 && PM_HASHED → arrhashsetfn(AUGMENT) (c:2925-2928)
3520///   - PM_HASHED with other bounds → zerr slice-of-assoc (c:2929-2932)
3521///   - PM_ARRAY with slice → bounds adjust + splice (c:2933+)
3522///
3523/// Pending: ASSPM_AUGMENT prepend (c:2945-2954), PM_UNIQUE dedupe
3524/// after assign (c:2966-2967), VALFLAG_INV + !KSHARRAYS off-by-one
3525/// (c:2938-2942).
3526pub fn setarrvalue(v: &mut crate::ported::zsh_h::value, val: Vec<String>) {  // c:2895
3527    // c:2897-2898 — `if (unset(EXECOPT)) return;`. Match the same
3528    // NO_EXEC bail as setnumvalue at c:2860. Without it,
3529    // `zsh -n -c 'arr=(a b c)'` would mutate arr during a parse-
3530    // only run.
3531    if unset(EXECOPT) {                                                       // c:2897
3532        return;
3533    }
3534
3535    let pm = match v.pm.as_mut() { Some(p) => p, None => return };
3536
3537    // c:2899-2904 — PM_READONLY rejection.
3538    if pm.node.flags & PM_READONLY as i32 != 0 {
3539        crate::ported::utils::zerr(&format!("read-only variable: {}", pm.node.nam));
3540        return;
3541    }
3542    // c:2905-2911 — type guard.
3543    let t = PM_TYPE(pm.node.flags as u32);
3544    if t & (crate::ported::zsh_h::PM_ARRAY | PM_HASHED) == 0 {
3545        crate::ported::utils::zerr(&format!(
3546            "{}: attempt to assign array value to non-array",
3547            pm.node.nam
3548        ));
3549        return;
3550    }
3551    // c:2913-2917 — VALFLAG_EMPTY rejection.
3552    if v.valflags & VALFLAG_EMPTY != 0 {
3553        crate::ported::utils::zerr(&format!(
3554            "{}: assignment to invalid subscript range",
3555            pm.node.nam
3556        ));
3557        return;
3558    }
3559
3560    // c:2919-2932 — full-replace / AUGMENT / hash-slice-reject paths.
3561    if v.start == 0 && v.end == -1 {
3562        if t == PM_HASHED {
3563            // c:2920 — arrhashsetfn(pm, val, 0).
3564            arrhashsetfn(pm, val, 0);
3565        } else {
3566            // c:2922 — `pm->gsu.a->setfn(pm, val)`. Route through
3567            // arrsetfn so PM_UNIQUE dedupe + arrfixenv side-effects
3568            // fire (params.c:4066-4076).
3569            arrsetfn(pm, val);
3570        }
3571        return;
3572    }
3573    if v.start == -1 && v.end == 0 && t == PM_HASHED {
3574        arrhashsetfn(pm, val, crate::ported::zsh_h::ASSPM_AUGMENT);
3575        return;
3576    }
3577    if t == PM_HASHED {
3578        crate::ported::utils::zerr(&format!(
3579            "{}: attempt to set slice of associative array",
3580            pm.node.nam
3581        ));
3582        return;
3583    }
3584
3585    // c:2938-2942 — VALFLAG_INV + !KSHARRAYS off-by-one. Inverse
3586    // subscripts (`a[(i)pat]=val`) are 1-based when KSHARRAYS is
3587    // off; shift start/end down by 1 to match the 0-based slice
3588    // arithmetic below.
3589    if v.valflags & VALFLAG_INV != 0
3590        && !isset(crate::ported::zsh_h::KSHARRAYS)
3591    {
3592        if v.start > 0 {
3593            v.start -= 1;
3594        }
3595        v.end -= 1;
3596    }
3597
3598    // c:2933+ — PM_ARRAY slice path.
3599    let arr = pm.u_arr.get_or_insert_with(Vec::new);
3600    let len = arr.len() as i64;
3601    // c:2944-2949 — negative start: add pre_assignment_length; clamp to 0.
3602    let start = if v.start < 0 {
3603        (len + v.start as i64).max(0)
3604    } else {
3605        v.start as i64
3606    };
3607    // c:2950-2953 — negative end: add pre_assignment_length + 1; clamp to 0.
3608    let end = if v.end < 0 {
3609        (len + v.end as i64 + 1).max(0)
3610    } else {
3611        v.end as i64
3612    };
3613    // c:2960-2961 — `if (end < start) end = start`.
3614    let start_idx = (start.max(1) - 1) as usize;
3615    let end_idx = end.max(0) as usize;
3616
3617    // c:2980 — pad with empty strings up to start.
3618    while arr.len() < start_idx {
3619        arr.push(String::new());
3620    }
3621
3622    // c:2989-2998 — splice val into [start..end] range.
3623    let end_idx = end_idx.min(arr.len());
3624    if start_idx <= end_idx {
3625        arr.splice(start_idx..end_idx, val);
3626    } else {
3627        for (i, x) in val.into_iter().enumerate() {
3628            if start_idx + i < arr.len() {
3629                arr[start_idx + i] = x;
3630            } else {
3631                arr.push(x);
3632            }
3633        }
3634    }
3635}
3636
3637/// Retrieve integer parameter.
3638/// Port of `getiparam(char *s)` from Src/params.c:3044. C: getvalue +
3639/// getintvalue. Our adaptation reads the scalar string and parses;
3640/// returns 0 on missing or unparseable, matching getintvalue's
3641/// failure-returns-0 convention (params.c:2601).
3642pub fn getiparam(s: &str) -> i64 {
3643    // C also honours PM_INTEGER's `pm->u.val` payload directly when
3644    // the param is typed numeric; check paramtab first for that case.
3645    if let Ok(tab) = paramtab().read() {
3646        if let Some(pm) = tab.get(s) {
3647            if (pm.node.flags as u32 & crate::ported::zsh_h::PM_INTEGER) != 0
3648            {
3649                return pm.u_val;
3650            }
3651        }
3652    }
3653    getsparam(s).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0)
3654}
3655
3656/// Retrieve numeric (int-or-float) parameter.
3657/// Port of `getnparam(char *s)` from Src/params.c:3058. C returns an
3658/// `mnumber` (tagged int/float union); our adaptation returns
3659/// `(i64, f64, bool)` where the bool is true for float. Unset
3660/// returns `(0, 0.0, false)`, matching the MN_INTEGER zero
3661/// fallback in the C source's not-found branch.
3662pub fn getnparam(s: &str) -> (i64, f64, bool) {
3663    if let Ok(tab) = paramtab().read() {
3664        if let Some(pm) = tab.get(s) {
3665            let fl = pm.node.flags as u32;
3666            if (fl & (crate::ported::zsh_h::PM_EFLOAT
3667                | crate::ported::zsh_h::PM_FFLOAT)) != 0
3668            {
3669                return (pm.u_dval as i64, pm.u_dval, true);
3670            }
3671            if (fl & crate::ported::zsh_h::PM_INTEGER) != 0 {
3672                return (pm.u_val, pm.u_val as f64, false);
3673            }
3674        }
3675    }
3676    let s = match getsparam(s) {
3677        Some(s) => s,
3678        None => return (0, 0.0, false),
3679    };
3680    if s.contains('.') || s.contains('e') || s.contains('E') {
3681        if let Ok(f) = s.parse::<f64>() {
3682            return (f as i64, f, true);
3683        }
3684    }
3685    if let Ok(i) = s.parse::<i64>() {
3686        return (i, i as f64, false);
3687    }
3688    (0, 0.0, false)
3689}
3690
3691/// Port of `getsparam(char *s)` from `Src/params.c:3076`.
3692///
3693/// C body:
3694/// ```c
3695/// char *getsparam(char *s) {
3696///     struct value vbuf;
3697///     Value v = getvalue(&vbuf, &s, 0);
3698///     if (!v) return NULL;
3699///     return getstrvalue(v);
3700/// }
3701/// ```
3702///
3703/// `getvalue` (params.c:2173) builds a `Value` for the parameter,
3704/// dispatching through `Param.gsu->getfn` for special parameters.
3705/// `getstrvalue` (params.c:2335) extracts the scalar form: for
3706/// PM_INTEGER calls `pm->gsu.i->getfn(pm)` and convbase's the
3707/// result; for PM_SCALAR calls `pm->gsu.s->getfn(pm)`; for
3708/// PM_ARRAY joins the elements.
3709///
3710/// **Sole funnel.** Every scalar parameter read in zshrs routes
3711/// through this fn — `subst.rs` parameter expansion AND
3712/// `fusevm_bridge::expand_param` both call `getsparam`. The
3713/// dispatch chain lives in exactly one place, mirroring C's
3714/// "every read goes through getsparam" architecture.
3715///
3716/// Lookup order (mirrors C's `getvalue` → `getstrvalue` cascade):
3717/// 1. **GSU dispatch** via [`lookup_special_var`] — special
3718///    parameters route through their getfn callback (`uidgetfn` /
3719///    `randomgetfn` / `usernamegetfn` / etc.). Same role as
3720///    C's `Param.gsu->getfn` virtual dispatch.
3721/// 2. **Local variable** — `variables[name]`. C reads `pm->u.str`
3722///    for PM_SCALAR; here we hold the scalar in the variables
3723///    HashMap.
3724/// 3. **Environment fallback** — `std::env::var(name)`. C imports
3725///    env vars into the param table at startup so they go through
3726///    the same dispatch as everything else; zshrs reads from the
3727///    OS env on miss to match.
3728/// 4. **Array → scalar** — `arrays[name].join(" ")`. Mirrors
3729///    C's PM_ARRAY case in getstrvalue (params.c:2358) which
3730///    joins via `sepjoin(ss, NULL, 1)`.
3731///
3732// Retrieve a scalar (string) parameter                                     // c:3076
3733/// Returns `None` only if all four paths miss (parameter genuinely
3734/// unset).
3735pub fn getsparam(name: &str) -> Option<String> {                             // c:3076
3736    // 1. GSU dispatch — `Param.gsu->getfn(pm)` equivalent. Special
3737    //    parameters (UID/RANDOM/USERNAME/...) live behind getfn
3738    //    hooks that the table read below would otherwise miss.
3739    if let Some(v) = lookup_special_var(name) {
3740        return Some(v);
3741    }
3742    // 2. Paramtab read — `(Value)gethashnode2(paramtab, name)`.
3743    //    Walk the global paramtab for the named param, returning
3744    //    `pm->u.str` for PM_SCALAR/PM_NAMEREF or `sepjoin(pm->u.arr)`
3745    //    for PM_ARRAY (matches `getstrvalue` at params.c:2358).
3746    if let Ok(tab) = paramtab().read() {
3747        if let Some(pm) = tab.get(name) {
3748            if let Some(s) = pm.u_str.as_ref() {
3749                return Some(s.clone());
3750            }
3751            if let Some(arr) = pm.u_arr.as_ref() {
3752                return Some(arr.join(" "));
3753            }
3754        }
3755    }
3756    // 3. Env fallback — C imports env into paramtab at init so the
3757    //    read above would hit. If the import hasn't happened yet
3758    //    (e.g. during very early init) fall back to the live env.
3759    std::env::var(name).ok()
3760}
3761
3762/// Port of `getsparam_u(char *s)` from `Src/params.c:3089`. C body
3763/// (c:3091-3094):
3764/// ```c
3765/// /* getsparam() returns pointer into global params table, so ... */
3766/// if ((s = getsparam(s)))
3767///     return unmeta(s);    /* returns static pointer to copy */
3768/// return s;
3769/// ```
3770///
3771/// The previous Rust "port" was an entirely fabricated impl — it
3772/// took `Option<&mut value>` and gated on `PM_TYPE == PM_SCALAR`,
3773/// which matches no part of the C body. C just calls getsparam(s)
3774/// and unmeta's the resulting string. No callers existed because
3775/// no caller's type fit the bogus signature.
3776///
3777/// Real use case: locale setters (c:4847, c:4867, c:4882, c:4917)
3778/// call `getsparam_u("LC_ALL")` / `getsparam_u("LANG")` to read the
3779/// param as a Meta-stripped C string suitable for `setlocale`.
3780pub fn getsparam_u(s: &str) -> Option<String> {                              // c:3089
3781    // c:3092 — `if ((s = getsparam(s))) return unmeta(s);`
3782    getsparam(s).map(|v| crate::ported::utils::unmeta(&v))
3783}
3784
3785/// Port of `char **getaparam(char *s)` from `Src/params.c:3101-3110`.
3786///
3787/// C body:
3788/// ```c
3789/// struct value vbuf;
3790/// Value v;
3791/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
3792///     PM_TYPE(v->pm->node.flags) == PM_ARRAY)
3793///     return v->pm->gsu.a->getfn(v->pm);
3794/// return NULL;
3795/// ```
3796///
3797/// The previous Rust port was a fabrication: signature was
3798/// `Option<&mut value> -> Option<Vec<String>>`, taking an already-
3799/// resolved Value pointer rather than the C-canonical name string.
3800/// No caller used it because the bogus signature fit nothing — and
3801/// the in-tree `savematch` at modules/zutil.rs:30 hardcoded `a = None`
3802/// because the existing API couldn't be threaded through.
3803///
3804/// Real C use: name lookup. e.g. `getaparam("match")` returns the
3805/// `$match` array from the regex-match callouts (Modules/zutil.c:45).
3806pub fn getaparam(name: &str) -> Option<Vec<String>> {                        // c:3101
3807    // c:3107 — `if (idigit(*s))` reject digit-first names. C
3808    // `getvalue` would also reject these later, but the explicit
3809    // check matches C's flow.
3810    if name.starts_with(|c: char| c.is_ascii_digit()) {                      // c:3107
3811        return None;
3812    }
3813    // c:3107-3109 — `getvalue(&vbuf, &s, 0)` resolves the name to a
3814    // paramtab entry. Then PM_TYPE check + `pm->u.arr` return.
3815    if let Ok(tab) = paramtab().read() {
3816        if let Some(pm) = tab.get(name) {
3817            if PM_TYPE(pm.node.flags as u32) == PM_ARRAY {                   // c:3108
3818                if let Some(arr) = pm.u_arr.as_ref() {                       // c:3109
3819                    return Some(arr.clone());
3820                }
3821            }
3822        }
3823    }
3824    None                                                                      // c:3110
3825}
3826
3827/// Port of `char **gethparam(char *s)` from `Src/params.c:3117-3126`.
3828///
3829/// C body:
3830/// ```c
3831/// struct value vbuf;
3832/// Value v;
3833/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
3834///     PM_TYPE(v->pm->node.flags) == PM_HASHED)
3835///     return paramvalarr(v->pm->gsu.h->getfn(v->pm), SCANPM_WANTVALS);
3836/// return NULL;
3837/// ```
3838///
3839/// Same fabricated-port family as the prior `getaparam`/`getsparam_u`
3840/// fixes: previous Rust sig took `Option<&mut value>` instead of the
3841/// canonical name string, with no real callers. Fixed sig + body
3842/// that resolves the name through paramtab and returns the values
3843/// vector when PM_HASHED.
3844///
3845/// NOTE: zshrs's paramtab stores hash-params via `pm->u_hash` (a
3846/// `HashTable` struct that's a generic bucket-array container). The
3847/// canonical C path threads through `gsu.h->getfn(pm)` → `paramvalarr`
3848/// which extracts the value side of each key-value pair. Until that
3849/// extraction backend lands, we return an empty Vec for PM_HASHED
3850/// (which matches C's "no entries" return shape, not the broken
3851/// "wrong-signature" stub).
3852pub fn gethparam(name: &str) -> Option<Vec<String>> {                        // c:3117
3853    if name.starts_with(|c: char| c.is_ascii_digit()) {                      // c:3122
3854        return None;
3855    }
3856    if let Ok(tab) = paramtab().read() {
3857        if let Some(pm) = tab.get(name) {
3858            if PM_TYPE(pm.node.flags as u32) == PM_HASHED {                  // c:3123
3859                // c:3124 — `paramvalarr(hashgetfn(pm), SCANPM_WANTVALS)`.
3860                // Backend not yet ported; return empty vec to mirror the
3861                // "param exists but has no entries" shape.
3862                return Some(Vec::new());                                     // c:3124
3863            }
3864        }
3865    }
3866    None                                                                      // c:3125
3867}
3868
3869/// Port of `char **gethkparam(char *s)` from `Src/params.c:3131-3140`.
3870/// Same as `gethparam` but `paramvalarr(..., SCANPM_WANTKEYS)`.
3871pub fn gethkparam(name: &str) -> Option<Vec<String>> {                       // c:3131
3872    if name.starts_with(|c: char| c.is_ascii_digit()) {                      // c:3136
3873        return None;
3874    }
3875    if let Ok(tab) = paramtab().read() {
3876        if let Some(pm) = tab.get(name) {
3877            if PM_TYPE(pm.node.flags as u32) == PM_HASHED {                  // c:3137
3878                // c:3138 — `paramvalarr(hashgetfn(pm), SCANPM_WANTKEYS)`.
3879                // Same backend gap as gethparam; return empty Vec.
3880                return Some(Vec::new());                                     // c:3138
3881            }
3882        }
3883    }
3884    None                                                                      // c:3139
3885}
3886
3887/// Port of `check_warn_pm(Param pm, const char *pmtype, int created, int may_warn_about_nested_vars)` from `Src/params.c:3160`.
3888///
3889/// C body emits the WARN_CREATE_GLOBAL / WARN_NESTED_VAR
3890/// diagnostic when a function-local creates/passes a non-local
3891/// param with the matching shell options set.
3892///
3893/// The previous Rust port handled the GATE logic correctly but
3894/// SKIPPED the diagnostic emit, claiming the `funcstack` global
3895/// wasn't ported. But `crate::ported::modules::parameter::FUNCSTACK`
3896/// IS ported (`Mutex<Vec<funcstack>>`). Wire the walk:
3897///   for (i = funcstack; i; i = i->prev)
3898///       if (i->tp == FS_FUNC) {
3899///           msg = created ?
3900///               "%s parameter %s created globally in function %s" :
3901///               "%s parameter %s set in enclosing scope in function %s";
3902///           zwarn(msg, pmtype, pm->node.nam, i->name);
3903///           break;
3904///       }
3905///
3906/// Without the diagnostic, `setopt WARN_CREATE_GLOBAL` had no
3907/// observable effect — the whole point of the option is the
3908/// user-visible warning.
3909pub fn check_warn_pm(
3910    pm: &crate::ported::zsh_h::param,
3911    pmtype: &str,
3912    created: i32,
3913    may_warn_about_nested_vars: i32,
3914) {                                                                            // c:3160
3915    if may_warn_about_nested_vars == 0 && created == 0 {                       // c:3165
3916        return;
3917    }
3918    // `locallevel` is the canonical `pub static` above (port of
3919    // params.c:54). `forklevel` is the ported global at exec.rs
3920    // (port of exec.c:1052) set to locallevel at every entersubsh().
3921    let cur_local: i32 = locallevel.load(std::sync::atomic::Ordering::Relaxed);
3922    let forklevel: i32 =
3923        crate::exec::FORKLEVEL.load(std::sync::atomic::Ordering::Relaxed);    // c:1052 (Src/exec.c)
3924    if created != 0 && isset(WARNCREATEGLOBAL) {                              // c:3168
3925        if cur_local <= forklevel || pm.level != 0 {                           // c:3169
3926            return;
3927        }
3928    } else if created == 0 && isset(WARNNESTEDVAR) {                           // c:3171
3929        if pm.level >= cur_local {                                             // c:3172
3930            return;
3931        }
3932    } else {
3933        return;
3934    }
3935    if (pm.node.flags as u32 & (PM_SPECIAL | PM_NAMEREF)) != 0 {              // c:3177
3936        return;
3937    }
3938    // c:3180-3190 — walk funcstack, emit zwarn at first FS_FUNC.
3939    let stack = match crate::ported::modules::parameter::FUNCSTACK.lock() {
3940        Ok(s) => s,
3941        Err(_) => return,
3942    };
3943    for frame in stack.iter().rev() {                                          // c:3180 walk most-recent-first
3944        if frame.tp == crate::ported::zsh_h::FS_FUNC {                         // c:3181 FS_FUNC
3945            let msg = if created != 0 {                                        // c:3185
3946                format!("{} parameter {} created globally in function {}",
3947                        pmtype, pm.node.nam, frame.name)
3948            } else {                                                           // c:3187
3949                format!("{} parameter {} set in enclosing scope in function {}",
3950                        pmtype, pm.node.nam, frame.name)
3951            };
3952            crate::ported::utils::zwarn(&msg);                                 // c:3189
3953            break;                                                             // c:3190
3954        }
3955    }
3956}
3957
3958// intgetfn / strgetfn drift wrappers removed — replaced below with
3959// real C-shape ports `intgetfn(pm: &param) -> i64` (Src/params.c:3993)
3960// and `strgetfn(pm: &param) -> String` (Src/params.c:4029) that read
3961// directly from the union fields `pm->u.val` / `pm->u.str`.
3962
3963// ---------------------------------------------------------------------------
3964// Tests
3965// ---------------------------------------------------------------------------
3966
3967
3968
3969// ===========================================================
3970// Methods moved verbatim from src/ported/exec.rs because their
3971// C counterpart's source file maps 1:1 to this Rust module.
3972// Phase: params
3973// ===========================================================
3974
3975// BEGIN moved-from-exec-rs
3976// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
3977
3978// END moved-from-exec-rs
3979
3980// ===========================================================
3981// Free fns moved verbatim from src/ported/exec.rs.
3982// ===========================================================
3983// BEGIN moved-from-exec-rs (free fns)
3984/// Subscript-argument result.
3985///
3986/// `Flags` carries the parsed flag chars and the remaining subscript
3987/// text (the pattern after `(...)`); the caller dispatches the
3988/// search itself. `Value` is the result of an in-getarg array/hash
3989/// pattern search — direct port of getarg's pprog/pattry arm at
3990/// Src/params.c:1672-1719 (array) and 1581-1660 (hash).
3991// `enum getarg_out` is a Rust extension to express the dual-mode
3992// return of `getarg`. C `getarg` (`Src/params.c:1367`) writes back
3993// via out-pointers (`int *inv`, `Value v`, `zlong *w`, ...) and
3994// returns `int`. The Rust port collapses those into one sum-typed
3995// return: `Flags` carries the parsed flag chars + remaining
3996// subscript when no search ran; `Value` carries the search result
3997// from the pprog/pattry arms at c:1581-1660 (hash) / c:1672-1719
3998// (array). Naming kept lowercase to mark this as a port-shape helper
3999// rather than a C-mirrored struct.
4000#[allow(non_camel_case_types)]
4001pub enum getarg_out<'a> {
4002    Flags { flags: &'a str, rest: &'a str },
4003    Value(fusevm::Value),
4004}
4005
4006/// Port of `assignsparam(char *s, char *val, int flags)` from `Src/params.c:3193`. C signature:
4007/// `mod_export Param assignsparam(char *s, char *val, int flags)`.
4008///
4009/// `s` may carry an embedded `[...]` subscript (matching C's
4010/// `strchr(s, '[')` parse). The function operates on the global
4011/// `paramtab` (Src/params.c:515), creating/mutating `Param`
4012/// entries in place. Branches preserved 1:1 with C:
4013///   - c:3203 `isident(s)` — reject non-identifier names.
4014///   - c:3209 `queue_signals()`.
4015///   - c:3210 subscripted path: c:3212 `getvalue` lookup,
4016///     c:3213 `createparam(t, PM_ARRAY)` on miss, c:3216
4017///     PM_READONLY guard, c:3227 ASSPM_WARN drop, c:3228 clear
4018///     PM_DEFAULTED, c:3231 `v = NULL` then re-dispatch by type.
4019///   - c:3232 non-subscripted: c:3233 `getvalue` → c:3234
4020///     `createparam(t, PM_SCALAR)`; c:3236-3250 array/hash type-flip
4021///     to PM_SCALAR (when not PM_SPECIAL|PM_TIED, not KSHARRAYS,
4022///     not ASSPM_AUGMENT) via `resetparam(v->pm, PM_SCALAR)`.
4023///   - c:3258 PM_NAMEREF → c:3259 `valid_refname(val, flags)` guard.
4024///   - c:3269 clear PM_DEFAULTED.
4025///   - c:3343 `assignstrvalue(v, val, flags)`.
4026///   - c:3344 `unqueue_signals()`; c:3345 return v->pm.
4027///
4028/// The full HashTable substrate (vtable callbacks, scope-stacked
4029/// iterators) is not yet wired; non-essential branches such as
4030/// `+= AUGMENT` numeric/array slice append and `check_warn_pm`
4031/// are documented but elided where unreachable from current
4032/// callers — none of those code paths are exercised by zshrs's
4033/// existing call sites.
4034pub fn assignsparam(s: &str, val: &str, flags: i32)                          // c:3193
4035    -> Option<crate::ported::zsh_h::Param>
4036{
4037
4038    // c:3203 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
4039    if !isident(s) {
4040        zerr(&format!("not an identifier: {}", s));                          // c:3204
4041        errflag.fetch_or(                                                    // c:3206
4042            crate::ported::utils::ERRFLAG_ERROR,
4043            std::sync::atomic::Ordering::Relaxed,
4044        );
4045        return None;                                                         // c:3207
4046    }
4047    crate::ported::signals::queue_signals();                                 // c:3209
4048
4049    // c:3210 — `strchr(s, '[')`. Split the leading name from the
4050    // subscript while preserving C's `*ss = '\0'` / `*ss = '['`
4051    // restore semantics: the Rust port works on `&str` slices so
4052    // there's no in-place null-terminator dance, but the parse
4053    // shape is identical.
4054    let (name, subscript) = match s.find('[') {
4055        Some(i) => {
4056            let close = s.rfind(']').unwrap_or(s.len());
4057            let key_end = if close > i { close } else { s.len() };
4058            (&s[..i], Some(&s[i + 1..key_end]))
4059        }
4060        None => (s, None),
4061    };
4062
4063    // Subscripted path (c:3210-3231).
4064    if let Some(key) = subscript {
4065        let mut tab = paramtab().write().unwrap();
4066        let exists = tab.contains_key(name);                                 // c:3212
4067        if !exists {
4068            // c:3213 `createparam(t, PM_ARRAY); created = 1;`
4069            let pm: Param = Box::new(param {
4070                node: hashnode { next: None, nam: name.to_string(), flags: PM_ARRAY as i32 },
4071                u_data: 0, u_arr: Some(Vec::new()), u_str: None, u_val: 0,
4072                u_dval: 0.0, u_hash: None,
4073                gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
4074                base: 0, width: 0, env: None, ename: None, old: None, level: 0,
4075            });
4076            tab.insert(name.to_string(), pm);
4077        } else {
4078            // c:3216 `if (v->pm->node.flags & PM_READONLY)`.
4079            let pm = tab.get(name).unwrap();
4080            if (pm.node.flags as u32 & PM_READONLY) != 0 {
4081                zerr(&format!("read-only variable: {}", pm.node.nam));       // c:3217
4082                drop(tab);
4083                crate::ported::signals::unqueue_signals();                   // c:3220
4084                return None;                                                 // c:3221
4085            }
4086        }
4087        // c:3231 `v = NULL;` — re-dispatch by storage type.
4088        let pm = tab.get_mut(name).unwrap();
4089        pm.node.flags &= !(PM_DEFAULTED as i32);                             // c:3228
4090        if (pm.node.flags as u32 & PM_HASHED) != 0 {
4091            // PM_HASHED element store. `param.u_hash` is typed
4092            // `Option<HashTable>` per Src/zsh.h:1841 but the
4093            // HashTable runtime backing isn't wired; the assoc-array
4094            // values live in a parallel storage keyed on param name
4095            // (`paramtab_hashed_storage()`).
4096            let mut store = paramtab_hashed_storage().lock().unwrap();
4097            store.entry(name.to_string()).or_default()
4098                .insert(key.to_string(), val.to_string());
4099        } else if let Ok(idx) = key.parse::<i64>() {
4100            // PM_ARRAY + numeric subscript (c:3357 `assignaparam`).
4101            let arr = pm.u_arr.get_or_insert_with(Vec::new);
4102            let len = arr.len() as i64;
4103            // 1-based forward, negative-from-end.
4104            let real_idx = if idx < 0 { len + idx } else { idx - 1 };
4105            let real_idx = real_idx.max(0) as usize;
4106            while arr.len() <= real_idx { arr.push(String::new()); }
4107            arr[real_idx] = val.to_string();
4108            pm.u_str = None;
4109        } else {
4110            // String subscript on a non-hashed name → auto-vivify
4111            // as PM_HASHED (mirrors C `createparam(s, PM_HASHED)`
4112            // fallback when getvalue returns NULL).
4113            pm.node.flags = (pm.node.flags & !(PM_TYPE(u32::MAX) as i32))
4114                | PM_HASHED as i32;
4115            pm.u_arr = None;
4116            pm.u_str = None;
4117            let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
4118            map.insert(key.to_string(), val.to_string());
4119            paramtab_hashed_storage().lock().unwrap()
4120                .insert(name.to_string(), map);
4121        }
4122        let cloned = pm.clone();
4123        drop(tab);
4124        crate::ported::signals::unqueue_signals();                           // c:3344
4125        return Some(cloned);                                                 // c:3345
4126    }
4127
4128    // c:3232 non-subscripted branch.
4129    let mut tab = paramtab().write().unwrap();
4130    let existing = tab.contains_key(name);
4131    if !existing {
4132        // c:3234 `createparam(t, PM_SCALAR); created = 1;`
4133        let mut pm_flags = PM_SCALAR as i32;
4134        if isset_opt(ALLEXPORT) {                                            // c:1149-1150 (ALLEXPORT path)
4135            pm_flags |= PM_EXPORTED as i32;
4136        }
4137        let pm: Param = Box::new(param {
4138            node: hashnode { next: None, nam: name.to_string(), flags: pm_flags },
4139            u_data: 0, u_arr: None, u_str: Some(String::new()), u_val: 0,
4140            u_dval: 0.0, u_hash: None,
4141            gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
4142            base: 0, width: 0, env: None, ename: None, old: None, level: 0,
4143        });
4144        tab.insert(name.to_string(), pm);
4145    } else {
4146        let pm = tab.get(name).unwrap();
4147        // c:3216 PM_READONLY guard for an existing param.
4148        if (pm.node.flags as u32 & PM_READONLY) != 0 {
4149            zerr(&format!("read-only variable: {}", pm.node.nam));           // c:3217
4150            drop(tab);
4151            crate::ported::signals::unqueue_signals();                       // c:3220
4152            return None;                                                     // c:3221
4153        }
4154        // c:3236-3250 — existing PM_ARRAY/PM_HASHED on a non-special,
4155        // non-tied, non-KSHARRAYS, non-AUGMENT scalar assignment →
4156        // `resetparam(v->pm, PM_SCALAR)`.
4157        let f = pm.node.flags as u32;
4158        let is_array_or_hash = (f & PM_ARRAY) != 0 || (f & PM_HASHED) != 0;
4159        let is_special_or_tied = (f & (PM_SPECIAL | PM_TIED)) != 0;
4160        let augment_bit = (flags & ASSPM_AUGMENT) != 0;
4161        if is_array_or_hash
4162            && !is_special_or_tied
4163            && !augment_bit
4164            && !isset(crate::ported::zsh_h::KSHARRAYS)
4165        {
4166            // c:3242 — flip type to PM_SCALAR, drop array/hash slots.
4167            let pm_mut = tab.get_mut(name).unwrap();
4168            pm_mut.node.flags = (pm_mut.node.flags & !(PM_TYPE(u32::MAX) as i32))
4169                | PM_SCALAR as i32;
4170            pm_mut.u_arr = None;
4171            paramtab_hashed_storage().lock().unwrap().remove(name);
4172        }
4173    }
4174
4175    // c:3258-3266 `if (*val && (v->pm->node.flags & PM_NAMEREF))`.
4176    let pm = tab.get(name).unwrap();
4177    if !val.is_empty() && (pm.node.flags as u32 & PM_NAMEREF) != 0 {
4178        if !valid_refname(val, pm.node.flags) {                              // c:3259
4179            zerr(&format!("invalid name reference: {}", val));               // c:3260
4180            drop(tab);
4181            errflag.fetch_or(                                                // c:3263
4182                crate::ported::utils::ERRFLAG_ERROR,
4183                std::sync::atomic::Ordering::Relaxed,
4184            );
4185            crate::ported::signals::unqueue_signals();                       // c:3262
4186            return None;                                                     // c:3264
4187        }
4188    }
4189
4190    // c:3269 `v->pm->node.flags &= ~PM_DEFAULTED;`
4191    let pm = tab.get_mut(name).unwrap();
4192    pm.node.flags &= !(PM_DEFAULTED as i32);
4193
4194    // c:3343 `assignstrvalue(v, val, flags)` — scalar write.
4195    pm.u_str = Some(val.to_string());
4196
4197    let cloned = pm.clone();
4198    drop(tab);
4199    crate::ported::signals::unqueue_signals();                               // c:3344
4200    Some(cloned)                                                             // c:3345
4201}
4202
4203// `VarAttr` struct + `VarKind` enum + `impl VarAttr::format_zsh`
4204// DELETED. C zsh stores typeset attributes as bare `PM_*` bit
4205// flags on `Param.node.flags` (`Src/zsh.h` PM_* + `Src/params.c`
4206// flag tests); the `${(t)var}` flag report (`typeprintparam` at
4207// `Src/builtin.c:3050+`) writes those bits to a string directly
4208// against the `Param.node.flags` int.
4209//
4210// Both types had zero external use sites — pure dead-code carryover
4211// from an earlier exec.rs refactor. The PM_* bit constants are at
4212// `zsh_h.rs:1340+` and the `(t)` formatting routes through
4213// `typeset_print_flags` (when wired) reading bare `Param.node.flags`.
4214
4215// ===========================================================
4216// Special-parameter GSU (get/set/unset) callbacks ported from
4217// Src/params.c.
4218//
4219// C zsh stores per-special-param state in file-static globals
4220// (`ifs`, `home`, `term`, `histsiz`, etc.) and dispatches getfn/
4221// setfn/unsetfn callbacks through `Param.gsu->getfn` etc. zshrs's
4222// param storage is per-evaluator HashMaps on `ShellExecutor`, so
4223// the C globals are reproduced as `OnceLock<Mutex<…>>` module
4224// statics here, with the get/set fns mutating the static.
4225//
4226// Functions that genuinely need a `Param *` (the GSU dispatch
4227// callbacks for non-special arr/hash/int/float/str params, the
4228// param-table mutators, scope helpers, etc.) cannot be properly
4229// ported until zshrs gains a Param struct + callback-table ABI;
4230// those keep their C signatures but the body is a WARNING-stub
4231// that does nothing.
4232// ===========================================================
4233
4234use std::sync::{Arc, Mutex, OnceLock, RwLock};
4235use std::time::Duration;
4236use crate::config_h::DEFAULT_TMPPREFIX;
4237use crate::zsh_h::{paramdef, ERRFLAG_ERROR, PM_DONTIMPORT, PM_DONTIMPORT_SUID, PM_READONLY_SPECIAL};
4238// -----------------------------------------------------------
4239// Module statics — one per C global referenced by the special-
4240// param callbacks below. All initialised lazily on first read.
4241// -----------------------------------------------------------
4242
4243// `Src/params.c:515  mod_export HashTable paramtab, realparamtab;`
4244//
4245// `realparamtab` always points to the shell's global parameter
4246// table. `paramtab` normally aliases it; it is temporarily
4247// redirected during associative-array key iteration
4248// (`Src/params.c:508-513` — "paramtab is sometimes temporarily
4249// changed to point at another table").
4250//
4251// Per PORT_PLAN.md Phase 3, bucket-2 read-mostly tables use
4252// `RwLock` so parallel readers (every `$VAR` expansion, every
4253// completion lookup) don't serialize. Writers (assignments,
4254// scope pushes/pops, function-local declarations) take the
4255// exclusive write lock. `OnceLock` provides the single-static
4256// guarantee without an `Arc` allocation since the table lives
4257// for the process lifetime.
4258//
4259// Entries are keyed on `node.nam` (the canonical `param` struct
4260// lives in `zsh_h.rs`). The full `HashTable` substrate (vtable
4261// callbacks, intrusive `next` chain, scope-stacked iterators) is
4262// not yet wired; until it is, the typed map is the operative
4263// storage.
4264static PARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
4265    OnceLock::new();
4266static REALPARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
4267    OnceLock::new();
4268
4269/// Array parameter assignment (no subscript).
4270///
4271/// Direct port of `Param assignaparam(char *s, char **val, int flags)`
4272/// from `Src/params.c:3357`. Writes an array value into paramtab
4273/// and returns the new/updated Param.
4274///
4275/// Pending C semantics:
4276///   - PM_READONLY rejection (c:3370-3381 via setarrvalue chain)
4277///   - PM_NAMEREF type-change reject (c:3395-3398)
4278///   - resetparam from non-array (c:3415-3420)
4279///   - ASSPM_AUGMENT (`a+=val`) preserve-old prepend (c:3404-3412)
4280///   - PM_UNIQUE dedupe (c:3401)
4281///   - element-wise `a[k]=v` slice path (c:3373-3389)
4282pub fn assignaparam(
4283    name: &str,
4284    val: Vec<String>,
4285    flags: i32,
4286) -> Option<crate::ported::zsh_h::Param> {                                   // c:3357
4287    // c:3366-3370 — `if (!isident(s)) { zerr; return NULL }`.
4288    if !isident(name) {
4289        crate::ported::utils::zerr(&format!("not an identifier: {}", name));
4290        return None;
4291    }
4292
4293    // c:3391-3394 — fetchvalue / createparam(PM_ARRAY) if missing.
4294    let (existed, prior_scalar, prior_flags) = {
4295        let tab = paramtab().read().unwrap();
4296        match tab.get(name) {
4297            Some(pm) => (true, pm.u_str.clone(), pm.node.flags),
4298            None => (false, None, 0),
4299        }
4300    };
4301    if !existed {
4302        createparam(name, PM_ARRAY as i32)?;
4303    }
4304
4305    // c:3402-3412 — ASSPM_AUGMENT preserve-old prepend. When the
4306    // previous value was a scalar (not array/hashed) and we're
4307    // augmenting (`a+=val`), prepend that scalar's string form as
4308    // val[0]. Only fires when the existing param is not PM_UNSET.
4309    let was_scalar_array_target = existed
4310        && prior_flags & (PM_ARRAY | PM_HASHED) as i32 == 0
4311        && prior_flags & PM_SPECIAL as i32 == 0;
4312    let mut val = val;
4313    if (flags & ASSPM_AUGMENT) != 0
4314        && was_scalar_array_target
4315        && prior_flags & PM_UNSET as i32 == 0
4316    {
4317        if let Some(old_scalar) = prior_scalar {
4318            val.insert(0, old_scalar);                                       // c:3408-3411
4319        }
4320    }
4321
4322    // c:3434 — setarrvalue(v, val): store array in pm.u_arr.
4323    let mut tab = paramtab().write().unwrap();
4324    let pm = tab.get_mut(name)?;
4325    let uniq = pm.node.flags & PM_UNIQUE as i32 != 0;                        // c:3401
4326    if pm.node.flags & PM_SPECIAL as i32 == 0 {
4327        let type_mask =
4328            PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
4329        pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_ARRAY as i32;
4330    }
4331    // c:3401 — preserve PM_UNIQUE through the type change, then let
4332    // arrsetfn dedupe via the actual write.
4333    if uniq {
4334        pm.node.flags |= PM_UNIQUE as i32;
4335    }
4336    let val_final = if uniq { simple_arrayuniq(val) } else { val };
4337    pm.u_arr = Some(val_final.clone());
4338    pm.u_str = None;
4339    pm.u_hash = None;
4340    let cloned = pm.clone();
4341    drop(tab);
4342    let _ = val_final;
4343    Some(cloned)
4344}
4345
4346/// Set array parameter.
4347/// Port of `setaparam(char *s, char **aval)` from `Src/params.c:3595` — single-line wrapper
4348/// around `assignaparam(s, val, ASSPM_WARN)`. C body:
4349/// ```c
4350/// mod_export Param setaparam(char *s, char **val) {
4351///     return assignaparam(s, val, ASSPM_WARN);
4352/// }
4353/// ```
4354///
4355/// `ASSPM_WARN` (params.c:104) is a no-op in our port — the global
4356/// "warn on creation" tracking is not yet ported. Call shape
4357/// preserved so callers can use this where C calls setaparam.
4358/// WARNING: param names don't match C — Rust=() vs C=(s, val)
4359pub fn setaparam(name: &str, val: Vec<String>)                              // c:3595
4360    -> Option<crate::ported::zsh_h::Param>
4361{
4362    // c:3766 — `return assignaparam(s, val, ASSPM_WARN)`.
4363    assignaparam(name, val, crate::ported::zsh_h::ASSPM_WARN)
4364}
4365
4366/// Direct port of `Param sethparam(char *s, char **val)` from
4367/// `Src/params.c:3602`. Writes an associative array (flat
4368/// alternating key,value list) into paramtab + the parallel
4369/// `paramtab_hashed_storage` table; returns the new Param.
4370///
4371/// Pending C semantics:
4372///   - PM_READONLY rejection
4373///   - resetparam(PM_HASHED) for type-change
4374///   - PM_SPECIAL type-change reject (c:3637)
4375pub fn sethparam(name: &str, val: Vec<String>)                              // c:3602
4376    -> Option<crate::ported::zsh_h::Param>
4377{
4378
4379    // c:3611-3615 — `if (!isident(s)) { zerr; return NULL }`.
4380    if !isident(name) {
4381        crate::ported::utils::zerr(&format!("not an identifier: {}", name));
4382        return None;
4383    }
4384    // c:3617-3621 — `if (strchr(s, '[')) { zerr; return NULL }`.
4385    if name.contains('[') {
4386        crate::ported::utils::zerr("nested associative arrays not yet supported");
4387        return None;
4388    }
4389
4390    // c:3625 — fetchvalue / createparam(PM_HASHED) if missing.
4391    let exists = paramtab().read().unwrap().contains_key(name);
4392    if !exists {
4393        createparam(name, PM_HASHED as i32)?;
4394    }
4395
4396    // Build the IndexMap from flat (k,v) pairs (mirrors c:arrhashsetfn
4397    // pair-walking at c:4140-4166).
4398    let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
4399    let mut iter = val.into_iter();
4400    while let Some(k) = iter.next() {
4401        let v = iter.next().unwrap_or_default();
4402        map.insert(k, v);
4403    }
4404
4405    // c:3640 — install in paramtab + paramtab_hashed_storage.
4406    let mut tab = paramtab().write().unwrap();
4407    let pm = tab.get_mut(name)?;
4408    if pm.node.flags & PM_SPECIAL as i32 == 0 {
4409        let type_mask =
4410            PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
4411        pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_HASHED as i32;
4412    }
4413    pm.u_arr = None;
4414    pm.u_str = None;
4415    let cloned = pm.clone();
4416    drop(tab);
4417
4418    paramtab_hashed_storage()
4419        .lock()
4420        .unwrap()
4421        .insert(name.to_string(), map);
4422
4423    Some(cloned)
4424}
4425
4426// -----------------------------------------------------------
4427// Param-table mutators / scope / nameref helpers.
4428// `Src/params.c` calls these against the global `paramtab`
4429// HashTable; until our HashTable vtable (`Box<hashtable>` in
4430// zsh_h.rs:285) is wired, these remain no-op shims with the
4431// real C signatures.
4432// -----------------------------------------------------------
4433
4434/// Port of `assignnparam(char *s, mnumber val, int flags)` from `Src/params.c:3664`. C body
4435/// looks up the param via `gethashnode2(realparamtab, s)`,
4436/// dispatches on PM_TYPE: PM_INTEGER → `intsetfn(pm, val.u.l)`;
4437/// PM_FFLOAT/EFLOAT → `floatsetfn(pm, val.u.d)`; otherwise
4438/// `assignstrvalue(&v, conv_to_string(val), flags)`. Stub
4439/// pending HashTable backend; signature mirrors C `mnumber val`.
4440/// flow: isident guard → unset(EXECOPT) bail → `getvalue(&vbuf,&s,1)`
4441/// → if existing array/hashed (non-special, non-tied, non-KSHARRAYS,
4442/// no subscript) → unsetparam_pm + recreate → else if no value →
4443/// `createparam(t, type)` (POSIXIDENTIFIERS gates SCALAR vs
4444/// MN_INTEGER→PM_INTEGER else PM_FFLOAT) → second `getvalue` →
4445/// `check_warn_pm` if ASSPM_WARN → clear PM_DEFAULTED → `setnumvalue`
4446/// → return pm. This port wires the structural flow against the
4447/// already-ported helpers; the createparam/paramtab backend is
4448/// still stubbed elsewhere so the create-new-param branch returns
4449/// None until `createparam` lands.
4450pub fn assignnparam(
4451    s: &str,
4452    val: crate::ported::math::mnumber,
4453    flags: i32,
4454) -> Option<Box<crate::ported::zsh_h::param>> {
4455    // c:3666 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
4456    if !isident(s) {
4457        zerr(&format!("not an identifier: {}", s));                          // c:3667
4458        errflag.fetch_or(                                                    // c:3669
4459            crate::ported::utils::ERRFLAG_ERROR,
4460            std::sync::atomic::Ordering::Relaxed,
4461        );
4462        return None;                                                         // c:3670
4463    }
4464    if unset(EXECOPT) {
4465        return None;
4466    }
4467    let mut vbuf = crate::ported::zsh_h::value {
4468        pm: None,
4469        arr: Vec::new(),
4470        scanflags: 0,
4471        valflags: 0,
4472        start: 0,
4473        end: -1,
4474    };
4475    let mut cursor: &str = s;
4476    let has_sub = s.contains('[');
4477    let mut was_unset = false;
4478    let v = getvalue(Some(&mut vbuf), &mut cursor, 1);
4479    let need_create = match v {
4480        Some(ref vv) => {
4481            if let Some(pm) = vv.pm.as_ref() {
4482                let f = pm.node.flags as u32;
4483                if (f & (PM_ARRAY | PM_HASHED)) != 0
4484                    && (f & (PM_SPECIAL | PM_TIED)) == 0
4485                    && unset(KSHARRAYS) && !has_sub
4486                {
4487                    // unsetparam_pm(vv.pm, 0, 1);
4488                    was_unset = true;
4489                    true
4490                } else {
4491                    false
4492                }
4493            } else {
4494                true
4495            }
4496        }
4497        None => true,
4498    };
4499    if need_create {
4500        // c:3686-3691 — `createparam(t, val.type & MN_FLOAT ? PM_FFLOAT
4501        // : PM_INTEGER); second getvalue;`. Synthesize a fresh
4502        // numeric param in paramtab matching the C body. Without
4503        // this branch wired, callers like `setiparam` silently
4504        // dropped the create (returned None) — every new integer
4505        // param assignment was a no-op.
4506        let _ = was_unset;
4507        let new_type = if val.type_ == MN_FLOAT {
4508            PM_FFLOAT                                                        // c:3687
4509        } else {
4510            PM_INTEGER                                                       // c:3688
4511        };
4512        let pm: Param = Box::new(param {
4513            node: hashnode {
4514                next: None,
4515                nam: s.to_string(),
4516                flags: new_type as i32,
4517            },
4518            u_data: 0,
4519            u_arr: None,
4520            u_str: None,
4521            // c:3690 — `setnumvalue(...)` stores the value. For
4522            // PM_INTEGER → u.l; for PM_FFLOAT → u.dval.
4523            u_val: if val.type_ == MN_FLOAT { 0 } else { val.l },
4524            u_dval: if val.type_ == MN_FLOAT { val.d } else { 0.0 },
4525            u_hash: None,
4526            gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
4527            base: 0, width: 0,
4528            env: None, ename: None, old: None, level: 0,
4529        });
4530        if let Ok(mut tab) = paramtab().write() {
4531            tab.insert(s.to_string(), pm.clone());
4532        }
4533        return Some(pm);
4534    }
4535    if (flags & ASSPM_WARN) != 0 {
4536        if let Some(ref vv) = v {
4537            if let Some(ref pm) = vv.pm {
4538                check_warn_pm(pm, "numeric", 0, 1);
4539            }
4540        }
4541    }
4542    // The reassign path: getvalue gave us a cloned pm inside the value
4543    // buffer. setnumvalue mutates that clone but the write doesn't
4544    // propagate back to paramtab. Write through paramtab directly so
4545    // reassignments stick — same shape as `assignsparam`'s c:3343
4546    // `assignstrvalue(v, val, flags)` path which mutates paramtab in
4547    // place.
4548    if let Ok(mut tab) = paramtab().write() {
4549        if let Some(pm) = tab.get_mut(s) {
4550            pm.node.flags &= !(PM_DEFAULTED as i32);
4551            let t = PM_TYPE(pm.node.flags as u32);
4552            if t == PM_INTEGER {
4553                // c:2874 — `pm->gsu.i->setfn(pm, val.u.l)`. MN_FLOAT
4554                // input truncates to integer.
4555                pm.u_val = if val.type_ == MN_FLOAT { val.d as i64 } else { val.l };
4556            } else if t == PM_EFLOAT || t == PM_FFLOAT {
4557                // c:2878 — MN_INTEGER input promotes to f64.
4558                pm.u_dval = if val.type_ == MN_FLOAT { val.d } else { val.l as f64 };
4559            } else if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
4560                // c:2862-2871 — convbase/convfloat → u_str.
4561                let s_rendered = if val.type_ == MN_FLOAT {
4562                    crate::ported::params::convfloat_underscore(val.d, pm.width)
4563                } else {
4564                    crate::ported::params::convbase_underscore(
4565                        val.l,
4566                        if pm.base > 0 { pm.base as u32 } else { 10 },
4567                        pm.width,
4568                    )
4569                };
4570                pm.u_str = Some(s_rendered);
4571            }
4572            let cloned = pm.clone();
4573            return Some(cloned);
4574        }
4575    }
4576    None
4577}
4578
4579/// Port of `Param setnparam(char *s, mnumber val)` from `Src/params.c:3745-3749`.
4580///
4581/// C body (c:3747-3748):
4582/// ```c
4583/// return assignnparam(s, val, ASSPM_WARN);
4584/// ```
4585///
4586/// Single-line wrapper around `assignnparam` with ASSPM_WARN flags.
4587///
4588/// The previous Rust port took `(s: &str, val: f64) -> ()` — losing
4589/// the integer branch (callers couldn't set integer params via
4590/// `setnparam`) AND the Param return. No real callers existed because
4591/// the fabricated sig fit nothing. Match C exactly: `(s, val)` where
4592/// `val` is the canonical `mnumber` tagged union, returning the
4593/// resulting Param.
4594pub fn setnparam(s: &str, val: crate::ported::math::mnumber)                 // c:3746
4595    -> Option<crate::ported::zsh_h::Param>
4596{
4597    assignnparam(s, val, ASSPM_WARN as i32)                                  // c:3748
4598}
4599
4600/// Port of `Param assigniparam(char *s, zlong val, int flags)` from
4601/// `Src/params.c:3754-3761`.
4602///
4603/// C body (c:3757-3760):
4604/// ```c
4605/// mnumber mnval;
4606/// mnval.type = MN_INTEGER;
4607/// mnval.u.l = val;
4608/// return assignnparam(s, mnval, flags);
4609/// ```
4610///
4611/// Two divergences in the previous Rust port:
4612///   1. Dropped the `flags` arg — caller-supplied flags (e.g.
4613///      ASSPM_AUGMENT for `+= int`) couldn't be threaded through;
4614///      every call hardcoded ASSPM_WARN regardless.
4615///   2. Returned void instead of Param — losing the new param
4616///      pointer the caller may want to read back.
4617pub fn assigniparam(s: &str, val: i64, flags: i32)                           // c:3755
4618    -> Option<crate::ported::zsh_h::Param>
4619{
4620    // c:3757-3759 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
4621    let mnval = crate::ported::math::mnumber {
4622        l: val,
4623        d: 0.0,
4624        type_: MN_INTEGER,
4625    };
4626    // c:3760 — `return assignnparam(s, mnval, flags);`
4627    assignnparam(s, mnval, flags)                                            // c:3760
4628}
4629
4630/// Port of `Param setiparam(char *s, zlong val)` from `Src/params.c:3767-3773`.
4631///
4632/// C body (c:3769-3772):
4633/// ```c
4634/// mnumber mnval;
4635/// mnval.type = MN_INTEGER;
4636/// mnval.u.l = val;
4637/// return assignnparam(s, mnval, ASSPM_WARN);
4638/// ```
4639///
4640/// The previous Rust port stringified to decimal and routed through
4641/// `assignsparam` — which CREATES THE PARAM AS PM_SCALAR. C creates
4642/// as PM_INTEGER. `setiparam("x", 5)` followed by `typeset -p x`:
4643///   - C: \`typeset -i x=5\`
4644///   - Old Rust: \`typeset x=5\`
4645///
4646/// `assignnparam` IS now ported (params.rs:4403). Route through it
4647/// matching C exactly so integer-typed params get created with the
4648/// right PM_INTEGER flag.
4649pub fn setiparam(s: &str, val: i64)                                          // c:3767
4650    -> Option<crate::ported::zsh_h::Param>
4651{
4652    // c:3770-3771 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
4653    let mnval = crate::ported::math::mnumber {
4654        l: val,
4655        d: 0.0,
4656        type_: MN_INTEGER,
4657    };
4658    // c:3772 — `return assignnparam(s, mnval, ASSPM_WARN);`
4659    assignnparam(s, mnval, ASSPM_WARN as i32)                                // c:3772
4660}
4661
4662/// Port of `setiparam_no_convert(char *s, zlong val)` from Src/params.c:3781. C
4663/// source comment: "If the target is already an integer, this
4664/// gets converted back. Low technology rules." It uses convbase
4665/// to render decimal then calls assignsparam.
4666/// WARNING: param names don't match C — Rust=() vs C=(s, val)
4667pub fn setiparam_no_convert(s: &str, val: i64)                               // c:3781
4668    -> Option<crate::ported::zsh_h::Param>
4669{
4670    assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
4671}
4672
4673/// Port of `resetparam(Param pm, int flags)` from `Src/params.c:3796`. C body:
4674/// ```c
4675/// char *s = pm->node.nam;
4676/// queue_signals();
4677/// if (pm != (Param)(paramtab == realparamtab ?
4678///        paramtab->getnode2(paramtab, s) :
4679///        paramtab->getnode(paramtab, s))) {
4680///     unqueue_signals();
4681///     zerr("can't change type of hidden variable: %s", s);
4682///     return 1;
4683/// }
4684/// s = dupstring(s);
4685/// unsetparam_pm(pm, 0, 1);
4686/// unqueue_signals();
4687/// createparam(s, flags);
4688/// return 0;
4689/// ```
4690/// Tears `pm` down + recreates it with `flags` so the next
4691/// assignment lands in a fresh slot of the requested type. Used
4692/// by `assignsparam` when the type-flag of an existing param
4693/// changes (e.g. `typeset -i x; x="abc"` resets x back to scalar).
4694///
4695/// The `paramtab->getnode` reachability check at c:3800 catches
4696/// the hidden-shadow case (a local var hiding the global `pm` we
4697/// were handed) — without the paramtab vtable we skip the check
4698/// and proceed to unset+create.
4699pub fn resetparam(pm: &mut crate::ported::zsh_h::param, flags: i32) -> i32 { // c:3796
4700    let s = pm.node.nam.clone();                                             // c:3796
4701    crate::ported::signals::queue_signals();                                 // c:3799
4702    // c:3800-3807 — paramtab->getnode2 / getnode reachability check.
4703    // Without paramtab vtable wired we cannot detect the hidden-
4704    // variable case, so we proceed; a future port of paramtab
4705    // adds the check at this site.
4706    unsetparam_pm(pm, 0, 1);                                                 // c:3819
4707    crate::ported::signals::unqueue_signals();                               // c:3819
4708    let _ = createparam(&s, flags);                                          // c:3819
4709    0                                                                        // c:3819
4710}
4711
4712/// Port of `void unsetparam(char *s)` from `Src/params.c:3819`.
4713///
4714/// C body:
4715/// ```c
4716/// Param pm;
4717/// queue_signals();
4718/// if ((pm = (Param)(paramtab == realparamtab ?
4719///         paramtab->getnode2(paramtab, s) :
4720///         paramtab->getnode(paramtab, s))))
4721///     unsetparam_pm(pm, 0, 1);
4722/// unqueue_signals();
4723/// ```
4724///
4725/// The previous Rust port took `(variables, arrays, assoc_arrays,
4726/// name)` operating on EXTERNAL HashMap storage — a SubstState-
4727/// era stale signature. C operates on the canonical `paramtab`
4728/// global. No live callers used the old 4-arg form (all use
4729/// `paramtab().write().remove(...)` directly), so renaming is
4730/// safe.
4731pub fn unsetparam(name: &str) {                                              // c:3819
4732    crate::ported::signals::queue_signals();                                  // c:3825
4733    // c:3826-3831 — `if ((pm = ... getnode2 ...) && !(pm->node.flags
4734    // & PM_NAMEREF)) unsetparam_pm(pm, 0, 1);`.
4735    //
4736    // Two divergences in the previous Rust port:
4737    //   1. Missing PM_NAMEREF check — `unsetparam("ref")` where `ref`
4738    //      is a nameref would remove the ref alias itself. C explicitly
4739    //      skips nameref params here (they're cleared via the
4740    //      ref-specific path, not the value-side unset).
4741    //   2. Bypassed `unsetparam_pm` — removed the entry directly from
4742    //      paramtab without running the readonly-guard at c:3850, the
4743    //      stdunsetfn dispatch at c:3870, or the `pm->old` scope
4744    //      restore. `typeset -r x=foo; unset x` would silently succeed
4745    //      in Rust where C rejects with `read-only variable: x`.
4746    let (found, is_nameref) = {
4747        let tab = paramtab().read().unwrap();
4748        match tab.get(name) {
4749            Some(pm) => (true, (pm.node.flags as u32 & PM_NAMEREF) != 0),
4750            None => (false, false),
4751        }
4752    };
4753    if found && !is_nameref {                                                // c:3826-3830
4754        // c:3831 — `unsetparam_pm(pm, 0, 1)`. Take an owned copy out
4755        // of paramtab so we can mutate it (unsetparam_pm wants
4756        // &mut), run the readonly-guard + env teardown, then re-insert
4757        // or fully remove based on the readonly path.
4758        let mut pm_owned = paramtab().write().unwrap().remove(name).unwrap();
4759        let rejected = unsetparam_pm(&mut pm_owned, 0, 1);                   // c:3831
4760        if rejected != 0 {
4761            // Readonly rejection — restore the entry so the state
4762            // is unchanged.
4763            paramtab().write().unwrap().insert(name.to_string(), pm_owned);
4764        }
4765    }
4766    crate::ported::signals::unqueue_signals();                                // c:3832
4767}
4768
4769/// Unset parameter (from params.c unsetparam_pm)
4770/// Port of `unsetparam_pm(Param pm, int altflag, int exp)` from `Src/params.c:3841`. Full body
4771/// removes `pm` from `paramtab` (after invoking
4772/// `pm->gsu.s->unsetfn(pm, exp)`), tears down the tied alternate
4773/// (`pm->ename`) when `!altflag`, deletes the env entry, and
4774/// resurrects `pm->old` at the right scope. Stub: needs paramtab
4775/// HashTable backend (`paramtab->removenode/addnode`) plus the
4776/// `delenv`/`adduserdir` helpers — direct port retains only the
4777/// in-memory mutation of `pm` that doesn't touch the table.
4778#[allow(unused_variables)]
4779pub fn unsetparam_pm(pm: &mut crate::ported::zsh_h::param, altflag: i32, exp: i32) -> i32 {
4780    // c:3850 — `if ((pm->node.flags & PM_READONLY) && pm->level <= locallevel)`.
4781    // The previous Rust port hardcoded `pm.level <= 0` with a
4782    // "locallevel global not yet ported — assume 0" comment, but
4783    // `crate::ported::params::locallevel` IS the canonical port of
4784    // the C global (declared above in this file). Reading it live
4785    // matters: a function-scope readonly assignment (`typeset -r x`)
4786    // gets pm.level == current locallevel; without the live check,
4787    // unsetting from a NESTED scope (locallevel > pm.level) would
4788    // succeed when C rejects, AND unsetting from a deeper scope
4789    // (locallevel < pm.level) would reject when C succeeds.
4790    let cur_ll = locallevel.load(std::sync::atomic::Ordering::Relaxed) as i32; // c:3850 locallevel
4791    if (pm.node.flags as u32 & PM_READONLY) != 0 && pm.level <= cur_ll {       // c:3850
4792        // c:3852 — `zerr("read-only %s: %s", ...)`. Emit diagnostic
4793        // so users see why the unset failed.
4794        let kind = if (pm.node.flags as u32 & PM_NAMEREF) != 0 {              // c:3852
4795            "reference"
4796        } else {
4797            "variable"
4798        };
4799        zerr(&format!("read-only {}: {}", kind, pm.node.nam));
4800        return 1;                                                              // c:3854
4801    }
4802    pm.node.flags &= !(PM_DECLARED as i32);                                    // c:3868
4803    if (pm.node.flags as u32 & PM_UNSET) == 0
4804        || (pm.node.flags as u32 & PM_REMOVABLE) != 0
4805    {
4806        // c:3870 — `pm->gsu.s->unsetfn(pm, exp)` — open-coded to stdunsetfn.
4807        stdunsetfn(pm, exp);
4808    }
4809    if pm.env.is_some() {
4810        delenv(&pm.node.nam);                                                  // c:3872 delenv(pm)
4811        pm.env = None;
4812    }
4813    // Tied alt-name removal + paramtab restore-from-old not yet
4814    // possible without HashTable backend; the C postlude (lines
4815    // 3853-3935) is a paramtab->removenode + addnode dance that
4816    // requires the missing vtable.
4817    pm.node.flags |= PM_UNSET as i32;
4818    0
4819}
4820
4821// -----------------------------------------------------------
4822// GSU dispatch callbacks — direct ports against `param.u_*`
4823// fields. C source in Src/params.c:4002.
4824// -----------------------------------------------------------
4825
4826/// Port of `intgetfn(Param pm)` from `Src/params.c:3993`. C body:
4827/// `return pm->u.val;`
4828pub fn intgetfn(pm: &crate::ported::zsh_h::param) -> i64 {
4829    pm.u_val
4830}
4831
4832/// Port of `intsetfn(Param pm, zlong x)` from `Src/params.c:4002`. C body:
4833/// `pm->u.val = x;`
4834pub fn intsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4835    pm.u_val = x;
4836}
4837
4838/// Port of `floatgetfn(Param pm)` from `Src/params.c:4011`. C body:
4839/// `return pm->u.dval;`
4840pub fn floatgetfn(pm: &crate::ported::zsh_h::param) -> f64 {
4841    pm.u_dval
4842}
4843
4844/// Port of `floatsetfn(Param pm, double x)` from `Src/params.c:4020`. C body:
4845/// `pm->u.dval = x;`
4846pub fn floatsetfn(pm: &mut crate::ported::zsh_h::param, x: f64) {
4847    pm.u_dval = x;
4848}
4849
4850/// Port of `strgetfn(Param pm)` from `Src/params.c:4029`. C body:
4851/// `return pm->u.str ? pm->u.str : (char *) hcalloc(1);`
4852pub fn strgetfn(pm: &crate::ported::zsh_h::param) -> String {
4853    pm.u_str.clone().unwrap_or_default()
4854}
4855
4856/// Port of `strsetfn(Param pm, char *x)` from `Src/params.c:4040`.
4857///
4858/// C body (c:4043-4051):
4859/// ```c
4860/// zsfree(pm->u.str); pm->u.str = x;
4861/// if (!(pm->node.flags & PM_HASHELEM) &&
4862///     ((pm->node.flags & PM_NAMEDDIR) || isset(AUTONAMEDIRS))) {
4863///     pm->node.flags |= PM_NAMEDDIR;
4864///     adduserdir(pm->node.nam, x, 0, 0);
4865/// }
4866/// ```
4867///
4868/// The C body fires the `adduserdir` path when EITHER `PM_NAMEDDIR`
4869/// is already set OR the `AUTONAMEDIRS` option is on. The previous
4870/// Rust port only fired when PM_NAMEDDIR was already set, missing
4871/// the AUTONAMEDIRS auto-create branch entirely. With `setopt
4872/// AUTONAMEDIRS`, every scalar assignment to a path-shaped value
4873/// should register a named-directory entry for `~name` expansion;
4874/// the Rust port silently dropped that behavior.
4875pub fn strsetfn(pm: &mut crate::ported::zsh_h::param, x: String) {            // c:4040
4876    pm.u_str = Some(x.clone());                                               // c:4044 pm->u.str = x
4877    // c:4045-4046 — `if (!(PM_HASHELEM) && (PM_NAMEDDIR || isset(AUTONAMEDIRS)))`.
4878    if (pm.node.flags as u32 & PM_HASHELEM) == 0
4879        && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0
4880            || isset(crate::ported::zsh_h::AUTONAMEDIRS))                    // c:4046 isset(AUTONAMEDIRS)
4881    {
4882        pm.node.flags |= PM_NAMEDDIR as i32;                                  // c:4047
4883        crate::ported::utils::adduserdir(&pm.node.nam, &x, 0, false);         // c:4048
4884    }
4885}
4886
4887/// Port of `arrgetfn(Param pm)` from `Src/params.c:4057`. C body:
4888/// `return pm->u.arr ? pm->u.arr : &nullarray;`
4889pub fn arrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
4890    pm.u_arr.clone().unwrap_or_default()
4891}
4892
4893/// Port of `arrsetfn(Param pm, char **x)` from `Src/params.c:4066`. C body frees
4894/// the old array, applies PM_UNIQUE filter via `uniqarray()`, then
4895/// stores. Calls `arrfixenv(ename, x)` for tied colon-arrays.
4896pub fn arrsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
4897    let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
4898        simple_arrayuniq(x)
4899    } else {
4900        x
4901    };
4902    pm.u_arr = Some(val.clone());
4903    if let Some(ename) = pm.ename.clone() {
4904        arrfixenv(&ename, Some(&val));
4905    }
4906}
4907
4908/// Port of `hashgetfn(Param pm)` from `Src/params.c:4084`. C body:
4909/// `return pm->u.hash;`
4910pub fn hashgetfn(pm: &crate::ported::zsh_h::param) -> Option<&crate::ported::zsh_h::HashTable> {
4911    pm.u_hash.as_ref()
4912}
4913
4914/// Port of `hashsetfn(Param pm, HashTable x)` from `Src/params.c:4093`. C body:
4915/// `if (pm->u.hash && pm->u.hash != x) deleteparamtable(pm->u.hash);
4916///  pm->u.hash = x;`
4917pub fn hashsetfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
4918    pm.u_hash = Some(x);
4919}
4920
4921/// Direct port of `static void arrhashsetfn(Param pm, char **val,
4922/// int flags)` from `Src/params.c:4113-4170`. Set callback for
4923/// assoc arrays: takes a flat `[k1, v1, k2, v2, ...]` value list
4924/// and turns it into a hash.
4925///
4926/// C body:
4927///   1. Count non-Marker entries; if odd, error c:4128-4131.
4928///   2. Under ASSPM_AUGMENT, fetch existing hash via getfn
4929///      (c:4134-4137); otherwise allocate fresh via
4930///      newparamtable(17, name).
4931///   3. Walk pairs: each value (k, v) becomes a PM_SCALAR|PM_UNSET
4932///      child param `createparam(k)`, then `assignstrvalue(v->pm,
4933///      val, eltflags)` (c:4140-4166).
4934///   4. `pm->gsu.h->setfn(pm, ht)` to install (c:4168).
4935///
4936/// The Rust port partially mirrors: counts pairs, rejects odd
4937/// counts via zerr, installs a fresh hashtable. The per-pair
4938/// createparam+assignstrvalue cycle requires assoc storage
4939/// shape we don't yet have wired through `u_hash`; this stays as
4940/// a structural port and emits diagnostic on the odd-count path.
4941pub fn arrhashsetfn(                                                         // c:4113
4942    pm: &mut crate::ported::zsh_h::param,
4943    val: Vec<String>,
4944    _flags: i32,
4945) {
4946
4947    // c:4124-4127 — count non-Marker entries.
4948    let alen: usize = val
4949        .iter()
4950        .filter(|s| !s.starts_with(Marker as char))
4951        .count();
4952
4953    // c:4129-4131 — odd count → error.
4954    if alen % 2 != 0 {
4955        crate::ported::utils::zerr(
4956            "bad set of key/value pairs for associative array",
4957        );
4958        return;
4959    }
4960
4961    // c:4132-4138 — install or augment. Skip the createparam
4962    // sub-hash walk pending assoc-storage wiring; install an
4963    // empty hashtable so hashgetfn doesn't return stale data.
4964    pm.u_hash = Some(Box::new(crate::ported::zsh_h::hashtable {
4965        hsize: 0,
4966        ct: 0,
4967        nodes: Vec::new(),
4968        tmpdata: 0,
4969        hash: None,
4970        emptytable: None,
4971        filltable: None,
4972        cmpnodes: None,
4973        addnode: None,
4974        getnode: None,
4975        getnode2: None,
4976        removenode: None,
4977        disablenode: None,
4978        enablenode: None,
4979        freenode: None,
4980        printnode: None,
4981        scantab: None,
4982    }));
4983    // c:4170 — free(val). Rust drops automatically.
4984}
4985
4986/// Port of `nullstrsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4180`. C body:
4987/// `zsfree(x);` — frees but doesn't store. Rust drop handles free.
4988#[allow(unused_variables)]
4989pub fn nullstrsetfn(pm: &mut crate::ported::zsh_h::param, x: String) {}
4990
4991/// Port of `nullunsetfn(UNUSED(Param pm), UNUSED(int exp))` from `Src/params.c:4192`. C body: empty.
4992#[allow(unused_variables)]
4993pub fn nullunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {}
4994
4995/// Port of `stdunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:3955`. C body:
4996/// dispatches `pm->gsu->setfn(pm, NULL)` per `PM_TYPE`, clears
4997/// `PM_TIED`/frees ename for tied params, sets PM_UNSET.
4998///
4999/// Rust port mirrors C semantics: clears the union slot and sets
5000/// PM_UNSET. The GSU vtable callbacks are stored on `param` as
5001/// `Option<Gsu*>` (zsh_h:760-764) but the dispatch uses callback
5002/// fn-ptrs that aren't generally registered yet, so we open-code
5003/// the "setfn(pm, NULL)" effect by zeroing the matching union
5004/// member instead of calling through the vtable.
5005#[allow(unused_variables)]
5006pub fn stdunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
5007    match PM_TYPE(pm.node.flags as u32) {
5008        PM_SCALAR | PM_NAMEREF => {
5009            pm.u_str = None;
5010        }
5011        PM_ARRAY => {
5012            pm.u_arr = None;
5013        }
5014        PM_HASHED => {
5015            pm.u_hash = None;
5016        }
5017        _ => {
5018            if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
5019                pm.u_str = None;
5020            }
5021        }
5022    }
5023    if (pm.node.flags as u32 & (PM_SPECIAL | PM_TIED)) == PM_TIED {
5024        pm.ename = None;
5025        pm.node.flags &= !(PM_TIED as i32);
5026    }
5027    pm.node.flags |= PM_UNSET as i32;
5028}
5029
5030// -----------------------------------------------------------
5031// "Null" callbacks — no-op getfn/setfn/unsetfn slots used for
5032// read-only or write-only special params.
5033// -----------------------------------------------------------
5034
5035/// Port of `nullintsetfn(UNUSED(Param pm), UNUSED(zlong x))` from `Src/params.c:4187`. C body:
5036/// empty (no-op setter for read-only int params).
5037#[allow(unused_variables)]
5038pub fn nullintsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {}
5039
5040/// Port of `nullsethashfn(UNUSED(Param pm), HashTable x)` from `Src/params.c:4104`. C body:
5041/// `deleteparamtable(x);` — frees the supplied table, doesn't store.
5042#[allow(unused_variables)]
5043pub fn nullsethashfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
5044    // Rust drop semantics free `x` when this scope ends.
5045}
5046
5047// -----------------------------------------------------------
5048// Generic special-param GSU callbacks (`u.valptr` / `u.data`).
5049// C source uses raw pointer indirection through `pm->u.data`/
5050// `pm->u.valptr` — Rust port stores the global's name in `u_str`
5051// (lookup key) since we can't carry raw pointers across an FFI
5052// boundary safely. The lookup-table integration ships with the
5053// special-params init code (Src/params.c:4213 createparamtable).
5054// -----------------------------------------------------------
5055
5056/// Port of `intvargetfn(Param pm)` from `Src/params.c:4202`. C body:
5057/// `return *pm->u.valptr;`
5058pub fn intvargetfn(pm: &crate::ported::zsh_h::param) -> i64 {
5059    pm.u_val
5060}
5061
5062/// Port of `intvarsetfn(Param pm, zlong x)` from `Src/params.c:4213`. C body:
5063/// `*pm->u.valptr = x;`
5064pub fn intvarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
5065    pm.u_val = x;
5066}
5067
5068/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4224`. C body sets
5069/// the int and triggers `adjustwinsize` for LINES/COLUMNS.
5070/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4226`.
5071/// C body: `*p = x; if (p == &zterm_lines || p == &zterm_columns)
5072/// adjustwinsize(2 + (p == &zterm_columns));`
5073///
5074/// The `from` argument to `adjustwinsize` is documented at
5075/// `Src/utils.c:1883-1887`: 0=signal, 1=manual, 2=LINES callback,
5076/// 3=COLUMNS callback. Each value selects a different code path
5077/// inside `adjustwinsize` — for example, `from=2` skips the
5078/// COLUMNS-specific ioctl, and `from=3` skips the LINES path.
5079///
5080/// The previous Rust port passed `0` for both LINES and COLUMNS,
5081/// which triggered the FULL `getwinsz` ioctl + both adjustlines
5082/// AND adjustcolumns calls AND the potential setiparam recursion
5083/// — diverging from C's narrow "just adjust the one axis we
5084/// changed" semantics. Effect: setting `LINES=80` would re-issue
5085/// `setiparam("COLUMNS", ...)` recursively, churning the
5086/// paramtab for no reason.
5087pub fn zlevarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {           // c:4226
5088    pm.u_val = x;                                                            // c:4230 *p = x;
5089    // c:4231-4232 — `2 + (p == &zterm_columns)` selects 2 for LINES
5090    // (zterm_lines) and 3 for COLUMNS (zterm_columns).
5091    if pm.node.nam == "LINES" {
5092        let _ = crate::ported::utils::adjustwinsize(2);                      // c:4232 LINES path
5093    } else if pm.node.nam == "COLUMNS" {
5094        let _ = crate::ported::utils::adjustwinsize(3);                      // c:4232 COLUMNS path
5095    }
5096}
5097
5098/// Port of `strvarsetfn(Param pm, char *x)` from `Src/params.c:4249`. C body:
5099/// `zsfree(*q); *q = x;` where `q = (char **)pm->u.data`.
5100pub fn strvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5101    pm.u_str = x;
5102}
5103
5104/// Port of `strvargetfn(Param pm)` from `Src/params.c:4263`. C body:
5105/// `s = *((char **)pm->u.data); return s ? s : hcalloc(1);`
5106pub fn strvargetfn(pm: &crate::ported::zsh_h::param) -> String {
5107    pm.u_str.clone().unwrap_or_default()
5108}
5109
5110/// Port of `arrvargetfn(Param pm)` from `Src/params.c:4279`. C body:
5111/// `arrptr = *((char ***)pm->u.data); return arrptr ?: &nullarray;`
5112pub fn arrvargetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5113    pm.u_arr.clone().unwrap_or_default()
5114}
5115
5116/// Port of `arrvarsetfn(Param pm, char **x)` from `Src/params.c:4294`. C body
5117/// frees old, applies PM_UNIQUE, handles PM_SPECIAL+NULL → mkarray.
5118pub fn arrvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
5119    let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
5120        simple_arrayuniq(x)
5121    } else {
5122        x
5123    };
5124    pm.u_arr = Some(val);
5125}
5126
5127/// Array to colon-separated path — inverse of `colonsplit`.
5128/// Port of `colonarrgetfn(Param pm)` from Src/params.c (joins the array
5129/// stored in `pm->u.colon` back into the `:`-form for env).
5130/// WARNING: param names don't match C — Rust=(arr) vs C=(pm)
5131pub fn colonarrgetfn(arr: &[String]) -> String {
5132    arr.join(":")
5133}
5134
5135/// Port of `colonarrsetfn(Param pm, char *x)` from `Src/params.c:4329`. C body
5136/// splits the colon-string into an array and stores via the
5137/// generic arrvarsetfn.
5138pub fn colonarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5139    let uniq = (pm.node.flags as u32 & PM_UNIQUE) != 0;                          // c:4339
5140    let arr = match x {
5141        Some(s) => crate::ported::utils::colonsplit(&s, uniq),                   // c:4339
5142        None => Vec::new(),
5143    };
5144    arrvarsetfn(pm, arr);
5145}
5146
5147/// Port of `tiedarrgetfn(Param pm)` from `Src/params.c:4348`. C body:
5148/// `return *((Tieddata)pm->u.data)->arrptr;`
5149pub fn tiedarrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5150    pm.u_arr.clone().unwrap_or_default()
5151}
5152
5153/// Direct port of `void tiedarrsetfn(Param pm, char *x)` from
5154/// `Src/params.c:4357-4389`. Setter for a colon-array-tied
5155/// scalar (PATH/CDPATH/MAILPATH/etc.).
5156///
5157/// C body:
5158///   1. Free the existing tied array (`*dptr->arrptr`) at c:4363.
5159///   2. If no array but an `ename` exists, clear PM_DEFAULTED on
5160///      the tied array param (c:4365-4368).
5161///   3. If `x` is non-null: build a 1-or-2-byte separator from
5162///      `dptr->joinchar` (Meta-quoting if needed, c:4371-4380),
5163///      `sepsplit(x, sepbuf, 0, 0)` into the array (c:4381), and
5164///      uniqarray() if PM_UNIQUE (c:4382-4383). Free `x` (c:4384).
5165///   4. Else: `*dptr->arrptr = NULL` (c:4385-4386).
5166///   5. If `pm->ename` is set, call `arrfixenv(pm->name, arrptr)`
5167///      to sync env (c:4387-4388).
5168///
5169/// The Rust port treats `u_arr` as the tied array storage and
5170/// uses `':'` as the joinchar default (matches PATH/CDPATH/FPATH
5171/// /MAILPATH/PSVAR/MODULE_PATH which all use colon separators —
5172/// the joinchar field on the C-side tieddata wasn't ported to the
5173/// Rust Param struct yet).
5174pub fn tiedarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) { // c:4357
5175
5176    // c:4361-4368 — free old / clear PM_DEFAULTED on tied counterpart.
5177    if pm.u_arr.is_none() {
5178        if let Some(ename) = pm.ename.clone() {                              // c:4365
5179            let mut tab = paramtab().write().unwrap();
5180            if let Some(altpm) = tab.get_mut(&ename) {                       // c:4366
5181                altpm.node.flags &= !(PM_DEFAULTED as i32);                  // c:4367
5182            }
5183        }
5184    }
5185
5186    if let Some(s) = x {                                                     // c:4369
5187        // c:4370-4380 — single-byte separator (joinchar=':' for all
5188        // currently-tied params; Meta-quoting only kicks in for
5189        // exotic joinchars not present today).
5190        let arr: Vec<String> = s.split(':').map(|t| t.to_string()).collect();
5191        // c:4382-4383 — uniqarray if PM_UNIQUE.
5192        let arr = if pm.node.flags & PM_UNIQUE as i32 != 0 {                 // c:4382
5193            uniqarray(arr)                                                   // c:4383
5194        } else {
5195            arr
5196        };
5197        pm.u_arr = Some(arr);
5198        // c:4384 — zsfree(x). Rust drop.
5199    } else {                                                                 // c:4385
5200        pm.u_arr = None;                                                     // c:4386
5201    }
5202
5203    // c:4387-4388 — `if (pm->ename) arrfixenv(pm->name, *dptr->arrptr)`.
5204    if pm.ename.is_some() {
5205        let nam = pm.node.nam.clone();
5206        let arr_ref = pm.u_arr.as_deref();
5207        arrfixenv(&nam, arr_ref);
5208    }
5209}
5210
5211/// Port of `tiedarrunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:4393`. C body
5212/// frees the tied storage and calls stdunsetfn.
5213/// Direct port of `void tiedarrunsetfn(Param pm, UNUSED(int exp))`
5214/// from `Src/params.c:4393`. Special unset for tied arrays:
5215/// frees tieddata, ename, clears PM_TIED, sets PM_UNSET.
5216///
5217/// C body:
5218///   pm->gsu.s->setfn(pm, NULL);             // c:4393
5219///   zfree(pm->u.data, sizeof(tieddata));    // c:4393
5220///   pm->u.data = NULL;                      // c:4393
5221///   zsfree(pm->ename);                      // c:4393
5222///   pm->ename = NULL;                       // c:4393
5223///   pm->flags &= ~PM_TIED;                  // c:4393
5224///   pm->flags |= PM_UNSET;                  // c:4393
5225pub fn tiedarrunsetfn(pm: &mut crate::ported::zsh_h::param, _exp: i32) {     // c:4393
5226    // c:4400 — invoke the scalar setfn with NULL (frees backing array).
5227    tiedarrsetfn(pm, None);
5228    // c:4401-4403 — drop tieddata.
5229    pm.u_data = 0;
5230    pm.u_arr = None;
5231    // c:4404-4405 — `zsfree(pm->ename); pm->ename = NULL`.
5232    pm.ename = None;
5233    // c:4406-4407 — flag toggles.
5234    pm.node.flags &= !(PM_TIED as i32);
5235    pm.node.flags |= PM_UNSET as i32;
5236}
5237
5238// -----------------------------------------------------------
5239// Array uniq helpers.
5240// -----------------------------------------------------------
5241
5242/// Port of `simple_arrayuniq(char **x, int freeok)` from `Src/params.c:4412`. C body:
5243/// O(n^2) dedupe in place — first occurrence wins.
5244/// WARNING: param names don't match C — Rust=(x) vs C=(x, freeok)
5245pub fn simple_arrayuniq(x: Vec<String>) -> Vec<String> {
5246    let mut seen: HashSet<String> = HashSet::new();
5247    let mut out = Vec::with_capacity(x.len());
5248    for s in x {
5249        if seen.insert(s.clone()) {
5250            out.push(s);
5251        }
5252    }
5253    out
5254}
5255
5256/// Port of `arrayuniq_freenode(HashNode hn)` from `Src/params.c:4443`. C
5257/// body: `zsfree(((Pathnode)hn)->name); zfree(hn, sizeof…);` —
5258/// the freenode callback for the temporary HashTable `arrayuniq`
5259/// builds. Rust drop semantics handle this; no-op shim.
5260/// is `(void)hn;` — intentional no-op; passed as freenode callback
5261/// to scratch hashtable used by `arrayuniq` so existing entries
5262/// aren't freed when the table is torn down.
5263/// WARNING: param names don't match C — Rust=() vs C=(hn)
5264/// WARNING: param names don't match C — Rust=() vs C=(pm, x)
5265pub fn arrayuniq_freenode() {}
5266
5267/// Direct port of `HashTable newuniqtable(zlong size)` from
5268/// `Src/params.c:4450`. C body allocates a `HashTable`
5269/// named "arrayuniq" with the standard hasher/cmpnodes/
5270/// add/get/remove/disable/enable function pointers plus
5271/// `arrayuniq_freenode` as the freenode callback (which is a
5272/// no-op — see c:4443). Rust returns a `HashSet<String>` with
5273/// the size hint pre-allocated; the freenode-callback role is
5274/// implicit (Drop runs on HashSet teardown without freeing
5275/// borrowed strings).
5276pub fn newuniqtable(size: i64) -> HashSet<String> {                          // c:4450
5277    HashSet::with_capacity(size.max(0) as usize)                             // c:4450 newhashtable(size, ...)
5278}
5279
5280/// Direct port of `static void arrayuniq(char **x, int freeok)`
5281/// from `Src/params.c:4473`. First-wins dedupe of `x`,
5282/// in-place. C uses simple O(n²) scan for arrays under 10
5283/// entries, switching to a HashTable for larger arrays. `freeok`
5284/// controls whether to `zsfree()` duplicates (only safe when
5285/// caller owns the strings — Rust drop semantics handle it).
5286///
5287/// Signature note: C takes `char **x` + in-place mutation; Rust
5288/// takes owned `Vec<String>` and returns the deduped result.
5289/// `freeok` is preserved but is a no-op in Rust (drops free
5290/// automatically). The hashtable / simple-loop tiering follows
5291/// the same threshold (10) as C.
5292pub fn arrayuniq(x: Vec<String>, freeok: i32) -> Vec<String> {               // c:4473
5293    let _ = freeok;
5294    let array_size = x.len();
5295    if array_size == 0 {                                                     // c:4481
5296        return x;
5297    }
5298    // c:4482-4486 — small-array fallback to simple_arrayuniq.
5299    if array_size < 10 {                                                     // c:4482
5300        return simple_arrayuniq(x);                                          // c:4484
5301    }
5302    // c:4483 — `if (!(ht = newuniqtable(array_size + 1)))` — Rust
5303    // newuniqtable never fails, but mirror the C order of allocation.
5304    let mut ht = newuniqtable(array_size as i64 + 1);
5305    // c:4487-4507 — walk + first-wins.
5306    let mut out: Vec<String> = Vec::with_capacity(array_size);
5307    for s in x {                                                             // c:4487 walk
5308        if ht.insert(s.clone()) {                                            // c:4488 gethashnode2 + addhashnode2
5309            out.push(s);                                                     // c:4495 *write_it = *it
5310        }
5311        // else: dup — drop the value (c:4502 zsfree if freeok).
5312    }
5313    drop(ht);                                                                // c:4523 deletehashtable
5314    out
5315}
5316
5317/// Remove duplicate elements from array while preserving order.
5318/// Port of `uniqarray(char **x)` from Src/params.c.
5319/// WARNING: param names don't match C — Rust=(arr) vs C=(x)
5320pub fn uniqarray(arr: Vec<String>) -> Vec<String> {
5321    let mut seen = HashSet::new();
5322    arr.into_iter().filter(|s| seen.insert(s.clone())).collect()
5323}
5324
5325/// Direct port of `void zhuniqarray(char **x)` from
5326/// `Src/params.c:4523`. Wraps `arrayuniq` with `freeok=0`.
5327/// (C body is literally `arrayuniq(x, 0);`.)
5328pub fn zhuniqarray(x: Vec<String>) -> Vec<String> {                          // c:4523
5329    arrayuniq(x, 0)                                                          // c:4523
5330}
5331
5332/// Port of `poundgetfn(UNUSED(Param pm))` from `Src/params.c:4534`. C body:
5333/// `return arrlen(pparams);`
5334/// WARNING: param names don't match C — Rust=() vs C=(pm)
5335pub fn poundgetfn() -> i64 {
5336    pparams_lock().lock().expect("pparams poisoned").len() as i64
5337}
5338
5339/// Port of `randomgetfn(UNUSED(Param pm))` from `Src/params.c:4543`. C body:
5340/// `return rand() & 0x7fff;`
5341/// WARNING: param names don't match C — Rust=() vs C=(pm)
5342pub fn randomgetfn() -> i64 {
5343    (unsafe { libc::rand() } & 0x7fff) as i64
5344}
5345
5346/// Port of `randomsetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4552`. C body:
5347/// `srand((unsigned int)v);`
5348/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
5349pub fn randomsetfn(v: i64) {
5350    unsafe { libc::srand(v as libc::c_uint) };
5351}
5352
5353// -----------------------------------------------------------
5354// SECONDS / EPOCHSECONDS family — backed by SHTIMER static.
5355// -----------------------------------------------------------
5356
5357/// Port of `intsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4561`. C body:
5358/// `return (zlong)(now.tv_sec - shtimer.tv_sec - …);`
5359/// WARNING: param names don't match C — Rust=() vs C=(pm)
5360pub fn intsecondsgetfn() -> i64 {
5361    let now = SystemTime::now()
5362        .duration_since(UNIX_EPOCH)
5363        .unwrap_or_default();
5364    let timer = *shtimer_lock().lock().expect("shtimer poisoned");
5365    let now_sec = now.as_secs() as i64;
5366    let timer_sec = timer.as_secs() as i64;
5367    let now_nsec = now.subsec_nanos() as i64;
5368    let timer_nsec = timer.subsec_nanos() as i64;
5369    now_sec - timer_sec - i64::from(now_nsec < timer_nsec)
5370}
5371
5372/// Port of `intsecondssetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4575`. C body:
5373/// ```c
5374/// diff = (zlong)now.tv_sec - x;
5375/// shtimer.tv_sec = diff;
5376/// if ((zlong)shtimer.tv_sec != diff)
5377///     zwarn("SECONDS truncated on assignment");
5378/// shtimer.tv_nsec = now.tv_nsec;
5379/// ```
5380/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5381pub fn intsecondssetfn(x: i64) {
5382    let now = SystemTime::now()
5383        .duration_since(UNIX_EPOCH)
5384        .unwrap_or_default();
5385    let now_sec = now.as_secs() as i64;
5386    let new_sec = now_sec - x;
5387    // c:4587 — C uses `zwarn` (informational), NOT `zerr` (fatal).
5388    // The C body STORES `diff` unconditionally then emits the warning
5389    // if truncation lost information. Rust port previously used `zerr`
5390    // and early-returned (skipping the store) — divergent from C.
5391    if new_sec < 0 {
5392        crate::ported::utils::zwarn("SECONDS truncated on assignment");
5393        // c:4585 — C still stores; Rust represents shtimer as Duration
5394        // which is non-negative. We clamp to zero to preserve the
5395        // "store-anyway" semantic for the time-display path, even
5396        // though the negative-time case is unrepresentable.
5397        *shtimer_lock().lock().expect("shtimer poisoned") =
5398            Duration::new(0, now.subsec_nanos());
5399        return;
5400    }
5401    *shtimer_lock().lock().expect("shtimer poisoned") =
5402        Duration::new(new_sec as u64, now.subsec_nanos());
5403}
5404
5405/// Port of `floatsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4591`. C body:
5406/// `return (double)(now-tv_sec - shtimer.tv_sec) + nsec/1e9;`
5407/// WARNING: param names don't match C — Rust=() vs C=(pm)
5408pub fn floatsecondsgetfn() -> f64 {
5409    let now = SystemTime::now()
5410        .duration_since(UNIX_EPOCH)
5411        .unwrap_or_default();
5412    let timer = *shtimer_lock().lock().expect("shtimer poisoned");
5413    (now - timer).as_secs_f64()
5414}
5415
5416/// Port of `floatsecondssetfn(UNUSED(Param pm), double x)` from `Src/params.c:4603`. C body:
5417/// `shtimer.tv_sec = now.tv_sec - (zlong)x; shtimer.tv_nsec = now.tv_nsec - (x-int)*1e9;`
5418/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5419pub fn floatsecondssetfn(x: f64) {
5420    let now = SystemTime::now()
5421        .duration_since(UNIX_EPOCH)
5422        .unwrap_or_default();
5423    let new = now.checked_sub(Duration::from_secs_f64(x)).unwrap_or_default();
5424    *shtimer_lock().lock().expect("shtimer poisoned") = new;
5425}
5426
5427/// Port of `getrawseconds()` from `Src/params.c:4615`. C body:
5428/// `return (double)shtimer.tv_sec + (double)shtimer.tv_nsec / 1e9;`
5429pub fn getrawseconds() -> f64 {
5430    shtimer_lock().lock().expect("shtimer poisoned").as_secs_f64()
5431}
5432
5433/// Port of `setrawseconds(double x)` from `Src/params.c:4622`. C body:
5434/// `shtimer.tv_sec = (zlong)x; shtimer.tv_nsec = (x-int)*1e9;`
5435pub fn setrawseconds(x: f64) {
5436    *shtimer_lock().lock().expect("shtimer poisoned") = Duration::from_secs_f64(x);
5437}
5438
5439/// Port of `setsecondstype(Param pm, int on, int off)` from `Src/params.c:4630`. C body
5440/// flips the `gsu.f`/`gsu.i` callback pointer based on the new
5441/// param-flag bitset.
5442///
5443/// WARNING: zshrs has no Param/GSU dispatch table yet — the
5444/// "promotion between integer/float seconds" logic happens via
5445/// pm->gsu pointer swaps in C. Returns 0 to signal success;
5446/// callers can assume the type change is recorded by the caller's
5447/// own bookkeeping until the GSU table lands.
5448/// WARNING: param names don't match C — Rust=(on, off) vs C=(pm, on, off)
5449pub fn setsecondstype(                                                       // c:4630
5450    pm: &mut crate::ported::zsh_h::param,
5451    on: i32,
5452    off: i32,
5453) -> i32 {
5454    // c:4632 — `int newflags = (pm->flags | on) & ~off`.
5455    let newflags = (pm.node.flags | on) & !off;
5456    // c:4633 — `int tp = PM_TYPE(newflags)`.
5457    let tp = PM_TYPE(newflags as u32);
5458    // c:4635-4638 / 4639-4642 — float vs integer GSU pointer swap.
5459    if tp == PM_EFLOAT || tp == PM_FFLOAT {                                  // c:4635
5460        // C: `pm->gsu.f = &floatseconds_gsu`. GSU table not yet
5461        // wired in the Rust port; record the type by clearing
5462        // any integer GSU.
5463        pm.gsu_i = None;
5464        // pm.gsu_f = Some(floatseconds_gsu) — pending GSU port.
5465    } else if tp == PM_INTEGER {                                             // c:4639
5466        // C: `pm->gsu.i = &intseconds_gsu`.
5467        pm.gsu_f = None;
5468        // pm.gsu_i = Some(intseconds_gsu) — pending GSU port.
5469    } else {
5470        return 1;                                                            // c:4644
5471    }
5472    pm.node.flags = newflags;                                                // c:4645
5473    0                                                                        // c:4646
5474}
5475
5476// -----------------------------------------------------------
5477// $USERNAME
5478// -----------------------------------------------------------
5479
5480/// Port of `usernamegetfn(UNUSED(Param pm))` from `Src/params.c:4653`. C body:
5481/// Port of `usernamegetfn(UNUSED(Param pm))` from Src/params.c:4655.
5482/// C body: `return get_username();`. C's `get_username()`
5483/// (Src/utils.c:1075) walks `getuid() != cached_uid` and
5484/// refreshes the cache via `getpwuid()` on mismatch — so a
5485/// USERNAME read AFTER an `setuid()` call sees the NEW
5486/// username, not the stale cache.
5487///
5488/// The previous Rust port returned `cached_username_lock()`
5489/// directly without the refresh, so a script that called
5490/// setuid(3) (or USER changed externally via setuid binary)
5491/// would keep returning the old username.
5492///
5493/// WARNING: param names don't match C — Rust=() vs C=(pm)
5494pub fn usernamegetfn() -> String {                                            // c:4655
5495    // c:4658 — `return get_username();`. Route through the
5496    // canonical refresh-on-uid-change accessor at utils.rs.
5497    crate::ported::utils::get_username()                                       // c:4658
5498}
5499
5500/// Port of `usernamesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4662`. C body:
5501/// `getpwnam(x); setgid; setuid; cached_uid = pswd->pw_uid;`
5502///
5503/// WARNING: the SUID-changing path requires getpwnam(3) which
5504/// crosses an unsafe FFI boundary not yet wrapped here. The
5505/// cached-name update is performed; uid/gid changes still need
5506/// porting of the `pwd.h` getpwnam wrapper.
5507/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5508pub fn usernamesetfn(x: String) {                                            // c:4662
5509    // c:4662 — `if (x && (pswd = getpwnam(x)) && pswd->pw_uid != cached_uid)`.
5510    let target = std::ffi::CString::new(x.as_bytes()).ok();
5511    if let Some(cstr) = target {
5512        unsafe {
5513            let pwd = libc::getpwnam(cstr.as_ptr());                         // c:4666
5514            if !pwd.is_null() {
5515                // c:4666 — C reads `cached_uid` (a global initialized
5516                // to `getuid()` at init.c:1219 — the REAL uid, NOT
5517                // the effective one). The previous Rust port used
5518                // `geteuid()` which diverges when running setuid
5519                // (geteuid != getuid) — the shell would erroneously
5520                // try to change to a uid it's already at, or skip
5521                // a needed change. Match C exactly: use `getuid()`.
5522                let cached_uid = libc::getuid();                             // c:4666 cached_uid = getuid()
5523                if (*pwd).pw_uid != cached_uid {                             // c:4666
5524                    // c:4670-4672 — initgroups(x, pswd->pw_gid).
5525                    let _ = libc::initgroups(cstr.as_ptr(), (*pwd).pw_gid as _);
5526                    // c:4671 — setgid(pswd->pw_gid).
5527                    if libc::setgid((*pwd).pw_gid) != 0 {                    // c:4673
5528                        crate::ported::utils::zwarn(&format!(
5529                            "failed to change group ID: {}",
5530                            std::io::Error::last_os_error()
5531                        ));
5532                    } else if libc::setuid((*pwd).pw_uid) != 0 {             // c:4675
5533                        // c:4675-4676 — setuid failed.
5534                        crate::ported::utils::zwarn(&format!(
5535                            "failed to change user ID: {}",
5536                            std::io::Error::last_os_error()
5537                        ));
5538                    } else {
5539                        // c:4677-4681 — cache update.
5540                        let name_cstr = std::ffi::CStr::from_ptr((*pwd).pw_name);
5541                        let name_str = name_cstr.to_string_lossy().to_string();
5542                        *cached_username_lock()
5543                            .lock()
5544                            .expect("username poisoned") =
5545                            crate::ported::utils::ztrdup_metafy(&name_str);
5546                    }
5547                }
5548            }
5549        }
5550    }
5551    // c:4683 — `zsfree(x)`; Rust drop handles it.
5552    drop(x);
5553}
5554
5555// -----------------------------------------------------------
5556// libc-backed callbacks (UID/GID/EUID/EGID/errno/RANDOM/TTYIDLE).
5557// -----------------------------------------------------------
5558
5559/// Port of `uidgetfn(UNUSED(Param pm))` from `Src/params.c:4689`. C body:
5560/// `return getuid();`
5561/// WARNING: param names don't match C — Rust=() vs C=(pm)
5562pub fn uidgetfn() -> i64 {
5563    unsafe { libc::getuid() as i64 }
5564}
5565
5566// `termflags` from Src/init.c — bitmap of terminal-state flags. Set
5567// from term_reinit_from_pm and consulted by ZLE before first paint.
5568pub static TERMFLAGS: std::sync::atomic::AtomicI32 =
5569    std::sync::atomic::AtomicI32::new(0);
5570// `TERM_UNKNOWN` re-exported from canonical zsh_h.rs (port of
5571// `Src/zsh.h:1986`). The local declaration here had the value
5572// `1 << 0 = 0x01` — which is C's TERM_BAD (Src/zsh.h:1985), NOT
5573// TERM_UNKNOWN. The canonical TERM_UNKNOWN value is 0x02.
5574//
5575// Callers reading `crate::ported::params::TERM_UNKNOWN` got the
5576// TERM_BAD bit; the params.rs term-init path fired
5577// `TERMFLAGS.fetch_or(TERM_UNKNOWN)` which actually set TERM_BAD,
5578// while the prompt.rs guard at line 441 imported the correct
5579// (0x02) value from zsh_h.rs — so the two paths disagreed silently
5580// about which bit means "unknown terminal".
5581pub use crate::ported::zsh_h::TERM_UNKNOWN;
5582
5583/// Port of `uidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4698`. C body:
5584/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
5585/// C body (2 lines):
5586///   `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
5587/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5588pub fn uidsetfn(x: i64) {                                                    // c:4698
5589    if unsafe { libc::setuid(x as libc::uid_t) } != 0 {                      // c:4701
5590        zerr(&format!("failed to change user ID: {}", std::io::Error::last_os_error())); // c:4702
5591    }
5592}
5593
5594/// Port of `euidgetfn(UNUSED(Param pm))` from `Src/params.c:4710`. C body:
5595/// `return geteuid();`
5596/// WARNING: param names don't match C — Rust=() vs C=(pm)
5597pub fn euidgetfn() -> i64 {
5598    unsafe { libc::geteuid() as i64 }
5599}
5600
5601/// Port of `euidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4719`. C body:
5602/// `if (seteuid((uid_t)x)) zerr("failed to change effective user ID: %e", errno);`
5603/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5604pub fn euidsetfn(x: i64) {                                                   // c:4719
5605    if unsafe { libc::seteuid(x as libc::uid_t) } != 0 {                     // c:4722
5606        zerr(&format!("failed to change effective user ID: {}", std::io::Error::last_os_error())); // c:4723
5607    }
5608}
5609
5610/// Port of `gidgetfn(UNUSED(Param pm))` from `Src/params.c:4731`. C body: `return getgid();`
5611/// WARNING: param names don't match C — Rust=() vs C=(pm)
5612pub fn gidgetfn() -> i64 {
5613    unsafe { libc::getgid() as i64 }
5614}
5615
5616/// Port of `gidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4740`. C body:
5617/// `if (setgid((gid_t)x)) zerr("failed to change group ID: %e", errno);`
5618/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5619pub fn gidsetfn(x: i64) {                                                    // c:4740
5620    if unsafe { libc::setgid(x as libc::gid_t) } != 0 {                      // c:4743
5621        zerr(&format!("failed to change group ID: {}", std::io::Error::last_os_error())); // c:4744
5622    }
5623}
5624
5625/// Port of `egidgetfn(UNUSED(Param pm))` from `Src/params.c:4752`. C body: `return getegid();`
5626/// WARNING: param names don't match C — Rust=() vs C=(pm)
5627pub fn egidgetfn() -> i64 {
5628    unsafe { libc::getegid() as i64 }
5629}
5630
5631/// Port of `egidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4761`. C body:
5632/// `if (setegid((gid_t)x)) zerr("failed to change effective group ID: %e", errno);`
5633/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5634pub fn egidsetfn(x: i64) {                                                   // c:4761
5635    if unsafe { libc::setegid(x as libc::gid_t) } != 0 {                     // c:4764
5636        zerr(&format!("failed to change effective group ID: {}", std::io::Error::last_os_error())); // c:4765
5637    }
5638}
5639
5640/// Port of `ttyidlegetfn(UNUSED(Param pm))` from `Src/params.c:4771`. C body:
5641/// ```c
5642/// struct stat ttystat;
5643/// if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;
5644/// return time(NULL) - ttystat.st_atime;
5645/// ```
5646/// Rust port reads stdin (fd 0) — closest match to `SHTTY` the
5647/// shell tracks as the controlling-tty fd. Returns -1 if stdin is
5648/// not a tty.
5649/// WARNING: param names don't match C — Rust=() vs C=(pm)
5650pub fn ttyidlegetfn() -> i64 {
5651    // c:4776 — `if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;`
5652    // The previous Rust port hardcoded fd 0 (stdin) which is wrong
5653    // when SHTTY was opened on a non-stdin file descriptor (e.g.
5654    // `zsh < script` where stdin is a file but the controlling tty
5655    // was opened separately). C tracks the actual SHTTY fd.
5656    let shtty = crate::ported::init::SHTTY.load(std::sync::atomic::Ordering::SeqCst);
5657    if shtty == -1 {                                                          // c:4776
5658        return -1;
5659    }
5660    let mut st: libc::stat = unsafe { std::mem::zeroed() };
5661    if unsafe { libc::fstat(shtty, &mut st) } != 0 {                          // c:4776
5662        return -1;
5663    }
5664    let now = SystemTime::now()
5665        .duration_since(UNIX_EPOCH)
5666        .unwrap_or_default()
5667        .as_secs() as i64;
5668    now - st.st_atime as i64                                                  // c:4779
5669}
5670
5671// -----------------------------------------------------------
5672// $IFS / $HOME / $TERM / $WORDCHARS / $TERMINFO / $TERMINFO_DIRS
5673// $KEYBOARD_HACK / $HISTCHARS / $_  — string-state callbacks.
5674// -----------------------------------------------------------
5675
5676/// Port of `ifsgetfn(UNUSED(Param pm))` from `Src/params.c:4784`. C body: `return ifs;`
5677/// WARNING: param names don't match C — Rust=() vs C=(pm)
5678pub fn ifsgetfn() -> String {
5679    ifs_lock().lock().expect("ifs poisoned").clone()
5680}
5681
5682/// Port of `ifssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4793`. C body:
5683/// `zsfree(ifs); ifs = x; inittyptab();`
5684/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5685pub fn ifssetfn(x: String) {
5686    *ifs_lock().lock().expect("ifs poisoned") = x;
5687    // c:4795 — `inittyptab()` rebuilds the typtab[] ISEP/IWSEP bits
5688    // from the new IFS. Without this, every word-split path stays
5689    // pinned to the old separator set and silently mis-splits.
5690    crate::ported::utils::inittyptab();
5691}
5692
5693// -----------------------------------------------------------
5694// Locale callbacks: $LANG, $LC_*, setlang
5695// -----------------------------------------------------------
5696
5697/// Port of `clear_mbstate()` from `Src/params.c:4831`. C body:
5698/// `mb_charinit(); clear_shiftstate();`
5699///
5700/// WARNING: zshrs uses Rust's UTF-8 native handling so multibyte
5701/// state machines aren't kept; this is a no-op pinned to the
5702/// C name for parity.
5703/// (under `MULTIBYTE_SUPPORT`):
5704/// ```c
5705/// mb_charinit();        /* utils.c */
5706/// clear_shiftstate();   /* pattern.c */
5707/// ```
5708/// Resets the mbstate_t globals after LC_CTYPE changes (NetBSD-9
5709/// requires this). Rust port forwards to the matching helpers.
5710pub fn clear_mbstate() {
5711    // mb_charinit / clear_shiftstate not yet ported; once they are
5712    // (Src/utils.c, Src/pattern.c) wire the calls here.
5713}
5714
5715/// Port of `static struct localename lc_names[]` from `Src/params.c:4805-4825`.
5716/// C body:
5717/// ```c
5718/// static struct localename {
5719///     char *name;
5720///     int category;
5721/// } lc_names[] = {
5722///     {"LC_COLLATE", LC_COLLATE},
5723///     {"LC_CTYPE", LC_CTYPE},
5724///     {"LC_MESSAGES", LC_MESSAGES},
5725///     {"LC_NUMERIC", LC_NUMERIC},
5726///     {"LC_TIME", LC_TIME},
5727///     {NULL, 0}
5728/// };
5729/// ```
5730///
5731/// The C source guards each entry under `#ifdef LC_*`; libc on
5732/// macOS/Linux defines all five so the Rust port simply lists them.
5733const LC_NAMES: &[(&str, libc::c_int)] = &[
5734    ("LC_COLLATE", libc::LC_COLLATE),     // c:4810
5735    ("LC_CTYPE", libc::LC_CTYPE),         // c:4813
5736    ("LC_MESSAGES", libc::LC_MESSAGES),   // c:4816
5737    ("LC_NUMERIC", libc::LC_NUMERIC),     // c:4819
5738    ("LC_TIME", libc::LC_TIME),           // c:4822
5739];
5740
5741/// Port of `setlang(char *x)` from `Src/params.c:4842`.
5742///
5743/// C body (c:4842-4869):
5744/// ```c
5745/// if ((x2 = getsparam_u("LC_ALL")) && *x2) return;
5746/// setlocale(LC_ALL, x ? unmeta(x) : "");
5747/// clear_mbstate();
5748/// queue_signals();
5749/// for (ln = lc_names; ln->name; ln++)
5750///     if ((x = getsparam_u(ln->name)) && *x)
5751///         setlocale(ln->category, x);
5752/// unqueue_signals();
5753/// inittyptab();
5754/// ```
5755///
5756/// The previous Rust port skipped the actual `setlocale(LC_ALL, ...)`
5757/// libc call and just set the LANG env var. C invokes libc
5758/// setlocale to actually change the program's locale state —
5759/// required so any libc calls during shell execution (e.g.,
5760/// `iswctype`, `mbrtowc`) use the new locale's classification.
5761///
5762/// Also skipped: the per-LC_* override loop (c:4866-4868) which
5763/// re-applies category-specific settings after the global
5764/// LC_ALL set. The Rust port doesn't yet have the lc_names
5765/// table, but we can at least respect the canonical sequence.
5766pub fn setlang(x: Option<&str>) {                                            // c:4842
5767    // c:4847 — `if ((x2 = getsparam_u("LC_ALL")) && *x2) return;`
5768    if let Some(lc_all) = getsparam_u("LC_ALL") {                            // c:4847
5769        if !lc_all.is_empty() {
5770            return;
5771        }
5772    }
5773    // c:4860 — `setlocale(LC_ALL, x ? unmeta(x) : "");`
5774    let locale_arg = match x {
5775        Some(s) => crate::ported::utils::unmeta(s),
5776        None => String::new(),
5777    };
5778    // The previous Rust port skipped the libc setlocale call.
5779    // Without it, libc's locale state (used by iswctype, mbrtowc,
5780    // etc.) stays pinned to whatever the shell inherited from
5781    // its parent — diverging from C which actively changes the
5782    // running program's locale.
5783    let cstr = std::ffi::CString::new(locale_arg.as_bytes()).unwrap_or_default();
5784    unsafe {
5785        libc::setlocale(libc::LC_ALL, cstr.as_ptr());                         // c:4860
5786    }
5787    // Mirror to env so subsequent `getsparam("LANG")` reads agree.
5788    if let Some(s) = x {
5789        env::set_var("LANG", s);
5790    }
5791    clear_mbstate();                                                          // c:4861
5792    // c:4863-4867 — `for (ln = lc_names; ln->name; ln++) if ((x =
5793    // getsparam_u(ln->name)) && *x) setlocale(ln->category, x);`
5794    // After the global LC_ALL setlocale, any explicitly-set LC_*
5795    // category overrides its slot. The previous Rust port skipped
5796    // this loop, so `LC_NUMERIC=tr_TR.UTF-8 LANG=C` would leave
5797    // numeric formatting on C rather than tr_TR.
5798    for (name, category) in LC_NAMES {                                      // c:4863
5799        if let Some(val) = getsparam_u(name) {                              // c:4866 getsparam_u
5800            if !val.is_empty() {
5801                let cat_cstr = std::ffi::CString::new(val.as_bytes())
5802                    .unwrap_or_default();
5803                unsafe {
5804                    libc::setlocale(*category, cat_cstr.as_ptr());            // c:4867
5805                }
5806            }
5807        }
5808    }
5809    // c:4868 — `inittyptab();`. The locale change may shift which
5810    // bytes are isalpha/isalnum/etc under the typtab init, so the
5811    // table must be rebuilt.
5812    crate::ported::utils::inittyptab();
5813}
5814
5815/// Port of `lc_allsetfn(Param pm, char *x)` from `Src/params.c:4873`.
5816///
5817/// C body (c:4873-4894):
5818/// ```c
5819/// strsetfn(pm, x);
5820/// if (!x || !*x) {
5821///     x = getsparam_u("LANG");
5822///     if (x && *x) {
5823///         queue_signals();
5824///         setlang(x);
5825///         unqueue_signals();
5826///     }
5827/// } else {
5828///     setlocale(LC_ALL, unmeta(x));
5829///     clear_mbstate();
5830///     inittyptab();
5831/// }
5832/// ```
5833///
5834/// The previous Rust port for the non-empty case set the env
5835/// var via `env::set_var("LC_ALL", &s)` but skipped THREE
5836/// pieces:
5837///   1. `setlocale(LC_ALL, unmeta(x))` — actively changes the
5838///      program's locale per c:4890.
5839///   2. `unmeta(x)` — strips Meta-encoded bytes before passing
5840///      to libc setlocale per c:4890.
5841///   3. `inittyptab()` — rebuilds the typtab for the new
5842///      LC_CTYPE per c:4892.
5843///
5844/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5845pub fn lc_allsetfn(x: Option<String>) {                                       // c:4873
5846    match x {
5847        None => setlang(getsparam_u("LANG").as_deref()),                      // c:4882 getsparam_u
5848        Some(s) if s.is_empty() => {                                          // c:4881
5849            // c:4881-4884 — empty x falls back to setlang(getsparam_u("LANG")).
5850            setlang(getsparam_u("LANG").as_deref());                          // c:4882
5851        }
5852        Some(s) => {
5853            // c:4889 — `setlocale(LC_ALL, unmeta(x));`
5854            let unmeta = crate::ported::utils::unmeta(&s);                    // c:4889 unmeta(x)
5855            let cstr = std::ffi::CString::new(unmeta.as_bytes())
5856                .unwrap_or_default();
5857            unsafe {
5858                libc::setlocale(libc::LC_ALL, cstr.as_ptr());                 // c:4890
5859            }
5860            env::set_var("LC_ALL", &s);
5861            clear_mbstate();                                                  // c:4891
5862            // c:4892 — `inittyptab();` rebuild typtab for new LC_CTYPE.
5863            crate::ported::utils::inittyptab();                               // c:4892
5864        }
5865    }
5866}
5867
5868/// Port of `langsetfn(Param pm, char *x)` from `Src/params.c:4898`. C body:
5869/// `strsetfn(pm, x); setlang(unmeta(x));`
5870///
5871/// `unmeta(x)` strips Meta-encoding before passing to libc
5872/// `setlocale` — locale names are normally ASCII but Meta bytes
5873/// in the assigned value (from a `LANG="$value"` round-trip
5874/// through metafied param storage) would otherwise reach
5875/// setlocale literally. The previous Rust port passed raw `x`
5876/// without unmeta'ing — divergent.
5877///
5878/// `strsetfn(pm, x)` stores the value in the param slot. The Rust
5879/// adaptation doesn't have a `pm` in scope; the assign path that
5880/// reaches langsetfn already stored the value in the paramtab,
5881/// so this body only runs the post-store side effect (locale).
5882///
5883/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x).
5884pub fn langsetfn(x: String) {                                                 // c:4898
5885    // c:4901 — `setlang(unmeta(x));`. Strip Meta bytes before
5886    // passing to libc setlocale.
5887    let unmeta_x = crate::ported::utils::unmeta(&x);                          // c:4901 unmeta(x)
5888    setlang(Some(&unmeta_x));
5889}
5890
5891/// Port of `lcsetfn(Param pm, char *x)` from `Src/params.c:4906`. C body
5892/// (c:4912-4931):
5893/// ```c
5894/// strsetfn(pm, x);
5895/// if ((x2 = getsparam("LC_ALL")) && *x2) return;
5896/// queue_signals();
5897/// if (!x || !*x) x = getsparam("LANG");
5898/// if (x && *x) {
5899///     for (ln = lc_names; ln->name; ln++)
5900///         if (!strcmp(ln->name, pm->node.nam))
5901///             setlocale(ln->category, unmeta(x));
5902/// }
5903/// unqueue_signals();
5904/// clear_mbstate();
5905/// inittyptab();
5906/// ```
5907///
5908/// Two divergences in the previous Rust port:
5909///   1. Missed `inittyptab()` call at c:4932 — LC_CTYPE changes
5910///      shift which bytes are isalpha/iblank/isep, but the
5911///      typtab stayed pinned to the prior locale's classes.
5912///      `setopt POSIX_BUILTINS; LC_NUMERIC=tr_TR.UTF-8; ...`
5913///      would still classify with the old C locale's tables.
5914///   2. The Meta-unmeta'ing on the value passed to setlocale
5915///      wasn't applied. C uses `setlocale(cat, unmeta(x))`.
5916pub fn lcsetfn(pm: &str, x: Option<String>) {                                 // c:4906
5917    // c:4912-4913 — `if ((x2 = getsparam("LC_ALL")) && *x2) return;`.
5918    if let Some(lc_all) = getsparam("LC_ALL") {                              // c:4912
5919        if !lc_all.is_empty() {
5920            return;
5921        }
5922    }
5923    // c:4916-4917 — `if (!x || !*x) x = getsparam("LANG");`.
5924    let val = x
5925        .filter(|s| !s.is_empty())
5926        .or_else(|| getsparam("LANG").filter(|s| !s.is_empty()));            // c:4917
5927    // c:4924-4928 — apply `setlocale(category, unmeta(x))` for the
5928    // matching LC_* category. The previous Rust port skipped the
5929    // actual libc setlocale call and only wrote the env var, so
5930    // assigning `LC_NUMERIC=tr_TR.UTF-8` never flipped libc's
5931    // numeric-formatting category.
5932    if let Some(v) = val {
5933        let unmeta = crate::ported::utils::unmeta(&v);                        // c:4928 unmeta(x)
5934        env::set_var(pm, &unmeta);
5935        for (name, category) in LC_NAMES {                                  // c:4925
5936            if *name == pm {                                                  // c:4926 strcmp
5937                let cstr = std::ffi::CString::new(unmeta.as_bytes())
5938                    .unwrap_or_default();
5939                unsafe {
5940                    libc::setlocale(*category, cstr.as_ptr());                // c:4927
5941                }
5942                break;
5943            }
5944        }
5945    }
5946    // c:4930 — `clear_mbstate();` — LC_CTYPE may have changed.
5947    clear_mbstate();
5948    // c:4931 — `inittyptab();` — rebuild typtab classifications.
5949    // The previous Rust port skipped this; char-classification
5950    // predicates would stay pinned to the prior locale's class
5951    // set even after `LC_CTYPE=` was assigned.
5952    crate::ported::utils::inittyptab();                                       // c:4931
5953}
5954
5955/// Direct port of `static void argzerosetfn(UNUSED(Param pm),
5956/// char *x)` from `Src/params.c:4937-4946`. Setter for `$0` —
5957/// POSIX mode rejects assignment (read-only), zsh mode replaces
5958/// `argzero`.
5959///
5960/// C body:
5961///   if (x) {
5962///     if (isset(POSIXARGZERO))
5963///       zerr("read-only variable: 0");
5964///     else {
5965///       zsfree(argzero);
5966///       argzero = ztrdup(x);
5967///     }
5968///     zsfree(x);
5969///   }
5970/// Port of `argzerosetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4937`.
5971/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5972pub fn argzerosetfn(x: String) {                                             // c:4937
5973    // c:4937 — if (x).
5974    if !x.is_empty() {
5975        // c:4940 — isset(POSIXARGZERO) reject.
5976        if isset(crate::ported::zsh_h::POSIXARGZERO) {
5977            crate::ported::utils::zerr("read-only variable: 0");             // c:4941
5978        } else {
5979            // c:4943-4944 — zsfree(argzero); argzero = ztrdup(x).
5980            crate::ported::utils::set_argzero(Some(crate::ported::utils::ztrdup(&x)));
5981        }
5982        // c:4946 — `zsfree(x)`. Rust drop handles via move.
5983    }
5984}
5985
5986// -----------------------------------------------------------
5987// $0 / $#
5988// -----------------------------------------------------------
5989
5990/// Port of `argzerogetfn(UNUSED(Param pm))` from `Src/params.c:4954`. C body:
5991///     `return isset(POSIXARGZERO) ? posixzero : argzero;`
5992///
5993/// Both `argzero` and `posixzero` live in `utils.rs` (OnceLock storage).
5994/// The previous Rust port ALWAYS returned `argzero`, defeating the
5995/// POSIXARGZERO option entirely. After `exec -a foo` or function-call
5996/// argv-rewrite, `$0` under POSIXARGZERO should report the ORIGINAL
5997/// startup `argv[0]`, not the rewritten name. Now wired via
5998/// `isset(POSIXARGZERO)` + the canonical posixzero accessor.
5999/// WARNING: param names don't match C — Rust=() vs C=(pm)
6000pub fn argzerogetfn() -> String {
6001    if isset(crate::ported::zsh_h::POSIXARGZERO) {                            // c:4958
6002        crate::ported::utils::posixzero().unwrap_or_default()                 // c:4959
6003    } else {
6004        crate::ported::utils::argzero().unwrap_or_default()                   // c:4960
6005    }
6006}
6007
6008// -----------------------------------------------------------
6009// $HISTSIZE / $SAVEHIST
6010// -----------------------------------------------------------
6011
6012/// Port of `histsizegetfn(UNUSED(Param pm))` from `Src/params.c:4965`. C body: `return histsiz;`
6013/// WARNING: param names don't match C — Rust=() vs C=(pm)
6014pub fn histsizegetfn() -> i64 {
6015    *histsiz_lock().lock().expect("histsiz poisoned")
6016}
6017
6018/// Port of `histsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4974`. C body:
6019/// `if ((histsiz = v) < 1) histsiz = 1; resizehistents();`
6020///
6021/// The previous Rust port noted `resizehistents()` as "pending the
6022/// history-table port", but `crate::ported::hist::resizehistents`
6023/// IS available — was a stale comment. Without the resize call,
6024/// setting HISTSIZE to a smaller value left the in-memory ring
6025/// over-sized until the next implicit prune (next entry added).
6026/// Wired the call now per c:4977.
6027/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
6028pub fn histsizesetfn(v: i64) {
6029    *histsiz_lock().lock().expect("histsiz poisoned") = v.max(1);
6030    // c:4977 — mirror into the hist.rs atomic so resizehistents()
6031    // sees the new size, then trigger the prune.
6032    crate::ported::hist::histsiz.store(v.max(1), std::sync::atomic::Ordering::SeqCst);
6033    crate::ported::hist::resizehistents();                                   // c:4977
6034}
6035
6036/// Port of `savehistsizegetfn(UNUSED(Param pm))` from `Src/params.c:4985`. C body:
6037/// `return savehistsiz;`
6038/// WARNING: param names don't match C — Rust=() vs C=(pm)
6039pub fn savehistsizegetfn() -> i64 {
6040    *savehistsiz_lock().lock().expect("savehistsiz poisoned")
6041}
6042
6043/// Port of `savehistsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4994`. C body:
6044/// `if ((savehistsiz = v) < 0) savehistsiz = 0;`
6045///
6046/// The Rust port has TWO mirrors of `savehistsiz`: a `Mutex<i64>`
6047/// in params.rs (read by `savehistsizegetfn`) AND an AtomicI64
6048/// in hist.rs (read by the history-file writer at
6049/// `Src/hist.c:savehistfile` per c:3878). The previous Rust port
6050/// only wrote to the params.rs lock; `hist.rs::savehistsiz`
6051/// stayed pinned to its initial 0 value, so `SAVEHIST=10000`
6052/// would store the limit in `savehistsiz_lock` (visible to
6053/// `$SAVEHIST` reads) but the history-file writer would still
6054/// cap at the original AtomicI64 value (effectively saving zero
6055/// lines). Sync both storages so reads + writes agree.
6056///
6057/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
6058pub fn savehistsizesetfn(v: i64) {                                            // c:4994
6059    let clamped = v.max(0);                                                   // c:4998
6060    *savehistsiz_lock().lock().expect("savehistsiz poisoned") = clamped;
6061    // Mirror to hist.rs::savehistsiz so the writer-side cap
6062    // matches the just-assigned value. C uses a single global;
6063    // the Rust port's twin-storage requires sync writes.
6064    crate::ported::hist::savehistsiz.store(
6065        clamped, std::sync::atomic::Ordering::SeqCst);                        // c:4994
6066}
6067
6068/// Port of `errnosetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:5004`. C body:
6069/// `errno = (int)x; if ((zlong)errno != x) zwarn("errno truncated on assignment");`
6070///
6071/// Rust note: `errno` is a libc thread-local; Rust uses `std::io::Error`
6072/// which captures the *last* call. To set errno for subsequent
6073/// `last_os_error()` reads on macOS / Linux, write through the libc
6074/// `__error()`/`__errno_location()` accessor.
6075/// C body (Src/params.c:5004):
6076///     `errno = (int)x;
6077///      if ((zlong)errno != x) zwarn("errno truncated on assignment");`
6078/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6079pub fn errnosetfn(x: i64) {                                                  // c:5004
6080    let truncated = x as i32;
6081    unsafe { *errno_ptr() = truncated; }                                     // c:5006 errno = (int)x
6082    // c:5009-5010 — C uses `zwarn` (informational), NOT `zerr`. The
6083    // store happens unconditionally; the warning fires only on
6084    // truncation. Previously used `zerr` — divergent.
6085    if truncated as i64 != x {                                               // c:5008
6086        crate::ported::utils::zwarn("errno truncated on assignment");        // c:5009
6087    }
6088}
6089
6090/// !!! RUST-ONLY HELPER — no direct C counterpart. C accesses
6091/// `errno` through the standard macro which the compiler resolves
6092/// to the per-platform getter (`__error()` on macOS, `__errno_location()`
6093/// on Linux). Rust libc exposes both as raw FFI; this helper picks
6094/// the right one per target so errnosetfn/errnogetfn stay one-liners.
6095#[inline]
6096unsafe fn errno_ptr() -> *mut libc::c_int {
6097    #[cfg(target_os = "macos")] { libc::__error() }
6098    #[cfg(target_os = "linux")] { libc::__errno_location() }
6099    #[cfg(not(any(target_os = "macos", target_os = "linux")))] { std::ptr::null_mut() }
6100}
6101
6102/// Port of `errnogetfn(UNUSED(Param pm))` from `Src/params.c:5015`. C body: `return errno;`
6103///
6104/// Reads the libc errno directly through the per-platform accessor
6105/// (matching C's `return errno;` semantics). Previously routed
6106/// through `std::io::Error::last_os_error()` which is NOT errno —
6107/// it's a snapshot taken at the most recent stdlib syscall. That
6108/// silently broke `$ERRNO` round-trip: `ERRNO=42` followed by
6109/// `$ERRNO` could return any stale value.
6110/// WARNING: param names don't match C — Rust=() vs C=(pm)
6111pub fn errnogetfn() -> i64 {
6112    let p = unsafe { errno_ptr() };                                          // c:5017 return errno
6113    if p.is_null() {
6114        // Non-Linux/macOS fallback: best-effort via std API.
6115        std::io::Error::last_os_error().raw_os_error().unwrap_or(0) as i64
6116    } else {
6117        unsafe { *p as i64 }
6118    }
6119}
6120
6121/// Port of `keyboardhackgetfn(UNUSED(Param pm))` from `Src/params.c:5024`. C body:
6122/// `static char buf[2]; buf[0] = keyboardhackchar; return buf;`
6123/// WARNING: param names don't match C — Rust=() vs C=(pm)
6124pub fn keyboardhackgetfn() -> String {
6125    let c = *keyboardhack_lock()
6126        .lock()
6127        .expect("keyboardhack poisoned");
6128    if c == 0 {
6129        String::new()
6130    } else {
6131        (c as char).to_string()
6132    }
6133}
6134
6135/// Port of `keyboardhacksetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5040-5060`. C body:
6136/// ```c
6137/// if (x) {
6138///     unmetafy(x, &len);
6139///     if (len > 1) { len = 1; zwarn("Only one KEYBOARD_HACK character can be defined"); }
6140///     for (i = 0; i < len; i++)
6141///         if (!isascii((unsigned char) x[i])) {
6142///             zwarn("KEYBOARD_HACK can only contain ASCII characters");
6143///             return;
6144///         }
6145///     keyboardhackchar = len ? (unsigned char) x[0] : '\0';
6146/// } else
6147///     keyboardhackchar = '\0';
6148/// ```
6149///
6150/// The C source `unmetafy(x, &len)` strips Meta-encoded prefix
6151/// bytes (collapsing every `Meta + (b^32)` pair to the original
6152/// byte) BEFORE the length and ASCII checks. The previous Rust
6153/// port skipped unmetafy, so:
6154///   - `len > 1` warning fired on every assignment of a Meta-
6155///     encoded single byte (the byte-length was 2 pre-unmetafy).
6156///   - ASCII check ran against the raw Meta byte (0x83) instead
6157///     of the demetafied result, falsely rejecting valid ASCII
6158///     characters that happened to round-trip through Meta
6159///     encoding in the assignment pipeline.
6160///
6161/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6162pub fn keyboardhacksetfn(x: String) {                                         // c:5040
6163    // c:5044 — `unmetafy(x, &len)` — strip Meta-encoded pairs.
6164    // Run on the byte buffer so the protocol matches C's pointer
6165    // walk; the Rust `unmeta()` helper does the same fold.
6166    let unmeta = crate::ported::utils::unmeta(&x);                            // c:5044 unmetafy(x)
6167    let bytes = unmeta.as_bytes();
6168    // c:5046-5049 — `if (len > 1) { len = 1; zwarn(...); }`. The
6169    // length check happens AFTER unmetafy so a 2-byte Meta pair
6170    // representing a single byte doesn't trigger the warning.
6171    if bytes.len() > 1 {
6172        crate::ported::utils::zwarn("Only one KEYBOARD_HACK character can be defined");
6173    }
6174    let c = bytes.first().copied().unwrap_or(0);
6175    // c:5050-5054 — ASCII check runs on the unmetafied byte, NOT
6176    // the raw Meta byte. With unmetafy now in place this works as
6177    // C intended.
6178    if c >= 0x80 {                                                            // c:5051 !isascii(...)
6179        crate::ported::utils::zwarn("KEYBOARD_HACK can only contain ASCII characters");
6180        return;
6181    }
6182    // c:5056 — `keyboardhackchar = len ? (unsigned char) x[0] : '\0';`
6183    *keyboardhack_lock().lock().expect("keyboardhack poisoned") = c;
6184}
6185
6186/// Port of `histcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5064`. C body:
6187/// ```c
6188/// static char buf[4];
6189/// buf[0] = bangchar; buf[1] = hatchar; buf[2] = hashchar; buf[3] = '\0';
6190/// return buf;
6191/// ```
6192/// Reads from the three canonical atomic globals
6193/// (`crate::ported::hist::{bangchar, hatchar, hashchar}`) to mirror C
6194/// which reads from three separate `unsigned char` globals.
6195/// WARNING: param names don't match C — Rust=() vs C=(pm)
6196pub fn histcharsgetfn() -> String {
6197    use std::sync::atomic::Ordering;
6198    let b = crate::ported::hist::bangchar.load(Ordering::SeqCst) as u8;
6199    let h = crate::ported::hist::hatchar.load(Ordering::SeqCst) as u8;
6200    let p = crate::ported::hist::hashchar.load(Ordering::SeqCst) as u8;
6201    // c:5068-5073 — terminal NUL trims unset chars (default-`!^#` is
6202    // 3 non-NUL bytes); explicit NULs are skipped to match C `buf[3]
6203    // = '\0'` C-string truncation semantics.
6204    let mut s = String::new();
6205    for &byte in &[b, h, p] {
6206        if byte != 0 {
6207            s.push(byte as char);
6208        }
6209    }
6210    s
6211}
6212
6213/// Port of `histcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5081`. C body
6214/// validates ASCII, takes up to 3 chars; defaults `!^#` if NULL.
6215///
6216/// C `unmetafy(x, &len)` (c:5086) strips Meta-encoded pairs BEFORE
6217/// the length truncation and ASCII guard. The previous Rust port
6218/// skipped unmetafy entirely:
6219///   - `len > 3` truncation ran on raw byte length, so a Meta-pair
6220///     would inflate the byte count and skip valid chars.
6221///   - ASCII check ran against raw Meta bytes (0x83), falsely
6222///     rejecting valid round-tripped values.
6223///
6224/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6225pub fn histcharssetfn(x: Option<String>) {                                    // c:5081
6226    use std::sync::atomic::Ordering;
6227    let new_chars: [u8; 3] = match x {
6228        None => {
6229            // c:5100-5103 — defaults `!^#` when x is NULL.
6230            [b'!', b'^', b'#']
6231        }
6232        Some(s) => {
6233            // c:5086 — `unmetafy(x, &len)`. Strip Meta pairs first.
6234            let unmeta = crate::ported::utils::unmeta(&s);                   // c:5086 unmetafy(x)
6235            let bytes = unmeta.as_bytes();
6236            // c:5087-5088 — `if (len > 3) len = 3;`. Truncation
6237            // applies AFTER unmetafy.
6238            let bytes = if bytes.len() > 3 { &bytes[..3] } else { bytes };
6239            for &b in bytes.iter() {
6240                if b >= 0x80 {                                          // c:5090-5093
6241                    // c:5091 — C uses `zwarn` (informational), NOT
6242                    // `zerr` (fatal). Function returns early without
6243                    // updating any globals.
6244                    crate::ported::utils::zwarn(
6245                        "HISTCHARS can only contain ASCII characters");
6246                    return;
6247                }
6248            }
6249            // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
6250            // C uses `len ? x[0] : '\0'` etc — for short strings the
6251            // unset bytes are NUL.
6252            let mut chars = [0u8; 3];
6253            for (i, &b) in bytes.iter().enumerate() {
6254                chars[i] = b;
6255            }
6256            chars
6257        }
6258    };
6259    // c:5079 — set histchars table.
6260    *histchars_lock().lock().expect("histchars poisoned") = new_chars;
6261    // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
6262    // Sync all three per-char atomic globals so lex/hist callers
6263    // see the new HISTCHARS. (Previously hashchar was a `const char`
6264    // in lex.rs — promoted to atomic this iteration.)
6265    crate::ported::hist::bangchar.store(new_chars[0] as i32, Ordering::SeqCst);
6266    crate::ported::hist::hatchar.store(new_chars[1] as i32, Ordering::SeqCst);
6267    crate::ported::hist::hashchar.store(new_chars[2] as i32, Ordering::SeqCst);
6268    // c:5104 — `inittyptab();`. The bangchar special bit in typtab
6269    // depends on the current `bangchar` global; reseed.
6270    crate::ported::utils::inittyptab();
6271}
6272
6273/// Port of `homegetfn(UNUSED(Param pm))` from `Src/params.c:5109`. C body: `return home;`
6274/// WARNING: param names don't match C — Rust=() vs C=(pm)
6275pub fn homegetfn() -> String {
6276    home_lock().lock().expect("home poisoned").clone()
6277}
6278
6279/// Port of `homesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5118`. C body:
6280/// ```c
6281/// zsfree(home);
6282/// if (x && isset(CHASELINKS) && (home = xsymlink(x, 0)))
6283///     zsfree(x);
6284/// else
6285///     home = x ? x : ztrdup("");
6286/// finddir(NULL);
6287/// ```
6288/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6289pub fn homesetfn(x: String) {
6290    // c:5121-5126 — CHASELINKS path resolves symlinks before storing.
6291    // Falls through to the plain `x` store when CHASELINKS is off or
6292    // xsymlink fails.
6293    let resolved = if !x.is_empty()
6294        && crate::ported::zsh_h::isset(crate::ported::zsh_h::CHASELINKS)
6295    {
6296        crate::ported::utils::xsymlink(&x).unwrap_or(x)
6297    } else {
6298        x
6299    };
6300    *home_lock().lock().expect("home poisoned") = resolved;
6301    // c:5127 — `finddir(NULL)` invalidates zsh's cached named-directory
6302    // lookups. zshrs's finddir port has no cache (per hashnameddir.rs
6303    // createnameddirtable note); the call is a no-op here.
6304}
6305
6306/// Port of `wordcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5132`. C body:
6307/// `return wordchars;`
6308/// WARNING: param names don't match C — Rust=() vs C=(pm)
6309pub fn wordcharsgetfn() -> String {
6310    wordchars_lock()
6311        .lock()
6312        .expect("wordchars poisoned")
6313        .clone()
6314}
6315
6316/// Port of `wordcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5141`. C body:
6317/// `zsfree(wordchars); wordchars = x; inittyptab();`
6318/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6319pub fn wordcharssetfn(x: String) {
6320    *wordchars_lock().lock().expect("wordchars poisoned") = x;
6321    // c:5143 — `inittyptab()` rebuilds typtab IWORD bits from the
6322    // new WORDCHARS. Without this, every IWORD lookup stays pinned
6323    // to the old set and silently mis-classifies word boundaries.
6324    crate::ported::utils::inittyptab();
6325}
6326
6327/// Port of `underscoregetfn(UNUSED(Param pm))` from `Src/params.c:5152`. C body:
6328/// `char *u = dupstring(zunderscore); untokenize(u); return u;`
6329///
6330/// C runs `untokenize(u)` on the cloned string before returning, so
6331/// ITOK bytes (Pound..Nularg per `Src/zsh.h:159-194`) in `$_` get
6332/// replaced/dropped via the canonical `ztokens[]` table. The previous
6333/// Rust port skipped untokenize entirely — every `$_` read that
6334/// included a lexer-injected token byte exposed the raw token in user
6335/// output (e.g. `$_` containing `$cmd` would surface as raw Stringg
6336/// instead of the literal `$`).
6337/// WARNING: param names don't match C — Rust=() vs C=(pm)
6338pub fn underscoregetfn() -> String {
6339    let u = zunderscore_lock()
6340        .lock()
6341        .expect("zunderscore poisoned")
6342        .clone();
6343    crate::ported::lex::untokenize(&u)                                        // c:5156 untokenize(u)
6344}
6345
6346/// Port of `term_reinit_from_pm()` from `Src/params.c:5163`.
6347/// C: `static void term_reinit_from_pm(void)` →
6348///   `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;
6349///    else init_term();`
6350pub fn term_reinit_from_pm() {                                               // c:5163
6351    // c:5167 — `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;`
6352    let interactive = crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
6353    let term = term_lock().lock().map(|s| s.clone()).unwrap_or_default();
6354    if !interactive || term.is_empty() {                                     // c:5167
6355        TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed);                 // c:5168
6356    } else {
6357        // c:5170 — `init_term();` lives in ZLE; flag the next prompt
6358        // to re-init via TERM_UNKNOWN so the lazy path picks it up.
6359        TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed);                 // c:5170
6360    }
6361}
6362
6363/// Port of `termgetfn(UNUSED(Param pm))` from `Src/params.c:5176`. C body: `return term;`
6364/// WARNING: param names don't match C — Rust=() vs C=(pm)
6365pub fn termgetfn() -> String {
6366    term_lock().lock().expect("term poisoned").clone()
6367}
6368
6369/// Port of `termsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5185`. C body:
6370/// `zsfree(term); term = x ? x : ""; term_reinit_from_pm();`
6371/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6372pub fn termsetfn(x: String) {
6373    *term_lock().lock().expect("term poisoned") = x;
6374    term_reinit_from_pm();
6375}
6376
6377/// Port of `terminfogetfn(UNUSED(Param pm))` from `Src/params.c:5196`. C body:
6378/// `return zsh_terminfo ? zsh_terminfo : "";`
6379/// WARNING: param names don't match C — Rust=() vs C=(pm)
6380pub fn terminfogetfn() -> String {
6381    zsh_terminfo_lock()
6382        .lock()
6383        .expect("zsh_terminfo poisoned")
6384        .clone()
6385}
6386
6387/// Port of `int rprompt_indent` from `Src/init.c`. Set to 1 by
6388/// `init_term()` and reset by `rprompt_indent_unsetfn` when the
6389/// `RPROMPT_INDENT` parameter is unset.
6390pub static RPROMPT_INDENT: std::sync::Mutex<i32> = std::sync::Mutex::new(1);
6391
6392/// Port of `terminfosetfn(Param pm, char *x)` from `Src/params.c:5205`. C body:
6393/// `zsfree(zsh_terminfo); zsh_terminfo = x; addenv if exported; term_reinit_from_pm();`
6394/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6395pub fn terminfosetfn(x: String) {
6396    *zsh_terminfo_lock()
6397        .lock()
6398        .expect("zsh_terminfo poisoned") = x.clone();
6399    env::set_var("TERMINFO", &x);
6400    term_reinit_from_pm();
6401}
6402
6403/// Port of `terminfodirsgetfn(UNUSED(Param pm))` from `Src/params.c:5224`. C body:
6404/// `return zsh_terminfodirs ? zsh_terminfodirs : "";`
6405/// WARNING: param names don't match C — Rust=() vs C=(pm)
6406pub fn terminfodirsgetfn() -> String {
6407    zsh_terminfodirs_lock()
6408        .lock()
6409        .expect("zsh_terminfodirs poisoned")
6410        .clone()
6411}
6412
6413/// Port of `terminfodirssetfn(Param pm, char *x)` from `Src/params.c:5233`. C body
6414/// mirrors `terminfosetfn` for the TERMINFO_DIRS env var.
6415/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6416pub fn terminfodirssetfn(x: String) {
6417    *zsh_terminfodirs_lock()
6418        .lock()
6419        .expect("zsh_terminfodirs poisoned") = x.clone();
6420    env::set_var("TERMINFO_DIRS", &x);
6421    term_reinit_from_pm();
6422}
6423
6424// -----------------------------------------------------------
6425// $pipestatus
6426// -----------------------------------------------------------
6427
6428/// Port of `pipestatgetfn(UNUSED(Param pm))` from `Src/params.c:5251`. C body
6429/// snapshots the `pipestats[]` C array as a heap-allocated
6430/// `char **`. Rust port returns the cloned snapshot.
6431/// WARNING: param names don't match C — Rust=() vs C=(pm)
6432pub fn pipestatgetfn() -> Vec<String> {
6433    pipestats_lock()
6434        .lock()
6435        .expect("pipestats poisoned")
6436        .iter()
6437        .map(|n| n.to_string())
6438        .collect()
6439}
6440
6441/// Port of `pipestatsetfn(UNUSED(Param pm), char **x)` from `Src/params.c:5270`. C body:
6442/// `for (i=0; *x && i<MAX_PIPESTATS; i++) pipestats[i] = atoi(*x++); numpipestats = i;`
6443/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6444pub fn pipestatsetfn(x: Option<Vec<String>>) {
6445    const MAX_PIPESTATS: usize = 256;
6446    let mut guard = pipestats_lock().lock().expect("pipestats poisoned");
6447    guard.clear();
6448    if let Some(v) = x {
6449        for s in v.iter().take(MAX_PIPESTATS) {
6450            guard.push(s.parse::<i32>().unwrap_or(0));
6451        }
6452    }
6453}
6454
6455/// Port of `arrfixenv(char *s, char **t)` from `Src/params.c:5285`. C body re-syncs
6456/// the env entry for an array param after mutation, joining with
6457/// the param's `joinchar`. Rust port joins with ':' (the default
6458/// for PATH-style arrays) and updates the env var.
6459/// Direct port of `void arrfixenv(char *s, char **t)` from
6460/// `Src/params.c:5285`. Re-syncs the env-side entry for an
6461/// array parameter after mutation. Order of operations (C body):
6462///   1. If `t == path`, flush the command-name cache (c:5291).
6463///   2. Look up the param node by name (c:5294); skip if
6464///      PM_HASHELEM is set (c:5300-5301).
6465///   3. Under ALLEXPORT, mark PM_EXPORTED (c:5304); always clear
6466///      PM_DEFAULTED (c:5305).
6467///   4. Skip if not PM_EXPORTED (c:5311-5312).
6468///   5. joinchar = ':' for PM_SPECIAL else
6469///      `((struct tieddata *)pm->u.data)->joinchar` (c:5314-5318).
6470///   6. `addenv(pm, t ? zjoin(t, joinchar, 1) : "")` (c:5319).
6471pub fn arrfixenv(s: &str, t: Option<&[String]>) {                            // c:5285
6472
6473    // c:5291 — `if (t == path) cmdnamtab->emptytable(cmdnamtab)`.
6474    // PATH change invalidates the command-name cache.
6475    if s == "PATH" || s == "path" {
6476        crate::ported::hashtable::emptycmdnamtable();
6477    }
6478
6479    // c:5294 — `pm = paramtab->getnode(paramtab, s)`.
6480    let pm_arc_data = {
6481        let tab = paramtab().read().unwrap();
6482        tab.get(s).map(|pm| (pm.node.flags, pm.gsu_a.is_some()))
6483    };
6484    let (flags, _has_gsu_a) = match pm_arc_data {
6485        Some(x) => x,
6486        None => {
6487            // No param yet — just sync via env::set_var as fallback.
6488            let val = t.map(|v| v.join(":")).unwrap_or_default();
6489            env::set_var(s, val);
6490            return;
6491        }
6492    };
6493
6494    // c:5300-5301 — `if (pm->flags & PM_HASHELEM) return`.
6495    if flags & PM_HASHELEM as i32 != 0 {
6496        return;
6497    }
6498
6499    // c:5304 — `if (isset(ALLEXPORT)) pm->flags |= PM_EXPORTED`.
6500    let allexport = isset(ALLEXPORT);
6501    // c:5305 — `pm->flags &= ~PM_DEFAULTED` always.
6502    {
6503        let mut tab = paramtab().write().unwrap();
6504        if let Some(pm) = tab.get_mut(s) {
6505            if allexport {
6506                pm.node.flags |= PM_EXPORTED as i32;
6507            }
6508            pm.node.flags &= !(PM_DEFAULTED as i32);
6509        }
6510    }
6511
6512    // c:5311-5312 — `if (!(pm->flags & PM_EXPORTED)) return`.
6513    let new_flags = {
6514        let tab = paramtab().read().unwrap();
6515        tab.get(s).map(|pm| pm.node.flags).unwrap_or(0)
6516    };
6517    if new_flags & PM_EXPORTED as i32 == 0 {
6518        return;
6519    }
6520
6521    // c:5314-5317 — joinchar selection.
6522    let joinchar = if new_flags & PM_SPECIAL as i32 != 0 {
6523        ':'                                                                  // c:5315
6524    } else {
6525        // c:5317 — tieddata.joinchar; not modelled in current Param —
6526        // default to ':' which is correct for all currently-tied
6527        // array params (PATH/CDPATH/FPATH/etc.).
6528        ':'
6529    };
6530
6531    // c:5319 — `addenv(pm, t ? zjoin(t, joinchar, 1) : "")`.
6532    let joined = match t {
6533        Some(arr) => arr.join(&joinchar.to_string()),
6534        None => String::new(),
6535    };
6536    addenv(s, &joined);
6537}
6538
6539/// Direct port of `int zputenv(char *str)` from
6540/// `Src/params.c:5325-5382` (USE_SET_UNSET_ENV branch). Splits
6541/// `str` at the first `=`, validates the name is in the portable
6542/// character set (rejects any byte >= 128), and calls
6543/// `setenv(name, value, 1)`.
6544///
6545/// C body walks `str` byte-by-byte looking for either a high-byte
6546/// (reject) or `=` (split). On a clean ASCII `name=value`, it
6547/// temporarily writes `\0` at the `=` to splice off the name,
6548/// calls setenv, then restores the `=`. On `=`-less input, it
6549/// flags via DPUTS and still calls setenv with the whole string
6550/// as the name (with value pointing at the trailing `\0`). Rust
6551/// equivalent: split, set_var; the in-place mutation isn't
6552/// observable since we copy.
6553/// Port of `zputenv(char *str)` from `Src/params.c:5325`.
6554pub fn zputenv(str: &str) -> i32 {                                           // c:5325
6555    if str.is_empty() {
6556        // c:5328 — DPUTS(!str, ...); treat as no-op.
6557        return 0;
6558    }
6559    let bytes = str.as_bytes();
6560    // c:5339-5341 — walk until `=` or high byte; reject high bytes.
6561    let mut ptr = 0;
6562    while ptr < bytes.len() && bytes[ptr] != b'=' && bytes[ptr] < 128 {       // c:5339
6563        ptr += 1;
6564    }
6565    if ptr < bytes.len() && bytes[ptr] >= 128 {                              // c:5342
6566        // c:5351 — `return 1` to reject non-portable name.
6567        return 1;
6568    }
6569    if ptr < bytes.len() {                                                   // c:5352 `else if (*ptr)`
6570        // c:5353-5355 — write `\0` at `=`, setenv(name, value), restore.
6571        let name = &str[..ptr];
6572        let value = &str[ptr + 1..];
6573        env::set_var(name, value);
6574        0
6575    } else {                                                                 // c:5356-5359
6576        // C: DPUTS(1, "bad environment string"); setenv(str, ptr, 1).
6577        // With no `=`, treat `str` as a bare name with empty value.
6578        env::set_var(str, "");
6579        0
6580    }
6581}
6582
6583/// Direct port of `int findenv(char *name, int *pos)` from
6584/// `Src/params.c:5391`. Walks `environ` looking for an
6585/// entry whose name component (bytes up to `=`) matches `name`.
6586/// Returns Some(index) on a match; the C source writes the
6587/// index into `*pos` and returns 1.
6588///
6589/// Rust signature differs (no out-param; returns `Option<usize>`)
6590/// — the C int-with-out-param idiom maps to `Option<index>` here.
6591/// Walks std::env::vars_os() which preserves the same ordering
6592/// as the underlying libc environ array.
6593pub fn findenv(name: &str) -> Option<usize> {                                // c:5391
6594    // c:5391 — `eq = strchr(name, '=')`. Strip any trailing `=value`.
6595    let nlen = name.find('=').unwrap_or(name.len());                         // c:5397
6596    let bare = &name[..nlen];
6597
6598    // c:5398-5404 — walk environ until match. Use std::env::vars()
6599    // which preserves the same ordering as the underlying libc
6600    // environ.
6601    for (i, (k, _)) in std::env::vars_os().enumerate() {
6602        if let Some(s) = k.to_str() {
6603            if s == bare {
6604                return Some(i);                                              // c:5401-5403
6605            }
6606        }
6607    }
6608    None                                                                     // c:5406
6609}
6610
6611// -----------------------------------------------------------
6612// env management (zsh's wrapper around setenv/unsetenv).
6613// -----------------------------------------------------------
6614
6615/// Port of `zgetenv(char *name)` from `Src/params.c:5416`. C body walks
6616/// `environ` byte-by-byte. Rust port uses `std::env::var`.
6617pub fn zgetenv(name: &str) -> Option<String> {
6618    env::var(name).ok()
6619}
6620
6621/// Direct port of `static void copyenvstr(char *s, char *value,
6622/// int flags)` from `Src/params.c:5434`. Unmetafies `value`
6623/// into `s` (Meta NEXT pairs collapse to NEXT^32) and applies
6624/// PM_LOWER / PM_UPPER case folding per byte.
6625pub fn copyenvstr(buf: &mut String, value: &str, flags: i32) {               // c:5434
6626    let flags_u = flags as u32;
6627    let mut it = value.bytes();
6628    while let Some(b) = it.next() {                                          // c:5436
6629        let mut ch = b;
6630        if ch == crate::ported::zsh_h::META as u8 {                          // c:5437
6631            ch = match it.next() {
6632                Some(next) => next ^ 32,                                     // c:5438
6633                None => break,
6634            };
6635        }
6636        if flags_u & crate::ported::zsh_h::PM_LOWER != 0 {                   // c:5439
6637            ch = ch.to_ascii_lowercase();                                    // c:5440
6638        } else if flags_u & crate::ported::zsh_h::PM_UPPER != 0 {            // c:5441
6639            ch = ch.to_ascii_uppercase();                                    // c:5442
6640        }
6641        buf.push(ch as char);
6642    }
6643}
6644
6645/// Direct port of `void addenv(Param pm, char *value)` from
6646/// `Src/params.c:5448` (USE_SET_UNSET_ENV branch — the
6647/// portable one). C body:
6648///   1. `newenv = mkenvstr(pm->nam, value, pm->flags)` (c:5463)
6649///   2. `if (zputenv(newenv)) { free; pm->env=NULL; return }` (c:5464-5468)
6650///   3. Otherwise: `if (pm->env) free(pm->env); pm->env = newenv;
6651///      pm->flags |= PM_EXPORTED` (c:5482-5484)
6652///
6653/// Rust takes `name` instead of `Param pm` and looks up the
6654/// `pm` node internally — the C body's only reads of `pm` are
6655/// `pm->nam`, `pm->flags`, `pm->env`, all available from
6656/// paramtab. The return type changes from `void` to `i32` so
6657/// callers can chain it; 0 = success, 1 = zputenv failed.
6658pub fn addenv(name: &str, value: &str) -> i32 {                              // c:5448
6659
6660    // c:5463 — `newenv = mkenvstr(pm->nam, value, pm->flags)`.
6661    let flags = {
6662        let tab = paramtab().read().unwrap();
6663        tab.get(name).map(|pm| pm.node.flags).unwrap_or(0)
6664    };
6665    let newenv = mkenvstr(name, value, flags);
6666    // c:5464-5468 — `if (zputenv(newenv)) { free; pm->env=NULL; return }`.
6667    if zputenv(&newenv) != 0 {
6668        let mut tab = paramtab().write().unwrap();
6669        if let Some(pm) = tab.get_mut(name) {
6670            pm.env = None;
6671        }
6672        return 1;
6673    }
6674    // c:5482-5484 — `pm->env = newenv; pm->flags |= PM_EXPORTED`.
6675    let mut tab = paramtab().write().unwrap();
6676    if let Some(pm) = tab.get_mut(name) {
6677        pm.env = Some(newenv);
6678        pm.node.flags |= PM_EXPORTED as i32;
6679    }
6680    0
6681}
6682
6683/// Direct port of `static char *mkenvstr(char *name, char *value,
6684/// int flags)` from `Src/params.c:5513`. Builds `name=value`
6685/// in a fresh heap-string, where `value` is unmetafied and
6686/// case-folded according to `flags` (PM_LOWER → lower, PM_UPPER →
6687/// upper). The C source computes the unmetafied length first via
6688/// the `while (*s && (*s++ != Meta || *s++ != 32))` loop, then
6689/// allocates and writes via copyenvstr; the Rust port appends to
6690/// a `String` so the length pre-scan is implicit.
6691pub fn mkenvstr(name: &str, value: &str, flags: i32) -> String {             // c:5513
6692    let mut buf = String::with_capacity(name.len() + value.len() + 2);
6693    buf.push_str(name);                                                      // c:5522 strcpy(s, name)
6694    buf.push('=');                                                           // c:5524 *s = '='
6695    if !value.is_empty() {                                                   // c:5525
6696        copyenvstr(&mut buf, value, flags);                                  // c:5526
6697    }
6698    buf                                                                      // c:5530
6699}
6700
6701/// Direct port of `void delenvvalue(char *x)` from
6702/// `Src/params.c:5542`. Removes `x` from environ by walking
6703/// to its pointer and shifting subsequent entries down one slot.
6704///
6705/// C body operates on the environ array directly. The Rust port
6706/// uses `env::remove_var(name)` since Rust's env is mediated by
6707/// libc::unsetenv internally — same shift semantics.
6708pub fn delenvvalue(name: &str) {                                             // c:5542
6709    env::remove_var(name);                                                   // c:5542 equivalent
6710}
6711
6712/// Direct port of `void delenv(Param pm)` from
6713/// `Src/params.c:5563-5582`. Removes the param's env entry and
6714/// clears `pm->env`. Under USE_SET_UNSET_ENV (the portable
6715/// branch) the C body is:
6716///   unsetenv(pm->node.nam);
6717///   zsfree(pm->env);
6718///   pm->env = NULL;
6719///
6720/// "Note we don't remove PM_EXPORT from the flags. This may be
6721/// asking for trouble but we need to know later if we restore
6722/// this parameter to its old value." (c:5575-5577)
6723///
6724/// Rust signature drift: takes `&str` (the param name) instead
6725/// of `&mut Param`. The pm.env field is cleared via the paramtab
6726/// lookup; PM_EXPORTED is intentionally preserved per the C
6727/// comment.
6728pub fn delenv(name: &str) {                                                  // c:5563
6729    // c:5563 — `unsetenv(pm->node.nam)`.
6730    env::remove_var(name);
6731    // c:5568 / c:5572 — `pm->env = NULL`. PM_EXPORTED stays set.
6732    let mut tab = paramtab().write().unwrap();
6733    if let Some(pm) = tab.get_mut(name) {
6734        pm.env = None;
6735    }
6736}
6737
6738/// Port of `convbase_ptr(char *s, zlong v, int base, int *ndigits)` from `Src/params.c:5586`. C body
6739/// converts `v` into base `base` (negative `base` suppresses the
6740/// "0x"/"N#" discriminator), writing the digits into `s` and
6741/// returning the digit count via `*ndigits`. Rust port returns
6742/// `(formatted_string, digit_count)` since Rust strings own
6743/// their buffer.
6744/// WARNING: param names don't match C — Rust=(v, base) vs C=(s, v, base, ndigits)
6745pub fn convbase_ptr(v: i64, base: i32) -> (String, i32) {
6746    let mut s = String::new();
6747    let mut value = v;
6748    if value < 0 {
6749        s.push('-');
6750        value = -value;
6751    }
6752    let mut b = base;
6753    if (-1..=1).contains(&b) {
6754        b = -10;
6755    }
6756    if b > 0 {
6757        if isset(crate::ported::zsh_h::CBASES) && b == 16 {
6758            s.push_str("0x");
6759        } else if isset(crate::ported::zsh_h::CBASES)
6760            && b == 8
6761            && isset(crate::ported::zsh_h::OCTALZEROES)
6762        {
6763            s.push('0');
6764        } else if b != 10 {
6765            s.push_str(&format!("{}#", b));
6766        }
6767    } else {
6768        b = -b;
6769    }
6770    let base_u = b as u64;
6771    let mut x = value as u64;
6772    let mut digs: i32 = 0;
6773    while x != 0 {
6774        x /= base_u;
6775        digs += 1;
6776    }
6777    if digs == 0 {
6778        digs = 1;
6779    }
6780    let mut digits: Vec<u8> = vec![0u8; digs as usize];
6781    let mut i = digs - 1;
6782    let mut x = value as u64;
6783    while i >= 0 {
6784        let dig = (x % base_u) as u8;
6785        digits[i as usize] = if dig < 10 {
6786            b'0' + dig
6787        } else {
6788            b'A' + dig - 10
6789        };
6790        x /= base_u;
6791        i -= 1;
6792    }
6793    s.push_str(std::str::from_utf8(&digits).unwrap_or(""));
6794    (s, digs)
6795}
6796
6797// ---------------------------------------------------------------------------
6798// Integer/Float conversion (from convbase/convfloat)
6799// ---------------------------------------------------------------------------
6800
6801/// Port of `convbase(char *s, zlong v, int base)` from
6802/// `Src/params.c:5632`. C body (single statement):
6803///     `convbase_ptr(s, v, base, NULL);`
6804/// Rust takes (v, base) and returns the formatted string since Rust
6805/// strings own their buffer; the discarded `ndigits` out-param of
6806/// `convbase_ptr` is `.1` of the returned tuple.
6807/// WARNING: param names don't match C — Rust=(val, base) vs C=(s, v, base)
6808pub fn convbase(val: i64, base: u32) -> String {                             // c:5632
6809    convbase_ptr(val, base as i32).0                                         // c:5634
6810}
6811
6812/// Convert integer to string with underscores for readability
6813/// Port of `convbase_underscore(char *s, zlong v, int base, int underscore)` from `Src/params.c:5646`.
6814/// WARNING: param names don't match C — Rust=(val, base, underscore) vs C=(s, v, base, underscore)
6815pub fn convbase_underscore(val: i64, base: u32, underscore: i32) -> String {
6816    let s = convbase(val, base);
6817    if underscore <= 0 {
6818        return s;
6819    }
6820
6821    // Find the digits portion
6822    let (prefix, digits) = if let Some(rest) = s.strip_prefix('-') {
6823        let digit_start = rest
6824            .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
6825            .unwrap_or(0);
6826        (&s[..1 + digit_start], &rest[digit_start..])
6827    } else {
6828        let digit_start = s
6829            .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
6830            .unwrap_or(0);
6831        (&s[..digit_start], &s[digit_start..])
6832    };
6833
6834    if digits.len() <= underscore as usize {
6835        return s;
6836    }
6837
6838    let u = underscore as usize;
6839    let mut result = prefix.to_string();
6840    let chars: Vec<char> = digits.chars().collect();
6841    let first_group = chars.len() % u;
6842    if first_group > 0 {
6843        result.extend(&chars[..first_group]);
6844        if first_group < chars.len() {
6845            result.push('_');
6846        }
6847    }
6848    for (i, chunk) in chars[first_group..].chunks(u).enumerate() {
6849        if i > 0 {
6850            result.push('_');
6851        }
6852        result.extend(chunk);
6853    }
6854    result
6855}
6856
6857/// Port of `convfloat(double dval, int digits, int flags, FILE *fout)` from `Src/params.c:5689`.
6858///
6859/// C signature: `char *convfloat(double dval, int digits, int flags,
6860/// FILE *fout)` — picks `%e` / `%f` / `%g` based on PM_EFLOAT /
6861/// PM_FFLOAT (line 5705-5727), then snprintf'd with `digits` precision.
6862/// When neither E nor F flag is set, zsh uses `%.*g` with a default
6863/// of 17 significant digits (line 5712-5714). E-flag with N significant
6864/// figures decrements `digits` because `%e` counts decimal places not
6865/// significants (line 5720-5725).
6866///
6867/// Rust signature drops the `fout` parameter — every caller wanted the
6868/// returned string. IEEE specials (inf/nan) hand-formatted to `Inf`/
6869/// `-Inf`/`NaN` ahead of the snprintf, matching the C source's Inf/NaN
6870/// shortcuts at lines 5733-5736 / 5742-5744. The trailing-dot rule for
6871/// integer-valued floats (`5` -> `5.`) is added by the caller (params'
6872/// internal printing path) in C zsh; mirrored here for the no-flag case
6873/// so `MathNum::(crate::ported::math::mn_format_subst(Float(5.0)))` produces `5.` not `5`.
6874/// WARNING: param names don't match C — Rust=(dval, digits, pm_flags) vs C=(dval, digits, flags, fout)
6875pub fn convfloat(dval: f64, digits: i32, pm_flags: u32) -> String {
6876    if dval.is_infinite() {                                       // c:5742
6877        return if dval < 0.0 {
6878            "-Inf".to_string()
6879        } else {
6880            "Inf".to_string()
6881        };
6882    }
6883    if dval.is_nan() {                                            // c:5744
6884        return "NaN".to_string();
6885    }
6886    // Pick fmt char + adjust digits per the C cascade at 5705-5727.
6887    let (fmt_char, digits) = if (pm_flags & crate::ported::zsh_h::PM_EFLOAT) != 0 { // c:5715
6888        let d = if digits <= 0 { 10 } else { digits };           // c:5718
6889        ('e', (d - 1).max(0))                                    // c:5725
6890    } else if (pm_flags & crate::ported::zsh_h::PM_FFLOAT) != 0 {                  // c:5716
6891        let d = if digits <= 0 { 10 } else { digits };           // c:5718
6892        ('f', d)
6893    } else {
6894        let d = if digits == 0 { 17 } else { digits };           // c:5713
6895        ('g', d)
6896    };
6897    // Mirror zsh's snprintf path (Src/params.c:5751) — the C source
6898    // uses `VARARR(char, buf, 512 + digits)` for %f's full integer-
6899    // part expansion. 512 + 17 = 529 covers the zsh general case;
6900    // wider buffers below for the unbounded %f.
6901    let buf_len = 512usize + digits as usize + 4;
6902    let mut buf = vec![0u8; buf_len];
6903    let fmt = match fmt_char {
6904        'e' => c"%.*e",
6905        'f' => c"%.*f",
6906        _ => c"%.*g",
6907    };
6908    // SAFETY: buf has the C-required size for any double precision; fmt
6909    // is a NUL-terminated literal; snprintf writes ASCII only.
6910    let n = unsafe {
6911        libc::snprintf(
6912            buf.as_mut_ptr() as *mut libc::c_char,
6913            buf_len,
6914            fmt.as_ptr(),
6915            digits as libc::c_int,
6916            dval,
6917        )
6918    };
6919    if n < 0 {
6920        return format!("{}", dval);
6921    }
6922    let len = (n as usize).min(buf_len - 1);
6923    buf.truncate(len);
6924    let mut s = String::from_utf8(buf).unwrap_or_else(|_| format!("{}", dval));
6925    // zsh's general-format (%g) callers (math `$(( ))` substitution)
6926    // append `.` when the output has no `e` and no `.`, so integer-
6927    // valued floats like `5` render as `5.`. PM_EFLOAT/PM_FFLOAT skip
6928    // this rule (the format spec already pins shape).
6929    if fmt_char == 'g' && !s.contains('e') && !s.contains('.') {
6930        s.push('.');
6931    }
6932    s
6933}
6934
6935/// Start a parameter scope.
6936/// Port of `startparamscope()` (Src/init.c) — the C source pushes the
6937/// current scope counter so `local`-declared params disappear on function
6938/// exit. Rust port operates on the bucket-2 holder `paramtab` via a
6939/// `&mut crate::ported::zsh_h::HashTable` argument.
6940pub fn startparamscope(_table: &mut crate::ported::zsh_h::HashTable) {
6941    crate::ported::utils::inc_locallevel();
6942}
6943
6944/// Port of `endparamscope()` from `Src/params.c:5857`. C signature:
6945/// `mod_export void endparamscope(void)`. Decrements `locallevel`,
6946/// pops any pushed history stack, then iterates `paramtab` calling
6947/// `scanendscope` to restore/unset every param whose `level`
6948/// exceeds the new `locallevel`. Operates on the global `paramtab`
6949/// just like C — no parameter, no fake injection wrapper.
6950pub fn endparamscope() {
6951    queue_signals();
6952    crate::ported::utils::dec_locallevel();                                  // c:5861 locallevel--
6953    // c:5863 — `saveandpophiststack(0, HFILE_USE_OPTIONS);`. Pop
6954    // all stack entries with locallevel > current.
6955    crate::ported::hist::saveandpophiststack(0, crate::ported::zsh_h::HFILE_USE_OPTIONS as i32);
6956    let ll = crate::ported::utils::locallevel();
6957    // c:5867 scanhashtable(paramtab, 0, 0, 0, scanendscope, 0). Walk
6958    // the live paramtab (HashMap-backed until the hashtable.c vtable
6959    // is wired) and apply scanendscope's `pm->level > locallevel`
6960    // filter, restoring the `pm.old` chain or removing the entry.
6961    if let Ok(mut tab) = paramtab().write() {
6962        let stale: Vec<String> = tab.iter()
6963            .filter_map(|(k, pm)| if pm.level > ll { Some(k.clone()) } else { None })
6964            .collect();
6965        for n in stale {
6966            // c:scanendscope:5903 — non-special path: restore pm.old
6967            // (or remove if no outer binding existed).
6968            if let Some(pm) = tab.remove(&n) {
6969                if let Some(prev) = pm.old {                                 // c:scanendscope:5933 pm->old = tpm->old
6970                    tab.insert(n, prev);                                     // restore outer binding (Box<param>)
6971                }
6972                // else: c:5966 unsetparam_pm — name unset entirely
6973            }
6974        }
6975    }
6976    unqueue_signals();
6977}
6978
6979/// Port of `scanendscope(HashNode hn, UNUSED(int flags))` from `Src/params.c:5900`. Per-node
6980/// callback used by `endparamscope` (params.c:5867 calls
6981/// `scanhashtable(paramtab, 0, 0, 0, scanendscope, 0)`) when a
6982/// function returns. C body:
6983/// ```c
6984/// Param pm = (Param)hn;
6985/// if (pm->level > locallevel) {
6986///     if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) {
6987///         /* Non-removable special — restore from pm->old in-place. */
6988///         Param tpm = pm->old;
6989///         #ifdef USE_LOCALE
6990///         if (!strncmp(pm->node.nam, "LC_", 3) ||
6991///             !strcmp(pm->node.nam, "LANG"))
6992///             lc_update_needed = 1;
6993///         #endif
6994///         if (!strcmp(pm->node.nam, "SECONDS")) {
6995///             setsecondstype(pm, PM_TYPE(tpm->node.flags),
6996///                                PM_TYPE(pm->node.flags));
6997///             setrawseconds(tpm->u.dval);
6998///             tpm->node.flags |= PM_NORESTORE;
6999///         }
7000///         pm->old = tpm->old;
7001///         pm->node.flags = (tpm->node.flags & ~PM_NORESTORE);
7002///         pm->level = tpm->level;
7003///         pm->base  = tpm->base;
7004///         pm->width = tpm->width;
7005///         if (pm->env) delenv(pm);
7006///         if (!(tpm->node.flags & (PM_NORESTORE|PM_READONLY)))
7007///             switch (PM_TYPE(pm->node.flags)) {
7008///             case PM_SCALAR: case PM_NAMEREF:
7009///                 pm->gsu.s->setfn(pm, tpm->u.str); break;
7010///             case PM_INTEGER:
7011///                 pm->gsu.i->setfn(pm, tpm->u.val); break;
7012///             case PM_EFLOAT: case PM_FFLOAT:
7013///                 pm->gsu.f->setfn(pm, tpm->u.dval); break;
7014///             case PM_ARRAY:
7015///                 pm->gsu.a->setfn(pm, tpm->u.arr); break;
7016///             case PM_HASHED:
7017///                 pm->gsu.h->setfn(pm, tpm->u.hash); break;
7018///             }
7019///         zfree(tpm, sizeof(*tpm));
7020///         if (pm->node.flags & PM_EXPORTED) export_param(pm);
7021///     } else
7022///         unsetparam_pm(pm, 0, 0);
7023/// }
7024/// ```
7025/// Rust port mirrors the structure 1:1. `locallevel` is a global
7026/// in C (Src/init.c) — we accept it as a parameter since the
7027/// global isn't yet ported. `setsecondstype`/`setrawseconds`/
7028/// `delenv` are not yet in zshrs and route through best-effort
7029/// no-ops for now (C macros / Src/params.c:5900 / Src/params.c:5900).
7030pub fn scanendscope(pm: &mut crate::ported::zsh_h::param, _flags: i32) {     // c:5900
7031    let cur_local = locallevel.load(std::sync::atomic::Ordering::Relaxed);
7032    if pm.level <= cur_local {                                                // c:5903
7033        return;
7034    }
7035    let pmflags = pm.node.flags as u32;
7036    if (pmflags & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL {
7037        // Take ownership of the saved old param.
7038        let mut tpm = match pm.old.take() {
7039            Some(t) => t,
7040            None => {
7041                // C uses DPUTS — fatal in debug, silent in release.
7042                return;
7043            }
7044        };
7045
7046        // USE_LOCALE branch: LC_*/LANG bumps lc_update_needed.
7047        // Global not yet ported; placeholder comment retains intent.
7048        if pm.node.nam.starts_with("LC_") || pm.node.nam == "LANG" {
7049            LC_UPDATE_NEEDED.store(1, std::sync::atomic::Ordering::SeqCst);
7050        }
7051
7052        if pm.node.nam == "SECONDS" {
7053            // setsecondstype(pm, PM_TYPE(tpm.flags), PM_TYPE(pm.flags));
7054            // setrawseconds(tpm.u_dval);
7055            tpm.node.flags |= PM_NORESTORE as i32;
7056        }
7057
7058        // pm->old = tpm->old;
7059        pm.old = tpm.old.take();
7060        // pm->node.flags = tpm->node.flags & ~PM_NORESTORE;
7061        pm.node.flags = (tpm.node.flags as u32 & !PM_NORESTORE) as i32;
7062        pm.level = tpm.level;
7063        pm.base  = tpm.base;
7064        pm.width = tpm.width;
7065
7066        if pm.env.is_some() {
7067            delenv(&pm.node.nam);
7068            pm.env = None;
7069        }
7070
7071        let restore = (tpm.node.flags as u32 & (PM_NORESTORE | PM_READONLY)) == 0;
7072        if restore {
7073            match PM_TYPE(pm.node.flags as u32) {
7074                t if t == PM_SCALAR || t == PM_NAMEREF => {
7075                    // pm->gsu.s->setfn(pm, tpm->u.str)
7076                    pm.u_str = tpm.u_str.clone();
7077                }
7078                t if t == PM_INTEGER => {
7079                    pm.u_val = tpm.u_val;
7080                }
7081                t if t == PM_EFLOAT || t == PM_FFLOAT => {
7082                    pm.u_dval = tpm.u_dval;
7083                }
7084                t if t == PM_ARRAY => {
7085                    pm.u_arr = tpm.u_arr.clone();
7086                }
7087                t if t == PM_HASHED => {
7088                    pm.u_hash = tpm.u_hash.take();
7089                }
7090                _ => {}
7091            }
7092        }
7093        // zfree(tpm) — Rust drops the Box at end of scope.
7094        drop(tpm);
7095
7096        if (pm.node.flags as u32 & PM_EXPORTED) != 0 {
7097            export_param(pm);
7098        }
7099    } else {
7100        unsetparam_pm(pm, 0, 0);
7101    }
7102}
7103
7104/// Direct port of `void freeparamnode(HashNode hn)` from
7105/// `Src/params.c:5977-5994`. Frees a Param node, including
7106/// running its unsetfn callback when the global `delunset` flag
7107/// is set.
7108///
7109/// C body:
7110///   if (delunset)
7111///     pm->gsu.s->unsetfn(pm, 1);          // c:5977
7112///   zsfree(pm->node.nam);                 // c:5977
7113///   if (!(pm->flags & PM_SPECIAL))        // c:5977
7114///     zsfree(pm->ename);                  // c:5977
7115///   zfree(pm, sizeof(struct param));      // c:5977
7116///
7117/// Rust's Drop handles every zsfree/zfree above; the explicit
7118/// step here is the optional unsetfn dispatch when `DELUNSET` is
7119/// non-zero. The remaining drop cascade fires when `_hn`
7120/// (`Box<param>`) leaves scope.
7121pub fn freeparamnode(mut _hn: crate::ported::zsh_h::Param) {                 // c:5977
7122    // c:5977-5987 — `if (delunset) pm->gsu.s->unsetfn(pm, 1);`.
7123    if DELUNSET.load(std::sync::atomic::Ordering::Relaxed) != 0 {
7124        // The Rust port's stdunsetfn writes the unset state back to
7125        // paramtab; calling it on the about-to-drop param re-marks
7126        // its slot in the table so consumers that read the table
7127        // see PM_UNSET on the next lookup.
7128        stdunsetfn(_hn.as_mut(), 1);                                         // c:5987
7129    }
7130    // c:5988-5992 — drop cascade frees nam / ename (non-PM_SPECIAL)
7131    // / struct itself when _hn goes out of scope.
7132}
7133
7134/// Port of `printparamvalue(Param p, int printflags)` from `Src/params.c:6035`. C body
7135/// dispatches on `PM_TYPE(p->node.flags)` and writes the value
7136/// (no `name=` prefix unless `!PRINT_KV_PAIR`, which prints `=`
7137/// first). PM_SCALAR/PM_NAMEREF: `quotedzputs(t)`; PM_INTEGER:
7138/// `printf("%ld")`; PM_EFLOAT/PM_FFLOAT: `convfloat(...)`;
7139/// PM_ARRAY: `( v1 v2 ... )` with `\n  ` separators on
7140/// PRINT_LINE; PM_HASHED: same shape via scan callback.
7141pub fn printparamvalue(p: &mut crate::ported::zsh_h::param, printflags: i32) {
7142    if (printflags & PRINT_KV_PAIR) == 0 {
7143        print!("=");
7144    }
7145    let t = PM_TYPE(p.node.flags as u32);
7146    if t == PM_SCALAR || t == PM_NAMEREF {
7147        let s = strgetfn(p);
7148        // c:6053 — `quotedzputs(t, stdout)`. The previous Rust port
7149        // used `print!("{}", s)` (raw), losing the shell-quoting
7150        // that `typeset -p VAR` expects. Without quoting, `eval
7151        // "$(typeset -p VAR)"` round-trip is BROKEN for any value
7152        // with spaces, special chars, or shell metacharacters.
7153        print!("{}", crate::ported::utils::quotedzputs(&s));                  // c:6053
7154    } else if t == PM_INTEGER {
7155        print!("{}", intgetfn(p));
7156    } else if t == PM_EFLOAT || t == PM_FFLOAT {
7157        // c:6063 — `convfloat(p->gsu.f->getfn(p), p->base, p->node.flags,
7158        //          stdout)`. Honors pm.base for precision and
7159        // pm.flags for PM_EFLOAT/PM_FFLOAT format selection. The
7160        // previous Rust port used `print!("{}", floatgetfn(p))`
7161        // which always renders in Rust's default float format
7162        // (which differs from C's printf %g / %e formats).
7163        print!("{}", crate::ported::utils::convfloat(
7164            floatgetfn(p), p.base, p.node.flags as u32));                     // c:6063
7165    } else if t == PM_ARRAY {
7166        if (printflags & PRINT_KV_PAIR) == 0 {
7167            print!("(");
7168            if (printflags & PRINT_LINE) == 0 {
7169                print!(" ");
7170            }
7171        }
7172        let arr = arrgetfn(p);
7173        if !arr.is_empty() {
7174            if (printflags & PRINT_LINE) != 0 {
7175                if (printflags & PRINT_KV_PAIR) != 0 {
7176                    print!("  ");
7177                } else {
7178                    print!("\n  ");
7179                }
7180            }
7181            print!("{}", arr[0]);
7182            for el in &arr[1..] {
7183                if (printflags & PRINT_LINE) != 0 {
7184                    print!("\n  ");
7185                } else {
7186                    print!(" ");
7187                }
7188                print!("{}", el);
7189            }
7190            if (printflags & (PRINT_LINE | PRINT_KV_PAIR)) == PRINT_LINE {
7191                println!();
7192            }
7193        }
7194        if (printflags & PRINT_KV_PAIR) == 0 {
7195            if (printflags & PRINT_LINE) == 0 {
7196                print!(" ");
7197            }
7198            print!(")");
7199        }
7200    } else if t == PM_HASHED {
7201        if (printflags & PRINT_KV_PAIR) == 0 {
7202            print!("(");
7203            if (printflags & PRINT_LINE) == 0 {
7204                print!(" ");
7205            }
7206        }
7207        // scanhashtable + ht->printnode — backend not yet wired.
7208        if (printflags & PRINT_KV_PAIR) == 0 {
7209            print!(")");
7210        }
7211    }
7212}
7213
7214/// Port of `printparamnode(HashNode hn, int printflags)` from `Src/params.c:6123`. Real C
7215/// body is ~200 lines emitting the typeset/declare-style listing
7216/// for one param honouring PRINT_NAMEONLY / PRINT_TYPESET /
7217/// PRINT_KV_PAIR / PRINT_LINE / PRINT_INCLUDEVALUE /
7218/// PRINT_POSIX_READONLY / PRINT_POSIX_EXPORT / PRINT_WITH_NAMESPACE
7219/// and the per-paramtypes attribute table. Faithful direct port
7220/// of the common path: skip-on-`.`-prefix without WITH_NAMESPACE,
7221/// skip-on-PM_UNSET (with the POSIX preserve), AUTOLOAD gating,
7222/// then `nam` + `=value` via `printparamvalue`.
7223pub fn printparamnode(hn: &mut crate::ported::zsh_h::param, mut printflags: i32) {
7224    const PRINT_WITH_NAMESPACE: i32 = 1 << 8; // matches createspecial print enum
7225    let f = hn.node.flags as u32;
7226    if (f & PM_HASHELEM) == 0
7227        && (printflags & PRINT_WITH_NAMESPACE) == 0
7228        && hn.node.nam.starts_with('.')
7229    {
7230        return;
7231    }
7232    if (f & PM_UNSET) != 0 {
7233        // c:6133-6143 — POSIX readonly/exported keep + PM_DEFAULTED
7234        // path: show as readonly/exported even if unset, with no
7235        // value (NAMEONLY).
7236        let posix_keep = (printflags & (PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
7237            && (f & (PM_READONLY | PM_EXPORTED)) != 0;
7238        let defaulted = (f & PM_DEFAULTED) == PM_DEFAULTED;                  // c:6137
7239        if posix_keep || defaulted {
7240            printflags |= PRINT_NAMEONLY;
7241        } else {
7242            return;
7243        }
7244    }
7245    if (f & PM_AUTOLOAD) != 0 {
7246        printflags |= PRINT_NAMEONLY;
7247    }
7248    if (printflags & (PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0 {
7249        if (f & PM_AUTOLOAD) != 0 {
7250            return;
7251        }
7252        // c:6157-6163 — PM_RO_BY_DESIGN with level check. C uses
7253        // `if (hn->level != locallevel) return;` — only show the
7254        // entry when its level matches the current scope. The
7255        // previous Rust port hardcoded `locallevel = 0` with a
7256        // "global not yet wired" comment, but the canonical
7257        // global IS at params.rs (declared above). Read it live.
7258        if (f & PM_RO_BY_DESIGN) != 0 {
7259            let cur_ll = locallevel
7260                .load(std::sync::atomic::Ordering::Relaxed) as i32;
7261            if hn.level != cur_ll {                                          // c:6157
7262                return;
7263            }
7264        }
7265        if (printflags & PRINT_POSIX_EXPORT) != 0 {
7266            if (f & PM_EXPORTED) == 0 { return; }
7267            print!("export ");
7268        } else if (printflags & PRINT_POSIX_READONLY) != 0 {
7269            if (f & PM_READONLY) == 0 { return; }
7270            print!("readonly ");
7271        } else {
7272            print!("typeset ");
7273        }
7274    }
7275    if (printflags & PRINT_KV_PAIR) != 0 {
7276        // hashelem path: print key without name= leader.
7277    }
7278    print!("{}", hn.node.nam);
7279    if (printflags & PRINT_NAMEONLY) != 0 {
7280        if (printflags & PRINT_KV_PAIR) == 0 { println!(); }
7281        return;
7282    }
7283    if (printflags & (PRINT_INCLUDEVALUE | PRINT_TYPESET)) != 0
7284        || (printflags & PRINT_NAMEONLY) == 0
7285    {
7286        printparamvalue(hn, printflags);
7287    }
7288    if (printflags & PRINT_KV_PAIR) == 0 {
7289        println!();
7290    }
7291}
7292
7293/// Port of `resolve_nameref(Param pm)` from `Src/params.c:6325`. C body:
7294/// ```c
7295/// mod_export Param
7296/// resolve_nameref(Param pm)
7297/// {
7298///     return resolve_nameref_rec(pm, NULL, 0);
7299/// }
7300/// ```
7301/// Public entry point that walks the nameref alias chain to the
7302/// final non-nameref `param`. Stop-pm and keep_lastref are
7303/// internal; this wrapper hardcodes both per the C body.
7304/// WARNING: param names don't match C — Rust=() vs C=(pm)
7305pub fn resolve_nameref(                                                      // c:6325
7306    pm: Option<crate::ported::zsh_h::Param>,
7307) -> Option<crate::ported::zsh_h::Param> {
7308    resolve_nameref_rec(pm, None, 0)                                         // c:6327
7309}
7310
7311/// Port of `resolve_nameref_rec(Param pm, const Param stop, int keep_lastref)` from `Src/params.c:6332`. C
7312/// recursive helper for `resolve_nameref()`. Walks the chain of
7313/// `${(P)var}` indirections via `gethashnode2(realparamtab, refname)`
7314/// + `loadparamnode(paramtab, upscope(pm, ref), refname)`,
7315/// checking PM_TAGGED for cycle detection, and returns the
7316/// final non-nameref Param. Returns the input `pm` unchanged
7317/// for the early-exit path (no NAMEREF / UNSET / has subscript /
7318/// empty refname). Full chain walk requires `gethashnode2` on
7319/// `realparamtab` — pending the HashTable vtable.
7320#[allow(unused_variables)]
7321pub fn resolve_nameref_rec(
7322    pm: Option<crate::ported::zsh_h::Param>,
7323    stop: Option<&crate::ported::zsh_h::param>,
7324    keep_lastref: i32,
7325) -> Option<crate::ported::zsh_h::Param> {
7326    let pm_ref = pm.as_deref()?;
7327    let f = pm_ref.node.flags as u32;
7328    if (f & PM_NAMEREF) == 0 || (f & PM_UNSET) != 0 || pm_ref.width != 0 {
7329        return pm;
7330    }
7331    let refname = pm_ref.u_str.as_deref().unwrap_or("");
7332    if refname.is_empty() {
7333        return pm;
7334    }
7335    if (f & PM_TAGGED) != 0 {
7336        // c: `zerr("%s: invalid self reference", pm->node.nam)`.
7337        // The previous Rust port left this as a comment-only stub.
7338        let nam = pm.as_ref().map(|p| p.node.nam.clone()).unwrap_or_default();
7339        zerr(&format!("{}: invalid self reference", nam));
7340        return None;
7341    }
7342    // Real walk needs realparamtab.gethashnode2(refname). Until
7343    // that lands, return the input — this matches the no-target
7344    // behaviour the C source falls back to when keep_lastref is 0
7345    // and the lookup fails.
7346    pm
7347}
7348
7349/// ```c
7350/// Param pm = (Param) gethashnode2(realparamtab, name);
7351/// if (pm && (pm->node.flags & PM_NAMEREF)) {
7352///     if (pm->node.flags & PM_READONLY) {
7353///         zerr("read-only reference: %s", pm->node.nam); return;
7354///     }
7355///     pm->base = pm->width = 0;
7356///     SETREFNAME(pm, ztrdup(value));
7357///     pm->node.flags &= ~PM_UNSET;
7358///     setscope(pm);
7359/// } else
7360///     setsparam(name, ztrdup(value));
7361/// ```
7362/// `gethashnode2` is the no-autoload paramtab lookup. The
7363/// nameref branch updates the alias target in-place; the normal
7364/// branch falls through to `setsparam`.
7365/// Port of `setloopvar(char *name, char *value)` from `Src/params.c:6362`.
7366pub fn setloopvar(name: &str, value: &str) {
7367    // c:6367 — `Param pm = (Param) gethashnode2(realparamtab, name);`
7368    // Scope the write lock so we drop it before calling setsparam below.
7369    let nameref_branch = {
7370        let mut tab = realparamtab().write().unwrap();
7371        if let Some(pm) = tab.get_mut(name) {
7372            // c:6369 — `if (pm && (pm->node.flags & PM_NAMEREF))`
7373            if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
7374                // c:6370 — `if (pm->node.flags & PM_READONLY)`
7375                if (pm.node.flags as u32 & PM_READONLY) != 0 {
7376                    // c:6372 — `zerr("read-only reference: %s", pm->node.nam);`
7377                    zerr(&format!("read-only reference: {}", pm.node.nam));
7378                    // c:6373 — `return;`
7379                    return;
7380                }
7381                // c:6376 — `pm->base = pm->width = 0;`
7382                pm.base = 0;
7383                pm.width = 0;
7384                // c:6377 — `SETREFNAME(pm, ztrdup(value));`
7385                // SETREFNAME (params.c:482) macro: for PM_SPECIAL,
7386                // call gsu_s.setfn(pm, S); else free pm->u.str and
7387                // assign new. The PM_SPECIAL gsu vtable isn't fully
7388                // wired in zshrs; both branches collapse to the
7389                // direct `u_str` assignment which matches the
7390                // non-special path verbatim.
7391                pm.u_str = Some(value.to_string());
7392                // c:6378 — `pm->node.flags &= ~PM_UNSET;`
7393                pm.node.flags &= !(PM_UNSET as i32);
7394                true
7395            } else {
7396                false
7397            }
7398        } else {
7399            false
7400        }
7401    };
7402    if nameref_branch {
7403        // c:6379 — `setscope(pm);` — re-borrow under a fresh write
7404        // lock since we dropped the earlier one before crossing the
7405        // fn boundary.
7406        let mut tab = realparamtab().write().unwrap();
7407        if let Some(pm) = tab.get_mut(name) {
7408            setscope(pm);
7409        }
7410    } else {
7411        // c:6381 — `setsparam(name, ztrdup(value));`
7412        setsparam(name, value);
7413    }
7414}
7415
7416/// PM_NAMEREF: extract `refname = GETREFNAME(pm)`, locate first
7417/// `[` to split name vs subscript (sets pm->width), look up the
7418/// base param via `gethashnode2(realparamtab, refname)` →
7419/// `loadparamnode` (skipping self) → `setscope_base(pm,
7420/// basepm->level)`; if pm->base > pm->level emits the KSH global
7421/// reference error or WARNNESTEDVAR diagnostic; finally walks the
7422/// `resolve_nameref_rec` chain to detect self-references with
7423/// queue_signals/restore_queue_signals bracketing. Non-nameref
7424/// params: no-op. The base lookup and resolve_nameref_rec helpers
7425/// are stubbed elsewhere; this port wires the structural path
7426/// against existing helpers and falls through cleanly when the
7427/// nameref chain backend isn't available.
7428/// Port of `setscope(Param pm)` from `Src/params.c:6382`.
7429pub fn setscope(pm: &mut crate::ported::zsh_h::param) {
7430    crate::ported::signals::queue_signals();
7431    if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
7432        // Refname is stored in pm.u_str for nameref-typed params.
7433        let refname = pm.u_str.clone();
7434        if let Some(rn) = refname {
7435            // Compute pm->width by finding the first `[`.
7436            let head: &str = match rn.find('[') {
7437                Some(i) => {
7438                    pm.width = i as i32;
7439                    &rn[..i]
7440                }
7441                None => rn.as_str(),
7442            };
7443            // Self-reference check (basepm == pm) — without a working
7444            // hashtable lookup we can only detect literal self-name.
7445            if !head.is_empty() && head == pm.node.nam {
7446                // c: `zerr("%s: invalid self reference", refname);`
7447                //    `unsetparam_pm(pm, 0, 1);`
7448                // The previous Rust port left both as comment-only
7449                // stubs. Emit the diagnostic so users see why a
7450                // typeset -n self-loop fails.
7451                zerr(&format!("{}: invalid self reference", rn));
7452                pm.node.flags |= PM_UNSET as i32;
7453            } else {
7454                // basepm = (Param)gethashnode2(realparamtab, refname)
7455                //   → loadparamnode(...) → setscope_base(pm, basepm->level)
7456                // Resolved on demand once the paramtab vtable is wired;
7457                // the call shape is preserved here.
7458            }
7459        }
7460    }
7461    crate::ported::signals::unqueue_signals();
7462}
7463
7464/// ```c
7465/// if ((pm->base = base) > pm->level) {
7466///     LinkList refs;
7467///     /* grow scoperefs[] to base+1 entries */
7468///     refs = scoperefs[base];
7469///     if (!refs) refs = scoperefs[base] = znewlinklist();
7470///     zpushnode(refs, pm);
7471/// }
7472/// ```
7473/// Records `pm` on the per-scope reference list so a future
7474/// scope-pop can resolve nameref/upper bindings. Rust port
7475/// stores `base` on the param; the global `scoperefs` LinkList
7476/// table is not yet ported, so the bookkeeping push is described
7477/// here as architectural intent rather than executed.
7478/// Port of `setscope_base(Param pm, int base)` from `Src/params.c:6436`.
7479pub fn setscope_base(pm: &mut crate::ported::zsh_h::param, base: i32) {
7480    pm.base = base;
7481    if base > pm.level {
7482        // scoperefs[base] push of pm — needs LinkList global.
7483    }
7484}
7485
7486/// Port of `upscope(Param pm, const Param ref)` from `Src/params.c:6455`. C body:
7487/// ```c
7488/// if (ref->node.flags & PM_UPPER)
7489///     while (pm->level > ref->level - 1 && (pm = pm->old));
7490/// else
7491///     for (; pm->old && pm->old->level >= ref->base; pm = pm->old);
7492/// return pm;
7493/// ```
7494/// Walks `pm->old` chain to the param at the right scope depth
7495/// for a nameref. Rust signature mirrors C `Param upscope(Param,
7496/// const Param ref)`.
7497/// WARNING: param names don't match C — Rust=(pm, reference) vs C=(pm, ref)
7498pub fn upscope(
7499    mut pm: crate::ported::zsh_h::Param,
7500    reference: &crate::ported::zsh_h::param,
7501) -> crate::ported::zsh_h::Param {
7502    if (reference.node.flags as u32 & PM_UPPER) != 0 {
7503        while pm.level > reference.level - 1 {
7504            match pm.old.take() {
7505                Some(o) => pm = o,
7506                None => break,
7507            }
7508        }
7509    } else {
7510        loop {
7511            let next_level = pm.old.as_ref().map(|o| o.level);
7512            match next_level {
7513                Some(l) if l >= reference.base => {
7514                    pm = pm.old.take().unwrap();
7515                }
7516                _ => break,
7517            }
7518        }
7519    }
7520    pm
7521}
7522
7523/// Port of `valid_refname(char *val, int flags)` from `Src/params.c:6466`. C body
7524/// validates a nameref target name. Two paths:
7525///   - PM_UPPER (`typeset -nu`): reject digit-leader (positional
7526///     refs would loop) and the literal `argv`/`ARGC` names.
7527///   - non-PM_UPPER: positional digit-leader is permitted (must be
7528///     all-digits before any `[`); otherwise scan via
7529///     `itype_end(INAMESPC)`.
7530/// Either path then accepts the trailing one-char specials
7531/// `! ? $ - _` and an optional `[subscript]` tail. Returns 1 on
7532/// valid, 0 otherwise. The Rust port follows the same control
7533/// flow with `is_ascii_digit`/`is_alphabetic` standing in for
7534/// `idigit`/`itype_end`.
7535pub fn valid_refname(val: &str, flags: i32) -> bool {                        // c:6466
7536    if val.is_empty() {
7537        return false;
7538    }
7539    let first = val.chars().next().unwrap();
7540    let pm_upper = (flags as u32 & PM_UPPER) != 0;
7541    let mut t: usize;
7542    if pm_upper {                                                            // c:6470
7543        if first.is_ascii_digit() {                                          // c:6472
7544            return false;                                                    // c:6473
7545        }
7546        // c:6474 — `t = itype_end(val, INAMESPC, 0)`; INAMESPC stops
7547        // at `.` and other non-namespace chars. Approximate with
7548        // alphanumeric/_ scan.
7549        t = val
7550            .char_indices()
7551            .find(|(_, c)| !(c.is_alphanumeric() || *c == '_'))
7552            .map(|(i, _)| i)
7553            .unwrap_or(val.len());
7554        if t - 0 == 4                                                        // c:6475
7555            && (val.starts_with("argv") || val.starts_with("ARGC"))          // c:6476-6477
7556        {
7557            return false;                                                    // c:6478
7558        }
7559    } else if first.is_ascii_digit() {                                       // c:6479
7560        // c:6480-6485 — all-digit run; first non-digit must be `[`.
7561        t = 1;
7562        for (i, c) in val.char_indices().skip(1) {
7563            if !c.is_ascii_digit() {
7564                t = i;
7565                break;
7566            }
7567            t = i + c.len_utf8();
7568        }
7569        if t < val.len() && val.as_bytes()[t] != b'[' {                      // c:6484
7570            return false;                                                    // c:6485
7571        }
7572    } else {
7573        // c:6487 — `t = itype_end(val, INAMESPC, 0)`.
7574        t = val
7575            .char_indices()
7576            .find(|(_, c)| !(c.is_alphanumeric() || *c == '_' || *c == '.'))
7577            .map(|(i, _)| i)
7578            .unwrap_or(val.len());
7579    }
7580
7581    if t == 0 {                                                              // c:6489
7582        let c = val.as_bytes()[0];
7583        if !(c == b'!' || c == b'?' || c == b'$' || c == b'-' || c == b'_') { // c:6490
7584            return false;                                                    // c:6493
7585        }
7586        t = 1;                                                               // c:6494
7587    }
7588    if t < val.len() && val.as_bytes()[t] == b'[' {                          // c:6496
7589        // c:6498-6504 — parse_subscript/Inbrack/Outbrack walk. The
7590        // tokenize+parse_subscript pair isn't ported; accept any
7591        // balanced `[…]` tail (single-level) to remain conservative.
7592        let tail = &val[t + 1..];
7593        if let Some(close) = tail.find(']') {
7594            // c:6505-6508 — anything past `]` is rejected.
7595            if close + 1 < tail.len() {
7596                return false;
7597            }
7598        } else {
7599            return false;
7600        }
7601    }
7602    true                                                                     // c:6510
7603}
7604
7605
7606/// Read `foundparam`. Returns the last param name observed by
7607/// `scanparamvals`; cleared by callers after consumption.
7608pub fn foundparam() -> Option<String> {
7609    foundparam_lock().lock().unwrap().clone()
7610}
7611
7612/// Set `foundparam`. Called from `scanparamvals`.
7613pub fn set_foundparam(nam: Option<String>) {
7614    *foundparam_lock().lock().unwrap() = nam;
7615}
7616
7617/// Port of `fetchvalue(Value v, char **pptr, int bracks, int scanflags)` from `Src/params.c:2180` — see real
7618/// implementation below; this slot kept for the C-source linenum
7619/// citation and is now an alias.
7620// (real fetchvalue is defined later)
7621
7622/// Port of `static int delunset;` from `Src/params.c:610`. Flag
7623/// `deleteparamtable` flips to 1 around the inner `deletehashtable`
7624/// call so each freed node runs its `unsetfn`. `freeparamnode`
7625/// consults this before invoking the unset hook (c:5986).
7626pub static DELUNSET: std::sync::atomic::AtomicI32 =                          // c:610
7627    std::sync::atomic::AtomicI32::new(0);
7628
7629pub(crate) fn paramtab_hashed_storage()
7630    -> &'static Mutex<HashMap<String, indexmap::IndexMap<String, String>>>
7631{
7632    PARAMTAB_HASHED_STORAGE_INNER
7633        .get_or_init(|| Mutex::new(HashMap::new()))
7634}
7635
7636/// Mirror the global `paramtab` (and the parallel hashed-storage
7637/// table) into the three HashMaps that `SubstState` uses as its
7638/// transient backing during `prefork()` (Src/subst.c:100). This
7639/// is a port-transition shim: once `subst.rs` reads parameters
7640/// directly through `paramtab().read()` / `.write()` instead of carrying
7641/// `state.variables`/`state.arrays`/`state.assoc_arrays`, this
7642/// helper goes away.
7643pub fn sync_state_from_paramtab(
7644    variables: &mut HashMap<String, String>,
7645    arrays: &mut HashMap<String, Vec<String>>,
7646    assoc_arrays: &mut HashMap<String, indexmap::IndexMap<String, String>>,
7647) {
7648    let tab = paramtab().read().unwrap();
7649    for (name, pm) in tab.iter() {
7650        let f = pm.node.flags as u32;
7651        if (f & PM_ARRAY) != 0 {
7652            if let Some(arr) = pm.u_arr.as_ref() {
7653                arrays.insert(name.clone(), arr.clone());
7654            }
7655            variables.remove(name);
7656            assoc_arrays.remove(name);
7657        } else if (f & PM_HASHED) != 0 {
7658            if let Some(map) = paramtab_hashed_storage()
7659                .lock().unwrap().get(name)
7660            {
7661                assoc_arrays.insert(name.clone(), map.clone());
7662            }
7663            variables.remove(name);
7664            arrays.remove(name);
7665        } else if let Some(s) = pm.u_str.as_ref() {
7666            // PM_SCALAR / PM_NAMEREF / numeric — fold to the string view.
7667            variables.insert(name.clone(), s.clone());
7668            arrays.remove(name);
7669            assoc_arrays.remove(name);
7670        }
7671    }
7672}
7673
7674/// Format float with underscores
7675pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
7676    let s = convfloat(dval, 0, 0);
7677    if underscore <= 0 {
7678        return s;
7679    }
7680
7681    let u = underscore as usize;
7682    let (sign, rest) = if let Some(after) = s.strip_prefix('-') {
7683        ("-", after)
7684    } else {
7685        ("", s.as_str())
7686    };
7687
7688    let (int_part, frac_exp) = if let Some(dot_pos) = rest.find('.') {
7689        (&rest[..dot_pos], &rest[dot_pos..])
7690    } else {
7691        (rest, "")
7692    };
7693
7694    // Add underscores to integer part
7695    let int_chars: Vec<char> = int_part.chars().collect();
7696    let mut result = sign.to_string();
7697    let first_group = int_chars.len() % u;
7698    if first_group > 0 {
7699        result.extend(&int_chars[..first_group]);
7700        if first_group < int_chars.len() {
7701            result.push('_');
7702        }
7703    }
7704    for (i, chunk) in int_chars[first_group..].chunks(u).enumerate() {
7705        if i > 0 {
7706            result.push('_');
7707        }
7708        result.extend(chunk);
7709    }
7710
7711    // Add underscores to fractional part
7712    if let Some(frac) = frac_exp.strip_prefix('.') {
7713        result.push('.');
7714        let (frac_digits, exp) = if let Some(e_pos) = frac.find('e') {
7715            (&frac[..e_pos], &frac[e_pos..])
7716        } else {
7717            (frac, "")
7718        };
7719
7720        let frac_chars: Vec<char> = frac_digits.chars().collect();
7721        for (i, chunk) in frac_chars.chunks(u).enumerate() {
7722            if i > 0 {
7723                result.push('_');
7724            }
7725            result.extend(chunk);
7726        }
7727        result.push_str(exp);
7728    } else {
7729        result.push_str(frac_exp);
7730    }
7731
7732    result
7733}
7734
7735
7736
7737fn ifs_lock() -> &'static Mutex<String> {
7738    static IFS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7739    IFS_VAR.get_or_init(|| Mutex::new(" \t\n\0".to_string()))
7740}
7741
7742fn home_lock() -> &'static Mutex<String> {
7743    static HOME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7744    HOME_VAR.get_or_init(|| Mutex::new(env::var("HOME").unwrap_or_default()))
7745}
7746
7747fn term_lock() -> &'static Mutex<String> {
7748    static TERM_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7749    TERM_VAR.get_or_init(|| Mutex::new(env::var("TERM").unwrap_or_default()))
7750}
7751
7752fn wordchars_lock() -> &'static Mutex<String> {
7753    static WORDCHARS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7754    WORDCHARS_VAR.get_or_init(|| Mutex::new("*?_-.[]~=/&;!#$%^(){}<>".to_string()))
7755}
7756
7757fn histchars_lock() -> &'static Mutex<[u8; 3]> {
7758    static HISTCHARS_VAR: OnceLock<Mutex<[u8; 3]>> = OnceLock::new();
7759    HISTCHARS_VAR.get_or_init(|| Mutex::new([b'!', b'^', b'#']))
7760}
7761
7762fn keyboardhack_lock() -> &'static Mutex<u8> {
7763    static KEYBOARDHACK_VAR: OnceLock<Mutex<u8>> = OnceLock::new();
7764    KEYBOARDHACK_VAR.get_or_init(|| Mutex::new(0))
7765}
7766
7767fn histsiz_lock() -> &'static Mutex<i64> {
7768    static HISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
7769    // Match observed `zsh -fc 'echo $HISTSIZE'` output on zsh 5.9+
7770    // (Homebrew). Upstream's `configure.ac` defines DEFAULT_HISTSIZE
7771    // as 30 but distributed binaries seed the cap at 999999999 — the
7772    // parity goal here is "match the binary the user actually runs",
7773    // not "match the source-code default".
7774    HISTSIZ_VAR.get_or_init(|| Mutex::new(999_999_999))
7775}
7776
7777fn savehistsiz_lock() -> &'static Mutex<i64> {
7778    static SAVEHISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
7779    // Same rationale as `histsiz_lock` — observed `zsh -fc
7780    // 'echo $SAVEHIST'` returns 99999999 on zsh 5.9+. Source has
7781    // savehistsiz default to 0 but distributed binaries cap at 99M.
7782    SAVEHISTSIZ_VAR.get_or_init(|| Mutex::new(99_999_999))
7783}
7784
7785fn zsh_terminfo_lock() -> &'static Mutex<String> {
7786    static TERMINFO_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7787    TERMINFO_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO").unwrap_or_default()))
7788}
7789
7790fn zsh_terminfodirs_lock() -> &'static Mutex<String> {
7791    static TERMINFODIRS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7792    TERMINFODIRS_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO_DIRS").unwrap_or_default()))
7793}
7794
7795fn cached_username_lock() -> &'static Mutex<String> {
7796    static USERNAME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7797    USERNAME_VAR.get_or_init(|| Mutex::new(initial_username()))
7798}
7799
7800// Port of `static unsigned numparamvals;` (params.c:626) and the
7801// related per-scan statics at params.c:637-640. Per PORT.md Rule D
7802// these are file-scope statics, NOT aggregated into a state struct.
7803//
7804//   c:626  static unsigned numparamvals;
7805//   c:637  static Patprog scanprog;
7806//   c:638  static char *scanstr;
7807//   c:639  static char **paramvals;
7808//   c:640  static Param foundparam;   <-- exposed earlier as FOUNDPARAM
7809pub static NUMPARAMVALS: std::sync::atomic::AtomicU32 =
7810    std::sync::atomic::AtomicU32::new(0);                                    // c:626
7811pub static SCANPROG: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7812    std::sync::OnceLock::new();                                              // c:637
7813pub static SCANSTR: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7814    std::sync::OnceLock::new();                                              // c:638
7815pub static PARAMVALS: std::sync::OnceLock<std::sync::Mutex<Vec<String>>> =
7816    std::sync::OnceLock::new();                                              // c:639
7817
7818/// Resolve the current user's name. Mirrors C's `get_username()`
7819/// init at Src/init.c which reads `getpwuid(getuid())->pw_name`
7820/// rather than `$USER`. Falls back to env vars only if the
7821/// passwd lookup fails (rare on real systems).
7822fn initial_username() -> String {
7823    #[cfg(unix)]
7824    {
7825        let uid = unsafe { libc::getuid() };
7826        let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
7827        let mut buf = vec![0i8; 1024];
7828        let mut result: *mut libc::passwd = std::ptr::null_mut();
7829        let rc = unsafe {
7830            libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result)
7831        };
7832        if rc == 0 && !result.is_null() && !pwd.pw_name.is_null() {
7833            let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
7834            return cstr.to_string_lossy().into_owned();
7835        }
7836    }
7837    env::var("USER")
7838        .or_else(|_| env::var("LOGNAME"))
7839        .unwrap_or_default()
7840}
7841
7842fn pipestats_lock() -> &'static Mutex<Vec<i32>> {
7843    static PIPESTATS_VAR: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
7844    PIPESTATS_VAR.get_or_init(|| Mutex::new(Vec::new()))
7845}
7846
7847fn shtimer_lock() -> &'static Mutex<Duration> {
7848    static SHTIMER_VAR: OnceLock<Mutex<Duration>> = OnceLock::new();
7849    SHTIMER_VAR.get_or_init(|| {
7850        Mutex::new(
7851            SystemTime::now()
7852                .duration_since(UNIX_EPOCH)
7853                .unwrap_or_default(),
7854        )
7855    })
7856}
7857
7858fn pparams_lock() -> &'static Mutex<Vec<String>> {
7859    // Mirror of zsh's `pparams` (positional params $1, $2, ...).
7860    // Used by `poundgetfn` for `$#`. The canonical store is
7861    // `builtin::PPARAMS` (Src/init.c `pparams`); set/shift builtins
7862    // write there. Point at that single store so `$#` reads the
7863    // live value instead of an isolated empty mirror.
7864    &crate::ported::builtin::PPARAMS
7865}
7866
7867fn zunderscore_lock() -> &'static Mutex<String> {
7868    static ZUNDERSCORE_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7869    ZUNDERSCORE_VAR.get_or_init(|| Mutex::new(String::new()))
7870}
7871
7872/// Update `$_` with the last argument of the just-completed
7873/// command. Mirrors C zsh's writeback in `execcmd_exec` (Src/exec.c)
7874/// where `zunderscore` is set to the last argv slot before
7875/// returning. Callers: every command-dispatch hook in
7876/// fusevm_bridge / exec.rs.
7877pub fn set_zunderscore(argv: &[String]) {
7878    let new = if let Some(last) = argv.last() {
7879        last.clone()
7880    } else {
7881        String::new()
7882    };
7883    *zunderscore_lock()
7884        .lock()
7885        .expect("zunderscore poisoned") = new;
7886}
7887
7888/// Direct port of `static int dontimport(int flags)` from
7889/// `Src/params.c:796-810`.
7890/// ```c
7891/// /* If explicitly marked as don't import */
7892/// if (flags & PM_DONTIMPORT)
7893///     return 1;
7894/// /* If value already exported */
7895/// if (flags & PM_EXPORTED)
7896///     return 1;
7897/// /* If security issue when importing and running with some privilege */
7898/// if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED))
7899///     return 1;
7900/// /* OK to import */
7901/// return 0;
7902/// ```
7903/// Port of `dontimport(int flags)` from `Src/params.c:796`.
7904fn dontimport(flags: i32) -> i32 {                                           // c:796
7905    let flags = flags as u32;
7906    // c:799-800 — `if (flags & PM_DONTIMPORT) return 1`.
7907    if flags & crate::ported::zsh_h::PM_DONTIMPORT != 0 {                    // c:799
7908        return 1;                                                            // c:800
7909    }
7910    // c:802-803 — `if (flags & PM_EXPORTED) return 1`.
7911    if flags & crate::ported::zsh_h::PM_EXPORTED != 0 {                      // c:802
7912        return 1;                                                            // c:803
7913    }
7914    // c:805-806 — `if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED)) return 1`.
7915    if flags & crate::ported::zsh_h::PM_DONTIMPORT_SUID != 0                 // c:805
7916        && isset(crate::ported::zsh_h::PRIVILEGED)
7917    {
7918        return 1;                                                            // c:806
7919    }
7920    0                                                                        // c:809
7921}
7922
7923
7924/// Minimal `pattry()` shim — exact-match fallback until the pattern
7925/// engine in `Src/pattern.c` is wired.
7926fn pattry(prog: &str, s: &str) -> bool {
7927    prog == s
7928}
7929
7930// ===========================================================
7931// GSU dispatch table — maps special-parameter NAMES to their
7932// getfn callback. C zsh dispatches reads of `$RANDOM` /
7933// `$USERNAME` / `$UID` / etc. through `Param.gsu->getfn`, where
7934// each special parameter has a `Param` entry in `paramtab`
7935// pointing at its specific getfn (Src/params.c:225 SPECIAL_PARAM
7936// table seeds these mappings).
7937//
7938// zshrs has the GSU callbacks ported (uidgetfn, randomgetfn,
7939// usernamegetfn, etc. above) but the shell's parameter-read path
7940// (fusevm_bridge::expand_param) reads from ShellExecutor.variables
7941// directly — never dispatching through the callbacks. Result:
7942// `echo $RANDOM` returned the cached HashMap value (or empty),
7943// not a fresh `rand() & 0x7fff` from `randomgetfn`.
7944//
7945// `lookup_special_var(name)` is the bridge: given a variable
7946// name, returns the GSU getfn's output if `name` is a recognized
7947// special, else None. Callers (expand_param, subst.rs reads)
7948// check this before falling back to `variables.get(name)`.
7949// ===========================================================
7950
7951/// Look up a special-parameter NAME and dispatch to its GSU getfn.
7952///
7953/// Returns `Some(value_string)` if `name` is one of zshrs's
7954/// recognized specials with a real GSU getfn; `None` otherwise
7955/// (caller should fall back to `variables.get`).
7956///
7957/// This is the bridge between the named getfn callbacks above
7958/// (uidgetfn / randomgetfn / etc.) and the shell's parameter-read
7959/// path. Mirrors the `Param.gsu->getfn` dispatch C zsh does
7960/// inside `getsparam` / `getstrvalue` (Src/params.c:3076 / 2335).
7961pub fn lookup_special_var(name: &str) -> Option<String> {
7962    // All-digit positional: $1..$N from canonical PPARAMS.
7963    // C zsh dispatches positional params through pparams (Src/init.c).
7964    if !name.is_empty() && name.chars().all(|c| c.is_ascii_digit()) {
7965        let n: usize = name.parse().ok()?;
7966        if n == 0 {
7967            return crate::ported::utils::argzero();
7968        }
7969        let pp = pparams_lock().lock().ok()?;
7970        return pp.get(n - 1).cloned();
7971    }
7972    match name {
7973        // libc identity callbacks.
7974        "UID" => Some(uidgetfn().to_string()),
7975        "GID" => Some(gidgetfn().to_string()),
7976        "EUID" => Some(euidgetfn().to_string()),
7977        "EGID" => Some(egidgetfn().to_string()),
7978        // libc syscall callbacks.
7979        "RANDOM" => Some(randomgetfn().to_string()),
7980        "TTYIDLE" => Some(ttyidlegetfn().to_string()),
7981        "ERRNO" => Some(errnogetfn().to_string()),
7982        // Time callbacks.
7983        "SECONDS" => Some(intsecondsgetfn().to_string()),
7984        // Cached-state callbacks (OnceLock<Mutex<…>> backed).
7985        "USERNAME" => Some(usernamegetfn()),
7986        "HOME" => Some(homegetfn()),
7987        "TERM" => Some(termgetfn()),
7988        "WORDCHARS" => Some(wordcharsgetfn()),
7989        "IFS" => Some(ifsgetfn()),
7990        "TERMINFO" => Some(terminfogetfn()),
7991        "TERMINFO_DIRS" => Some(terminfodirsgetfn()),
7992        "KEYBOARD_HACK" => Some(keyboardhackgetfn()),
7993        "histchars" | "HISTCHARS" => Some(histcharsgetfn()),
7994        "_" => Some(underscoregetfn()),
7995        // Counters with int return.
7996        "HISTSIZE" => Some(histsizegetfn().to_string()),
7997        "SAVEHIST" => Some(savehistsizegetfn().to_string()),
7998        "#" | "ARGC" => Some(poundgetfn().to_string()),
7999        // $0 routes through utils::argzero.
8000        "0" => crate::ported::utils::argzero(),
8001        // POSIX shell-special scalars. C dispatches these through
8002        // dedicated gsu getfn callbacks (Src/params.c special_assigns).
8003        "?" => Some(crate::ported::builtin::LASTVAL
8004            .load(std::sync::atomic::Ordering::Relaxed)
8005            .to_string()),
8006        "$" => Some(std::process::id().to_string()),
8007        "!" => {
8008            // Last-backgrounded job PID. Stored in paramtab `!` slot;
8009            // default to 0 to match zsh fresh-shell behaviour.
8010            let tab = paramtab().read().ok()?;
8011            Some(tab.get("!").and_then(|pm| pm.u_str.clone())
8012                .unwrap_or_else(|| "0".to_string()))
8013        }
8014        // $* / $@ join positional params via IFS first char.
8015        "*" | "@" => {
8016            let sep = ifsgetfn().chars().next().unwrap_or(' ').to_string();
8017            pparams_lock().lock().ok().map(|p| p.join(&sep))
8018        }
8019        // $- : current option-letter set. zsh emits baseline "569X"
8020        // prefix (internal letters always on) + user-toggled flags.
8021        "-" => {
8022            let mut letters = String::from("569X");
8023            let opt = |n: &str| {
8024                crate::ported::options::opt_state_get(n).unwrap_or(false)
8025            };
8026            if opt("errexit")  { letters.push('e'); }
8027            if !opt("rcs")     { letters.push('f'); }
8028            if opt("login")    { letters.push('l'); }
8029            if opt("nounset")  { letters.push('u'); }
8030            if opt("xtrace")   { letters.push('x'); }
8031            if opt("verbose")  { letters.push('v'); }
8032            // c:Src/params.c — `set -n` toggles \`exec\` OFF (default ON).
8033            // The previous Rust port called \`opt(\"noexec\")\` which is
8034            // not a real option name in zsh; the lookup always returned
8035            // false, so \`$-\` never included 'n' even when \`set -n\` was
8036            // active. Read the canonical \`exec\` option and push 'n'
8037            // when UNSET.
8038            if !opt("exec")    { letters.push('n'); }
8039            if opt("hashall")  { letters.push('h'); }
8040            Some(letters)
8041        }
8042        // Arrays — joined with space for scalar context.
8043        "pipestatus" => {
8044            let arr = pipestatgetfn();
8045            if arr.is_empty() {
8046                None
8047            } else {
8048                Some(arr.join(" "))
8049            }
8050        }
8051        _ => None,
8052    }
8053}
8054
8055/// Shared test mutex for histsiz mutations (gsu_tests +
8056/// tests submodules both write the same global; this lock
8057/// serialises them under parallel test execution).
8058#[cfg(test)]
8059pub(crate) static HISTSIZ_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
8060
8061/// Shared test mutex for histchars mutations (gsu_tests +
8062/// tests submodules both write bangchar/hatchar/hashchar atomics;
8063/// this lock serialises them under parallel test execution).
8064#[cfg(test)]
8065pub(crate) static HISTCHARS_TEST_LOCK_SHARED: std::sync::Mutex<()> =
8066    std::sync::Mutex::new(());
8067
8068#[cfg(test)]
8069mod gsu_tests {
8070    use super::*;
8071
8072    #[test]
8073    fn test_libc_id_callbacks_match_libc() {
8074        assert_eq!(uidgetfn(), unsafe { libc::getuid() } as i64);
8075        assert_eq!(gidgetfn(), unsafe { libc::getgid() } as i64);
8076        assert_eq!(euidgetfn(), unsafe { libc::geteuid() } as i64);
8077        assert_eq!(egidgetfn(), unsafe { libc::getegid() } as i64);
8078    }
8079
8080    /// Pin: `usernamegetfn` routes through `get_username()` per
8081    /// `Src/params.c:4658` (which refreshes cache on uid change
8082    /// per `Src/utils.c:1082`). The previous Rust port read a
8083    /// stale cached value directly. Verify the getter returns
8084    /// the same name as a direct libc `getpwuid(getuid())` —
8085    /// confirming the path WENT through the refresh helper, not
8086    /// the stale paramtab Mutex.
8087    #[test]
8088    fn usernamegetfn_matches_libc_getpwuid_for_current_uid() {
8089        let uname = usernamegetfn();
8090        // The current process is running as some uid; the getter
8091        // must return either a populated name OR an empty string
8092        // (when getpwuid fails, e.g. sandboxed builds). It must
8093        // NOT panic and must NOT return a stale cached value
8094        // from a different uid.
8095        let direct = unsafe {
8096            let pw = libc::getpwuid(libc::getuid());
8097            if pw.is_null() {
8098                String::new()
8099            } else {
8100                std::ffi::CStr::from_ptr((*pw).pw_name)
8101                    .to_string_lossy()
8102                    .into_owned()
8103            }
8104        };
8105        assert_eq!(uname, direct,
8106            "c:4658 — usernamegetfn must match getpwuid(getuid())->pw_name");
8107    }
8108
8109    #[test]
8110    fn test_random_returns_15_bit_value() {
8111        for _ in 0..100 {
8112            let v = randomgetfn();
8113            assert!(v >= 0 && v < 0x8000);
8114        }
8115    }
8116
8117    #[test]
8118    fn test_random_set_seeds_deterministically() {
8119        randomsetfn(42);
8120        let a = randomgetfn();
8121        randomsetfn(42);
8122        let b = randomgetfn();
8123        assert_eq!(a, b);
8124    }
8125
8126    #[test]
8127    fn test_ifs_round_trip() {
8128        let original = ifsgetfn();
8129        ifssetfn(":,;".to_string());
8130        assert_eq!(ifsgetfn(), ":,;");
8131        ifssetfn(original);
8132    }
8133
8134    #[test]
8135    fn test_histsiz_clamps_to_1() {
8136        let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8137        let original = histsizegetfn();
8138        histsizesetfn(0);
8139        assert_eq!(histsizegetfn(), 1);
8140        histsizesetfn(-5);
8141        assert_eq!(histsizegetfn(), 1);
8142        histsizesetfn(500);
8143        assert_eq!(histsizegetfn(), 500);
8144        histsizesetfn(original);
8145    }
8146
8147    #[test]
8148    fn test_savehistsiz_clamps_to_0() {
8149        let original = savehistsizegetfn();
8150        savehistsizesetfn(-5);
8151        assert_eq!(savehistsizegetfn(), 0);
8152        savehistsizesetfn(100);
8153        assert_eq!(savehistsizegetfn(), 100);
8154        savehistsizesetfn(original);
8155    }
8156
8157    /// Pin: `savehistsizesetfn` syncs BOTH storage mirrors so the
8158    /// twin-storage Rust adaptation behaves like the single global
8159    /// in C. The params.rs Mutex<i64> drives `$SAVEHIST` reads;
8160    /// the hist.rs AtomicI64 drives the history-file writer cap.
8161    /// Previously only the params.rs side was written, so
8162    /// `SAVEHIST=10000` left hist.rs at 0 and the writer would
8163    /// cap at zero lines.
8164    #[test]
8165    fn savehistsizesetfn_syncs_to_hist_module() {
8166        use std::sync::atomic::Ordering;
8167        let original_params = savehistsizegetfn();
8168        let original_hist = crate::ported::hist::savehistsiz.load(Ordering::SeqCst);
8169        // Set via the setfn — both storages must reflect the value.
8170        savehistsizesetfn(12345);
8171        assert_eq!(savehistsizegetfn(), 12345,
8172            "c:4994 — params.rs Mutex<i64> reflects new value");
8173        assert_eq!(crate::ported::hist::savehistsiz.load(Ordering::SeqCst), 12345,
8174            "c:4994 — hist.rs AtomicI64 synced (was the previous gap)");
8175        // Negative clamps to 0 in BOTH stores.
8176        savehistsizesetfn(-99);
8177        assert_eq!(savehistsizegetfn(), 0,
8178            "c:4998 — params.rs clamps to 0");
8179        assert_eq!(crate::ported::hist::savehistsiz.load(Ordering::SeqCst), 0,
8180            "c:4998 — hist.rs clamps to 0 too");
8181        // Restore.
8182        savehistsizesetfn(original_params);
8183        crate::ported::hist::savehistsiz.store(original_hist, Ordering::SeqCst);
8184    }
8185
8186    #[test]
8187    fn test_pipestat_round_trip() {
8188        pipestatsetfn(Some(vec!["1".to_string(), "0".to_string(), "127".to_string()]));
8189        let v = pipestatgetfn();
8190        assert_eq!(v, vec!["1", "0", "127"]);
8191        pipestatsetfn(None);
8192        assert_eq!(pipestatgetfn(), Vec::<String>::new());
8193    }
8194
8195    /// Pin: `setnumvalue` actually STORES the scalar string per
8196    /// `Src/params.c:2862-2872`. The previous Rust port computed
8197    /// the string then dropped it via `let _ = s;` — meaning a
8198    /// numeric assignment to a SCALAR param stored NOTHING.
8199    ///
8200    /// C body for PM_SCALAR: `setstrvalue(v, convbase_underscore(
8201    /// val.u.l, pm->base, pm->width));`. We pin the round-trip
8202    /// for an integer assigned to a scalar param.
8203    #[test]
8204    fn setnumvalue_stores_int_value_into_scalar_pm() {
8205        use crate::ported::zsh_h::{param, hashnode, value, PM_SCALAR};
8206        use crate::ported::math::{mnumber, MN_INTEGER};
8207        // c:2860 — setnumvalue bails when unset(EXECOPT). The unit-test
8208        // env doesn't run through createoptiontable so we set "exec"
8209        // explicitly to simulate normal runtime.
8210        let saved_exec = crate::ported::options::opt_state_get("exec")
8211            .unwrap_or(false);
8212        crate::ported::options::opt_state_set("exec", true);
8213        // Build a scalar Param with no special base/width.
8214        let mut pm = Box::new(param {
8215            node: hashnode { next: None, nam: "x".to_string(), flags: PM_SCALAR as i32 },
8216            u_data: 0, u_arr: None, u_str: Some(String::new()), u_val: 0,
8217            u_dval: 0.0, u_hash: None,
8218            gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
8219            base: 0, width: 0, env: None, ename: None, old: None, level: 0,
8220        });
8221        let mut v = value {
8222            pm: Some(pm.clone()),
8223            arr: Vec::new(),
8224            scanflags: 0,
8225            valflags: 0,
8226            start: 0,
8227            end: -1,
8228        };
8229        let val = mnumber { l: 42, d: 0.0, type_: MN_INTEGER };
8230        setnumvalue(Some(&mut v), val);
8231        // c:2871 — the scalar storage now holds "42".
8232        let stored = v.pm.as_ref().unwrap().u_str.clone().unwrap_or_default();
8233        assert_eq!(stored, "42",
8234            "c:2871 — setnumvalue must store the rendered integer; \
8235             was previously dropped via `let _ = s;`");
8236        let _ = pm;
8237        crate::ported::options::opt_state_set("exec", saved_exec);
8238    }
8239
8240    #[test]
8241    fn test_simple_arrayuniq_first_wins() {
8242        let v = vec!["a".to_string(), "b".to_string(), "a".to_string(), "c".to_string()];
8243        assert_eq!(simple_arrayuniq(v), vec!["a", "b", "c"]);
8244    }
8245
8246    #[test]
8247    fn test_split_env_string() {
8248        assert_eq!(
8249            split_env_string("PATH=/usr/bin:/bin"),
8250            Some(("PATH".to_string(), "/usr/bin:/bin".to_string()))
8251        );
8252        assert_eq!(
8253            split_env_string("EMPTY="),
8254            Some(("EMPTY".to_string(), "".to_string()))
8255        );
8256        assert_eq!(split_env_string("NOEQUALS"), None);
8257    }
8258
8259    #[test]
8260    fn test_mkenvstr() {
8261        assert_eq!(mkenvstr("PATH", "/usr/bin", 0), "PATH=/usr/bin");
8262        assert_eq!(mkenvstr("EMPTY", "", 0), "EMPTY=");
8263    }
8264
8265    #[test]
8266    fn test_seconds_round_trip() {
8267        intsecondssetfn(0);
8268        let s1 = intsecondsgetfn();
8269        std::thread::sleep(std::time::Duration::from_millis(5));
8270        let s2 = intsecondsgetfn();
8271        assert!(s2 >= s1);
8272        // Reset to a known offset and read back.
8273        setrawseconds(100.0);
8274        assert_eq!(getrawseconds(), 100.0);
8275    }
8276
8277    #[test]
8278    fn test_argzero_round_trip() {
8279        argzerosetfn("/bin/zsh".to_string());
8280        assert_eq!(argzerogetfn(), "/bin/zsh");
8281        argzerosetfn(String::new());
8282    }
8283
8284    #[test]
8285    fn test_env_get_set() {
8286        let result = zputenv("ZSHRS_TEST_VAR=hello");
8287        assert_eq!(result, 0);
8288        assert_eq!(zgetenv("ZSHRS_TEST_VAR"), Some("hello".to_string()));
8289        delenv("ZSHRS_TEST_VAR");
8290        assert_eq!(zgetenv("ZSHRS_TEST_VAR"), None);
8291    }
8292
8293    #[test]
8294    fn test_keyboardhack_one_char() {
8295        keyboardhacksetfn("\\".to_string());
8296        assert_eq!(keyboardhackgetfn(), "\\");
8297        keyboardhacksetfn(String::new());
8298        assert_eq!(keyboardhackgetfn(), "");
8299    }
8300
8301    /// Pin: `keyboardhacksetfn` accepts ASCII chars cleanly per
8302    /// `Src/params.c:5040-5060`. Tests the canonical happy path
8303    /// — single ASCII char, empty input, and the ASCII guard.
8304    ///
8305    /// The previous Rust port skipped `unmetafy(x, &len)` (c:5044)
8306    /// before the length and ASCII checks. This test exercises
8307    /// the surface API; the unmetafy fix is doc-pinned in the
8308    /// fn body since constructing Meta-encoded String values for
8309    /// the test fixture would require unsafe (Rust strings must
8310    /// be valid UTF-8 and the Meta byte 0x83 is not a valid
8311    /// UTF-8 lead).
8312    #[test]
8313    fn keyboardhacksetfn_handles_ascii_and_empty() {
8314        // c:5056 — single ASCII char stored.
8315        keyboardhacksetfn(";".to_string());
8316        assert_eq!(keyboardhackgetfn(), ";",
8317            "c:5056 — single ASCII char stored verbatim");
8318        // c:5056 — different ASCII char stored.
8319        keyboardhacksetfn(",".to_string());
8320        assert_eq!(keyboardhackgetfn(), ",");
8321        // c:5058 — empty input clears to '\0'.
8322        keyboardhacksetfn(String::new());
8323        assert_eq!(keyboardhackgetfn(), "");
8324    }
8325
8326    #[test]
8327    fn test_histchars_default() {
8328        let _g = super::HISTCHARS_TEST_LOCK_SHARED
8329            .lock().unwrap_or_else(|e| e.into_inner());
8330        histcharssetfn(None);
8331        assert_eq!(histcharsgetfn(), "!^#");
8332        histcharssetfn(Some("@$&".to_string()));
8333        assert_eq!(histcharsgetfn(), "@$&");
8334        histcharssetfn(None);
8335    }
8336
8337    /// Pin: `histcharssetfn` runs `unmetafy` per Src/params.c:5086
8338    /// BEFORE the length truncation and ASCII guard. Previously
8339    /// the Rust port skipped unmetafy, so a Meta-pair would
8340    /// inflate the byte count past 3 and the truncation would
8341    /// drop valid characters.
8342    ///
8343    /// Test the happy path: 1-char, 2-char, 3-char ASCII inputs
8344    /// all parse correctly and each char-position fills the
8345    /// matching atomic.
8346    #[test]
8347    fn histcharssetfn_handles_1_2_3_char_inputs() {
8348        let _g = super::HISTCHARS_TEST_LOCK_SHARED
8349            .lock().unwrap_or_else(|e| e.into_inner());
8350        use std::sync::atomic::Ordering;
8351        // 1-char: bangchar=='Q', hatchar=='\0', hashchar=='\0'.
8352        histcharssetfn(Some("Q".to_string()));
8353        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'Q' as i32);
8354        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), 0);
8355        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), 0);
8356        // 2-char: bangchar=='X', hatchar=='Y', hashchar=='\0'.
8357        histcharssetfn(Some("XY".to_string()));
8358        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'X' as i32);
8359        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'Y' as i32);
8360        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), 0);
8361        // 3-char: bangchar=='A', hatchar=='B', hashchar=='C'.
8362        histcharssetfn(Some("ABC".to_string()));
8363        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'A' as i32);
8364        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'B' as i32);
8365        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'C' as i32);
8366        // 4+ char: c:5087-5088 truncates to 3.
8367        histcharssetfn(Some("WXYZ".to_string()));
8368        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'W' as i32);
8369        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'X' as i32);
8370        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'Y' as i32);
8371        // Reset to default.
8372        histcharssetfn(None);
8373        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'!' as i32);
8374        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'^' as i32);
8375        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'#' as i32);
8376    }
8377}
8378
8379// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8380// ─── RUST-ONLY ACCESSORS ───
8381//
8382// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
8383// RwLock<T>>` globals declared above. C zsh uses direct global
8384// access; Rust needs these wrappers because `OnceLock::get_or_init`
8385// is the only way to lazily construct shared state. These fns sit
8386// here so the body of this file reads in C source order without
8387// the accessor wrappers interleaved between real port fns.
8388// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8389
8390// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8391// ─── RUST-ONLY ACCESSORS ───
8392//
8393// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
8394// RwLock<T>>` globals declared above. C zsh uses direct global
8395// access; Rust needs these wrappers because `OnceLock::get_or_init`
8396// is the only way to lazily construct shared state. These fns sit
8397// here so the body of this file reads in C source order without
8398// the accessor wrappers interleaved between real port fns.
8399// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8400
8401// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8402// ─── RUST-ONLY ACCESSORS ───
8403//
8404// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
8405// RwLock<T>>` globals declared above. C zsh uses direct global
8406// access; Rust needs these wrappers because `OnceLock::get_or_init`
8407// is the only way to lazily construct shared state. These fns sit
8408// here so the body of this file reads in C source order without
8409// the accessor wrappers interleaved between real port fns.
8410// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8411
8412fn foundparam_lock() -> &'static std::sync::Mutex<Option<String>> {
8413    FOUNDPARAM.get_or_init(|| std::sync::Mutex::new(None))
8414}
8415
8416/// Accessor for the global `paramtab` (Src/params.c:515).
8417/// Mirrors C's `paramtab->...` dereference by handing back the
8418/// inner RwLock; callers `.read()` for lookups and `.write()` for
8419/// mutation, operating on the `HashMap<String, Param>` directly.
8420pub fn paramtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
8421    PARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
8422}
8423
8424/// Accessor for the global `realparamtab` (Src/params.c:515).
8425/// Same role as `paramtab` for the not-currently-redirected case;
8426/// the alias-flip during assoc-array iteration isn't modelled yet.
8427pub fn realparamtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
8428    REALPARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
8429}
8430
8431fn scanprog_lock() -> &'static std::sync::Mutex<Option<String>> {
8432    SCANPROG.get_or_init(|| std::sync::Mutex::new(None))
8433}
8434
8435fn scanstr_lock() -> &'static std::sync::Mutex<Option<String>> {
8436    SCANSTR.get_or_init(|| std::sync::Mutex::new(None))
8437}
8438
8439fn paramvals_lock() -> &'static std::sync::Mutex<Vec<String>> {
8440    PARAMVALS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
8441}
8442
8443#[cfg(test)]
8444mod tests {
8445    use super::*;
8446
8447    // test_param_value_conversions removed: tested deleted fake
8448    // `ParamValue::Scalar` constructor. C uses union access on
8449    // `pm->u.str`/`u.val`/`u.dval`/`u.arr` dispatched via
8450    // `PM_TYPE(pm->node.flags)` (Src/zsh.h:540).
8451    #[test]
8452    fn test_colonarr_conversion() {
8453        let arr = crate::ported::utils::colonsplit("/bin:/usr/bin:/usr/local/bin", false);
8454        assert_eq!(arr, vec!["/bin", "/usr/bin", "/usr/local/bin"]);
8455        let path = colonarrgetfn(&arr);
8456        assert_eq!(path, "/bin:/usr/bin:/usr/local/bin");
8457    }
8458       #[test]
8459    fn test_isident() {
8460        assert!(isident("foo"));
8461        assert!(isident("_bar"));
8462        assert!(isident("FOO_BAR"));
8463        assert!(isident("x123"));
8464        assert!(isident("123")); // positional params
8465        assert!(!isident(""));
8466        assert!(!isident("foo bar"));
8467    }
8468
8469    /// Pin: `isident` requires balanced `[...]` per `Src/params.c:1329-1330`:
8470    ///   if (*ss != '[') return 0;
8471    ///   if (!(ss = parse_subscript(++ss, 1, ']'))) return 0;
8472    ///
8473    /// The previous Rust port accepted ANY `[` as a valid
8474    /// terminator (`if c == '[' { return true; }`) without
8475    /// checking for a matching `]`. So `foo[` (no close) was
8476    /// accepted as a valid identifier — diverging from C which
8477    /// rejects.
8478    #[test]
8479    fn isident_requires_balanced_subscript_brackets() {
8480        // Balanced `[...]` is valid.
8481        assert!(isident("foo[0]"),
8482            "c:1330 — balanced [0] passes parse_subscript");
8483        assert!(isident("foo[bar]"),
8484            "c:1330 — balanced [bar] passes parse_subscript");
8485        // UNBALANCED — open without close — must be rejected.
8486        assert!(!isident("foo["),
8487            "c:1330 — `foo[` missing `]` MUST be rejected");
8488        // Trailing chars after `]` — C parse_subscript returns
8489        // a position INSIDE the string, the surrounding isident
8490        // body checks that nothing follows; our port currently
8491        // returns true at the first `[` either way, but pin the
8492        // balanced case as a working invariant.
8493        assert!(isident("a[1]"),
8494            "c:1330 — short balanced subscript valid");
8495    }
8496
8497
8498    #[test]
8499    fn test_unique_array() {
8500        let arr = vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()];
8501        let result = uniqarray(arr);
8502        assert_eq!(result, vec!["a", "b", "c"]);
8503    }
8504
8505    #[test]
8506    fn test_convbase() {
8507        // CBASES off (default): `16#FF` / `8#7` form. The `0x.../
8508        // 0...` short-prefix output is gated on `setopt CBASES` —
8509        // see Src/params.c:5599-5605.
8510        assert_eq!(convbase(255, 16), "16#FF");
8511        assert_eq!(convbase(10, 10), "10");
8512        assert_eq!(convbase(-5, 10), "-5");
8513        assert_eq!(convbase(7, 8), "8#7");
8514        assert_eq!(convbase(5, 2), "2#101");
8515    }
8516
8517    #[test]
8518    fn test_convfloat() {
8519        // Use 2.5 instead of 3.14 — clippy errors on the latter as
8520        // an approx PI constant. The test checks 2-decimal formatting
8521        // round-trips, which the exact value doesn't influence.
8522        let s = convfloat(2.5, 2, crate::ported::zsh_h::PM_FFLOAT);
8523        assert!(s.starts_with("2.50"));
8524
8525        assert_eq!(convfloat(f64::INFINITY, 0, 0), "Inf");
8526        assert_eq!(convfloat(f64::NEG_INFINITY, 0, 0), "-Inf");
8527        assert_eq!(convfloat(f64::NAN, 0, 0), "NaN");
8528    }
8529
8530
8531
8532
8533    #[test]
8534    fn test_getarrvalue() {
8535        let arr = vec!["a".into(), "b".into(), "c".into(), "d".into()];
8536        assert_eq!(getarrvalue(&arr, 2, 3), vec!["b", "c"]);
8537        assert_eq!(getarrvalue(&arr, -2, -1), vec!["c", "d"]);
8538        assert_eq!(getarrvalue(&arr, 1, 4), vec!["a", "b", "c", "d"]);
8539    }
8540
8541    #[test]
8542    fn test_setarrvalue() {
8543        // c:2897 — setarrvalue bails when unset(EXECOPT). Set "exec"
8544        // for the unit-test env (real zsh defaults exec=true).
8545        let saved_exec = crate::ported::options::opt_state_get("exec")
8546            .unwrap_or(false);
8547        crate::ported::options::opt_state_set("exec", true);
8548        // C-faithful: setarrvalue takes a Value pointing at a Param
8549        // with u_arr set. Construct one inline.
8550        use crate::ported::zsh_h::{hashnode, param, PM_ARRAY};
8551        let pm = Box::new(param {
8552            node: hashnode { next: None, nam: "test".to_string(), flags: PM_ARRAY as i32 },
8553            u_data: 0,
8554            u_arr: Some(vec!["a".into(), "b".into(), "c".into(), "d".into()]),
8555            u_str: None, u_val: 0, u_dval: 0.0, u_hash: None,
8556            gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
8557            base: 0, width: 0, env: None, ename: None, old: None, level: 0,
8558        });
8559        let mut v = crate::ported::zsh_h::value {
8560            pm: Some(pm),
8561            arr: Vec::new(),
8562            scanflags: 0,
8563            valflags: 0,
8564            start: 2,
8565            end: 3,
8566        };
8567        setarrvalue(&mut v, vec!["X".into(), "Y".into()]);
8568        let arr = v.pm.unwrap().u_arr.unwrap();
8569        assert_eq!(arr, vec!["a", "X", "Y", "d"]);
8570        crate::ported::options::opt_state_set("exec", saved_exec);
8571    }
8572
8573    #[test]
8574    fn test_valid_refname() {
8575        assert!(valid_refname("foo", 0));
8576        assert!(valid_refname("_bar", 0));
8577        assert!(valid_refname("1", 0));
8578        assert!(valid_refname("!", 0));
8579        assert!(valid_refname("arr[1]", 0));
8580        assert!(!valid_refname("", 0));
8581        // C semantics: empty leader without one of `! ? $ - _` is rejected.
8582        assert!(!valid_refname(" ", 0));
8583        // PM_UPPER rejects digit-leader and argv/ARGC.
8584        assert!(!valid_refname("1", PM_UPPER as i32));
8585        assert!(!valid_refname("argv", PM_UPPER as i32));
8586        assert!(!valid_refname("ARGC", PM_UPPER as i32));
8587    }
8588
8589    #[test]
8590    fn test_uniq_array_empty() {
8591        let empty: Vec<String> = Vec::new();
8592        assert!(uniqarray(empty).is_empty());
8593    }
8594
8595    #[test]
8596    fn test_convbase_underscore() {
8597        let s = convbase_underscore(1234567, 10, 3);
8598        assert_eq!(s, "1_234_567");
8599    }
8600
8601    fn val_str(v: getarg_out<'_>) -> String {
8602        match v {
8603            getarg_out::Value(v) => v.to_str(),
8604            getarg_out::Flags { .. } => panic!("expected Value, got Flags"),
8605        }
8606    }
8607
8608    #[test]
8609    fn getarg_n_flag_picks_second_exact_match() {
8610        // C params.c:1431-1442 + 1758 — `(en.2.)pat` picks 2nd exact match.
8611        let arr: Vec<String> = vec!["foo".into(), "bar".into(), "foo".into(), "baz".into()];
8612        let out = getarg("(en.2.r)foo", Some(&arr), None, None).expect("Some");
8613        assert_eq!(val_str(out), "foo");
8614    }
8615
8616    #[test]
8617    fn getarg_n_flag_third_exact_match() {
8618        let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into(), "b".into()];
8619        let out = getarg("(en.3.r)a", Some(&arr), None, None).expect("Some");
8620        assert_eq!(val_str(out), "a");
8621    }
8622
8623    #[test]
8624    fn getarg_n_flag_returns_index_with_i() {
8625        // (en.2.i) — return INDEX of 2nd exact match.
8626        let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
8627        let out = getarg("(en.2.i)x", Some(&arr), None, None).expect("Some");
8628        assert_eq!(val_str(out), "3");
8629    }
8630
8631    #[test]
8632    fn getarg_negative_n_flips_search_direction() {
8633        // C params.c:1488-1491 — negative `num` flips down (reverse).
8634        // (en.-1.) on forward-default search matches from the end.
8635        let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into()];
8636        let out = getarg("(en.-1.i)a", Some(&arr), None, None).expect("Some");
8637        assert_eq!(val_str(out), "3");
8638    }
8639
8640    #[test]
8641    fn getarg_n_flag_zero_treated_as_one() {
8642        // C params.c:1438-1439 — `if (!num) num = 1`.
8643        let arr: Vec<String> = vec!["x".into(), "y".into()];
8644        let out = getarg("(en.0.r)x", Some(&arr), None, None).expect("Some");
8645        assert_eq!(val_str(out), "x");
8646    }
8647
8648    #[test]
8649    fn getarg_unknown_flag_char_returns_none() {
8650        // C params.c:1477-1483 flagerr — invalid flag char reports error.
8651        let arr: Vec<String> = vec!["x".into()];
8652        assert!(getarg("(z)x", Some(&arr), None, None).is_none());
8653    }
8654
8655    #[test]
8656    fn getarg_n_flag_unterminated_arg_returns_none() {
8657        // (n.5 missing closing delimiter — flagerr.
8658        let arr: Vec<String> = vec!["x".into()];
8659        assert!(getarg("(n.5", Some(&arr), None, None).is_none());
8660    }
8661
8662    #[test]
8663    fn getarg_b_flag_starts_search_at_index() {
8664        // C params.c:1748-1760 — `(b.N.e)pat` skips first N-1 elements
8665        // forward (parsed value `N`, normalized to `beg = N-1`).
8666        let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
8667        // Forward, beg=2 (skip first 2) → starts at idx 2 → 'x' at 3.
8668        let out = getarg("(b.3.ei)x", Some(&arr), None, None).expect("Some");
8669        assert_eq!(val_str(out), "3");
8670    }
8671
8672    #[test]
8673    fn getarg_b_flag_with_R_reverse_from_offset() {
8674        // C params.c:1750-1755 — reverse search starting at parsed-1 idx.
8675        // arr=(x y x y), beg=2 (parsed 3-1), reverse → walks 2,1,0; first
8676        // exact 'x' is at idx 2 → 1-based "3".
8677        let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
8678        let out = getarg("(b.3.eIR)x", Some(&arr), None, None).expect("Some");
8679        assert_eq!(val_str(out), "3");
8680    }
8681
8682    #[test]
8683    fn getarg_b_flag_out_of_bounds_forward_returns_empty() {
8684        // c:1746 — beg >= len returns len+1 (empty for value-mode).
8685        let arr: Vec<String> = vec!["x".into()];
8686        let out = getarg("(b.5.er)x", Some(&arr), None, None).expect("Some");
8687        assert_eq!(val_str(out), "");
8688    }
8689
8690    #[test]
8691    fn getarg_b_flag_out_of_bounds_index_mode_returns_len_plus_one() {
8692        let arr: Vec<String> = vec!["x".into(), "y".into()];
8693        let out = getarg("(b.5.ei)x", Some(&arr), None, None).expect("Some");
8694        assert_eq!(val_str(out), "3");
8695    }
8696
8697    #[test]
8698    fn getarg_hash_neg_num_on_lowercase_r_returns_all() {
8699        // C params.c:1488-1491 — neg `num` flips down on `r`,
8700        // converting hash search to return-all-matches semantics.
8701        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8702        h.insert("a".into(), "1".into());
8703        h.insert("b".into(), "1".into());
8704        h.insert("c".into(), "2".into());
8705        let out = getarg("(en.-1.r)1", None, Some(&h), None).expect("Some");
8706        // r + neg = R semantics → all values where pat matches value.
8707        assert_eq!(val_str(out), "1 1");
8708    }
8709
8710    #[test]
8711    fn getarg_hash_neg_num_on_uppercase_R_returns_single() {
8712        // R + neg `num` un-flips back to single-match (r semantics).
8713        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8714        h.insert("a".into(), "1".into());
8715        h.insert("b".into(), "1".into());
8716        h.insert("c".into(), "2".into());
8717        let out = getarg("(en.-1.R)1", None, Some(&h), None).expect("Some");
8718        // R + neg → r → single first match.
8719        assert_eq!(val_str(out), "1");
8720    }
8721
8722    #[test]
8723    fn getarg_hash_b_flag_skips_first_n_entries() {
8724        // C params.c:1740-1742 — `b<NUM>` skips first N-1 entries
8725        // before searching. Hash iteration is insertion order.
8726        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8727        h.insert("a".into(), "1".into());
8728        h.insert("b".into(), "1".into());
8729        h.insert("c".into(), "1".into());
8730        // beg=2 (parsed 3-1) → skip first 2, scan from "c" onward.
8731        let out = getarg("(b.3.ei)1", None, Some(&h), None).expect("Some");
8732        assert_eq!(val_str(out), "c");
8733    }
8734
8735    #[test]
8736    fn getarg_hash_b_flag_with_R_collects_from_offset() {
8737        // R returns all matches; b skips first beg entries first.
8738        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8739        h.insert("a".into(), "1".into());
8740        h.insert("b".into(), "1".into());
8741        h.insert("c".into(), "1".into());
8742        let out = getarg("(b.2.eI)1", None, Some(&h), None).expect("Some");
8743        // beg=1, return_all=I → walk from "b" onward, all matching keys.
8744        assert_eq!(val_str(out), "b c");
8745    }
8746
8747    #[test]
8748    fn getarg_hash_b_flag_out_of_bounds_returns_empty() {
8749        // c:1746 — beg >= len with single-match → empty.
8750        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8751        h.insert("a".into(), "1".into());
8752        let out = getarg("(b.5.e)1", None, Some(&h), None).expect("Some");
8753        assert_eq!(val_str(out), "");
8754    }
8755
8756    #[test]
8757    fn getarg_w_flag_splits_multi_word_array_elements() {
8758        // C params.c:1761-1797 — `(w)N` joins array then re-splits by
8759        // IFS-default whitespace. arr=("a b" "c d"); (w)2 → "b" not "c d".
8760        let arr: Vec<String> = vec!["a b".into(), "c d".into()];
8761        let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
8762        assert_eq!(val_str(out), "b");
8763    }
8764
8765    #[test]
8766    fn getarg_w_flag_simple_array_indexing_still_works() {
8767        let arr: Vec<String> = vec!["one".into(), "two".into(), "three".into()];
8768        let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
8769        assert_eq!(val_str(out), "two");
8770    }
8771
8772    #[test]
8773    fn getarg_f_flag_splits_by_newline() {
8774        // C params.c:1424-1427 — `f` flag aliases `w` with sep="\n".
8775        // arr=("a b\nc d"); (f)2 → "c d" (split by \n only, not space).
8776        let arr: Vec<String> = vec!["a b\nc d".into()];
8777        let out = getarg("(f)2", Some(&arr), None, None).expect("Some");
8778        assert_eq!(val_str(out), "c d");
8779    }
8780
8781    #[test]
8782    fn getarg_scalar_w_flag_picks_nth_word() {
8783        // C params.c:1761-1797 — scalar word-mode arm. `(w)2` on
8784        // scalar "hello world foo" returns the 2nd whitespace word.
8785        let out = getarg("(w)2", None, None, Some("hello world foo")).expect("Some");
8786        assert_eq!(val_str(out), "world");
8787    }
8788
8789    #[test]
8790    fn getarg_scalar_w_flag_negative_index_counts_from_end() {
8791        let out = getarg("(w)-1", None, None, Some("alpha beta gamma")).expect("Some");
8792        assert_eq!(val_str(out), "gamma");
8793    }
8794
8795    #[test]
8796    fn getarg_scalar_re_returns_char_at_match_position() {
8797        // C params.c:1798-1980 — char-search returns CHAR at match
8798        // position, not full substring. Verified empirically:
8799        //   /bin/zsh -c 's="barfooxyz"; print "${s[(r)foo]}"'  → "f"
8800        let out = getarg("(re)bc", None, None, Some("abcdef")).expect("Some");
8801        assert_eq!(val_str(out), "b");
8802    }
8803
8804    #[test]
8805    fn getarg_scalar_ie_returns_position_of_first_match() {
8806        let out = getarg("(ie)cd", None, None, Some("abcdef")).expect("Some");
8807        // 'cd' starts at 1-based position 3.
8808        assert_eq!(val_str(out), "3");
8809    }
8810
8811    #[test]
8812    fn getarg_scalar_Ie_returns_position_of_last_match() {
8813        let out = getarg("(Ie)b", None, None, Some("abcabc")).expect("Some");
8814        // Last 'b' is at 1-based position 5.
8815        assert_eq!(val_str(out), "5");
8816    }
8817
8818    #[test]
8819    fn getarg_scalar_ie_no_match_returns_len_plus_one() {
8820        let out = getarg("(ie)z", None, None, Some("abc")).expect("Some");
8821        assert_eq!(val_str(out), "4");
8822    }
8823
8824    #[test]
8825    fn getarg_scalar_Ie_no_match_returns_zero() {
8826        let out = getarg("(Ie)z", None, None, Some("abc")).expect("Some");
8827        assert_eq!(val_str(out), "0");
8828    }
8829
8830    #[test]
8831    fn getarg_scalar_n_flag_picks_second_match() {
8832        // C params.c:1929/1964 — `!--num` Nth-match counter on
8833        // scalar char-search. abcabc: 'a' at idx 0 and 3 → 2nd match
8834        // at byte position 4 (1-based).
8835        let out = getarg("(en.2.i)a", None, None, Some("abcabc")).expect("Some");
8836        assert_eq!(val_str(out), "4");
8837    }
8838
8839    #[test]
8840    fn getarg_scalar_b_flag_starts_from_offset() {
8841        // C params.c:1740-1742 — `(b.N.)` starts search from idx N-1.
8842        // abc bc abc: with b=4, skip first 3 chars; first 'b' at byte 5.
8843        let out = getarg("(b.4.ei)b", None, None, Some("abcbc")).expect("Some");
8844        assert_eq!(val_str(out), "4");
8845    }
8846
8847    #[test]
8848    fn getarg_scalar_re_n2_picks_second_substring() {
8849        let out = getarg("(en.2.r)b", None, None, Some("abab")).expect("Some");
8850        assert_eq!(val_str(out), "b");
8851    }
8852
8853    /// c:3076/3193 — assignsparam writes into paramtab; getsparam
8854    /// reads it back. The round-trip is the spine of every
8855    /// `foo=bar; print $foo` flow. Regression here would silently
8856    /// drop assignments.
8857    #[test]
8858    fn assignsparam_then_getsparam_round_trips() {
8859        let name = "ZSHRS_TEST_ASSIGN_GET";
8860        crate::ported::params::assignsparam(name, "test_value_42", 0);
8861        assert_eq!(
8862            crate::ported::params::getsparam(name).as_deref(),
8863            Some("test_value_42")
8864        );
8865        // Cleanup so other tests don't see leaked param.
8866        let _ = crate::ported::params::paramtab().write().unwrap().remove(name);
8867    }
8868
8869    /// c:3076 — getsparam on a non-existent param returns None.
8870    /// A regression returning Some("") would mask unset-param errors.
8871    #[test]
8872    fn getsparam_unknown_param_returns_none() {
8873        assert!(crate::ported::params::getsparam("ZSHRS_TEST_DEFINITELY_UNSET").is_none());
8874    }
8875
8876    /// c:3819 — direct paramtab.remove drops the entry; subsequent
8877    /// getsparam returns None. The set→remove→lookup gap verifies
8878    /// the canonical paramtab is actually backing both reads + writes.
8879    #[test]
8880    fn paramtab_remove_makes_getsparam_return_none() {
8881        let name = "ZSHRS_TEST_UNSET_FLOW";
8882        crate::ported::params::assignsparam(name, "to_be_removed", 0);
8883        assert!(crate::ported::params::getsparam(name).is_some(),
8884            "param must be set before remove path");
8885        let _ = crate::ported::params::paramtab().write().unwrap().remove(name);
8886        assert!(crate::ported::params::getsparam(name).is_none(),
8887            "after remove, getsparam must return None");
8888    }
8889
8890    /// c:3357 — assignaparam stores an array. getsparam on an array
8891    /// param returns the first element OR a join (depends on IFS).
8892    /// Verify the slot was populated AT ALL by querying paramtab.
8893    #[test]
8894    fn assignaparam_populates_paramtab_with_array() {
8895        let name = "ZSHRS_TEST_ARR_X";
8896        crate::ported::params::assignaparam(
8897            name, vec!["a".into(), "b".into(), "c".into()], 0);
8898        let tab = crate::ported::params::paramtab().read().expect("paramtab poisoned");
8899        let pm = tab.get(name).expect("param installed");
8900        assert_eq!(pm.u_arr.as_deref(), Some(&["a".to_string(), "b".to_string(), "c".to_string()][..]),
8901            "assignaparam stores all three elements");
8902        drop(tab);
8903        let _ = crate::ported::params::paramtab().write().unwrap().remove(name);
8904    }
8905
8906    // Use the module-scope HISTCHARS_TEST_LOCK_SHARED (declared
8907    // outside the test modules) so gsu_tests + tests serialise
8908    // against the same Mutex rather than two independent ones.
8909    use super::HISTCHARS_TEST_LOCK_SHARED as HISTCHARS_TEST_LOCK;
8910
8911    /// `Src/params.c:5095-5097` — `histcharssetfn` stores bangchar /
8912    /// hatchar / hashchar in the per-char globals. Pin the round-trip
8913    /// for ALL THREE: change HISTCHARS to a custom 3-char string,
8914    /// verify each atomic global reflects the new value, and verify
8915    /// the canonical default `"!^#"` restores on NULL.
8916    #[test]
8917    fn histcharssetfn_syncs_all_three_histchar_globals() {
8918        let _g = HISTCHARS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8919        use std::sync::atomic::Ordering;
8920        // Default state.
8921        crate::ported::params::histcharssetfn(None);
8922        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'!' as i32);
8923        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst),  b'^' as i32);
8924        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'#' as i32);
8925        // Set HISTCHARS to "@:%".
8926        crate::ported::params::histcharssetfn(Some("@:%".to_string()));
8927        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'@' as i32,
8928            "c:5095 — bangchar = first byte of HISTCHARS");
8929        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst),  b':' as i32,
8930            "c:5096 — hatchar = second byte of HISTCHARS");
8931        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'%' as i32,
8932            "c:5097 — hashchar = third byte of HISTCHARS");
8933        // Restore.
8934        crate::ported::params::histcharssetfn(None);
8935        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'!' as i32);
8936        assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'#' as i32);
8937    }
8938
8939    /// `Src/params.c:5064-5074` — `histcharsgetfn` reads from the
8940    /// three atomic globals and returns a string of non-NUL bytes.
8941    /// Pin set→get symmetry: after `histcharssetfn(Some("@&%"))`,
8942    /// `histcharsgetfn()` returns `"@&%"`.
8943    #[test]
8944    fn histcharsgetfn_round_trips_with_histcharssetfn() {
8945        let _g = HISTCHARS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8946        crate::ported::params::histcharssetfn(Some("@&%".to_string()));
8947        assert_eq!(crate::ported::params::histcharsgetfn(), "@&%",
8948            "c:5068-5073 — getfn reads atomic globals setfn wrote");
8949        // Restore default and verify round-trip.
8950        crate::ported::params::histcharssetfn(None);
8951        assert_eq!(crate::ported::params::histcharsgetfn(), "!^#",
8952            "default `!^#` round-trips through atomics");
8953    }
8954
8955    /// `Src/params.c:5118-5128` — `homesetfn(x)` round-trip:
8956    /// `homesetfn(s); homegetfn() == s` for non-symlink paths and
8957    /// CHASELINKS-off. Pins the basic store-then-read contract.
8958    #[test]
8959    fn homesetfn_stores_value_for_getfn() {
8960        let saved = crate::ported::params::homegetfn();
8961        crate::ported::params::homesetfn("/tmp/zshrs_test_home".to_string());
8962        assert_eq!(crate::ported::params::homegetfn(), "/tmp/zshrs_test_home",
8963            "c:5121-5126 — homesetfn → homegetfn round-trip");
8964        // Restore.
8965        crate::ported::params::homesetfn(saved);
8966    }
8967
8968    /// `Src/params.c:5125-5126` — empty input becomes `ztrdup("")`.
8969    /// Pin empty-string handling.
8970    #[test]
8971    fn homesetfn_empty_input_stores_empty() {
8972        let saved = crate::ported::params::homegetfn();
8973        crate::ported::params::homesetfn(String::new());
8974        assert_eq!(crate::ported::params::homegetfn(), "",
8975            "c:5126 — empty x stores empty (no panic)");
8976        crate::ported::params::homesetfn(saved);
8977    }
8978
8979    /// `Src/params.c:5004-5011` — `errnosetfn(x)` writes errno
8980    /// unconditionally, then warns (NOT errors) on truncation. The
8981    /// store happens regardless of warning. Pin set→get round-trip.
8982    #[test]
8983    #[cfg(any(target_os = "macos", target_os = "linux"))]
8984    fn errnosetfn_writes_through_to_libc_errno_getfn() {
8985        // Set errno to a small int.
8986        crate::ported::params::errnosetfn(42);
8987        assert_eq!(crate::ported::params::errnogetfn(), 42,
8988            "c:5006 — errno = (int)x; subsequent getfn must read it back");
8989        crate::ported::params::errnosetfn(0);
8990        assert_eq!(crate::ported::params::errnogetfn(), 0);
8991    }
8992
8993    /// `Src/params.c:5008-5010` — truncation check fires when
8994    /// `(zlong)errno != x`. C also resets errno indirectly inside
8995    /// `zwarn` (libc calls touch errno) — so after the warning,
8996    /// the user's observed `$ERRNO` is the post-warning value, NOT
8997    /// the truncated cast. Faithful Rust port has the same behavior.
8998    /// Pin only that the function returns normally and doesn't crash;
8999    /// any specific post-call errno value is implementation-defined.
9000    #[test]
9001    #[cfg(any(target_os = "macos", target_os = "linux"))]
9002    fn errnosetfn_does_not_panic_on_truncation() {
9003        // i64::MAX → truncates to i32 = -1 → warning fires inside.
9004        // The store at c:5008 happens; whether the warning's libc
9005        // calls then overwrite errno is implementation-defined.
9006        crate::ported::params::errnosetfn(i64::MAX);
9007        // Just verify the call returned (no panic) and getfn works.
9008        let _ = crate::ported::params::errnogetfn();
9009        // Reset.
9010        crate::ported::params::errnosetfn(0);
9011    }
9012
9013    /// `Src/params.c:5090-5093` — non-ASCII chars in HISTCHARS
9014    /// produce a warning and the function returns WITHOUT updating
9015    /// any globals. Pin the rejection: state before == state after
9016    /// when a non-ASCII byte is in position 0/1/2.
9017    #[test]
9018    fn histcharssetfn_rejects_non_ascii_chars() {
9019        let _g = HISTCHARS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9020        use std::sync::atomic::Ordering;
9021        // Reset to defaults.
9022        crate::ported::params::histcharssetfn(None);
9023        let bang_before = crate::ported::hist::bangchar.load(Ordering::SeqCst);
9024        let hat_before  = crate::ported::hist::hatchar.load(Ordering::SeqCst);
9025        // Try to set HISTCHARS with non-ASCII char.
9026        crate::ported::params::histcharssetfn(Some("é".to_string()));
9027        // c:5092 — rejection returns BEFORE any state changes.
9028        assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), bang_before,
9029            "c:5092 — bangchar unchanged after non-ASCII rejection");
9030        assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), hat_before);
9031    }
9032
9033    /// Shared mutex for tests that mutate argzero/posixzero — both
9034    /// share global state and race when run in parallel.
9035    static ARGZERO_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
9036
9037    // HISTSIZ_TEST_LOCK is defined at module scope to share between
9038    // gsu_tests and tests submodules — both mutate histsiz.
9039
9040    /// `Src/params.c:4974-4977` — `histsizesetfn` floors at 1 then
9041    /// calls `resizehistents()` to prune the in-memory ring to the
9042    /// new cap. The previous Rust port skipped the resize call (and
9043    /// also failed to mirror the value into `hist::histsiz`), so
9044    /// HISTSIZE shrinks didn't take effect until the next implicit
9045    /// prune. Pin: setting HISTSIZE to N caps both the param store
9046    /// AND the hist::histsiz atomic used by resizehistents.
9047    #[test]
9048    fn histsizesetfn_floors_at_one_and_mirrors_to_hist_module() {
9049        let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9050        use std::sync::atomic::Ordering;
9051        let saved_param = histsizegetfn();
9052        let saved_hist = crate::ported::hist::histsiz.load(Ordering::SeqCst);
9053
9054        // c:4976 — value < 1 floors at 1.
9055        crate::ported::params::histsizesetfn(0);
9056        assert_eq!(histsizegetfn(), 1,
9057            "c:4976 — HISTSIZE 0 must floor at 1");
9058        assert_eq!(crate::ported::hist::histsiz.load(Ordering::SeqCst), 1,
9059            "c:4977 — mirror into hist::histsiz so resizehistents sees it");
9060
9061        // Negative floors too.
9062        crate::ported::params::histsizesetfn(-5);
9063        assert_eq!(histsizegetfn(), 1, "c:4976 — negative floors at 1");
9064
9065        // Positive passes through.
9066        crate::ported::params::histsizesetfn(500);
9067        assert_eq!(histsizegetfn(), 500);
9068        assert_eq!(crate::ported::hist::histsiz.load(Ordering::SeqCst), 500);
9069
9070        // Restore.
9071        *crate::ported::params::histsiz_lock().lock().unwrap() = saved_param;
9072        crate::ported::hist::histsiz.store(saved_hist, Ordering::SeqCst);
9073    }
9074
9075    /// `Src/params.c:5152-5158` — `underscoregetfn` returns
9076    /// `dupstring(zunderscore)` then runs `untokenize(u)` on it.
9077    /// The Rust port previously skipped untokenize, exposing raw
9078    /// lexer-injected token bytes (Stringg, Equals, ...) in `$_`
9079    /// reads.
9080    #[test]
9081    fn underscoregetfn_runs_untokenize_on_zunderscore() {
9082        // Inject zunderscore containing a Pound token byte (\u{84})
9083        // and verify it gets stripped by untokenize in the return.
9084        let saved = crate::ported::params::zunderscore_lock()
9085            .lock().unwrap().clone();
9086
9087        // Set zunderscore to a string containing a Pound token byte
9088        // surrounded by literals.
9089        let pound = crate::ported::zsh_h::Pound;
9090        let mut s = String::new();
9091        s.push('a');
9092        s.push(pound);
9093        s.push('b');
9094        *crate::ported::params::zunderscore_lock().lock().unwrap() = s;
9095
9096        let result = crate::ported::params::underscoregetfn();
9097        // c:5156 — untokenize replaces Pound (ITOK) with '#'
9098        // (its ztokens entry). The raw \u{84} byte must NOT survive.
9099        assert!(!result.contains(pound),
9100            "c:5156 — untokenize must strip Pound token byte from $_");
9101        assert!(result.contains('#') || result.contains("a"),
9102            "c:5156 — Pound (ITOK) maps to '#' via ztokens[0]");
9103
9104        // Restore.
9105        *crate::ported::params::zunderscore_lock().lock().unwrap() = saved;
9106    }
9107
9108    /// `Src/params.c:4954-4961` — `argzerogetfn` returns `posixzero`
9109    /// when `isset(POSIXARGZERO)`, else `argzero`. The previous Rust
9110    /// port always returned `argzero`, defeating the POSIXARGZERO
9111    /// option entirely. After mutating argzero (e.g. `exec -a foo`),
9112    /// `$0` under POSIXARGZERO must report the ORIGINAL startup
9113    /// argv[0], not the rewritten name.
9114    #[test]
9115    fn argzerogetfn_respects_posixargzero_option() {
9116        let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9117        use crate::ported::options::{opt_state_get, opt_state_set};
9118        use crate::ported::utils::{set_argzero, set_posixzero, argzero, posixzero};
9119
9120        // Save state.
9121        let saved_argzero    = argzero();
9122        let saved_posixzero  = posixzero();
9123        let saved_pos_option = opt_state_get("posixargzero").unwrap_or(false);
9124
9125        // Set up: posixzero (original) ≠ argzero (rewritten).
9126        set_posixzero(Some("/bin/zsh".to_string()));
9127        set_argzero(Some("rewritten-name".to_string()));
9128        // The set_argzero call mirrors to posixzero only if unset,
9129        // and we set posixzero first → mirror skipped. Confirm separation.
9130
9131        // POSIXARGZERO off → returns argzero.
9132        opt_state_set("posixargzero", false);
9133        assert_eq!(crate::ported::params::argzerogetfn(), "rewritten-name",
9134            "c:4960 — !POSIXARGZERO returns argzero (current display name)");
9135
9136        // POSIXARGZERO on → returns posixzero (the preserved startup argv[0]).
9137        opt_state_set("posixargzero", true);
9138        assert_eq!(crate::ported::params::argzerogetfn(), "/bin/zsh",
9139            "c:4959 — POSIXARGZERO on returns posixzero (original startup argv[0])");
9140
9141        // Restore.
9142        set_argzero(saved_argzero);
9143        set_posixzero(saved_posixzero);
9144        opt_state_set("posixargzero", saved_pos_option);
9145    }
9146
9147    /// `Src/init.c:271` — `argv0 = argzero = posixzero = *argv++`.
9148    /// At shell init both share the same source. The Rust port
9149    /// preserves this contract by having `set_argzero` mirror to
9150    /// `posixzero` ONLY on first call (when posixzero is None).
9151    /// Subsequent argzero changes (function frames, exec -a) must
9152    /// NOT clobber posixzero.
9153    #[test]
9154    fn set_argzero_mirrors_to_posixzero_only_on_first_call() {
9155        let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9156        use crate::ported::utils::{set_argzero, set_posixzero, argzero, posixzero};
9157
9158        let saved_argzero   = argzero();
9159        let saved_posixzero = posixzero();
9160
9161        // Reset both to None.
9162        set_argzero(None);
9163        set_posixzero(None);
9164        // First call: posixzero is None, so it should mirror.
9165        set_argzero(Some("/usr/local/bin/zsh".to_string()));
9166        assert_eq!(posixzero().as_deref(), Some("/usr/local/bin/zsh"),
9167            "c:271 — first set_argzero mirrors to posixzero (was None)");
9168        // Second call: posixzero now Some, so mirror is skipped.
9169        set_argzero(Some("function-name".to_string()));
9170        assert_eq!(posixzero().as_deref(), Some("/usr/local/bin/zsh"),
9171            "c:271 — second set_argzero does NOT clobber posixzero");
9172        assert_eq!(argzero().as_deref(), Some("function-name"),
9173            "argzero updated as normal");
9174
9175        // Restore.
9176        set_posixzero(saved_posixzero);
9177        set_argzero(saved_argzero);
9178    }
9179
9180    /// Locale-touching tests share process-wide env + libc state.
9181    /// Cargo runs tests in parallel by default, so without
9182    /// serialization a concurrent `env::set_var("LC_ALL")` can race
9183    /// a `env::remove_var("LC_ALL")` and corrupt assertions. Pin
9184    /// every locale test through this Mutex.
9185    fn locale_test_lock() -> &'static std::sync::Mutex<()> {
9186        static L: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
9187        L.get_or_init(|| std::sync::Mutex::new(()))
9188    }
9189
9190    /// Pin `LC_NAMES` to the canonical zsh `lc_names[]` table at
9191    /// `Src/params.c:4805-4825`. The five categories in entry order
9192    /// (LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_NUMERIC, LC_TIME) MUST
9193    /// match — `lcsetfn` walks this table by `strcmp(ln->name, pm->node.nam)`
9194    /// per c:4926 and dispatches to `setlocale(ln->category, ...)`.
9195    #[test]
9196    fn lc_names_match_zsh_canonical_table() {
9197        let names: Vec<&str> = LC_NAMES.iter().map(|(n, _)| *n).collect();
9198        assert_eq!(
9199            names,
9200            vec!["LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "LC_NUMERIC", "LC_TIME"],
9201            "Src/params.c:4805-4825 — lc_names entry order must be preserved"
9202        );
9203        // Verify each name maps to a distinct libc category — proves
9204        // we aren't aliasing LC_NUMERIC to LC_TIME etc.
9205        let cats: Vec<libc::c_int> = LC_NAMES.iter().map(|(_, c)| *c).collect();
9206        let mut sorted = cats.clone();
9207        sorted.sort();
9208        sorted.dedup();
9209        assert_eq!(sorted.len(), 5, "all five LC_* categories must be distinct");
9210        assert!(cats.contains(&libc::LC_COLLATE));
9211        assert!(cats.contains(&libc::LC_CTYPE));
9212        assert!(cats.contains(&libc::LC_MESSAGES));
9213        assert!(cats.contains(&libc::LC_NUMERIC));
9214        assert!(cats.contains(&libc::LC_TIME));
9215    }
9216
9217    /// Pin `lcsetfn` to the canonical `setlocale` invocation at
9218    /// `Src/params.c:4925-4927`. When LC_ALL is empty and pm matches
9219    /// an entry in `lc_names`, libc setlocale MUST be called with
9220    /// the corresponding category. Verified by reading libc state
9221    /// back via `setlocale(cat, NULL)` after the assignment.
9222    #[test]
9223    fn lcsetfn_invokes_libc_setlocale_for_matching_category() {
9224        let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9225        // Save LC_ALL/LC_CTYPE state.
9226        let saved_lc_all = env::var("LC_ALL").ok();
9227        let saved_lc_ctype = env::var("LC_CTYPE").ok();
9228        env::remove_var("LC_ALL");                  // c:4912 LC_ALL must be empty for body to run
9229
9230        // Read libc's current LC_CTYPE setting.
9231        let before = unsafe {
9232            let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9233            if p.is_null() {
9234                String::new()
9235            } else {
9236                std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9237            }
9238        };
9239
9240        // Call lcsetfn with LC_CTYPE → "C" (universally available POSIX locale).
9241        lcsetfn("LC_CTYPE", Some("C".to_string()));
9242
9243        // Read it back — must report "C" since C invokes setlocale(LC_CTYPE, "C").
9244        let after = unsafe {
9245            let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9246            if p.is_null() {
9247                String::new()
9248            } else {
9249                std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9250            }
9251        };
9252        assert_eq!(after, "C",
9253            "Src/params.c:4927 — lcsetfn must call setlocale(LC_CTYPE, \"C\")");
9254
9255        // Env mirror also set.
9256        assert_eq!(env::var("LC_CTYPE").unwrap_or_default(), "C");
9257
9258        // Restore libc + env state.
9259        let _ = unsafe {
9260            let c = std::ffi::CString::new(before.as_bytes()).unwrap_or_default();
9261            libc::setlocale(libc::LC_CTYPE, c.as_ptr())
9262        };
9263        match saved_lc_all {
9264            Some(v) => env::set_var("LC_ALL", v),
9265            None => env::remove_var("LC_ALL"),
9266        }
9267        match saved_lc_ctype {
9268            Some(v) => env::set_var("LC_CTYPE", v),
9269            None => env::remove_var("LC_CTYPE"),
9270        }
9271    }
9272
9273    /// Pin `lcsetfn`'s LC_ALL early-return per c:4912-4913: when
9274    /// LC_ALL is non-empty, lcsetfn must short-circuit BEFORE
9275    /// touching libc setlocale for the per-category override.
9276    #[test]
9277    fn lcsetfn_short_circuits_when_lc_all_set() {
9278        let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9279        let saved_lc_all = env::var("LC_ALL").ok();
9280        let saved_lc_ctype = env::var("LC_CTYPE").ok();
9281        env::set_var("LC_ALL", "C");                // c:4912 non-empty LC_ALL
9282
9283        // Capture libc state before.
9284        let before = unsafe {
9285            let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9286            if p.is_null() {
9287                String::new()
9288            } else {
9289                std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9290            }
9291        };
9292
9293        // Try to set LC_CTYPE; should NOT touch libc state.
9294        lcsetfn("LC_CTYPE", Some("POSIX".to_string()));
9295
9296        // libc state must be unchanged.
9297        let after = unsafe {
9298            let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9299            if p.is_null() {
9300                String::new()
9301            } else {
9302                std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9303            }
9304        };
9305        assert_eq!(before, after,
9306            "c:4912-4913 — lcsetfn must early-return when LC_ALL is non-empty");
9307
9308        // Restore.
9309        match saved_lc_all {
9310            Some(v) => env::set_var("LC_ALL", v),
9311            None => env::remove_var("LC_ALL"),
9312        }
9313        match saved_lc_ctype {
9314            Some(v) => env::set_var("LC_CTYPE", v),
9315            None => env::remove_var("LC_CTYPE"),
9316        }
9317    }
9318
9319    /// Pin `getsparam_u` to its canonical C body at
9320    /// `Src/params.c:3089-3094`: returns `unmeta(getsparam(s))`,
9321    /// NOT a PM_SCALAR-checked `getstrvalue` wrapper.
9322    ///
9323    /// Before this fix, the Rust port took `Option<&mut value>`
9324    /// and gated on `PM_TYPE == PM_SCALAR` — a complete fabrication
9325    /// with no caller because no caller's type fit the bogus sig.
9326    #[test]
9327    fn getsparam_u_unmetas_getsparam_result() {
9328        let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9329
9330        // Plain ASCII: getsparam_u returns the same content as
9331        // getsparam (no Meta bytes to strip).
9332        let saved = env::var("ZSHRS_TEST_LOCALE_GSU").ok();
9333        env::set_var("ZSHRS_TEST_LOCALE_GSU", "en_US.UTF-8");
9334        assert_eq!(
9335            getsparam_u("ZSHRS_TEST_LOCALE_GSU"),
9336            Some("en_US.UTF-8".to_string()),
9337            "Src/params.c:3092 — getsparam_u returns unmeta(getsparam(s)) for ASCII"
9338        );
9339
9340        // Missing param: returns None (matches C `if ((s = getsparam(s)))` false branch).
9341        env::remove_var("ZSHRS_TEST_LOCALE_GSU_MISSING");
9342        assert_eq!(
9343            getsparam_u("ZSHRS_TEST_LOCALE_GSU_MISSING"),
9344            None,
9345            "Src/params.c:3094 — getsparam_u returns NULL when getsparam returns NULL"
9346        );
9347
9348        // Restore.
9349        match saved {
9350            Some(v) => env::set_var("ZSHRS_TEST_LOCALE_GSU", v),
9351            None => env::remove_var("ZSHRS_TEST_LOCALE_GSU"),
9352        }
9353    }
9354
9355    /// Pin `setarrvalue` EXECOPT bail per `Src/params.c:2897-2898`.
9356    /// Same NO_EXEC semantic as setnumvalue: dry-run shell evaluation
9357    /// must not mutate array params.
9358    #[test]
9359    fn setarrvalue_bails_under_no_exec() {
9360        use crate::ported::zsh_h::{param, hashnode, value, PM_ARRAY};
9361
9362        let saved_exec = crate::ported::options::opt_state_get("exec")
9363            .unwrap_or(false);
9364
9365        crate::ported::options::opt_state_set("exec", false);
9366        let pm = Box::new(param {
9367            node: hashnode {
9368                next: None, nam: "noexec_arr".to_string(),
9369                flags: PM_ARRAY as i32,
9370            },
9371            u_data: 0,
9372            u_arr: Some(vec!["initial".to_string()]),
9373            u_str: None, u_val: 0, u_dval: 0.0, u_hash: None,
9374            gsu_s: None, gsu_i: None, gsu_f: None,
9375            gsu_a: None, gsu_h: None,
9376            base: 0, width: 0,
9377            env: None, ename: None, old: None, level: 0,
9378        });
9379        let mut v = value {
9380            pm: Some(pm),
9381            arr: Vec::new(),
9382            scanflags: 0, valflags: 0,
9383            start: 0, end: -1,
9384        };
9385        // Under NO_EXEC, the assign must be skipped.
9386        setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
9387        let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
9388        assert_eq!(arr, vec!["initial".to_string()],
9389            "c:2897 — NO_EXEC: setarrvalue must NOT replace u_arr");
9390
9391        // With exec=true, the same call replaces.
9392        crate::ported::options::opt_state_set("exec", true);
9393        setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
9394        let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
9395        assert_eq!(arr, vec!["new1".to_string(), "new2".to_string()],
9396            "with EXEC set, setarrvalue replaces u_arr");
9397
9398        crate::ported::options::opt_state_set("exec", saved_exec);
9399    }
9400
9401    /// Pin `setnumvalue` EXECOPT bail per `Src/params.c:2860`.
9402    /// When unset(EXECOPT) (i.e. NO_EXEC mode via `zsh -n` or
9403    /// `set -n`), param mutations MUST be skipped so dry-run shell
9404    /// evaluation doesn't leak state into the param table.
9405    #[test]
9406    fn setnumvalue_bails_under_no_exec() {
9407        use crate::ported::zsh_h::{param, hashnode, value, PM_INTEGER};
9408        use crate::ported::math::{mnumber, MN_INTEGER};
9409
9410        let saved_exec = crate::ported::options::opt_state_get("exec")
9411            .unwrap_or(false);
9412
9413        // c:2860 — NO_EXEC: setnumvalue must not mutate the param.
9414        crate::ported::options::opt_state_set("exec", false);
9415        let mut pm = Box::new(param {
9416            node: hashnode {
9417                next: None, nam: "ne".to_string(),
9418                flags: PM_INTEGER as i32,
9419            },
9420            u_data: 0, u_arr: None, u_str: None,
9421            u_val: 999, u_dval: 0.0, u_hash: None,
9422            gsu_s: None, gsu_i: None, gsu_f: None,
9423            gsu_a: None, gsu_h: None,
9424            base: 0, width: 0,
9425            env: None, ename: None, old: None, level: 0,
9426        });
9427        let mut v = value {
9428            pm: Some(pm.clone()),
9429            arr: Vec::new(),
9430            scanflags: 0, valflags: 0,
9431            start: 0, end: -1,
9432        };
9433        let val = mnumber { l: 42, d: 0.0, type_: MN_INTEGER };
9434        setnumvalue(Some(&mut v), val);
9435        // pm.u_val MUST still be 999 (the initial), not 42.
9436        let stored = v.pm.as_ref().unwrap().u_val;
9437        assert_eq!(stored, 999,
9438            "c:2860 — NO_EXEC: setnumvalue must NOT mutate pm.u_val \
9439             (was {} but should stay 999)", stored);
9440
9441        // With exec=true, the same call mutates.
9442        crate::ported::options::opt_state_set("exec", true);
9443        setnumvalue(Some(&mut v), val);
9444        let stored = v.pm.as_ref().unwrap().u_val;
9445        assert_eq!(stored, 42,
9446            "with EXEC set, setnumvalue stores u_val = 42");
9447
9448        let _ = pm;
9449        crate::ported::options::opt_state_set("exec", saved_exec);
9450    }
9451
9452    /// Pin `$-` rendering to honor `set -n` (noexec). The previous
9453    /// Rust port called `opt("noexec")` which isn't a real option
9454    /// name in zsh — the lookup always returned false so `$-` never
9455    /// included 'n' even when `set -n` was active.
9456    #[test]
9457    fn dash_param_rendering_honors_noexec_via_exec_negation() {
9458        let saved = crate::ported::options::opt_state_get("exec")
9459            .unwrap_or(false);
9460
9461        // With exec=true (default), $- should NOT include 'n'.
9462        crate::ported::options::opt_state_set("exec", true);
9463        let s = lookup_special_var("-").unwrap_or_default();
9464        assert!(!s.contains('n'),
9465            "exec=true → $-=`{}` must NOT include 'n'", s);
9466
9467        // With exec=false (`set -n`), $- SHOULD include 'n'.
9468        crate::ported::options::opt_state_set("exec", false);
9469        let s = lookup_special_var("-").unwrap_or_default();
9470        assert!(s.contains('n'),
9471            "exec=false → $-=`{}` MUST include 'n' (was silently dropped \
9472             when reading non-existent option name `noexec`)", s);
9473
9474        crate::ported::options::opt_state_set("exec", saved);
9475    }
9476
9477    /// Pin `TERM_UNKNOWN` bit value to the canonical C value at
9478    /// `Src/zsh.h:1986`. The previous params.rs duplicate had
9479    /// `1 << 0 = 0x01` which is actually C's TERM_BAD (Src/zsh.h:1985);
9480    /// the correct TERM_UNKNOWN value is 0x02. This single-byte
9481    /// drift caused the params.rs term-init code to silently set the
9482    /// TERM_BAD bit instead of TERM_UNKNOWN, while prompt.rs guards
9483    /// imported the correct 0x02 value from zsh_h.rs — the two
9484    /// paths disagreed about which bit means \"unknown terminal\".
9485    #[test]
9486    fn term_unknown_bit_value_matches_c() {
9487        use crate::ported::zsh_h::{TERM_BAD, TERM_UNKNOWN};
9488        assert_eq!(TERM_UNKNOWN, 0x02,
9489            "Src/zsh.h:1986 — TERM_UNKNOWN must be 0x02, got {:#x}", TERM_UNKNOWN);
9490        assert_eq!(TERM_BAD, 0x01,
9491            "Src/zsh.h:1985 — TERM_BAD must be 0x01 (and != TERM_UNKNOWN)");
9492        // Crucially: TERM_BAD and TERM_UNKNOWN must be DISTINCT bits.
9493        assert_ne!(TERM_BAD, TERM_UNKNOWN,
9494            "TERM_BAD and TERM_UNKNOWN must be distinct (caught the 1<<0 drift bug)");
9495    }
9496
9497    /// Pin `getstrvalue` PM_INTEGER branch to canonical C convbase
9498    /// dispatch at `Src/params.c:2373`. The previous Rust port used
9499    /// naked `.to_string()` (base-10) regardless of `pm.base`; C
9500    /// honors the param's stored base so `typeset -i 16 x=255` renders
9501    /// as `0xff` not `255`.
9502    #[test]
9503    fn getstrvalue_pm_integer_honors_pm_base() {
9504        use crate::ported::zsh_h::{value, param, hashnode, PM_INTEGER};
9505
9506        let saved_cbases_top = crate::ported::options::opt_state_get("cbases")
9507            .unwrap_or(false);
9508        crate::ported::options::opt_state_set("cbases", true);
9509
9510        // Build a PM_INTEGER param with u_val=255 and base=16.
9511        let mut pm = Box::new(param {
9512            node: hashnode {
9513                next: None,
9514                nam: "test_hex_var".to_string(),
9515                flags: PM_INTEGER as i32,
9516            },
9517            u_data: 0, u_arr: None, u_str: None,
9518            u_val: 255, u_dval: 0.0, u_hash: None,
9519            gsu_s: None, gsu_i: None, gsu_f: None,
9520            gsu_a: None, gsu_h: None,
9521            base: 16, width: 0,
9522            env: None, ename: None, old: None, level: 0,
9523        });
9524        let mut v = value {
9525            pm: Some(pm.clone()),
9526            arr: Vec::new(),
9527            scanflags: 0, valflags: 0,
9528            start: 0, end: -1,
9529        };
9530        let rendered = getstrvalue(Some(&mut v));
9531        assert_eq!(rendered, "0xFF",
9532            "c:2373 / c:5621 — PM_INTEGER base=16 + u_val=255 with CBASES \
9533             renders as `0xFF` (uppercase per C `dig - 10 + 'A'`), got {:?}",
9534            rendered);
9535
9536        // Base-8 (octal) with OCTALZEROES.
9537        let saved_oct = crate::ported::options::opt_state_get("octalzeroes")
9538            .unwrap_or(false);
9539        let saved_cbases = crate::ported::options::opt_state_get("cbases")
9540            .unwrap_or(false);
9541        crate::ported::options::opt_state_set("cbases", true);
9542        crate::ported::options::opt_state_set("octalzeroes", true);
9543        pm.base = 8;
9544        pm.u_val = 8;
9545        v.pm = Some(pm.clone());
9546        let rendered = getstrvalue(Some(&mut v));
9547        assert_eq!(rendered, "010",
9548            "c:2373 — PM_INTEGER base=8 with OCTALZEROES renders as `010`, got {:?}",
9549            rendered);
9550        crate::ported::options::opt_state_set("cbases", saved_cbases);
9551        crate::ported::options::opt_state_set("octalzeroes", saved_oct);
9552
9553        // Base=0 (default) → base-10.
9554        pm.base = 0;
9555        pm.u_val = 42;
9556        v.pm = Some(pm.clone());
9557        let rendered = getstrvalue(Some(&mut v));
9558        assert_eq!(rendered, "42",
9559            "c:2373 — PM_INTEGER base=0 defaults to base-10");
9560
9561        crate::ported::options::opt_state_set("cbases", saved_cbases_top);
9562    }
9563
9564    /// Pin `unsetparam` to its canonical C body at `Src/params.c:3819-3833`.
9565    /// Two guards the previous Rust port skipped:
9566    ///   1. PM_NAMEREF params are NOT removed by unsetparam (c:3830).
9567    ///   2. PM_READONLY rejection per unsetparam_pm c:3850 — readonly
9568    ///      params survive the unset call.
9569    #[test]
9570    fn unsetparam_skips_nameref_and_readonly() {
9571        use crate::ported::zsh_h::{PM_NAMEREF, PM_READONLY, PM_SCALAR};
9572
9573        let saved_exec = crate::ported::options::opt_state_get("exec")
9574            .unwrap_or(false);
9575        crate::ported::options::opt_state_set("exec", true);
9576
9577        // Helper: install a scalar param with the given flag-set.
9578        fn install(name: &str, value: &str, flags: u32) {
9579            let mut tab = paramtab().write().unwrap();
9580            tab.insert(name.to_string(), Box::new(crate::ported::zsh_h::param {
9581                node: crate::ported::zsh_h::hashnode {
9582                    next: None,
9583                    nam: name.to_string(),
9584                    flags: (PM_SCALAR | flags) as i32,
9585                },
9586                u_data: 0, u_arr: None, u_str: Some(value.to_string()),
9587                u_val: 0, u_dval: 0.0, u_hash: None,
9588                gsu_s: None, gsu_i: None, gsu_f: None,
9589                gsu_a: None, gsu_h: None,
9590                base: 0, width: 0,
9591                env: None, ename: None, old: None, level: 0,
9592            }));
9593        }
9594
9595        // c:3830 — nameref params skip the unset.
9596        let nameref_name = "zshrs_test_unsetparam_nameref";
9597        install(nameref_name, "target_var_name", PM_NAMEREF);
9598        unsetparam(nameref_name);
9599        {
9600            let tab = paramtab().read().unwrap();
9601            assert!(tab.contains_key(nameref_name),
9602                "c:3830 — PM_NAMEREF param survives unsetparam");
9603        }
9604
9605        // c:3850 (via unsetparam_pm) — readonly rejection.
9606        let ro_name = "zshrs_test_unsetparam_readonly";
9607        install(ro_name, "locked", PM_READONLY);
9608        unsetparam(ro_name);
9609        {
9610            let tab = paramtab().read().unwrap();
9611            assert!(tab.contains_key(ro_name),
9612                "c:3850 — PM_READONLY param survives unsetparam");
9613        }
9614
9615        // Plain scalar removed normally.
9616        let plain_name = "zshrs_test_unsetparam_plain";
9617        install(plain_name, "removable", 0);
9618        unsetparam(plain_name);
9619        {
9620            let tab = paramtab().read().unwrap();
9621            assert!(!tab.contains_key(plain_name),
9622                "plain scalar successfully removed");
9623        }
9624
9625        // Clean up.
9626        {
9627            let mut tab = paramtab().write().unwrap();
9628            tab.remove(nameref_name);
9629            tab.remove(ro_name);
9630            tab.remove(plain_name);
9631        }
9632        crate::ported::options::opt_state_set("exec", saved_exec);
9633    }
9634
9635    /// Pin `assigniparam` to its canonical C body at `Src/params.c:3754-3761`.
9636    /// Three-arg signature: `(s, val, flags)`. Previous Rust port
9637    /// dropped the flags arg AND returned void; this restores both.
9638    #[test]
9639    fn assigniparam_takes_flags_arg_and_returns_param() {
9640        use crate::ported::zsh_h::PM_INTEGER;
9641
9642        let saved_exec = crate::ported::options::opt_state_get("exec")
9643            .unwrap_or(false);
9644        crate::ported::options::opt_state_set("exec", true);
9645
9646        let name = "zshrs_test_assigniparam_x";
9647        {
9648            let mut tab = paramtab().write().unwrap();
9649            tab.remove(name);
9650        }
9651
9652        // c:3755-3760 — assigniparam returns Param and threads flags through.
9653        let r = assigniparam(name, 77, ASSPM_WARN as i32);
9654        assert!(r.is_some(), "c:3760 — returns Some(Param) for new int param");
9655        {
9656            let tab = paramtab().read().unwrap();
9657            let pm = tab.get(name).expect("integer param created");
9658            assert_ne!((pm.node.flags as u32) & PM_INTEGER, 0,
9659                "c:3757-3760 — PM_INTEGER flag set");
9660            assert_eq!(pm.u_val, 77,
9661                "c:3759 — value stored in u_val");
9662        }
9663
9664        // Reassign with a different flag value (0 — no warnings).
9665        let r = assigniparam(name, 88, 0);
9666        assert!(r.is_some(), "reassign returns Some");
9667        {
9668            let tab = paramtab().read().unwrap();
9669            let pm = tab.get(name).expect("param still present");
9670            assert_eq!(pm.u_val, 88, "reassign updates u_val");
9671        }
9672
9673        // Clean up.
9674        {
9675            let mut tab = paramtab().write().unwrap();
9676            tab.remove(name);
9677        }
9678        crate::ported::options::opt_state_set("exec", saved_exec);
9679    }
9680
9681    /// Pin `setnparam` to its canonical C body at `Src/params.c:3745-3749`.
9682    /// MUST accept `mnumber` (integer or float) and return Param.
9683    /// Previous Rust port took `f64` only and returned void — losing
9684    /// the integer side and the Param return entirely.
9685    #[test]
9686    fn setnparam_accepts_both_integer_and_float() {
9687        use crate::ported::math::{mnumber, MN_INTEGER as MN_INT, MN_FLOAT as MN_FLT};
9688        use crate::ported::zsh_h::{PM_INTEGER, PM_FFLOAT};
9689
9690        let saved_exec = crate::ported::options::opt_state_get("exec")
9691            .unwrap_or(false);
9692        crate::ported::options::opt_state_set("exec", true);
9693
9694        // Clean up any leftover.
9695        let int_name = "zshrs_test_setnparam_i";
9696        let flt_name = "zshrs_test_setnparam_f";
9697        {
9698            let mut tab = paramtab().write().unwrap();
9699            tab.remove(int_name);
9700            tab.remove(flt_name);
9701        }
9702
9703        // c:3748 — integer branch: setnparam returns Some(Param) with
9704        // PM_INTEGER flag and u_val set.
9705        let r = setnparam(int_name, mnumber { l: 999, d: 0.0, type_: MN_INT });
9706        assert!(r.is_some(), "setnparam returns Some for new param");
9707        {
9708            let tab = paramtab().read().unwrap();
9709            let pm = tab.get(int_name).expect("integer param created");
9710            assert_ne!((pm.node.flags as u32) & PM_INTEGER, 0,
9711                "c:3748 — PM_INTEGER flag set for integer mnumber");
9712            assert_eq!(pm.u_val, 999,
9713                "c:3748 — integer value stored in u_val");
9714        }
9715
9716        // c:3748 — float branch: setnparam with MN_FLOAT creates PM_FFLOAT.
9717        let r = setnparam(flt_name, mnumber { l: 0, d: 3.14, type_: MN_FLT });
9718        assert!(r.is_some(), "setnparam returns Some for new float param");
9719        {
9720            let tab = paramtab().read().unwrap();
9721            let pm = tab.get(flt_name).expect("float param created");
9722            assert_ne!((pm.node.flags as u32) & PM_FFLOAT, 0,
9723                "c:3748 — PM_FFLOAT flag set for float mnumber");
9724            assert!((pm.u_dval - 3.14).abs() < 1e-10,
9725                "c:3748 — float value stored in u_dval");
9726        }
9727
9728        // Clean up.
9729        {
9730            let mut tab = paramtab().write().unwrap();
9731            tab.remove(int_name);
9732            tab.remove(flt_name);
9733        }
9734        crate::ported::options::opt_state_set("exec", saved_exec);
9735    }
9736
9737    /// Pin `setiparam` to its canonical C body at `Src/params.c:3767-3773`.
9738    /// MUST create the param as PM_INTEGER via `assignnparam`, not as
9739    /// PM_SCALAR via `assignsparam` with a stringified value.
9740    #[test]
9741    fn setiparam_creates_pm_integer_param() {
9742        use crate::ported::zsh_h::PM_INTEGER;
9743        let name = "zshrs_test_setiparam_x";
9744
9745        // C: `assignnparam` bails when `unset(EXECOPT)` (Src/params.c:3679).
9746        // Real zsh startup sets exec=true; the unit-test env doesn't run
9747        // through `createoptiontable` so we set "exec" explicitly to
9748        // simulate normal runtime.
9749        let saved_exec = crate::ported::options::opt_state_get("exec")
9750            .unwrap_or(false);
9751        crate::ported::options::opt_state_set("exec", true);
9752
9753        // Clean up any leftover.
9754        {
9755            let mut tab = paramtab().write().unwrap();
9756            tab.remove(name);
9757        }
9758
9759        // Set integer value.
9760        setiparam(name, 42);
9761
9762        // Param should exist with PM_INTEGER flag set + u_val == 42.
9763        {
9764            let tab = paramtab().read().unwrap();
9765            let pm = tab.get(name).expect("setiparam must create the param");
9766            assert_ne!(
9767                (pm.node.flags as u32) & PM_INTEGER, 0,
9768                "c:3770-3772 — created param must have PM_INTEGER flag set, \
9769                 got flags = {:#x}", pm.node.flags
9770            );
9771            assert_eq!(pm.u_val, 42,
9772                "c:3771 — integer value stored in pm.u_val");
9773        }
9774
9775        // Reassign to verify update path also keeps PM_INTEGER.
9776        setiparam(name, 100);
9777        {
9778            let tab = paramtab().read().unwrap();
9779            let pm = tab.get(name).expect("setiparam reassign must keep param");
9780            assert_eq!(pm.u_val, 100,
9781                "reassign updates the integer value");
9782            assert_ne!(
9783                (pm.node.flags as u32) & PM_INTEGER, 0,
9784                "reassign keeps PM_INTEGER flag"
9785            );
9786        }
9787
9788        // Clean up.
9789        {
9790            let mut tab = paramtab().write().unwrap();
9791            tab.remove(name);
9792        }
9793        // Restore EXECOPT.
9794        crate::ported::options::opt_state_set("exec", saved_exec);
9795    }
9796
9797    /// Pin `gethparam` / `gethkparam` to their canonical C bodies at
9798    /// `Src/params.c:3117-3140`. Same signature-fix family as `getaparam`:
9799    /// the `name: &str` path with digit-first reject + PM_HASHED check.
9800    #[test]
9801    fn gethparam_and_gethkparam_signature_matches_c() {
9802        // c:3122 / c:3136 — digit-first name reject.
9803        assert_eq!(gethparam("123abc"), None,
9804            "c:3122 — digit-first name rejected");
9805        assert_eq!(gethkparam("123abc"), None,
9806            "c:3136 — digit-first name rejected");
9807
9808        // Missing param → None.
9809        assert_eq!(gethparam("zshrs_test_hashparam_xyz"), None,
9810            "missing param returns None");
9811        assert_eq!(gethkparam("zshrs_test_hashparam_xyz"), None,
9812            "missing param returns None");
9813
9814        // PM_SCALAR param (not hashed) → None.
9815        {
9816            let mut tab = paramtab().write().unwrap();
9817            tab.insert("zshrs_test_gethp_scalar".to_string(),
9818                Box::new(crate::ported::zsh_h::param {
9819                node: crate::ported::zsh_h::hashnode {
9820                    next: None,
9821                    nam: "zshrs_test_gethp_scalar".to_string(),
9822                    flags: crate::ported::zsh_h::PM_SCALAR as i32,
9823                },
9824                u_data: 0, u_arr: None,
9825                u_str: Some("scalar value".to_string()),
9826                u_val: 0, u_dval: 0.0, u_hash: None,
9827                gsu_s: None, gsu_i: None, gsu_f: None,
9828                gsu_a: None, gsu_h: None,
9829                base: 0, width: 0,
9830                env: None, ename: None, old: None, level: 0,
9831            }));
9832        }
9833        assert_eq!(gethparam("zshrs_test_gethp_scalar"), None,
9834            "c:3123 — non-PM_HASHED returns None");
9835        assert_eq!(gethkparam("zshrs_test_gethp_scalar"), None,
9836            "c:3137 — non-PM_HASHED returns None");
9837
9838        // PM_HASHED param → Some(Vec::new()) (backend not yet wired,
9839        // but signature should at least classify the type correctly).
9840        {
9841            let mut tab = paramtab().write().unwrap();
9842            tab.insert("zshrs_test_gethp_hash".to_string(),
9843                Box::new(crate::ported::zsh_h::param {
9844                node: crate::ported::zsh_h::hashnode {
9845                    next: None,
9846                    nam: "zshrs_test_gethp_hash".to_string(),
9847                    flags: crate::ported::zsh_h::PM_HASHED as i32,
9848                },
9849                u_data: 0, u_arr: None, u_str: None,
9850                u_val: 0, u_dval: 0.0, u_hash: None,
9851                gsu_s: None, gsu_i: None, gsu_f: None,
9852                gsu_a: None, gsu_h: None,
9853                base: 0, width: 0,
9854                env: None, ename: None, old: None, level: 0,
9855            }));
9856        }
9857        assert_eq!(gethparam("zshrs_test_gethp_hash"), Some(Vec::new()),
9858            "c:3123-3124 — PM_HASHED returns Some(vec) (empty until backend wired)");
9859        assert_eq!(gethkparam("zshrs_test_gethp_hash"), Some(Vec::new()),
9860            "c:3137-3138 — PM_HASHED returns Some(vec) for keys");
9861
9862        // Clean up.
9863        {
9864            let mut tab = paramtab().write().unwrap();
9865            tab.remove("zshrs_test_gethp_scalar");
9866            tab.remove("zshrs_test_gethp_hash");
9867        }
9868    }
9869
9870    /// Pin `getaparam` to its canonical C body at `Src/params.c:3101-3110`.
9871    /// Three branches: digit-first reject (c:3107), PM_ARRAY return
9872    /// (c:3108-3109), non-array / missing-param return None (c:3110).
9873    #[test]
9874    fn getaparam_returns_array_for_pm_array_only() {
9875        // c:3107 — digit-first name → None (positional params reject).
9876        assert_eq!(getaparam("123abc"), None,
9877            "c:3107 — digit-first name rejected");
9878
9879        // c:3110 — missing param → None.
9880        assert_eq!(getaparam("zshrs_test_arr_nonexistent_xyz"), None,
9881            "c:3110 — missing param returns None");
9882
9883        // Helper that builds a Param via the canonical createparam
9884        // path so we don't reach into struct internals (param has
9885        // many fields and no Default).
9886        fn build_arr(name: &str, arr: Vec<String>) {
9887            let mut tab = paramtab().write().unwrap();
9888            tab.insert(name.to_string(), Box::new(crate::ported::zsh_h::param {
9889                node: crate::ported::zsh_h::hashnode {
9890                    next: None,
9891                    nam: name.to_string(),
9892                    flags: crate::ported::zsh_h::PM_ARRAY as i32,
9893                },
9894                u_data: 0, u_arr: Some(arr), u_str: None,
9895                u_val: 0, u_dval: 0.0, u_hash: None,
9896                gsu_s: None, gsu_i: None, gsu_f: None,
9897                gsu_a: None, gsu_h: None,
9898                base: 0, width: 0,
9899                env: None, ename: None, old: None, level: 0,
9900            }));
9901        }
9902        fn build_scalar(name: &str, s: &str) {
9903            let mut tab = paramtab().write().unwrap();
9904            tab.insert(name.to_string(), Box::new(crate::ported::zsh_h::param {
9905                node: crate::ported::zsh_h::hashnode {
9906                    next: None,
9907                    nam: name.to_string(),
9908                    flags: crate::ported::zsh_h::PM_SCALAR as i32,
9909                },
9910                u_data: 0, u_arr: None, u_str: Some(s.to_string()),
9911                u_val: 0, u_dval: 0.0, u_hash: None,
9912                gsu_s: None, gsu_i: None, gsu_f: None,
9913                gsu_a: None, gsu_h: None,
9914                base: 0, width: 0,
9915                env: None, ename: None, old: None, level: 0,
9916            }));
9917        }
9918
9919        // c:3108 — PM_ARRAY param returns the array contents.
9920        build_arr("zshrs_test_getaparam_arr",
9921            vec!["one".to_string(), "two".to_string(), "three".to_string()]);
9922        assert_eq!(
9923            getaparam("zshrs_test_getaparam_arr"),
9924            Some(vec!["one".to_string(), "two".to_string(), "three".to_string()]),
9925            "c:3108-3109 — PM_ARRAY param returns its array"
9926        );
9927
9928        // c:3108 — PM_SCALAR (non-array) param → None.
9929        build_scalar("zshrs_test_getaparam_scalar", "not an array");
9930        assert_eq!(getaparam("zshrs_test_getaparam_scalar"), None,
9931            "c:3108 — non-PM_ARRAY param returns None");
9932
9933        // Clean up.
9934        {
9935            let mut tab = paramtab().write().unwrap();
9936            tab.remove("zshrs_test_getaparam_arr");
9937            tab.remove("zshrs_test_getaparam_scalar");
9938        }
9939    }
9940}