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
66fn foundparam_lock() -> &'static std::sync::Mutex<Option<String>> {
67    FOUNDPARAM.get_or_init(|| std::sync::Mutex::new(None))
68}
69
70/// Read `foundparam`. Returns the last param name observed by
71/// `scanparamvals`; cleared by callers after consumption.
72pub fn foundparam() -> Option<String> {
73    foundparam_lock().lock().unwrap().clone()
74}
75
76/// Set `foundparam`. Called from `scanparamvals`.
77pub fn set_foundparam(nam: Option<String>) {
78    *foundparam_lock().lock().unwrap() = nam;
79}
80
81// ---------------------------------------------------------------------------
82// Parameter flags (from zsh.h PM_* flags)
83// ---------------------------------------------------------------------------
84
85// What level of localness we are at.                                       // c:47
86//                                                                          // c:48
87// Hand-wavingly, this is incremented at every function call and decremented // c:49
88// at every function return.  See startparamscope().                        // c:50
89
90/// Port of `mod_export int locallevel;` from `Src/params.c:54`.
91/// Tracks function-local-scope nesting depth. Bumped by
92/// `startparamscope()` (params.c:5879) on every function call,
93/// decremented by `endparamscope()` (params.c:5950) on return.
94#[allow(non_upper_case_globals)]
95pub static locallevel: std::sync::atomic::AtomicI32 =                        // c:54
96    std::sync::atomic::AtomicI32::new(0);
97
98// ---------------------------------------------------------------------------
99// Real `param` struct lives in Src/zsh.h:1829 (port at zsh_h.rs:750).
100// It uses C-union flattening: u_str / u_arr / u_val / u_dval / u_hash
101// dispatched on `PM_TYPE(node.flags)`. There is NO `ParamValue` enum in
102// C; do not reintroduce one.
103// ---------------------------------------------------------------------------
104
105pub use crate::ported::zsh_h::param;
106
107
108// =============================================================================
109// IPDEF{1,2,4,5,5U,6,7,7R,7U,8,9,10} + LCIPDEF — special-parameter
110// table entry constructors. All defined as macros in
111// `Src/params.c:296-406`. Each produces one row of the
112// `special_params[]` table; the differences are flag combinations
113// + which gsu (getter/setter union) the entry binds.
114//
115// In C, `BR(p)` is `{(void *)(p)}` for the param's `u` data field;
116// `GSU(g)` is the `&g` of the named gsu_scalar/gsu_integer/etc.
117// The Rust port stores `var` and `gsu` as `usize` slot indexes
118// into per-evaluator tables, matching the existing PARAMDEF helper
119// above. The flag bit combinations mirror the C macros line-by-line.
120// =============================================================================
121
122/// Port of `IPDEF1(A,B,C)` from `Src/params.c:296` —
123/// `{{NULL,A,PM_INTEGER|PM_SPECIAL|C},BR(NULL),GSU(B),10,0,...}`.
124#[inline] #[allow(non_snake_case)]
125pub fn IPDEF1(A: &str, B: usize, C: i32) -> paramdef {        // c:params.c:296
126    paramdef {
127        name: A.to_string(),
128        flags: (PM_INTEGER | PM_SPECIAL) as i32 | C,
129        var: 0, gsu: B,
130        getnfn: None, scantfn: None, pm: None,
131    }
132}
133
134/// Port of `IPDEF2(A,B,C)` from `Src/params.c:309` —
135/// `{{NULL,A,PM_SCALAR|PM_SPECIAL|C},BR(NULL),GSU(B),0,0,...}`.
136#[inline] #[allow(non_snake_case)]
137pub fn IPDEF2(A: &str, B: usize, C: i32) -> paramdef {        // c:params.c:309
138    paramdef {
139        name: A.to_string(),
140        flags: (PM_SCALAR | PM_SPECIAL) as i32 | C,
141        var: 0, gsu: B,
142        getnfn: None, scantfn: None, pm: None,
143    }
144}
145
146/// Port of `IPDEF4(A,B)` from `Src/params.c:344` —
147/// `{{NULL,A,PM_INTEGER|PM_READONLY_SPECIAL},BR((void*)B),
148///   GSU(varint_readonly_gsu),10,0,...}`.
149#[inline] #[allow(non_snake_case)]
150pub fn IPDEF4(A: &str, B: usize) -> paramdef {                          // c:params.c:344
151    paramdef {
152        name: A.to_string(),
153        flags: (PM_INTEGER | PM_READONLY_SPECIAL) as i32,
154        var: B, gsu: 0,
155        getnfn: None, scantfn: None, pm: None,
156    }
157}
158
159/// Port of `IPDEF5(A,B,F)` from `Src/params.c:353` —
160/// `{{NULL,A,PM_INTEGER|PM_SPECIAL},BR((void*)B),GSU(F),10,0,...}`.
161#[inline] #[allow(non_snake_case)]
162pub fn IPDEF5(A: &str, B: usize, F: usize) -> paramdef {              // c:params.c:353
163    paramdef {
164        name: A.to_string(),
165        flags: (PM_INTEGER | PM_SPECIAL) as i32,
166        var: B, gsu: F,
167        getnfn: None, scantfn: None, pm: None,
168    }
169}
170
171/// Port of `IPDEF5U(A,B,F)` from `Src/params.c:354` — c:353 + PM_UNSET.
172#[inline] #[allow(non_snake_case)]
173pub fn IPDEF5U(A: &str, B: usize, F: usize) -> paramdef {             // c:params.c:354
174    paramdef {
175        name: A.to_string(),
176        flags: (PM_INTEGER | PM_SPECIAL | PM_UNSET) as i32,
177        var: B, gsu: F,
178        getnfn: None, scantfn: None, pm: None,
179    }
180}
181
182/// Port of `IPDEF6(A,B,F)` from `Src/params.c:362` — c:353 + PM_DONTIMPORT.
183#[inline] #[allow(non_snake_case)]
184pub fn IPDEF6(A: &str, B: usize, F: usize) -> paramdef {              // c:params.c:362
185    paramdef {
186        name: A.to_string(),
187        flags: (PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT) as i32,
188        var: B, gsu: F,
189        getnfn: None, scantfn: None, pm: None,
190    }
191}
192
193/// Port of `IPDEF7(A,B)` from `Src/params.c:367` —
194/// `{{NULL,A,PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(varscalar_gsu),0,0,...}`.
195#[inline] #[allow(non_snake_case)]
196pub fn IPDEF7(A: &str, B: usize) -> paramdef {                          // c:params.c:367
197    paramdef {
198        name: A.to_string(),
199        flags: (PM_SCALAR | PM_SPECIAL) as i32,
200        var: B, gsu: 0,
201        getnfn: None, scantfn: None, pm: None,
202    }
203}
204
205/// Port of `IPDEF7R(A,B)` from `Src/params.c:368` — c:367 + PM_DONTIMPORT_SUID.
206#[inline] #[allow(non_snake_case)]
207pub fn IPDEF7R(A: &str, B: usize) -> paramdef {                         // c:params.c:368
208    paramdef {
209        name: A.to_string(),
210        flags: (PM_SCALAR | PM_SPECIAL | PM_DONTIMPORT_SUID) as i32,
211        var: B, gsu: 0,
212        getnfn: None, scantfn: None, pm: None,
213    }
214}
215
216/// Port of `IPDEF7U(A,B)` from `Src/params.c:369` — c:367 + PM_UNSET.
217#[inline] #[allow(non_snake_case)]
218pub fn IPDEF7U(A: &str, B: usize) -> paramdef {                         // c:params.c:369
219    paramdef {
220        name: A.to_string(),
221        flags: (PM_SCALAR | PM_SPECIAL | PM_UNSET) as i32,
222        var: B, gsu: 0,
223        getnfn: None, scantfn: None, pm: None,
224    }
225}
226
227/// Port of `IPDEF8(A,B,C,D)` from `Src/params.c:394` —
228/// `{{NULL,A,D|PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(colonarr_gsu),
229///   0,0,NULL,C,NULL,0}`.
230/// `C` is the colon-arr field; the Rust port stores it in `getnfn`
231/// since `paramdef` lacks a dedicated colon-arr slot until that's
232/// ported.
233#[inline] #[allow(non_snake_case)]
234pub fn IPDEF8(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:394
235    paramdef {
236        name: A.to_string(),
237        flags: (PM_SCALAR | PM_SPECIAL) as i32 | D,
238        var: B, gsu: 0,
239        getnfn: None, scantfn: None, pm: None,
240    }
241}
242
243/// Port of `IPDEF9(A,B,C,D)` from `Src/params.c:431` —
244/// `{{NULL,A,D|PM_ARRAY|PM_SPECIAL|PM_DONTIMPORT},BR((void*)B),
245///   GSU(vararray_gsu),0,0,NULL,C,NULL,0}`.
246#[inline] #[allow(non_snake_case)]
247pub fn IPDEF9(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:384
248    paramdef {
249        name: A.to_string(),
250        flags: (PM_ARRAY | PM_SPECIAL | PM_DONTIMPORT) as i32 | D,
251        var: B, gsu: 0,
252        getnfn: None, scantfn: None, pm: None,
253    }
254}
255
256/// Port of `IPDEF10(A,B)` from `Src/params.c:438` —
257/// `{{NULL,A,PM_ARRAY|PM_SPECIAL},BR(NULL),GSU(B),10,0,...}`.
258#[inline] #[allow(non_snake_case)]
259pub fn IPDEF10(A: &str, B: usize) -> paramdef {                         // c:params.c:406
260    paramdef {
261        name: A.to_string(),
262        flags: (PM_ARRAY | PM_SPECIAL) as i32,
263        var: 0, gsu: B,
264        getnfn: None, scantfn: None, pm: None,
265    }
266}
267
268/// Port of `LCIPDEF(name)` from `Src/params.c:324` —
269/// `IPDEF2(name, lc_blah_gsu, PM_UNSET)`.
270#[inline] #[allow(non_snake_case)]
271pub fn LCIPDEF(name: &str) -> paramdef {                                     // c:params.c:324
272    IPDEF2(name, 0, PM_UNSET as i32)                                         // c:324 lc_blah_gsu (slot 0)
273}
274
275
276
277// ---------------------------------------------------------------------------
278// Numeric type for parameters (from params.c mnumber)
279// ---------------------------------------------------------------------------
280
281
282// ---------------------------------------------------------------------------
283// Value struct - mirrors C's struct value for subscript access
284// ---------------------------------------------------------------------------
285// ---------------------------------------------------------------------------
286// Shell parameter
287// ---------------------------------------------------------------------------
288
289
290// ---------------------------------------------------------------------------
291// Tied parameter data
292// ---------------------------------------------------------------------------
293
294// TiedData removed: was a Rust-only sidecar for the deleted `ParamTable`'s
295// `tied: HashMap<String, TiedData>` field. C source stores tied-pair
296// metadata via `pm->ename` (the partner name) and `pm->u.data` (the
297// separator char) on the real `param` struct (Src/zsh.h:750 / Src/params.c
298// `bin_typeset()` typeset -T branch).
299
300
301// ---------------------------------------------------------------------------
302// Parameter table print types (from printparamnode)
303// ---------------------------------------------------------------------------
304
305// ---------------------------------------------------------------------------
306// Special parameter definitions table (mirrors special_params[] in C)
307// ---------------------------------------------------------------------------
308
309/// Special-parameter definition — Rust extension paralleling the
310/// `IPDEF*` macro entries in `Src/params.c:297-392`. C uses
311/// `struct paramdef` (`Src/zsh.h:2082`, mirrored at `zsh_h.rs:950`)
312/// with `var` + `gsu` pointers; the Rust port carries a trimmed
313/// shape with `pm_type`/`pm_flags`/`tied_name` until the full
314/// `gsu`-callback plumbing lands. Canonical `paramdef` is the
315/// long-term target.
316#[allow(non_camel_case_types)]
317#[derive(Clone, Debug)]
318pub struct special_paramdef {
319    pub name: &'static str,
320    pub pm_type: u32,  // PM_INTEGER | PM_SCALAR | PM_ARRAY
321    pub pm_flags: u32, // PM_READONLY_SPECIAL, PM_DONTIMPORT, etc.
322    pub tied_name: Option<&'static str>,
323}
324
325/// Index of the first entry in `special_params` that lives in the
326/// zsh-only section (after the `{{NULL,NULL,0}, BR(NULL), ...}`
327/// sentinel at `Src/params.c:392`). Entries before this index are
328/// always loaded; entries at and after this index are only loaded
329/// under non-sh/non-ksh emulation. Mirrors the C two-section table
330/// terminated by an inner NULL sentinel.
331pub const SPECIAL_PARAMS_ZSH_START: usize = 54;                              // c:392
332
333/// All special parameters from params.c special_params[]
334pub const special_params: &[special_paramdef] = &[
335    // Integer specials with custom GSU
336    special_paramdef {
337        name: "#",
338        pm_type: PM_INTEGER,
339        pm_flags: PM_READONLY,
340        tied_name: None,
341    },
342    special_paramdef {
343        name: "ERRNO",
344        pm_type: PM_INTEGER,
345        pm_flags: PM_UNSET,
346        tied_name: None,
347    },
348    special_paramdef {
349        name: "GID",
350        pm_type: PM_INTEGER,
351        pm_flags: PM_DONTIMPORT,
352        tied_name: None,
353    },
354    special_paramdef {
355        name: "EGID",
356        pm_type: PM_INTEGER,
357        pm_flags: PM_DONTIMPORT,
358        tied_name: None,
359    },
360    special_paramdef {
361        name: "HISTSIZE",
362        pm_type: PM_INTEGER,
363        pm_flags: 0,
364        tied_name: None,
365    },
366    special_paramdef {
367        name: "RANDOM",
368        pm_type: PM_INTEGER,
369        pm_flags: 0,
370        tied_name: None,
371    },
372    special_paramdef {
373        name: "SAVEHIST",
374        pm_type: PM_INTEGER,
375        pm_flags: 0,
376        tied_name: None,
377    },
378    special_paramdef {
379        name: "SECONDS",
380        pm_type: PM_INTEGER,
381        pm_flags: 0,
382        tied_name: None,
383    },
384    special_paramdef {
385        name: "UID",
386        pm_type: PM_INTEGER,
387        pm_flags: PM_DONTIMPORT,
388        tied_name: None,
389    },
390    special_paramdef {
391        name: "EUID",
392        pm_type: PM_INTEGER,
393        pm_flags: PM_DONTIMPORT,
394        tied_name: None,
395    },
396    special_paramdef {
397        name: "TTYIDLE",
398        pm_type: PM_INTEGER,
399        pm_flags: PM_READONLY,
400        tied_name: None,
401    },
402    // Scalar specials with custom GSU
403    special_paramdef {
404        name: "USERNAME",
405        pm_type: PM_SCALAR,
406        pm_flags: PM_DONTIMPORT,
407        tied_name: None,
408    },
409    special_paramdef {
410        name: "-",
411        pm_type: PM_SCALAR,
412        pm_flags: PM_READONLY,
413        tied_name: None,
414    },
415    special_paramdef {
416        name: "histchars",
417        pm_type: PM_SCALAR,
418        pm_flags: PM_DONTIMPORT,
419        tied_name: None,
420    },
421    special_paramdef {
422        name: "HOME",
423        pm_type: PM_SCALAR,
424        pm_flags: PM_UNSET,
425        tied_name: None,
426    },
427    special_paramdef {
428        name: "TERM",
429        pm_type: PM_SCALAR,
430        pm_flags: PM_UNSET,
431        tied_name: None,
432    },
433    special_paramdef {
434        name: "TERMINFO",
435        pm_type: PM_SCALAR,
436        pm_flags: PM_UNSET,
437        tied_name: None,
438    },
439    special_paramdef {
440        name: "TERMINFO_DIRS",
441        pm_type: PM_SCALAR,
442        pm_flags: PM_UNSET,
443        tied_name: None,
444    },
445    special_paramdef {
446        name: "WORDCHARS",
447        pm_type: PM_SCALAR,
448        pm_flags: 0,
449        tied_name: None,
450    },
451    special_paramdef {
452        name: "IFS",
453        pm_type: PM_SCALAR,
454        pm_flags: PM_DONTIMPORT,
455        tied_name: None,
456    },
457    special_paramdef {
458        name: "_",
459        pm_type: PM_SCALAR,
460        pm_flags: PM_DONTIMPORT,
461        tied_name: None,
462    },
463    special_paramdef {
464        name: "KEYBOARD_HACK",
465        pm_type: PM_SCALAR,
466        pm_flags: PM_DONTIMPORT,
467        tied_name: None,
468    },
469    special_paramdef {
470        name: "0",
471        pm_type: PM_SCALAR,
472        pm_flags: 0,
473        tied_name: None,
474    },
475    // Readonly integer variables bound to C globals
476    special_paramdef {
477        name: "!",
478        pm_type: PM_INTEGER,
479        pm_flags: PM_READONLY,
480        tied_name: None,
481    },
482    special_paramdef {
483        name: "$",
484        pm_type: crate::ported::zsh_h::PM_INTEGER,
485        pm_flags: crate::ported::zsh_h::PM_READONLY,
486        tied_name: None,
487    },
488    special_paramdef {
489        name: "?",
490        pm_type: crate::ported::zsh_h::PM_INTEGER,
491        pm_flags: crate::ported::zsh_h::PM_READONLY,
492        tied_name: None,
493    },
494    special_paramdef {
495        name: "HISTCMD",
496        pm_type: crate::ported::zsh_h::PM_INTEGER,
497        pm_flags: crate::ported::zsh_h::PM_READONLY,
498        tied_name: None,
499    },
500    special_paramdef {
501        name: "LINENO",
502        pm_type: crate::ported::zsh_h::PM_INTEGER,
503        pm_flags: crate::ported::zsh_h::PM_READONLY,
504        tied_name: None,
505    },
506    special_paramdef {
507        name: "PPID",
508        pm_type: crate::ported::zsh_h::PM_INTEGER,
509        pm_flags: crate::ported::zsh_h::PM_READONLY,
510        tied_name: None,
511    },
512    special_paramdef {
513        name: "ZSH_SUBSHELL",
514        pm_type: crate::ported::zsh_h::PM_INTEGER,
515        pm_flags: crate::ported::zsh_h::PM_READONLY,
516        tied_name: None,
517    },
518    // Settable integer variables
519    special_paramdef {
520        name: "COLUMNS",
521        pm_type: crate::ported::zsh_h::PM_INTEGER,
522        pm_flags: 0,
523        tied_name: None,
524    },
525    special_paramdef {
526        name: "LINES",
527        pm_type: crate::ported::zsh_h::PM_INTEGER,
528        pm_flags: 0,
529        tied_name: None,
530    },
531    special_paramdef {
532        name: "ZLE_RPROMPT_INDENT",
533        pm_type: crate::ported::zsh_h::PM_INTEGER,
534        pm_flags: crate::ported::zsh_h::PM_UNSET,
535        tied_name: None,
536    },
537    special_paramdef {
538        name: "SHLVL",
539        pm_type: crate::ported::zsh_h::PM_INTEGER,
540        pm_flags: 0,
541        tied_name: None,
542    },
543    special_paramdef {
544        name: "FUNCNEST",
545        pm_type: crate::ported::zsh_h::PM_INTEGER,
546        pm_flags: 0,
547        tied_name: None,
548    },
549    special_paramdef {
550        name: "OPTIND",
551        pm_type: crate::ported::zsh_h::PM_INTEGER,
552        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
553        tied_name: None,
554    },
555    special_paramdef {
556        name: "TRY_BLOCK_ERROR",
557        pm_type: crate::ported::zsh_h::PM_INTEGER,
558        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
559        tied_name: None,
560    },
561    special_paramdef {
562        name: "TRY_BLOCK_INTERRUPT",
563        pm_type: crate::ported::zsh_h::PM_INTEGER,
564        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
565        tied_name: None,
566    },
567    // Scalar variables bound to C globals
568    special_paramdef {
569        name: "OPTARG",
570        pm_type: crate::ported::zsh_h::PM_SCALAR,
571        pm_flags: 0,
572        tied_name: None,
573    },
574    special_paramdef {
575        name: "NULLCMD",
576        pm_type: crate::ported::zsh_h::PM_SCALAR,
577        pm_flags: 0,
578        tied_name: None,
579    },
580    special_paramdef {
581        name: "POSTEDIT",
582        pm_type: crate::ported::zsh_h::PM_SCALAR,
583        pm_flags: crate::ported::zsh_h::PM_UNSET,
584        tied_name: None,
585    },
586    special_paramdef {
587        name: "READNULLCMD",
588        pm_type: crate::ported::zsh_h::PM_SCALAR,
589        pm_flags: 0,
590        tied_name: None,
591    },
592    special_paramdef {
593        name: "PS1",
594        pm_type: crate::ported::zsh_h::PM_SCALAR,
595        pm_flags: 0,
596        tied_name: None,
597    },
598    special_paramdef {
599        name: "RPS1",
600        pm_type: crate::ported::zsh_h::PM_SCALAR,
601        pm_flags: crate::ported::zsh_h::PM_UNSET,
602        tied_name: None,
603    },
604    special_paramdef {
605        name: "RPROMPT",
606        pm_type: crate::ported::zsh_h::PM_SCALAR,
607        pm_flags: crate::ported::zsh_h::PM_UNSET,
608        tied_name: None,
609    },
610    special_paramdef {
611        name: "PS2",
612        pm_type: crate::ported::zsh_h::PM_SCALAR,
613        pm_flags: 0,
614        tied_name: None,
615    },
616    special_paramdef {
617        name: "RPS2",
618        pm_type: crate::ported::zsh_h::PM_SCALAR,
619        pm_flags: crate::ported::zsh_h::PM_UNSET,
620        tied_name: None,
621    },
622    special_paramdef {
623        name: "RPROMPT2",
624        pm_type: crate::ported::zsh_h::PM_SCALAR,
625        pm_flags: crate::ported::zsh_h::PM_UNSET,
626        tied_name: None,
627    },
628    special_paramdef {
629        name: "PS3",
630        pm_type: crate::ported::zsh_h::PM_SCALAR,
631        pm_flags: 0,
632        tied_name: None,
633    },
634    special_paramdef {
635        name: "PS4",
636        pm_type: crate::ported::zsh_h::PM_SCALAR,
637        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT_SUID,
638        tied_name: None,
639    },
640    special_paramdef {
641        name: "SPROMPT",
642        pm_type: crate::ported::zsh_h::PM_SCALAR,
643        pm_flags: 0,
644        tied_name: None,
645    },
646    // Readonly arrays
647    special_paramdef {
648        name: "*",
649        pm_type: crate::ported::zsh_h::PM_ARRAY,
650        pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
651        tied_name: None,
652    },
653    special_paramdef {
654        name: "@",
655        pm_type: crate::ported::zsh_h::PM_ARRAY,
656        pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
657        tied_name: None,
658    },
659    // ===================================================================
660    // c:388-392 — `/* This empty row indicates the end of parameters
661    // available in all emulations. */` NULL sentinel terminates the
662    // "always loaded" section. Entries below this line are only added
663    // under zsh emulation (else-branch of EMULATION(EMULATE_SH|EMULATE_KSH)
664    // at createparamtable c:840-846).
665    // SPECIAL_PARAMS_ZSH_START tracks this section boundary.
666    // ===================================================================
667    // Tied colon-separated/array pairs
668    special_paramdef {
669        name: "CDPATH",
670        pm_type: crate::ported::zsh_h::PM_SCALAR,
671        pm_flags: crate::ported::zsh_h::PM_TIED,
672        tied_name: Some("cdpath"),
673    },
674    special_paramdef {
675        name: "FIGNORE",
676        pm_type: crate::ported::zsh_h::PM_SCALAR,
677        pm_flags: crate::ported::zsh_h::PM_TIED,
678        tied_name: Some("fignore"),
679    },
680    special_paramdef {
681        name: "FPATH",
682        pm_type: crate::ported::zsh_h::PM_SCALAR,
683        pm_flags: crate::ported::zsh_h::PM_TIED,
684        tied_name: Some("fpath"),
685    },
686    special_paramdef {
687        name: "MAILPATH",
688        pm_type: crate::ported::zsh_h::PM_SCALAR,
689        pm_flags: crate::ported::zsh_h::PM_TIED,
690        tied_name: Some("mailpath"),
691    },
692    special_paramdef {
693        name: "PATH",
694        pm_type: crate::ported::zsh_h::PM_SCALAR,
695        pm_flags: crate::ported::zsh_h::PM_TIED,
696        tied_name: Some("path"),
697    },
698    special_paramdef {
699        name: "PSVAR",
700        pm_type: crate::ported::zsh_h::PM_SCALAR,
701        pm_flags: crate::ported::zsh_h::PM_TIED,
702        tied_name: Some("psvar"),
703    },
704    special_paramdef {
705        name: "ZSH_EVAL_CONTEXT",
706        pm_type: crate::ported::zsh_h::PM_SCALAR,
707        pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_TIED,
708        tied_name: Some("zsh_eval_context"),
709    },
710    special_paramdef {
711        name: "MODULE_PATH",
712        pm_type: crate::ported::zsh_h::PM_SCALAR,
713        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT | crate::ported::zsh_h::PM_TIED,
714        tied_name: Some("module_path"),
715    },
716    special_paramdef {
717        name: "MANPATH",
718        pm_type: crate::ported::zsh_h::PM_SCALAR,
719        pm_flags: crate::ported::zsh_h::PM_TIED,
720        tied_name: Some("manpath"),
721    },
722    // Locale
723    special_paramdef {
724        name: "LANG",
725        pm_type: crate::ported::zsh_h::PM_SCALAR,
726        pm_flags: crate::ported::zsh_h::PM_UNSET,
727        tied_name: None,
728    },
729    special_paramdef {
730        name: "LC_ALL",
731        pm_type: crate::ported::zsh_h::PM_SCALAR,
732        pm_flags: crate::ported::zsh_h::PM_UNSET,
733        tied_name: None,
734    },
735    special_paramdef {
736        name: "LC_COLLATE",
737        pm_type: crate::ported::zsh_h::PM_SCALAR,
738        pm_flags: crate::ported::zsh_h::PM_UNSET,
739        tied_name: None,
740    },
741    special_paramdef {
742        name: "LC_CTYPE",
743        pm_type: crate::ported::zsh_h::PM_SCALAR,
744        pm_flags: crate::ported::zsh_h::PM_UNSET,
745        tied_name: None,
746    },
747    special_paramdef {
748        name: "LC_MESSAGES",
749        pm_type: crate::ported::zsh_h::PM_SCALAR,
750        pm_flags: crate::ported::zsh_h::PM_UNSET,
751        tied_name: None,
752    },
753    special_paramdef {
754        name: "LC_NUMERIC",
755        pm_type: crate::ported::zsh_h::PM_SCALAR,
756        pm_flags: crate::ported::zsh_h::PM_UNSET,
757        tied_name: None,
758    },
759    special_paramdef {
760        name: "LC_TIME",
761        pm_type: crate::ported::zsh_h::PM_SCALAR,
762        pm_flags: crate::ported::zsh_h::PM_UNSET,
763        tied_name: None,
764    },
765    // Zsh-only aliases
766    special_paramdef {
767        name: "ARGC",
768        pm_type: crate::ported::zsh_h::PM_INTEGER,
769        pm_flags: crate::ported::zsh_h::PM_READONLY,
770        tied_name: None,
771    },
772    special_paramdef {
773        name: "HISTCHARS",
774        pm_type: crate::ported::zsh_h::PM_SCALAR,
775        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
776        tied_name: None,
777    },
778    special_paramdef {
779        name: "status",
780        pm_type: crate::ported::zsh_h::PM_INTEGER,
781        pm_flags: crate::ported::zsh_h::PM_READONLY,
782        tied_name: None,
783    },
784    special_paramdef {
785        name: "prompt",
786        pm_type: crate::ported::zsh_h::PM_SCALAR,
787        pm_flags: 0,
788        tied_name: None,
789    },
790    special_paramdef {
791        name: "PROMPT",
792        pm_type: crate::ported::zsh_h::PM_SCALAR,
793        pm_flags: 0,
794        tied_name: None,
795    },
796    special_paramdef {
797        name: "PROMPT2",
798        pm_type: crate::ported::zsh_h::PM_SCALAR,
799        pm_flags: 0,
800        tied_name: None,
801    },
802    special_paramdef {
803        name: "PROMPT3",
804        pm_type: crate::ported::zsh_h::PM_SCALAR,
805        pm_flags: 0,
806        tied_name: None,
807    },
808    special_paramdef {
809        name: "PROMPT4",
810        pm_type: crate::ported::zsh_h::PM_SCALAR,
811        pm_flags: 0,
812        tied_name: None,
813    },
814    special_paramdef {
815        name: "argv",
816        pm_type: crate::ported::zsh_h::PM_ARRAY,
817        pm_flags: 0,
818        tied_name: None,
819    },
820    // pipestatus array
821    special_paramdef {
822        name: "pipestatus",
823        pm_type: crate::ported::zsh_h::PM_ARRAY,
824        pm_flags: 0,
825        tied_name: None,
826    },
827];
828
829/// Port of `static initparam special_params_sh[]` from
830/// `Src/params.c:447-460`. "Alternative versions of colon-separated
831/// path parameters for sh emulation. These don't link to the array
832/// versions." Loaded by `createparamtable` (c:840-844) when
833/// `EMULATION(EMULATE_SH|EMULATE_KSH)` is non-zero, instead of the
834/// zsh-only section of `special_params`. All entries are scalars
835/// (`IPDEF8` macro adds `PM_SCALAR|PM_SPECIAL`); the C-side
836/// `tied_name` is NULL so these aren't tied to lowercase array
837/// counterparts.
838pub const special_params_sh: &[special_paramdef] = &[
839    special_paramdef {                                                        // c:448
840        name: "CDPATH",
841        pm_type: crate::ported::zsh_h::PM_SCALAR,
842        pm_flags: 0,
843        tied_name: None,
844    },
845    special_paramdef {                                                        // c:449
846        name: "FIGNORE",
847        pm_type: crate::ported::zsh_h::PM_SCALAR,
848        pm_flags: 0,
849        tied_name: None,
850    },
851    special_paramdef {                                                        // c:450
852        name: "FPATH",
853        pm_type: crate::ported::zsh_h::PM_SCALAR,
854        pm_flags: 0,
855        tied_name: None,
856    },
857    special_paramdef {                                                        // c:451
858        name: "MAILPATH",
859        pm_type: crate::ported::zsh_h::PM_SCALAR,
860        pm_flags: 0,
861        tied_name: None,
862    },
863    special_paramdef {                                                        // c:452
864        name: "PATH",
865        pm_type: crate::ported::zsh_h::PM_SCALAR,
866        pm_flags: 0,
867        tied_name: None,
868    },
869    special_paramdef {                                                        // c:453
870        name: "PSVAR",
871        pm_type: crate::ported::zsh_h::PM_SCALAR,
872        pm_flags: 0,
873        tied_name: None,
874    },
875    special_paramdef {                                                        // c:454
876        name: "ZSH_EVAL_CONTEXT",
877        pm_type: crate::ported::zsh_h::PM_SCALAR,
878        pm_flags: crate::ported::zsh_h::PM_READONLY,
879        tied_name: None,
880    },
881    special_paramdef {                                                        // c:457 (security comment)
882        name: "MODULE_PATH",
883        pm_type: crate::ported::zsh_h::PM_SCALAR,
884        pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
885        tied_name: None,
886    },
887];
888
889// ---------------------------------------------------------------------------
890// Parameter table
891// ---------------------------------------------------------------------------
892
893/// Parameter table.
894/// Port of the `paramtab` HashTable Src/params.c maintains —
895/// `createparamtable()` (line 817) initializes it with all the
896/// IPDEF*-declared special params; `createparam()` (line 1030)
897/// adds user variables.
898// ---------------------------------------------------------------------------
899// Free functions matching the C API
900// ---------------------------------------------------------------------------
901
902/// Port of `getintvalue(Value v)` from `Src/params.c:2601`.
903/// C body:
904/// ```c
905/// if (!v) return 0;
906/// if (v->valflags & VALFLAG_INV) return v->start;
907/// if (v->scanflags) {
908///     char **arr = getarrvalue(v);
909///     if (arr) { char *scal = sepjoin(arr, NULL, 1); return mathevali(scal); }
910///     return 0;
911/// }
912/// if (PM_TYPE(v->pm->node.flags) == PM_INTEGER)
913///     return v->pm->gsu.i->getfn(v->pm);
914/// if (v->pm->node.flags & (PM_EFLOAT|PM_FFLOAT))
915///     return (zlong)v->pm->gsu.f->getfn(v->pm);
916/// return mathevali(getstrvalue(v));
917/// ```
918pub fn getintvalue(v: Option<&mut crate::ported::zsh_h::value>) -> i64 {
919    let v = match v { Some(v) => v, None => return 0 };
920    if (v.valflags & VALFLAG_INV) != 0 {
921        return v.start as i64;
922    }
923    if v.scanflags != 0 {
924        // sepjoin(arr, NULL, 1) → mathevali(scal); arr backend missing.
925        return 0;
926    }
927    let pm = match v.pm.as_mut() { Some(p) => p, None => return 0 };
928    if PM_TYPE(pm.node.flags as u32) == PM_INTEGER {
929        return intgetfn(pm);
930    }
931    if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
932        return floatgetfn(pm) as i64;
933    }
934    // mathevali(getstrvalue(v)) — best-effort decimal parse.
935    let pm = v.pm.as_mut().unwrap();
936    strgetfn(pm).parse::<i64>().unwrap_or(0)
937}
938
939/// Port of `getstrvalue(Value v)` from `Src/params.c:2335`.
940/// Full C body dispatches on `PM_TYPE(v->pm->node.flags)`:
941/// PM_HASHED (KSH path: `[0]` index lookup), PM_ARRAY (sepjoin
942/// when v->scanflags else `ss[v->start]`), PM_INTEGER (`convbase`),
943/// PM_EFLOAT|PM_FFLOAT (`convfloat`), PM_SCALAR|PM_NAMEREF
944/// (`pm->gsu.s->getfn(pm)`). Then PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
945/// padding when VALFLAG_SUBST is set.
946pub fn getstrvalue(v: Option<&mut crate::ported::zsh_h::value>) -> String {
947
948    let v = match v { Some(v) => v, None => return String::new() };
949    // c:2344-2348 — `if (VALFLAG_INV && !PM_HASHED) return sprintf("%d", v->start)`.
950    if (v.valflags & VALFLAG_INV) != 0 {
951        let hashed = v.pm.as_ref().map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
952            .unwrap_or(false);
953        if !hashed {
954            return v.start.to_string();
955        }
956    }
957    let pm = match v.pm.as_mut() { Some(p) => p, None => return String::new() };
958    let t = PM_TYPE(pm.node.flags as u32);
959    let pmflags = pm.node.flags as u32;
960
961    // c:2350-2370 — PM_TYPE dispatch.
962    let mut s: String = if t == PM_HASHED || t == PM_ARRAY {                 // c:2351-2370
963        let arr = arrgetfn(pm);
964        if v.scanflags != 0 {                                                // c:2361
965            arr.join(" ")
966        } else {
967            let mut start = v.start;
968            if start < 0 { start += arr.len() as i32; }                       // c:2364
969            if start < 0 || (start as usize) >= arr.len() {                   // c:2365-2366
970                String::new()
971            } else {
972                arr[start as usize].clone()
973            }
974        }
975    } else if t == PM_INTEGER {                                              // c:2371
976        // c:2373 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
977        // Without the base-aware convbase port, default to base-10.
978        intgetfn(pm).to_string()
979    } else if t == PM_EFLOAT || t == PM_FFLOAT {                             // c:2375
980        // c:2377 — `convfloat(getfn(pm), pm->base, pm->flags, NULL)`.
981        floatgetfn(pm).to_string()
982    } else if t == PM_SCALAR || t == PM_NAMEREF {                            // c:2380
983        strgetfn(pm)
984    } else {
985        // c:2384 — `DPUTS(1, "BUG: param node without valid type")`.
986        String::new()
987    };
988
989    // c:2390-2538 — VALFLAG_SUBST padding (PM_LEFT / PM_RIGHT_B /
990    // PM_RIGHT_Z). Multibyte is approximated via `chars().count()`
991    // (codepoint count) since the Rust port stores strings as
992    // UTF-8 rather than the C meta-byte encoding.
993    if v.valflags & VALFLAG_SUBST != 0 {
994        let pad_flags = pmflags & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
995        if pad_flags != 0 {
996            let fwidth = if pm.width > 0 {
997                pm.width as usize
998            } else {
999                s.chars().count()
1000            };
1001            if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
1002                // c:2393-2424 — left-justify: optional zero/blank trim,
1003                // truncate to fwidth, right-pad with spaces.
1004                let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
1005                    s.trim_start_matches('0')
1006                } else {
1007                    s.trim_start_matches(|c: char| c == ' ' || c == '\t')
1008                };
1009                let len = trimmed.chars().count();
1010                let take = len.min(fwidth);
1011                let mut out: String =
1012                    trimmed.chars().take(take).collect();
1013                if fwidth > take {
1014                    out.extend(std::iter::repeat(' ').take(fwidth - take));
1015                }
1016                s = out;
1017            } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
1018                // c:2426-2510 — right-justify with optional zero-padding
1019                // honouring leading-blank/minus/0x/base# prefix
1020                // detection for numeric values.
1021                let charlen = s.chars().count();
1022                if charlen < fwidth {
1023                    let mut zero = true;
1024                    let mut valprefend: usize = 0;
1025                    let numeric_pm = (pmflags
1026                        & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT))
1027                        != 0;
1028                    if pad_flags & PM_RIGHT_Z != 0 {
1029                        // c:2446-2466 — find the prefix to keep
1030                        // (blanks → minus → 0x / base#).
1031                        let bytes = s.as_bytes();
1032                        let mut t = 0usize;
1033                        while t < bytes.len()
1034                            && (bytes[t] == b' ' || bytes[t] == b'\t')
1035                        {
1036                            t += 1;                                          // c:2446-2447
1037                        }
1038                        if numeric_pm && t < bytes.len() && bytes[t] == b'-'
1039                        {
1040                            t += 1;                                          // c:2454-2455
1041                        }
1042                        if (pmflags & PM_INTEGER) != 0 {
1043                            let cbases =
1044                                crate::ported::options::optlookup("cbases")
1045                                    > 0;
1046                            if cbases
1047                                && t + 1 < bytes.len()
1048                                && bytes[t] == b'0'
1049                                && bytes[t + 1] == b'x'
1050                            {
1051                                t += 2;                                      // c:2462-2463
1052                            } else if let Some(hash_off) = bytes[t..]
1053                                .iter()
1054                                .position(|&b| b == b'#')
1055                            {
1056                                t += hash_off + 1;                           // c:2464-2465
1057                            }
1058                        }
1059                        valprefend = t;
1060                        if t == bytes.len() {
1061                            zero = false;                                    // c:2468-2469
1062                        } else if !numeric_pm && !bytes[t].is_ascii_digit() {
1063                            zero = false;                                    // c:2473-2474
1064                        }
1065                    }
1066                    // c:2483 — pad char picks: ' ' if PM_RIGHT_B or
1067                    // numeric-prefix detection failed, else '0'.
1068                    let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero
1069                    {
1070                        ' '
1071                    } else {
1072                        '0'
1073                    };
1074                    let need = fwidth - charlen;
1075                    let prefix = &s[..valprefend];
1076                    let rest = &s[valprefend..];
1077                    let mut out = String::with_capacity(need + s.len());
1078                    out.push_str(prefix);                                    // c:2491
1079                    out.extend(std::iter::repeat(pad_char).take(need));      // c:2483-2485
1080                    out.push_str(rest);                                      // c:2492-2493
1081                    s = out;
1082                } else if charlen > fwidth {
1083                    // c:2496-2500 — truncate from the front to fit fwidth
1084                    // codepoints (C uses MB_METACHARLEN; Rust uses chars).
1085                    let skip = charlen - fwidth;
1086                    s = s.chars().skip(skip).collect();
1087                }
1088            }
1089        }
1090    }
1091
1092    s
1093}
1094
1095/// Port of `getsparam_u(char *s)` from `Src/params.c:3088`. C body:
1096/// ```c
1097/// struct value vbuf;
1098/// Value v;
1099/// if (!(v = getvalue(&vbuf, &s, 0))) return NULL;
1100/// if (PM_TYPE(v->pm->node.flags) != PM_SCALAR) return NULL;
1101/// return getstrvalue(v);
1102/// ```
1103/// Returns the string value only when the param is PM_SCALAR.
1104/// WARNING: param names don't match C — Rust=(v) vs C=()
1105pub fn getsparam_u(v: Option<&mut crate::ported::zsh_h::value>) -> Option<String> {
1106    let v = v?;
1107    let pm = v.pm.as_ref()?;
1108    if PM_TYPE(pm.node.flags as u32) != PM_SCALAR {
1109        return None;
1110    }
1111    Some(getstrvalue(Some(v)))
1112}
1113
1114/// Port of `getaparam(char *s)` from `Src/params.c:3100`. C body:
1115/// ```c
1116/// struct value vbuf; Value v; char *t = s;
1117/// if (idigit(*s)) return NULL;
1118/// if ((v = fetchvalue(&vbuf, &s, 0, SCANPM_ARRONLY)) &&
1119///     PM_TYPE(v->pm->node.flags) == PM_ARRAY)
1120///     return v->pm->gsu.a->getfn(v->pm);
1121/// return NULL;
1122/// ```
1123/// Returns `pm->u.arr` when the param is PM_ARRAY.
1124pub fn getaparam(s: Option<&mut crate::ported::zsh_h::value>) -> Option<Vec<String>> {
1125    let s = s?;
1126    let pm = s.pm.as_mut()?;
1127    if PM_TYPE(pm.node.flags as u32) != PM_ARRAY {
1128        return None;
1129    }
1130    Some(arrgetfn(pm))
1131}
1132
1133/// Port of `gethparam(char *s)` from `Src/params.c:3115`. C body
1134/// (analogous to getaparam): fetchvalue + return
1135/// `paramvalarr(v->pm->gsu.h->getfn(v->pm), SCANPM_WANTVALS)`
1136/// when PM_TYPE == PM_HASHED.
1137pub fn gethparam(s: Option<&mut crate::ported::zsh_h::value>) -> Option<Vec<String>> {
1138    let s = s?;
1139    let pm = s.pm.as_mut()?;
1140    if PM_TYPE(pm.node.flags as u32) != PM_HASHED {
1141        return None;
1142    }
1143    // hashgetfn(pm) returns the HashTable; flattening to values
1144    // requires scanhashtable backend — return empty for now.
1145    let _ = hashgetfn(pm);
1146    Some(Vec::new())
1147}
1148
1149/// Port of `gethkparam(char *s)` from `Src/params.c:3130`. Same as
1150/// `gethparam` but returns keys via `paramvalarr(..., SCANPM_WANTKEYS)`.
1151pub fn gethkparam(s: Option<&mut crate::ported::zsh_h::value>) -> Option<Vec<String>> {
1152    let s = s?;
1153    let pm = s.pm.as_mut()?;
1154    if PM_TYPE(pm.node.flags as u32) != PM_HASHED {
1155        return None;
1156    }
1157    let _ = hashgetfn(pm);
1158    Some(Vec::new())
1159}
1160
1161/// Port of `getnumvalue(Value v)` from `Src/params.c:2624`. Returns an
1162/// `mnumber` (tagged int/float). C body dispatches on `valflags &
1163/// VALFLAG_INV` (returns start as int), `scanflags` (sepjoin →
1164/// matheval), then PM_TYPE: PM_INTEGER → mn.l = pm->gsu.i->getfn,
1165/// PM_EFLOAT|PM_FFLOAT → mn.type=MN_FLOAT; mn.d = pm->gsu.f->getfn,
1166/// else matheval(getstrvalue(v)).
1167pub fn getnumvalue(v: Option<&mut crate::ported::zsh_h::value>) -> crate::ported::math::mnumber {
1168    let v = match v { Some(v) => v, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
1169    if (v.valflags & VALFLAG_INV) != 0 {
1170        return mnumber { l: v.start as i64, d: 0.0, type_: MN_INTEGER };
1171    }
1172    if v.scanflags != 0 {
1173        return mnumber { l: 0, d: 0.0, type_: MN_INTEGER };
1174    }
1175    let pm = match v.pm.as_mut() { Some(p) => p, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
1176    let t = PM_TYPE(pm.node.flags as u32);
1177    if t == PM_INTEGER {
1178        return mnumber { l: intgetfn(pm), d: 0.0, type_: MN_INTEGER };
1179    }
1180    if t == PM_EFLOAT || t == PM_FFLOAT {
1181        return mnumber { l: 0, d: floatgetfn(pm), type_: MN_FLOAT };
1182    }
1183    let s = strgetfn(pm);
1184    if let Ok(i) = s.parse::<i64>() { return mnumber { l: i, d: 0.0, type_: MN_INTEGER }; }
1185    if let Ok(f) = s.parse::<f64>() { return mnumber { l: 0, d: f, type_: MN_FLOAT }; }
1186    mnumber { l: 0, d: 0.0, type_: MN_INTEGER }
1187}
1188
1189/// Port of `setstrvalue(Value v, char *val)` from `Src/params.c:2685`. C body is a
1190/// one-liner: `assignstrvalue(v, val, 0);` — the real workhorse
1191/// is `assignstrvalue` (params.c:2692).
1192pub fn setstrvalue(v: Option<&mut crate::ported::zsh_h::value>, val: &str) {
1193    assignstrvalue(v, Some(val.to_string()), 0);
1194}
1195
1196/// Port of `assigniparam(char *s, zlong val, int flags)` from `Src/params.c:3753` (and its
1197/// internal use as the integer branch of `setvalue`). C body
1198/// builds an `mnumber{ .type = MN_INTEGER, .u.l = val }` and
1199/// calls `assignnparam(s, mn, ASSPM_WARN)`.
1200pub fn assigniparam(vbuf: &str, t: i64) {
1201    assignnparam(vbuf, crate::ported::math::mnumber { l: t, d: 0.0, type_: MN_INTEGER }, crate::ported::zsh_h::ASSPM_WARN);
1202}
1203
1204/// Set array parameter.
1205/// Port of `setaparam(char *s, char **aval)` from `Src/params.c:3595` — single-line wrapper
1206/// around `assignaparam(s, val, ASSPM_WARN)`. C body:
1207/// ```c
1208/// mod_export Param setaparam(char *s, char **val) {
1209///     return assignaparam(s, val, ASSPM_WARN);
1210/// }
1211/// ```
1212///
1213/// `ASSPM_WARN` (params.c:104) is a no-op in our port — the global
1214/// "warn on creation" tracking is not yet ported. Call shape
1215/// preserved so callers can use this where C calls setaparam.
1216/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1217pub fn setaparam(name: &str, val: Vec<String>)                              // c:3595
1218    -> Option<crate::ported::zsh_h::Param>
1219{
1220    // c:3766 — `return assignaparam(s, val, ASSPM_WARN)`.
1221    assignaparam(name, val, crate::ported::zsh_h::ASSPM_WARN)
1222}
1223
1224/// Port of `assignsparam(char *s, char *val, int flags)` from `Src/params.c:3193`. C signature:
1225/// `mod_export Param assignsparam(char *s, char *val, int flags)`.
1226///
1227/// `s` may carry an embedded `[...]` subscript (matching C's
1228/// `strchr(s, '[')` parse). The function operates on the global
1229/// `paramtab` (Src/params.c:515), creating/mutating `Param`
1230/// entries in place. Branches preserved 1:1 with C:
1231///   - c:3203 `isident(s)` — reject non-identifier names.
1232///   - c:3209 `queue_signals()`.
1233///   - c:3210 subscripted path: c:3212 `getvalue` lookup,
1234///     c:3213 `createparam(t, PM_ARRAY)` on miss, c:3216
1235///     PM_READONLY guard, c:3227 ASSPM_WARN drop, c:3228 clear
1236///     PM_DEFAULTED, c:3231 `v = NULL` then re-dispatch by type.
1237///   - c:3232 non-subscripted: c:3233 `getvalue` → c:3234
1238///     `createparam(t, PM_SCALAR)`; c:3236-3250 array/hash type-flip
1239///     to PM_SCALAR (when not PM_SPECIAL|PM_TIED, not KSHARRAYS,
1240///     not ASSPM_AUGMENT) via `resetparam(v->pm, PM_SCALAR)`.
1241///   - c:3258 PM_NAMEREF → c:3259 `valid_refname(val, flags)` guard.
1242///   - c:3269 clear PM_DEFAULTED.
1243///   - c:3343 `assignstrvalue(v, val, flags)`.
1244///   - c:3344 `unqueue_signals()`; c:3345 return v->pm.
1245///
1246/// The full HashTable substrate (vtable callbacks, scope-stacked
1247/// iterators) is not yet wired; non-essential branches such as
1248/// `+= AUGMENT` numeric/array slice append and `check_warn_pm`
1249/// are documented but elided where unreachable from current
1250/// callers — none of those code paths are exercised by zshrs's
1251/// existing call sites.
1252pub fn assignsparam(s: &str, val: &str, flags: i32)                          // c:3193
1253    -> Option<crate::ported::zsh_h::Param>
1254{
1255
1256    // c:3203 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
1257    if !isident(s) {
1258        zerr(&format!("not an identifier: {}", s));                          // c:3204
1259        errflag.fetch_or(                                                    // c:3206
1260            crate::ported::utils::ERRFLAG_ERROR,
1261            std::sync::atomic::Ordering::Relaxed,
1262        );
1263        return None;                                                         // c:3207
1264    }
1265    crate::ported::signals::queue_signals();                                 // c:3209
1266
1267    // c:3210 — `strchr(s, '[')`. Split the leading name from the
1268    // subscript while preserving C's `*ss = '\0'` / `*ss = '['`
1269    // restore semantics: the Rust port works on `&str` slices so
1270    // there's no in-place null-terminator dance, but the parse
1271    // shape is identical.
1272    let (name, subscript) = match s.find('[') {
1273        Some(i) => {
1274            let close = s.rfind(']').unwrap_or(s.len());
1275            let key_end = if close > i { close } else { s.len() };
1276            (&s[..i], Some(&s[i + 1..key_end]))
1277        }
1278        None => (s, None),
1279    };
1280
1281    // Subscripted path (c:3210-3231).
1282    if let Some(key) = subscript {
1283        let mut tab = paramtab().write().unwrap();
1284        let exists = tab.contains_key(name);                                 // c:3212
1285        if !exists {
1286            // c:3213 `createparam(t, PM_ARRAY); created = 1;`
1287            let pm: Param = Box::new(param {
1288                node: hashnode { next: None, nam: name.to_string(), flags: PM_ARRAY as i32 },
1289                u_data: 0, u_arr: Some(Vec::new()), u_str: None, u_val: 0,
1290                u_dval: 0.0, u_hash: None,
1291                gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
1292                base: 0, width: 0, env: None, ename: None, old: None, level: 0,
1293            });
1294            tab.insert(name.to_string(), pm);
1295        } else {
1296            // c:3216 `if (v->pm->node.flags & PM_READONLY)`.
1297            let pm = tab.get(name).unwrap();
1298            if (pm.node.flags as u32 & PM_READONLY) != 0 {
1299                zerr(&format!("read-only variable: {}", pm.node.nam));       // c:3217
1300                drop(tab);
1301                crate::ported::signals::unqueue_signals();                   // c:3220
1302                return None;                                                 // c:3221
1303            }
1304        }
1305        // c:3231 `v = NULL;` — re-dispatch by storage type.
1306        let pm = tab.get_mut(name).unwrap();
1307        pm.node.flags &= !(PM_DEFAULTED as i32);                             // c:3228
1308        if (pm.node.flags as u32 & PM_HASHED) != 0 {
1309            // PM_HASHED element store. `param.u_hash` is typed
1310            // `Option<HashTable>` per Src/zsh.h:1841 but the
1311            // HashTable runtime backing isn't wired; the assoc-array
1312            // values live in a parallel storage keyed on param name
1313            // (`paramtab_hashed_storage()`).
1314            let mut store = paramtab_hashed_storage().lock().unwrap();
1315            store.entry(name.to_string()).or_default()
1316                .insert(key.to_string(), val.to_string());
1317        } else if let Ok(idx) = key.parse::<i64>() {
1318            // PM_ARRAY + numeric subscript (c:3357 `assignaparam`).
1319            let arr = pm.u_arr.get_or_insert_with(Vec::new);
1320            let len = arr.len() as i64;
1321            // 1-based forward, negative-from-end.
1322            let real_idx = if idx < 0 { len + idx } else { idx - 1 };
1323            let real_idx = real_idx.max(0) as usize;
1324            while arr.len() <= real_idx { arr.push(String::new()); }
1325            arr[real_idx] = val.to_string();
1326            pm.u_str = None;
1327        } else {
1328            // String subscript on a non-hashed name → auto-vivify
1329            // as PM_HASHED (mirrors C `createparam(s, PM_HASHED)`
1330            // fallback when getvalue returns NULL).
1331            pm.node.flags = (pm.node.flags & !(PM_TYPE(u32::MAX) as i32))
1332                | PM_HASHED as i32;
1333            pm.u_arr = None;
1334            pm.u_str = None;
1335            let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
1336            map.insert(key.to_string(), val.to_string());
1337            paramtab_hashed_storage().lock().unwrap()
1338                .insert(name.to_string(), map);
1339        }
1340        let cloned = pm.clone();
1341        drop(tab);
1342        crate::ported::signals::unqueue_signals();                           // c:3344
1343        return Some(cloned);                                                 // c:3345
1344    }
1345
1346    // c:3232 non-subscripted branch.
1347    let mut tab = paramtab().write().unwrap();
1348    let existing = tab.contains_key(name);
1349    if !existing {
1350        // c:3234 `createparam(t, PM_SCALAR); created = 1;`
1351        let mut pm_flags = PM_SCALAR as i32;
1352        if isset_opt(ALLEXPORT) {                                            // c:1149-1150 (ALLEXPORT path)
1353            pm_flags |= PM_EXPORTED as i32;
1354        }
1355        let pm: Param = Box::new(param {
1356            node: hashnode { next: None, nam: name.to_string(), flags: pm_flags },
1357            u_data: 0, u_arr: None, u_str: Some(String::new()), u_val: 0,
1358            u_dval: 0.0, u_hash: None,
1359            gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
1360            base: 0, width: 0, env: None, ename: None, old: None, level: 0,
1361        });
1362        tab.insert(name.to_string(), pm);
1363    } else {
1364        let pm = tab.get(name).unwrap();
1365        // c:3216 PM_READONLY guard for an existing param.
1366        if (pm.node.flags as u32 & PM_READONLY) != 0 {
1367            zerr(&format!("read-only variable: {}", pm.node.nam));           // c:3217
1368            drop(tab);
1369            crate::ported::signals::unqueue_signals();                       // c:3220
1370            return None;                                                     // c:3221
1371        }
1372        // c:3236-3250 — existing PM_ARRAY/PM_HASHED on a non-special,
1373        // non-tied, non-KSHARRAYS, non-AUGMENT scalar assignment →
1374        // `resetparam(v->pm, PM_SCALAR)`.
1375        let f = pm.node.flags as u32;
1376        let is_array_or_hash = (f & PM_ARRAY) != 0 || (f & PM_HASHED) != 0;
1377        let is_special_or_tied = (f & (PM_SPECIAL | PM_TIED)) != 0;
1378        let augment_bit = (flags & ASSPM_AUGMENT) != 0;
1379        if is_array_or_hash
1380            && !is_special_or_tied
1381            && !augment_bit
1382            && !isset(crate::ported::zsh_h::KSHARRAYS)
1383        {
1384            // c:3242 — flip type to PM_SCALAR, drop array/hash slots.
1385            let pm_mut = tab.get_mut(name).unwrap();
1386            pm_mut.node.flags = (pm_mut.node.flags & !(PM_TYPE(u32::MAX) as i32))
1387                | PM_SCALAR as i32;
1388            pm_mut.u_arr = None;
1389            paramtab_hashed_storage().lock().unwrap().remove(name);
1390        }
1391    }
1392
1393    // c:3258-3266 `if (*val && (v->pm->node.flags & PM_NAMEREF))`.
1394    let pm = tab.get(name).unwrap();
1395    if !val.is_empty() && (pm.node.flags as u32 & PM_NAMEREF) != 0 {
1396        if !valid_refname(val, pm.node.flags) {                              // c:3259
1397            zerr(&format!("invalid name reference: {}", val));               // c:3260
1398            drop(tab);
1399            errflag.fetch_or(                                                // c:3263
1400                crate::ported::utils::ERRFLAG_ERROR,
1401                std::sync::atomic::Ordering::Relaxed,
1402            );
1403            crate::ported::signals::unqueue_signals();                       // c:3262
1404            return None;                                                     // c:3264
1405        }
1406    }
1407
1408    // c:3269 `v->pm->node.flags &= ~PM_DEFAULTED;`
1409    let pm = tab.get_mut(name).unwrap();
1410    pm.node.flags &= !(PM_DEFAULTED as i32);
1411
1412    // c:3343 `assignstrvalue(v, val, flags)` — scalar write.
1413    pm.u_str = Some(val.to_string());
1414
1415    let cloned = pm.clone();
1416    drop(tab);
1417    crate::ported::signals::unqueue_signals();                               // c:3344
1418    Some(cloned)                                                             // c:3345
1419}
1420
1421/// Parallel storage for PM_HASHED parameter values. `param.u_hash`
1422/// is typed `Option<HashTable>` per Src/zsh.h:1841 but the full
1423/// HashTable substrate isn't wired yet; the assoc-array values live
1424/// here keyed on param name until that lands.
1425static PARAMTAB_HASHED_STORAGE_INNER: OnceLock<
1426    Mutex<HashMap<String, indexmap::IndexMap<String, String>>>,
1427> = OnceLock::new();
1428
1429pub(crate) fn paramtab_hashed_storage()
1430    -> &'static Mutex<HashMap<String, indexmap::IndexMap<String, String>>>
1431{
1432    PARAMTAB_HASHED_STORAGE_INNER
1433        .get_or_init(|| Mutex::new(HashMap::new()))
1434}
1435
1436/// Mirror the global `paramtab` (and the parallel hashed-storage
1437/// table) into the three HashMaps that `SubstState` uses as its
1438/// transient backing during `prefork()` (Src/subst.c:100). This
1439/// is a port-transition shim: once `subst.rs` reads parameters
1440/// directly through `paramtab().read()` / `.write()` instead of carrying
1441/// `state.variables`/`state.arrays`/`state.assoc_arrays`, this
1442/// helper goes away.
1443pub fn sync_state_from_paramtab(
1444    variables: &mut HashMap<String, String>,
1445    arrays: &mut HashMap<String, Vec<String>>,
1446    assoc_arrays: &mut HashMap<String, indexmap::IndexMap<String, String>>,
1447) {
1448    let tab = paramtab().read().unwrap();
1449    for (name, pm) in tab.iter() {
1450        let f = pm.node.flags as u32;
1451        if (f & PM_ARRAY) != 0 {
1452            if let Some(arr) = pm.u_arr.as_ref() {
1453                arrays.insert(name.clone(), arr.clone());
1454            }
1455            variables.remove(name);
1456            assoc_arrays.remove(name);
1457        } else if (f & PM_HASHED) != 0 {
1458            if let Some(map) = paramtab_hashed_storage()
1459                .lock().unwrap().get(name)
1460            {
1461                assoc_arrays.insert(name.clone(), map.clone());
1462            }
1463            variables.remove(name);
1464            arrays.remove(name);
1465        } else if let Some(s) = pm.u_str.as_ref() {
1466            // PM_SCALAR / PM_NAMEREF / numeric — fold to the string view.
1467            variables.insert(name.clone(), s.clone());
1468            arrays.remove(name);
1469            assoc_arrays.remove(name);
1470        }
1471    }
1472}
1473
1474/// Array parameter assignment (no subscript).
1475///
1476/// Direct port of `Param assignaparam(char *s, char **val, int flags)`
1477/// from `Src/params.c:3357`. Writes an array value into paramtab
1478/// and returns the new/updated Param.
1479///
1480/// Pending C semantics:
1481///   - PM_READONLY rejection (c:3370-3381 via setarrvalue chain)
1482///   - PM_NAMEREF type-change reject (c:3395-3398)
1483///   - resetparam from non-array (c:3415-3420)
1484///   - ASSPM_AUGMENT (`a+=val`) preserve-old prepend (c:3404-3412)
1485///   - PM_UNIQUE dedupe (c:3401)
1486///   - element-wise `a[k]=v` slice path (c:3373-3389)
1487pub fn assignaparam(
1488    name: &str,
1489    val: Vec<String>,
1490    flags: i32,
1491) -> Option<crate::ported::zsh_h::Param> {                                   // c:3357
1492    // c:3366-3370 — `if (!isident(s)) { zerr; return NULL }`.
1493    if !isident(name) {
1494        crate::ported::utils::zerr(&format!("not an identifier: {}", name));
1495        return None;
1496    }
1497
1498    // c:3391-3394 — fetchvalue / createparam(PM_ARRAY) if missing.
1499    let (existed, prior_scalar, prior_flags) = {
1500        let tab = paramtab().read().unwrap();
1501        match tab.get(name) {
1502            Some(pm) => (true, pm.u_str.clone(), pm.node.flags),
1503            None => (false, None, 0),
1504        }
1505    };
1506    if !existed {
1507        createparam(name, PM_ARRAY as i32)?;
1508    }
1509
1510    // c:3402-3412 — ASSPM_AUGMENT preserve-old prepend. When the
1511    // previous value was a scalar (not array/hashed) and we're
1512    // augmenting (`a+=val`), prepend that scalar's string form as
1513    // val[0]. Only fires when the existing param is not PM_UNSET.
1514    let was_scalar_array_target = existed
1515        && prior_flags & (PM_ARRAY | PM_HASHED) as i32 == 0
1516        && prior_flags & PM_SPECIAL as i32 == 0;
1517    let mut val = val;
1518    if (flags & ASSPM_AUGMENT) != 0
1519        && was_scalar_array_target
1520        && prior_flags & PM_UNSET as i32 == 0
1521    {
1522        if let Some(old_scalar) = prior_scalar {
1523            val.insert(0, old_scalar);                                       // c:3408-3411
1524        }
1525    }
1526
1527    // c:3434 — setarrvalue(v, val): store array in pm.u_arr.
1528    let mut tab = paramtab().write().unwrap();
1529    let pm = tab.get_mut(name)?;
1530    let uniq = pm.node.flags & PM_UNIQUE as i32 != 0;                        // c:3401
1531    if pm.node.flags & PM_SPECIAL as i32 == 0 {
1532        let type_mask =
1533            PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
1534        pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_ARRAY as i32;
1535    }
1536    // c:3401 — preserve PM_UNIQUE through the type change, then let
1537    // arrsetfn dedupe via the actual write.
1538    if uniq {
1539        pm.node.flags |= PM_UNIQUE as i32;
1540    }
1541    let val_final = if uniq { simple_arrayuniq(val) } else { val };
1542    pm.u_arr = Some(val_final.clone());
1543    pm.u_str = None;
1544    pm.u_hash = None;
1545    let cloned = pm.clone();
1546    drop(tab);
1547    let _ = val_final;
1548    Some(cloned)
1549}
1550
1551/// Direct port of `Param sethparam(char *s, char **val)` from
1552/// `Src/params.c:3602`. Writes an associative array (flat
1553/// alternating key,value list) into paramtab + the parallel
1554/// `paramtab_hashed_storage` table; returns the new Param.
1555///
1556/// Pending C semantics:
1557///   - PM_READONLY rejection
1558///   - resetparam(PM_HASHED) for type-change
1559///   - PM_SPECIAL type-change reject (c:3637)
1560pub fn sethparam(name: &str, val: Vec<String>)                              // c:3602
1561    -> Option<crate::ported::zsh_h::Param>
1562{
1563
1564    // c:3611-3615 — `if (!isident(s)) { zerr; return NULL }`.
1565    if !isident(name) {
1566        crate::ported::utils::zerr(&format!("not an identifier: {}", name));
1567        return None;
1568    }
1569    // c:3617-3621 — `if (strchr(s, '[')) { zerr; return NULL }`.
1570    if name.contains('[') {
1571        crate::ported::utils::zerr("nested associative arrays not yet supported");
1572        return None;
1573    }
1574
1575    // c:3625 — fetchvalue / createparam(PM_HASHED) if missing.
1576    let exists = paramtab().read().unwrap().contains_key(name);
1577    if !exists {
1578        createparam(name, PM_HASHED as i32)?;
1579    }
1580
1581    // Build the IndexMap from flat (k,v) pairs (mirrors c:arrhashsetfn
1582    // pair-walking at c:4140-4166).
1583    let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
1584    let mut iter = val.into_iter();
1585    while let Some(k) = iter.next() {
1586        let v = iter.next().unwrap_or_default();
1587        map.insert(k, v);
1588    }
1589
1590    // c:3640 — install in paramtab + paramtab_hashed_storage.
1591    let mut tab = paramtab().write().unwrap();
1592    let pm = tab.get_mut(name)?;
1593    if pm.node.flags & PM_SPECIAL as i32 == 0 {
1594        let type_mask =
1595            PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
1596        pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_HASHED as i32;
1597    }
1598    pm.u_arr = None;
1599    pm.u_str = None;
1600    let cloned = pm.clone();
1601    drop(tab);
1602
1603    paramtab_hashed_storage()
1604        .lock()
1605        .unwrap()
1606        .insert(name.to_string(), map);
1607
1608    Some(cloned)
1609}
1610
1611/// Unset parameter (from params.c unsetparam_pm)
1612/// Port of `unsetparam_pm(Param pm, int altflag, int exp)` from `Src/params.c:3841`. Full body
1613/// removes `pm` from `paramtab` (after invoking
1614/// `pm->gsu.s->unsetfn(pm, exp)`), tears down the tied alternate
1615/// (`pm->ename`) when `!altflag`, deletes the env entry, and
1616/// resurrects `pm->old` at the right scope. Stub: needs paramtab
1617/// HashTable backend (`paramtab->removenode/addnode`) plus the
1618/// `delenv`/`adduserdir` helpers — direct port retains only the
1619/// in-memory mutation of `pm` that doesn't touch the table.
1620#[allow(unused_variables)]
1621pub fn unsetparam_pm(pm: &mut crate::ported::zsh_h::param, altflag: i32, exp: i32) -> i32 {
1622    // Readonly check (locallevel global not yet ported — assume 0).
1623    if (pm.node.flags as u32 & PM_READONLY) != 0 && pm.level <= 0 {
1624        // zerr("read-only %s: %s", ref?"reference":"variable", nam);
1625        let _kind = if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
1626            "reference"
1627        } else {
1628            "variable"
1629        };
1630        return 1;
1631    }
1632    pm.node.flags &= !(PM_DECLARED as i32);
1633    if (pm.node.flags as u32 & PM_UNSET) == 0
1634        || (pm.node.flags as u32 & PM_REMOVABLE) != 0
1635    {
1636        // pm->gsu.s->unsetfn(pm, exp) — open-coded to stdunsetfn.
1637        stdunsetfn(pm, exp);
1638    }
1639    if pm.env.is_some() {
1640        delenv(&pm.node.nam);
1641        pm.env = None;
1642    }
1643    // Tied alt-name removal + paramtab restore-from-old not yet
1644    // possible without HashTable backend; the C postlude (lines
1645    // 3853-3935) is a paramtab->removenode + addnode dance that
1646    // requires the missing vtable.
1647    pm.node.flags |= PM_UNSET as i32;
1648    0
1649}
1650
1651/// Empty special-hash sentinel.
1652/// Port of `shempty()` from Src/params.c:1166. The C source uses
1653/// it as a no-op getfn callback for special hashes that need an
1654/// addressable function pointer but no actual work. Provided here
1655/// so future callers that match the C source's signature can call
1656/// it directly.
1657pub fn shempty() {}
1658
1659/// Port of `setsparam(char *s, char *val)` from Src/params.c:3350.
1660/// C body: `return assignsparam(s, val, ASSPM_WARN);`
1661/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1662pub fn setsparam(s: &str, val: &str)                                         // c:3350
1663    -> Option<crate::ported::zsh_h::Param>
1664{
1665    assignsparam(s, val, ASSPM_WARN as i32)                                  // c:3352
1666}
1667
1668/// Port of `setiparam(char *s, zlong val)` from Src/params.c:3765. The C source
1669/// constructs an `mnumber` and calls `assignnparam(s, mnval,
1670/// ASSPM_WARN)`. The Rust port renders to decimal and routes
1671/// through `assignsparam` until the integer-typed `assignnparam`
1672/// store path lands.
1673/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1674pub fn setiparam(s: &str, val: i64)                                          // c:3765
1675    -> Option<crate::ported::zsh_h::Param>
1676{
1677    assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
1678}
1679
1680/// Port of `setiparam_no_convert(char *s, zlong val)` from Src/params.c:3781. C
1681/// source comment: "If the target is already an integer, this
1682/// gets converted back. Low technology rules." It uses convbase
1683/// to render decimal then calls assignsparam.
1684/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1685pub fn setiparam_no_convert(s: &str, val: i64)                               // c:3781
1686    -> Option<crate::ported::zsh_h::Param>
1687{
1688    assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
1689}
1690
1691/// Port of `getsparam(char *s)` from `Src/params.c:3076`.
1692///
1693/// C body:
1694/// ```c
1695/// char *getsparam(char *s) {
1696///     struct value vbuf;
1697///     Value v = getvalue(&vbuf, &s, 0);
1698///     if (!v) return NULL;
1699///     return getstrvalue(v);
1700/// }
1701/// ```
1702///
1703/// `getvalue` (params.c:2173) builds a `Value` for the parameter,
1704/// dispatching through `Param.gsu->getfn` for special parameters.
1705/// `getstrvalue` (params.c:2335) extracts the scalar form: for
1706/// PM_INTEGER calls `pm->gsu.i->getfn(pm)` and convbase's the
1707/// result; for PM_SCALAR calls `pm->gsu.s->getfn(pm)`; for
1708/// PM_ARRAY joins the elements.
1709///
1710/// **Sole funnel.** Every scalar parameter read in zshrs routes
1711/// through this fn — `subst.rs` parameter expansion AND
1712/// `fusevm_bridge::expand_param` both call `getsparam`. The
1713/// dispatch chain lives in exactly one place, mirroring C's
1714/// "every read goes through getsparam" architecture.
1715///
1716/// Lookup order (mirrors C's `getvalue` → `getstrvalue` cascade):
1717/// 1. **GSU dispatch** via [`lookup_special_var`] — special
1718///    parameters route through their getfn callback (`uidgetfn` /
1719///    `randomgetfn` / `usernamegetfn` / etc.). Same role as
1720///    C's `Param.gsu->getfn` virtual dispatch.
1721/// 2. **Local variable** — `variables[name]`. C reads `pm->u.str`
1722///    for PM_SCALAR; here we hold the scalar in the variables
1723///    HashMap.
1724/// 3. **Environment fallback** — `std::env::var(name)`. C imports
1725///    env vars into the param table at startup so they go through
1726///    the same dispatch as everything else; zshrs reads from the
1727///    OS env on miss to match.
1728/// 4. **Array → scalar** — `arrays[name].join(" ")`. Mirrors
1729///    C's PM_ARRAY case in getstrvalue (params.c:2358) which
1730///    joins via `sepjoin(ss, NULL, 1)`.
1731///
1732// Retrieve a scalar (string) parameter                                     // c:3076
1733/// Returns `None` only if all four paths miss (parameter genuinely
1734/// unset).
1735pub fn getsparam(name: &str) -> Option<String> {                             // c:3076
1736    // 1. GSU dispatch — `Param.gsu->getfn(pm)` equivalent. Special
1737    //    parameters (UID/RANDOM/USERNAME/...) live behind getfn
1738    //    hooks that the table read below would otherwise miss.
1739    if let Some(v) = lookup_special_var(name) {
1740        return Some(v);
1741    }
1742    // 2. Paramtab read — `(Value)gethashnode2(paramtab, name)`.
1743    //    Walk the global paramtab for the named param, returning
1744    //    `pm->u.str` for PM_SCALAR/PM_NAMEREF or `sepjoin(pm->u.arr)`
1745    //    for PM_ARRAY (matches `getstrvalue` at params.c:2358).
1746    if let Ok(tab) = paramtab().read() {
1747        if let Some(pm) = tab.get(name) {
1748            if let Some(s) = pm.u_str.as_ref() {
1749                return Some(s.clone());
1750            }
1751            if let Some(arr) = pm.u_arr.as_ref() {
1752                return Some(arr.join(" "));
1753            }
1754        }
1755    }
1756    // 3. Env fallback — C imports env into paramtab at init so the
1757    //    read above would hit. If the import hasn't happened yet
1758    //    (e.g. during very early init) fall back to the live env.
1759    std::env::var(name).ok()
1760}
1761
1762/// Retrieve integer parameter.
1763/// Port of `getiparam(char *s)` from Src/params.c:3044. C: getvalue +
1764/// getintvalue. Our adaptation reads the scalar string and parses;
1765/// returns 0 on missing or unparseable, matching getintvalue's
1766/// failure-returns-0 convention (params.c:2601).
1767pub fn getiparam(s: &str) -> i64 {
1768    // C also honours PM_INTEGER's `pm->u.val` payload directly when
1769    // the param is typed numeric; check paramtab first for that case.
1770    if let Ok(tab) = paramtab().read() {
1771        if let Some(pm) = tab.get(s) {
1772            if (pm.node.flags as u32 & crate::ported::zsh_h::PM_INTEGER) != 0
1773            {
1774                return pm.u_val;
1775            }
1776        }
1777    }
1778    getsparam(s).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0)
1779}
1780
1781/// Retrieve numeric (int-or-float) parameter.
1782/// Port of `getnparam(char *s)` from Src/params.c:3058. C returns an
1783/// `mnumber` (tagged int/float union); our adaptation returns
1784/// `(i64, f64, bool)` where the bool is true for float. Unset
1785/// returns `(0, 0.0, false)`, matching the MN_INTEGER zero
1786/// fallback in the C source's not-found branch.
1787pub fn getnparam(s: &str) -> (i64, f64, bool) {
1788    if let Ok(tab) = paramtab().read() {
1789        if let Some(pm) = tab.get(s) {
1790            let fl = pm.node.flags as u32;
1791            if (fl & (crate::ported::zsh_h::PM_EFLOAT
1792                | crate::ported::zsh_h::PM_FFLOAT)) != 0
1793            {
1794                return (pm.u_dval as i64, pm.u_dval, true);
1795            }
1796            if (fl & crate::ported::zsh_h::PM_INTEGER) != 0 {
1797                return (pm.u_val, pm.u_val as f64, false);
1798            }
1799        }
1800    }
1801    let s = match getsparam(s) {
1802        Some(s) => s,
1803        None => return (0, 0.0, false),
1804    };
1805    if s.contains('.') || s.contains('e') || s.contains('E') {
1806        if let Ok(f) = s.parse::<f64>() {
1807            return (f as i64, f, true);
1808        }
1809    }
1810    if let Ok(i) = s.parse::<i64>() {
1811        return (i, i as f64, false);
1812    }
1813    (0, 0.0, false)
1814}
1815
1816/// Port of `resetparam(Param pm, int flags)` from `Src/params.c:3796`. C body:
1817/// ```c
1818/// char *s = pm->node.nam;
1819/// queue_signals();
1820/// if (pm != (Param)(paramtab == realparamtab ?
1821///        paramtab->getnode2(paramtab, s) :
1822///        paramtab->getnode(paramtab, s))) {
1823///     unqueue_signals();
1824///     zerr("can't change type of hidden variable: %s", s);
1825///     return 1;
1826/// }
1827/// s = dupstring(s);
1828/// unsetparam_pm(pm, 0, 1);
1829/// unqueue_signals();
1830/// createparam(s, flags);
1831/// return 0;
1832/// ```
1833/// Tears `pm` down + recreates it with `flags` so the next
1834/// assignment lands in a fresh slot of the requested type. Used
1835/// by `assignsparam` when the type-flag of an existing param
1836/// changes (e.g. `typeset -i x; x="abc"` resets x back to scalar).
1837///
1838/// The `paramtab->getnode` reachability check at c:3800 catches
1839/// the hidden-shadow case (a local var hiding the global `pm` we
1840/// were handed) — without the paramtab vtable we skip the check
1841/// and proceed to unset+create.
1842pub fn resetparam(pm: &mut crate::ported::zsh_h::param, flags: i32) -> i32 { // c:3796
1843    let s = pm.node.nam.clone();                                             // c:3796
1844    crate::ported::signals::queue_signals();                                 // c:3799
1845    // c:3800-3807 — paramtab->getnode2 / getnode reachability check.
1846    // Without paramtab vtable wired we cannot detect the hidden-
1847    // variable case, so we proceed; a future port of paramtab
1848    // adds the check at this site.
1849    unsetparam_pm(pm, 0, 1);                                                 // c:3819
1850    crate::ported::signals::unqueue_signals();                               // c:3819
1851    let _ = createparam(&s, flags);                                          // c:3819
1852    0                                                                        // c:3819
1853}
1854
1855/// Unset a parameter from all storage.
1856/// Port of `unsetparam(char *s)` from Src/params.c:3819. C uses a single
1857/// HashTable; our SubstState-style storage spans variables /
1858/// arrays / assoc_arrays, so removal must touch all three to be
1859/// thorough (matches `unsetparam_pm`'s flag-aware tear-down).
1860/// WARNING: param names don't match C — Rust=(arrays, assoc_arrays, name) vs C=(s)
1861pub fn unsetparam(                                                          // c:3819
1862    variables: &mut std::collections::HashMap<String, String>,
1863    arrays: &mut std::collections::HashMap<String, Vec<String>>,
1864    assoc_arrays: &mut std::collections::HashMap<String, indexmap::IndexMap<String, String>>,
1865    name: &str,
1866) {
1867    variables.remove(name);
1868    arrays.remove(name);
1869    assoc_arrays.remove(name);
1870}
1871
1872/// Port of `export_param(Param pm)` from `Src/params.c:2653`. C body
1873/// converts `pm`'s value to its scalar form per `PM_TYPE`
1874/// (`convbase`/`convfloat`/`gsu.s->getfn`) then calls
1875/// `addenv(pm, val)`. PM_ARRAY/PM_HASHED early-returns (export
1876/// not supported for them outside KSH emulation).
1877pub fn export_param(pm: &mut crate::ported::zsh_h::param) {
1878    let t = PM_TYPE(pm.node.flags as u32);
1879    if (t & (PM_ARRAY | PM_HASHED)) != 0 {
1880        return;
1881    }
1882    let val: String = if t == PM_INTEGER {
1883        // convbase(buf, pm->gsu.i->getfn(pm), pm->base)
1884        format!("{}", intgetfn(pm))
1885    } else if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
1886        // convfloat(pm->gsu.f->getfn(pm), pm->base, pm->node.flags, NULL)
1887        format!("{}", floatgetfn(pm))
1888    } else {
1889        strgetfn(pm)
1890    };
1891    addenv(&pm.node.nam, &val);
1892    pm.env = Some(val);
1893}
1894
1895/// Start a parameter scope.
1896/// Port of `startparamscope()` (Src/init.c) — the C source pushes the
1897/// current scope counter so `local`-declared params disappear on function
1898/// exit. Rust port operates on the bucket-2 holder `paramtab` via a
1899/// `&mut crate::ported::zsh_h::HashTable` argument.
1900pub fn startparamscope(_table: &mut crate::ported::zsh_h::HashTable) {
1901    crate::ported::utils::inc_locallevel();
1902}
1903
1904/// Port of `endparamscope()` from `Src/params.c:5857`. C signature:
1905/// `mod_export void endparamscope(void)`. Decrements `locallevel`,
1906/// pops any pushed history stack, then iterates `paramtab` calling
1907/// `scanendscope` to restore/unset every param whose `level`
1908/// exceeds the new `locallevel`. Operates on the global `paramtab`
1909/// just like C — no parameter, no fake injection wrapper.
1910pub fn endparamscope() {
1911    queue_signals();
1912    crate::ported::utils::dec_locallevel();                                  // c:5861 locallevel--
1913    crate::ported::hist::saveandpophiststack(crate::ported::zsh_h::HFILE_USE_OPTIONS as i32);
1914    let ll = crate::ported::utils::locallevel();
1915    // c:5867 scanhashtable(paramtab, 0, 0, 0, scanendscope, 0). Walk
1916    // the live paramtab (HashMap-backed until the hashtable.c vtable
1917    // is wired) and apply scanendscope's `pm->level > locallevel`
1918    // filter, restoring the `pm.old` chain or removing the entry.
1919    if let Ok(mut tab) = paramtab().write() {
1920        let stale: Vec<String> = tab.iter()
1921            .filter_map(|(k, pm)| if pm.level > ll { Some(k.clone()) } else { None })
1922            .collect();
1923        for n in stale {
1924            // c:scanendscope:5903 — non-special path: restore pm.old
1925            // (or remove if no outer binding existed).
1926            if let Some(pm) = tab.remove(&n) {
1927                if let Some(prev) = pm.old {                                 // c:scanendscope:5933 pm->old = tpm->old
1928                    tab.insert(n, prev);                                     // restore outer binding (Box<param>)
1929                }
1930                // else: c:5966 unsetparam_pm — name unset entirely
1931            }
1932        }
1933    }
1934    unqueue_signals();
1935}
1936
1937
1938// ---------------------------------------------------------------------------
1939// Utility functions
1940// ---------------------------------------------------------------------------
1941
1942/// Check if string is valid identifier (from params.c isident)
1943// Return 1 if the string s is a valid identifier, else return 0.         // c:1288
1944pub fn isident(s: &str) -> bool {                                           // c:1288
1945    if s.is_empty() {
1946        return false;
1947    }
1948    let mut chars = s.chars().peekable();
1949
1950    // Handle namespace prefix (e.g. "ns.var")
1951    if chars.peek() == Some(&'.') {
1952        chars.next();
1953        if chars.peek().is_none_or(|c| c.is_ascii_digit()) {
1954            return false;
1955        }
1956    }
1957
1958    let first = match chars.next() {
1959        Some(c) => c,
1960        None => return false,
1961    };
1962
1963    if first.is_ascii_digit() {
1964        // All-digit names are valid (positional params)
1965        return chars.all(|c| c.is_ascii_digit());
1966    }
1967
1968    if !first.is_alphabetic() && first != '_' {
1969        return false;
1970    }
1971
1972    for c in chars {
1973        if c == '[' {
1974            // Subscript is OK at end
1975            return true;
1976        }
1977        if !c.is_alphanumeric() && c != '_' && c != '.' {
1978            return false;
1979        }
1980    }
1981    true
1982}
1983
1984/// Port of `valid_refname(char *val, int flags)` from `Src/params.c:6466`. C body
1985/// validates a nameref target name. Two paths:
1986///   - PM_UPPER (`typeset -nu`): reject digit-leader (positional
1987///     refs would loop) and the literal `argv`/`ARGC` names.
1988///   - non-PM_UPPER: positional digit-leader is permitted (must be
1989///     all-digits before any `[`); otherwise scan via
1990///     `itype_end(INAMESPC)`.
1991/// Either path then accepts the trailing one-char specials
1992/// `! ? $ - _` and an optional `[subscript]` tail. Returns 1 on
1993/// valid, 0 otherwise. The Rust port follows the same control
1994/// flow with `is_ascii_digit`/`is_alphabetic` standing in for
1995/// `idigit`/`itype_end`.
1996pub fn valid_refname(val: &str, flags: i32) -> bool {                        // c:6466
1997    if val.is_empty() {
1998        return false;
1999    }
2000    let first = val.chars().next().unwrap();
2001    let pm_upper = (flags as u32 & PM_UPPER) != 0;
2002    let mut t: usize;
2003    if pm_upper {                                                            // c:6470
2004        if first.is_ascii_digit() {                                          // c:6472
2005            return false;                                                    // c:6473
2006        }
2007        // c:6474 — `t = itype_end(val, INAMESPC, 0)`; INAMESPC stops
2008        // at `.` and other non-namespace chars. Approximate with
2009        // alphanumeric/_ scan.
2010        t = val
2011            .char_indices()
2012            .find(|(_, c)| !(c.is_alphanumeric() || *c == '_'))
2013            .map(|(i, _)| i)
2014            .unwrap_or(val.len());
2015        if t - 0 == 4                                                        // c:6475
2016            && (val.starts_with("argv") || val.starts_with("ARGC"))          // c:6476-6477
2017        {
2018            return false;                                                    // c:6478
2019        }
2020    } else if first.is_ascii_digit() {                                       // c:6479
2021        // c:6480-6485 — all-digit run; first non-digit must be `[`.
2022        t = 1;
2023        for (i, c) in val.char_indices().skip(1) {
2024            if !c.is_ascii_digit() {
2025                t = i;
2026                break;
2027            }
2028            t = i + c.len_utf8();
2029        }
2030        if t < val.len() && val.as_bytes()[t] != b'[' {                      // c:6484
2031            return false;                                                    // c:6485
2032        }
2033    } else {
2034        // c:6487 — `t = itype_end(val, INAMESPC, 0)`.
2035        t = val
2036            .char_indices()
2037            .find(|(_, c)| !(c.is_alphanumeric() || *c == '_' || *c == '.'))
2038            .map(|(i, _)| i)
2039            .unwrap_or(val.len());
2040    }
2041
2042    if t == 0 {                                                              // c:6489
2043        let c = val.as_bytes()[0];
2044        if !(c == b'!' || c == b'?' || c == b'$' || c == b'-' || c == b'_') { // c:6490
2045            return false;                                                    // c:6493
2046        }
2047        t = 1;                                                               // c:6494
2048    }
2049    if t < val.len() && val.as_bytes()[t] == b'[' {                          // c:6496
2050        // c:6498-6504 — parse_subscript/Inbrack/Outbrack walk. The
2051        // tokenize+parse_subscript pair isn't ported; accept any
2052        // balanced `[…]` tail (single-level) to remain conservative.
2053        let tail = &val[t + 1..];
2054        if let Some(close) = tail.find(']') {
2055            // c:6505-6508 — anything past `]` is rejected.
2056            if close + 1 < tail.len() {
2057                return false;
2058            }
2059        } else {
2060            return false;
2061        }
2062    }
2063    true                                                                     // c:6510
2064}
2065
2066/// Colon-separated path to array.
2067/// Port of `colonsplit(char *s, int uniq)` from Src/params.c.
2068pub fn colonsplit(s: &str) -> Vec<String> {
2069    s.split(':')
2070        .filter(|s| !s.is_empty())
2071        .map(String::from)
2072        .collect()
2073}
2074
2075/// Array to colon-separated path — inverse of `colonsplit`.
2076/// Port of `colonarrgetfn(Param pm)` from Src/params.c (joins the array
2077/// stored in `pm->u.colon` back into the `:`-form for env).
2078/// WARNING: param names don't match C — Rust=(arr) vs C=(pm)
2079pub fn colonarrgetfn(arr: &[String]) -> String {
2080    arr.join(":")
2081}
2082
2083/// Remove duplicate elements from array while preserving order.
2084/// Port of `uniqarray(char **x)` from Src/params.c.
2085/// WARNING: param names don't match C — Rust=(arr) vs C=(x)
2086pub fn uniqarray(arr: Vec<String>) -> Vec<String> {
2087    let mut seen = HashSet::new();
2088    arr.into_iter().filter(|s| seen.insert(s.clone())).collect()
2089}
2090
2091
2092/// Slice an indexed array using zsh 1-based inclusive semantics.
2093/// Port of `getarrvalue(Value v)` from Src/params.c:2548 — the slice
2094/// branch that resolves the start/end pair into a Vec. Negative
2095/// indices count from the end (`-1` is the last element);
2096/// out-of-range bounds collapse to empty (`${a[5,10]}` on len=3
2097/// returns empty, not clamped); `start > end` returns empty.
2098///
2099/// 0 has asymmetric meaning per C source's getarrvalue:
2100///   start=0 → "before first element" → resolved to 1
2101///   end=0   → "before first element" → empty slice
2102/// WARNING: param names don't match C — Rust=(arr, start, end) vs C=(v)
2103pub fn getarrvalue(arr: &[String], start: i64, end: i64) -> Vec<String> {
2104    let len = arr.len() as i64;
2105    if len == 0 {
2106        return Vec::new();
2107    }
2108    // Out-of-range starts (positive past len, or negative below
2109    // -len) collapse to empty per Src/params.c getarrvalue's
2110    // slice-resolution branches.
2111    if start > len {
2112        return Vec::new();
2113    }
2114    if end < 0 && (len + end + 1) < 1 {
2115        return Vec::new();
2116    }
2117    if start < 0 && end < 0 && start > end {
2118        return Vec::new();
2119    }
2120    if start < 0 && start < -len {
2121        return Vec::new();
2122    }
2123    let resolve_start = |i: i64| -> i64 {
2124        if i < 0 {
2125            (len + i + 1).max(1)
2126        } else if i == 0 {
2127            1
2128        } else {
2129            i.min(len)
2130        }
2131    };
2132    let resolve_end = |i: i64| -> i64 {
2133        if i < 0 {
2134            (len + i + 1).max(0)
2135        } else if i == 0 {
2136            0
2137        } else {
2138            i.min(len)
2139        }
2140    };
2141    let s = resolve_start(start);
2142    let e = resolve_end(end);
2143    if e < 1 || s > e {
2144        return Vec::new();
2145    }
2146    let s_idx = (s - 1) as usize;
2147    let e_idx = e as usize;
2148    arr[s_idx..e_idx.min(arr.len())].to_vec()
2149}
2150
2151/// Direct port of `void setarrvalue(Value v, char **val)` from
2152/// `Src/params.c:2895-3037`. Sets an array (or assoc-array via
2153/// arrhashsetfn) into the param identified by v.pm, honouring
2154/// PM_READONLY / type-guards / VALFLAG_EMPTY rejections and the
2155/// slice-bounds adjust for `[N,M]` subscripts.
2156///
2157/// C dispatch:
2158///   - !EXECOPT → silent return (c:2897-2898)
2159///   - PM_READONLY → zerr + return (c:2899-2904)
2160///   - !PM_ARRAY && !PM_HASHED → zerr (c:2905-2911)
2161///   - VALFLAG_EMPTY → zerr (c:2913-2917)
2162///   - start==0,end==-1 && PM_HASHED → arrhashsetfn(0) (c:2919-2922)
2163///   - start==0,end==-1 && PM_ARRAY → gsu.a->setfn (c:2922-2923)
2164///   - start==-1,end==0 && PM_HASHED → arrhashsetfn(AUGMENT) (c:2925-2928)
2165///   - PM_HASHED with other bounds → zerr slice-of-assoc (c:2929-2932)
2166///   - PM_ARRAY with slice → bounds adjust + splice (c:2933+)
2167///
2168/// Pending: ASSPM_AUGMENT prepend (c:2945-2954), PM_UNIQUE dedupe
2169/// after assign (c:2966-2967), VALFLAG_INV + !KSHARRAYS off-by-one
2170/// (c:2938-2942).
2171pub fn setarrvalue(v: &mut crate::ported::zsh_h::value, val: Vec<String>) {  // c:2895
2172
2173    let pm = match v.pm.as_mut() { Some(p) => p, None => return };
2174
2175    // c:2899-2904 — PM_READONLY rejection.
2176    if pm.node.flags & PM_READONLY as i32 != 0 {
2177        crate::ported::utils::zerr(&format!("read-only variable: {}", pm.node.nam));
2178        return;
2179    }
2180    // c:2905-2911 — type guard.
2181    let t = PM_TYPE(pm.node.flags as u32);
2182    if t & (crate::ported::zsh_h::PM_ARRAY | PM_HASHED) == 0 {
2183        crate::ported::utils::zerr(&format!(
2184            "{}: attempt to assign array value to non-array",
2185            pm.node.nam
2186        ));
2187        return;
2188    }
2189    // c:2913-2917 — VALFLAG_EMPTY rejection.
2190    if v.valflags & VALFLAG_EMPTY != 0 {
2191        crate::ported::utils::zerr(&format!(
2192            "{}: assignment to invalid subscript range",
2193            pm.node.nam
2194        ));
2195        return;
2196    }
2197
2198    // c:2919-2932 — full-replace / AUGMENT / hash-slice-reject paths.
2199    if v.start == 0 && v.end == -1 {
2200        if t == PM_HASHED {
2201            // c:2920 — arrhashsetfn(pm, val, 0).
2202            arrhashsetfn(pm, val, 0);
2203        } else {
2204            // c:2922 — `pm->gsu.a->setfn(pm, val)`. Route through
2205            // arrsetfn so PM_UNIQUE dedupe + arrfixenv side-effects
2206            // fire (params.c:4066-4076).
2207            arrsetfn(pm, val);
2208        }
2209        return;
2210    }
2211    if v.start == -1 && v.end == 0 && t == PM_HASHED {
2212        arrhashsetfn(pm, val, crate::ported::zsh_h::ASSPM_AUGMENT);
2213        return;
2214    }
2215    if t == PM_HASHED {
2216        crate::ported::utils::zerr(&format!(
2217            "{}: attempt to set slice of associative array",
2218            pm.node.nam
2219        ));
2220        return;
2221    }
2222
2223    // c:2938-2942 — VALFLAG_INV + !KSHARRAYS off-by-one. Inverse
2224    // subscripts (`a[(i)pat]=val`) are 1-based when KSHARRAYS is
2225    // off; shift start/end down by 1 to match the 0-based slice
2226    // arithmetic below.
2227    if v.valflags & VALFLAG_INV != 0
2228        && !isset(crate::ported::zsh_h::KSHARRAYS)
2229    {
2230        if v.start > 0 {
2231            v.start -= 1;
2232        }
2233        v.end -= 1;
2234    }
2235
2236    // c:2933+ — PM_ARRAY slice path.
2237    let arr = pm.u_arr.get_or_insert_with(Vec::new);
2238    let len = arr.len() as i64;
2239    // c:2944-2949 — negative start: add pre_assignment_length; clamp to 0.
2240    let start = if v.start < 0 {
2241        (len + v.start as i64).max(0)
2242    } else {
2243        v.start as i64
2244    };
2245    // c:2950-2953 — negative end: add pre_assignment_length + 1; clamp to 0.
2246    let end = if v.end < 0 {
2247        (len + v.end as i64 + 1).max(0)
2248    } else {
2249        v.end as i64
2250    };
2251    // c:2960-2961 — `if (end < start) end = start`.
2252    let start_idx = (start.max(1) - 1) as usize;
2253    let end_idx = end.max(0) as usize;
2254
2255    // c:2980 — pad with empty strings up to start.
2256    while arr.len() < start_idx {
2257        arr.push(String::new());
2258    }
2259
2260    // c:2989-2998 — splice val into [start..end] range.
2261    let end_idx = end_idx.min(arr.len());
2262    if start_idx <= end_idx {
2263        arr.splice(start_idx..end_idx, val);
2264    } else {
2265        for (i, x) in val.into_iter().enumerate() {
2266            if start_idx + i < arr.len() {
2267                arr[start_idx + i] = x;
2268            } else {
2269                arr.push(x);
2270            }
2271        }
2272    }
2273}
2274
2275// ---------------------------------------------------------------------------
2276// Integer/Float conversion (from convbase/convfloat)
2277// ---------------------------------------------------------------------------
2278
2279/// Convert integer to string with base (from params.c convbase)
2280/// Port of `convbase(char *s, zlong v, int base)` from `Src/params.c:5632`.
2281/// WARNING: param names don't match C — Rust=(val, base) vs C=(s, v, base)
2282pub fn convbase(val: i64, base: u32) -> String {
2283    if base == 0 || base == 10 {
2284        return val.to_string();
2285    }
2286
2287    let negative = val < 0;
2288    let mut v = if negative { (-val) as u64 } else { val as u64 };
2289
2290    if v == 0 {
2291        return match base {
2292            16 => "0x0".to_string(),
2293            8 => "00".to_string(),
2294            _ => format!("{}#0", base),
2295        };
2296    }
2297
2298    let mut digits = Vec::new();
2299    while v > 0 {
2300        let dig = (v % base as u64) as u8;
2301        digits.push(if dig < 10 {
2302            b'0' + dig
2303        } else {
2304            b'A' + dig - 10
2305        });
2306        v /= base as u64;
2307    }
2308    digits.reverse();
2309
2310    let prefix = match base {
2311        16 => "0x",
2312        8 => "0",
2313        10 => "",
2314        _ => "",
2315    };
2316
2317    let base_prefix = if base != 10 && base != 16 && base != 8 {
2318        format!("{}#", base)
2319    } else {
2320        prefix.to_string()
2321    };
2322
2323    let sign = if negative { "-" } else { "" };
2324    format!(
2325        "{}{}{}",
2326        sign,
2327        base_prefix,
2328        String::from_utf8_lossy(&digits)
2329    )
2330}
2331
2332/// Convert integer to string with underscores for readability
2333/// Port of `convbase_underscore(char *s, zlong v, int base, int underscore)` from `Src/params.c:5646`.
2334/// WARNING: param names don't match C — Rust=(val, base, underscore) vs C=(s, v, base, underscore)
2335pub fn convbase_underscore(val: i64, base: u32, underscore: i32) -> String {
2336    let s = convbase(val, base);
2337    if underscore <= 0 {
2338        return s;
2339    }
2340
2341    // Find the digits portion
2342    let (prefix, digits) = if let Some(rest) = s.strip_prefix('-') {
2343        let digit_start = rest
2344            .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
2345            .unwrap_or(0);
2346        (&s[..1 + digit_start], &rest[digit_start..])
2347    } else {
2348        let digit_start = s
2349            .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
2350            .unwrap_or(0);
2351        (&s[..digit_start], &s[digit_start..])
2352    };
2353
2354    if digits.len() <= underscore as usize {
2355        return s;
2356    }
2357
2358    let u = underscore as usize;
2359    let mut result = prefix.to_string();
2360    let chars: Vec<char> = digits.chars().collect();
2361    let first_group = chars.len() % u;
2362    if first_group > 0 {
2363        result.extend(&chars[..first_group]);
2364        if first_group < chars.len() {
2365            result.push('_');
2366        }
2367    }
2368    for (i, chunk) in chars[first_group..].chunks(u).enumerate() {
2369        if i > 0 {
2370            result.push('_');
2371        }
2372        result.extend(chunk);
2373    }
2374    result
2375}
2376
2377/// Port of `convfloat(double dval, int digits, int flags, FILE *fout)` from `Src/params.c:5689`.
2378///
2379/// C signature: `char *convfloat(double dval, int digits, int flags,
2380/// FILE *fout)` — picks `%e` / `%f` / `%g` based on PM_EFLOAT /
2381/// PM_FFLOAT (line 5705-5727), then snprintf'd with `digits` precision.
2382/// When neither E nor F flag is set, zsh uses `%.*g` with a default
2383/// of 17 significant digits (line 5712-5714). E-flag with N significant
2384/// figures decrements `digits` because `%e` counts decimal places not
2385/// significants (line 5720-5725).
2386///
2387/// Rust signature drops the `fout` parameter — every caller wanted the
2388/// returned string. IEEE specials (inf/nan) hand-formatted to `Inf`/
2389/// `-Inf`/`NaN` ahead of the snprintf, matching the C source's Inf/NaN
2390/// shortcuts at lines 5733-5736 / 5742-5744. The trailing-dot rule for
2391/// integer-valued floats (`5` -> `5.`) is added by the caller (params'
2392/// internal printing path) in C zsh; mirrored here for the no-flag case
2393/// so `MathNum::(crate::ported::math::mn_format_subst(Float(5.0)))` produces `5.` not `5`.
2394/// WARNING: param names don't match C — Rust=(dval, digits, pm_flags) vs C=(dval, digits, flags, fout)
2395pub fn convfloat(dval: f64, digits: i32, pm_flags: u32) -> String {
2396    if dval.is_infinite() {                                       // c:5742
2397        return if dval < 0.0 {
2398            "-Inf".to_string()
2399        } else {
2400            "Inf".to_string()
2401        };
2402    }
2403    if dval.is_nan() {                                            // c:5744
2404        return "NaN".to_string();
2405    }
2406    // Pick fmt char + adjust digits per the C cascade at 5705-5727.
2407    let (fmt_char, digits) = if (pm_flags & crate::ported::zsh_h::PM_EFLOAT) != 0 { // c:5715
2408        let d = if digits <= 0 { 10 } else { digits };           // c:5718
2409        ('e', (d - 1).max(0))                                    // c:5725
2410    } else if (pm_flags & crate::ported::zsh_h::PM_FFLOAT) != 0 {                  // c:5716
2411        let d = if digits <= 0 { 10 } else { digits };           // c:5718
2412        ('f', d)
2413    } else {
2414        let d = if digits == 0 { 17 } else { digits };           // c:5713
2415        ('g', d)
2416    };
2417    // Mirror zsh's snprintf path (Src/params.c:5751) — the C source
2418    // uses `VARARR(char, buf, 512 + digits)` for %f's full integer-
2419    // part expansion. 512 + 17 = 529 covers the zsh general case;
2420    // wider buffers below for the unbounded %f.
2421    let buf_len = 512usize + digits as usize + 4;
2422    let mut buf = vec![0u8; buf_len];
2423    let fmt = match fmt_char {
2424        'e' => c"%.*e",
2425        'f' => c"%.*f",
2426        _ => c"%.*g",
2427    };
2428    // SAFETY: buf has the C-required size for any double precision; fmt
2429    // is a NUL-terminated literal; snprintf writes ASCII only.
2430    let n = unsafe {
2431        libc::snprintf(
2432            buf.as_mut_ptr() as *mut libc::c_char,
2433            buf_len,
2434            fmt.as_ptr(),
2435            digits as libc::c_int,
2436            dval,
2437        )
2438    };
2439    if n < 0 {
2440        return format!("{}", dval);
2441    }
2442    let len = (n as usize).min(buf_len - 1);
2443    buf.truncate(len);
2444    let mut s = String::from_utf8(buf).unwrap_or_else(|_| format!("{}", dval));
2445    // zsh's general-format (%g) callers (math `$(( ))` substitution)
2446    // append `.` when the output has no `e` and no `.`, so integer-
2447    // valued floats like `5` render as `5.`. PM_EFLOAT/PM_FFLOAT skip
2448    // this rule (the format spec already pins shape).
2449    if fmt_char == 'g' && !s.contains('e') && !s.contains('.') {
2450        s.push('.');
2451    }
2452    s
2453}
2454
2455/// Format float with underscores
2456pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
2457    let s = convfloat(dval, 0, 0);
2458    if underscore <= 0 {
2459        return s;
2460    }
2461
2462    let u = underscore as usize;
2463    let (sign, rest) = if let Some(after) = s.strip_prefix('-') {
2464        ("-", after)
2465    } else {
2466        ("", s.as_str())
2467    };
2468
2469    let (int_part, frac_exp) = if let Some(dot_pos) = rest.find('.') {
2470        (&rest[..dot_pos], &rest[dot_pos..])
2471    } else {
2472        (rest, "")
2473    };
2474
2475    // Add underscores to integer part
2476    let int_chars: Vec<char> = int_part.chars().collect();
2477    let mut result = sign.to_string();
2478    let first_group = int_chars.len() % u;
2479    if first_group > 0 {
2480        result.extend(&int_chars[..first_group]);
2481        if first_group < int_chars.len() {
2482            result.push('_');
2483        }
2484    }
2485    for (i, chunk) in int_chars[first_group..].chunks(u).enumerate() {
2486        if i > 0 {
2487            result.push('_');
2488        }
2489        result.extend(chunk);
2490    }
2491
2492    // Add underscores to fractional part
2493    if let Some(frac) = frac_exp.strip_prefix('.') {
2494        result.push('.');
2495        let (frac_digits, exp) = if let Some(e_pos) = frac.find('e') {
2496            (&frac[..e_pos], &frac[e_pos..])
2497        } else {
2498            (frac, "")
2499        };
2500
2501        let frac_chars: Vec<char> = frac_digits.chars().collect();
2502        for (i, chunk) in frac_chars.chunks(u).enumerate() {
2503            if i > 0 {
2504                result.push('_');
2505            }
2506            result.extend(chunk);
2507        }
2508        result.push_str(exp);
2509    } else {
2510        result.push_str(frac_exp);
2511    }
2512
2513    result
2514}
2515
2516// intgetfn / strgetfn drift wrappers removed — replaced below with
2517// real C-shape ports `intgetfn(pm: &param) -> i64` (Src/params.c:3993)
2518// and `strgetfn(pm: &param) -> String` (Src/params.c:4029) that read
2519// directly from the union fields `pm->u.val` / `pm->u.str`.
2520
2521// ---------------------------------------------------------------------------
2522// Tests
2523// ---------------------------------------------------------------------------
2524
2525#[cfg(test)]
2526mod tests {
2527    use super::*;
2528
2529    // test_param_value_conversions removed: tested deleted fake
2530    // `ParamValue::Scalar` constructor. C uses union access on
2531    // `pm->u.str`/`u.val`/`u.dval`/`u.arr` dispatched via
2532    // `PM_TYPE(pm->node.flags)` (Src/zsh.h:540).
2533    #[test]
2534    fn test_colonarr_conversion() {
2535        let arr = colonsplit("/bin:/usr/bin:/usr/local/bin");
2536        assert_eq!(arr, vec!["/bin", "/usr/bin", "/usr/local/bin"]);
2537        let path = colonarrgetfn(&arr);
2538        assert_eq!(path, "/bin:/usr/bin:/usr/local/bin");
2539    }
2540       #[test]
2541    fn test_isident() {
2542        assert!(isident("foo"));
2543        assert!(isident("_bar"));
2544        assert!(isident("FOO_BAR"));
2545        assert!(isident("x123"));
2546        assert!(isident("123")); // positional params
2547        assert!(!isident(""));
2548        assert!(!isident("foo bar"));
2549    }
2550
2551
2552    #[test]
2553    fn test_unique_array() {
2554        let arr = vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()];
2555        let result = uniqarray(arr);
2556        assert_eq!(result, vec!["a", "b", "c"]);
2557    }
2558
2559    #[test]
2560    fn test_convbase() {
2561        assert_eq!(convbase(255, 16), "0xFF");
2562        assert_eq!(convbase(10, 10), "10");
2563        assert_eq!(convbase(-5, 10), "-5");
2564        assert_eq!(convbase(7, 8), "07");
2565        assert_eq!(convbase(5, 2), "2#101");
2566    }
2567
2568    #[test]
2569    fn test_convfloat() {
2570        // Use 2.5 instead of 3.14 — clippy errors on the latter as
2571        // an approx PI constant. The test checks 2-decimal formatting
2572        // round-trips, which the exact value doesn't influence.
2573        let s = convfloat(2.5, 2, crate::ported::zsh_h::PM_FFLOAT);
2574        assert!(s.starts_with("2.50"));
2575
2576        assert_eq!(convfloat(f64::INFINITY, 0, 0), "Inf");
2577        assert_eq!(convfloat(f64::NEG_INFINITY, 0, 0), "-Inf");
2578        assert_eq!(convfloat(f64::NAN, 0, 0), "NaN");
2579    }
2580
2581
2582
2583
2584    #[test]
2585    fn test_getarrvalue() {
2586        let arr = vec!["a".into(), "b".into(), "c".into(), "d".into()];
2587        assert_eq!(getarrvalue(&arr, 2, 3), vec!["b", "c"]);
2588        assert_eq!(getarrvalue(&arr, -2, -1), vec!["c", "d"]);
2589        assert_eq!(getarrvalue(&arr, 1, 4), vec!["a", "b", "c", "d"]);
2590    }
2591
2592    #[test]
2593    fn test_setarrvalue() {
2594        // C-faithful: setarrvalue takes a Value pointing at a Param
2595        // with u_arr set. Construct one inline.
2596        use crate::ported::zsh_h::{hashnode, param, PM_ARRAY};
2597        let pm = Box::new(param {
2598            node: hashnode { next: None, nam: "test".to_string(), flags: PM_ARRAY as i32 },
2599            u_data: 0,
2600            u_arr: Some(vec!["a".into(), "b".into(), "c".into(), "d".into()]),
2601            u_str: None, u_val: 0, u_dval: 0.0, u_hash: None,
2602            gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
2603            base: 0, width: 0, env: None, ename: None, old: None, level: 0,
2604        });
2605        let mut v = crate::ported::zsh_h::value {
2606            pm: Some(pm),
2607            arr: Vec::new(),
2608            scanflags: 0,
2609            valflags: 0,
2610            start: 2,
2611            end: 3,
2612        };
2613        setarrvalue(&mut v, vec!["X".into(), "Y".into()]);
2614        let arr = v.pm.unwrap().u_arr.unwrap();
2615        assert_eq!(arr, vec!["a", "X", "Y", "d"]);
2616    }
2617
2618    #[test]
2619    fn test_valid_refname() {
2620        assert!(valid_refname("foo", 0));
2621        assert!(valid_refname("_bar", 0));
2622        assert!(valid_refname("1", 0));
2623        assert!(valid_refname("!", 0));
2624        assert!(valid_refname("arr[1]", 0));
2625        assert!(!valid_refname("", 0));
2626        // C semantics: empty leader without one of `! ? $ - _` is rejected.
2627        assert!(!valid_refname(" ", 0));
2628        // PM_UPPER rejects digit-leader and argv/ARGC.
2629        assert!(!valid_refname("1", PM_UPPER as i32));
2630        assert!(!valid_refname("argv", PM_UPPER as i32));
2631        assert!(!valid_refname("ARGC", PM_UPPER as i32));
2632    }
2633
2634    #[test]
2635    fn test_uniq_array_empty() {
2636        let empty: Vec<String> = Vec::new();
2637        assert!(uniqarray(empty).is_empty());
2638    }
2639
2640    #[test]
2641    fn test_convbase_underscore() {
2642        let s = convbase_underscore(1234567, 10, 3);
2643        assert_eq!(s, "1_234_567");
2644    }
2645
2646    fn val_str(v: getarg_out<'_>) -> String {
2647        match v {
2648            getarg_out::Value(v) => v.to_str(),
2649            getarg_out::Flags { .. } => panic!("expected Value, got Flags"),
2650        }
2651    }
2652
2653    #[test]
2654    fn getarg_n_flag_picks_second_exact_match() {
2655        // C params.c:1431-1442 + 1758 — `(en.2.)pat` picks 2nd exact match.
2656        let arr: Vec<String> = vec!["foo".into(), "bar".into(), "foo".into(), "baz".into()];
2657        let out = getarg("(en.2.r)foo", Some(&arr), None, None).expect("Some");
2658        assert_eq!(val_str(out), "foo");
2659    }
2660
2661    #[test]
2662    fn getarg_n_flag_third_exact_match() {
2663        let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into(), "b".into()];
2664        let out = getarg("(en.3.r)a", Some(&arr), None, None).expect("Some");
2665        assert_eq!(val_str(out), "a");
2666    }
2667
2668    #[test]
2669    fn getarg_n_flag_returns_index_with_i() {
2670        // (en.2.i) — return INDEX of 2nd exact match.
2671        let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
2672        let out = getarg("(en.2.i)x", Some(&arr), None, None).expect("Some");
2673        assert_eq!(val_str(out), "3");
2674    }
2675
2676    #[test]
2677    fn getarg_negative_n_flips_search_direction() {
2678        // C params.c:1488-1491 — negative `num` flips down (reverse).
2679        // (en.-1.) on forward-default search matches from the end.
2680        let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into()];
2681        let out = getarg("(en.-1.i)a", Some(&arr), None, None).expect("Some");
2682        assert_eq!(val_str(out), "3");
2683    }
2684
2685    #[test]
2686    fn getarg_n_flag_zero_treated_as_one() {
2687        // C params.c:1438-1439 — `if (!num) num = 1`.
2688        let arr: Vec<String> = vec!["x".into(), "y".into()];
2689        let out = getarg("(en.0.r)x", Some(&arr), None, None).expect("Some");
2690        assert_eq!(val_str(out), "x");
2691    }
2692
2693    #[test]
2694    fn getarg_unknown_flag_char_returns_none() {
2695        // C params.c:1477-1483 flagerr — invalid flag char reports error.
2696        let arr: Vec<String> = vec!["x".into()];
2697        assert!(getarg("(z)x", Some(&arr), None, None).is_none());
2698    }
2699
2700    #[test]
2701    fn getarg_n_flag_unterminated_arg_returns_none() {
2702        // (n.5 missing closing delimiter — flagerr.
2703        let arr: Vec<String> = vec!["x".into()];
2704        assert!(getarg("(n.5", Some(&arr), None, None).is_none());
2705    }
2706
2707    #[test]
2708    fn getarg_b_flag_starts_search_at_index() {
2709        // C params.c:1748-1760 — `(b.N.e)pat` skips first N-1 elements
2710        // forward (parsed value `N`, normalized to `beg = N-1`).
2711        let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
2712        // Forward, beg=2 (skip first 2) → starts at idx 2 → 'x' at 3.
2713        let out = getarg("(b.3.ei)x", Some(&arr), None, None).expect("Some");
2714        assert_eq!(val_str(out), "3");
2715    }
2716
2717    #[test]
2718    fn getarg_b_flag_with_R_reverse_from_offset() {
2719        // C params.c:1750-1755 — reverse search starting at parsed-1 idx.
2720        // arr=(x y x y), beg=2 (parsed 3-1), reverse → walks 2,1,0; first
2721        // exact 'x' is at idx 2 → 1-based "3".
2722        let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
2723        let out = getarg("(b.3.eIR)x", Some(&arr), None, None).expect("Some");
2724        assert_eq!(val_str(out), "3");
2725    }
2726
2727    #[test]
2728    fn getarg_b_flag_out_of_bounds_forward_returns_empty() {
2729        // c:1746 — beg >= len returns len+1 (empty for value-mode).
2730        let arr: Vec<String> = vec!["x".into()];
2731        let out = getarg("(b.5.er)x", Some(&arr), None, None).expect("Some");
2732        assert_eq!(val_str(out), "");
2733    }
2734
2735    #[test]
2736    fn getarg_b_flag_out_of_bounds_index_mode_returns_len_plus_one() {
2737        let arr: Vec<String> = vec!["x".into(), "y".into()];
2738        let out = getarg("(b.5.ei)x", Some(&arr), None, None).expect("Some");
2739        assert_eq!(val_str(out), "3");
2740    }
2741
2742    #[test]
2743    fn getarg_hash_neg_num_on_lowercase_r_returns_all() {
2744        // C params.c:1488-1491 — neg `num` flips down on `r`,
2745        // converting hash search to return-all-matches semantics.
2746        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2747        h.insert("a".into(), "1".into());
2748        h.insert("b".into(), "1".into());
2749        h.insert("c".into(), "2".into());
2750        let out = getarg("(en.-1.r)1", None, Some(&h), None).expect("Some");
2751        // r + neg = R semantics → all values where pat matches value.
2752        assert_eq!(val_str(out), "1 1");
2753    }
2754
2755    #[test]
2756    fn getarg_hash_neg_num_on_uppercase_R_returns_single() {
2757        // R + neg `num` un-flips back to single-match (r semantics).
2758        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2759        h.insert("a".into(), "1".into());
2760        h.insert("b".into(), "1".into());
2761        h.insert("c".into(), "2".into());
2762        let out = getarg("(en.-1.R)1", None, Some(&h), None).expect("Some");
2763        // R + neg → r → single first match.
2764        assert_eq!(val_str(out), "1");
2765    }
2766
2767    #[test]
2768    fn getarg_hash_b_flag_skips_first_n_entries() {
2769        // C params.c:1740-1742 — `b<NUM>` skips first N-1 entries
2770        // before searching. Hash iteration is insertion order.
2771        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2772        h.insert("a".into(), "1".into());
2773        h.insert("b".into(), "1".into());
2774        h.insert("c".into(), "1".into());
2775        // beg=2 (parsed 3-1) → skip first 2, scan from "c" onward.
2776        let out = getarg("(b.3.ei)1", None, Some(&h), None).expect("Some");
2777        assert_eq!(val_str(out), "c");
2778    }
2779
2780    #[test]
2781    fn getarg_hash_b_flag_with_R_collects_from_offset() {
2782        // R returns all matches; b skips first beg entries first.
2783        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2784        h.insert("a".into(), "1".into());
2785        h.insert("b".into(), "1".into());
2786        h.insert("c".into(), "1".into());
2787        let out = getarg("(b.2.eI)1", None, Some(&h), None).expect("Some");
2788        // beg=1, return_all=I → walk from "b" onward, all matching keys.
2789        assert_eq!(val_str(out), "b c");
2790    }
2791
2792    #[test]
2793    fn getarg_hash_b_flag_out_of_bounds_returns_empty() {
2794        // c:1746 — beg >= len with single-match → empty.
2795        let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2796        h.insert("a".into(), "1".into());
2797        let out = getarg("(b.5.e)1", None, Some(&h), None).expect("Some");
2798        assert_eq!(val_str(out), "");
2799    }
2800
2801    #[test]
2802    fn getarg_w_flag_splits_multi_word_array_elements() {
2803        // C params.c:1761-1797 — `(w)N` joins array then re-splits by
2804        // IFS-default whitespace. arr=("a b" "c d"); (w)2 → "b" not "c d".
2805        let arr: Vec<String> = vec!["a b".into(), "c d".into()];
2806        let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
2807        assert_eq!(val_str(out), "b");
2808    }
2809
2810    #[test]
2811    fn getarg_w_flag_simple_array_indexing_still_works() {
2812        let arr: Vec<String> = vec!["one".into(), "two".into(), "three".into()];
2813        let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
2814        assert_eq!(val_str(out), "two");
2815    }
2816
2817    #[test]
2818    fn getarg_f_flag_splits_by_newline() {
2819        // C params.c:1424-1427 — `f` flag aliases `w` with sep="\n".
2820        // arr=("a b\nc d"); (f)2 → "c d" (split by \n only, not space).
2821        let arr: Vec<String> = vec!["a b\nc d".into()];
2822        let out = getarg("(f)2", Some(&arr), None, None).expect("Some");
2823        assert_eq!(val_str(out), "c d");
2824    }
2825
2826    #[test]
2827    fn getarg_scalar_w_flag_picks_nth_word() {
2828        // C params.c:1761-1797 — scalar word-mode arm. `(w)2` on
2829        // scalar "hello world foo" returns the 2nd whitespace word.
2830        let out = getarg("(w)2", None, None, Some("hello world foo")).expect("Some");
2831        assert_eq!(val_str(out), "world");
2832    }
2833
2834    #[test]
2835    fn getarg_scalar_w_flag_negative_index_counts_from_end() {
2836        let out = getarg("(w)-1", None, None, Some("alpha beta gamma")).expect("Some");
2837        assert_eq!(val_str(out), "gamma");
2838    }
2839
2840    #[test]
2841    fn getarg_scalar_re_returns_char_at_match_position() {
2842        // C params.c:1798-1980 — char-search returns CHAR at match
2843        // position, not full substring. Verified empirically:
2844        //   /bin/zsh -c 's="barfooxyz"; print "${s[(r)foo]}"'  → "f"
2845        let out = getarg("(re)bc", None, None, Some("abcdef")).expect("Some");
2846        assert_eq!(val_str(out), "b");
2847    }
2848
2849    #[test]
2850    fn getarg_scalar_ie_returns_position_of_first_match() {
2851        let out = getarg("(ie)cd", None, None, Some("abcdef")).expect("Some");
2852        // 'cd' starts at 1-based position 3.
2853        assert_eq!(val_str(out), "3");
2854    }
2855
2856    #[test]
2857    fn getarg_scalar_Ie_returns_position_of_last_match() {
2858        let out = getarg("(Ie)b", None, None, Some("abcabc")).expect("Some");
2859        // Last 'b' is at 1-based position 5.
2860        assert_eq!(val_str(out), "5");
2861    }
2862
2863    #[test]
2864    fn getarg_scalar_ie_no_match_returns_len_plus_one() {
2865        let out = getarg("(ie)z", None, None, Some("abc")).expect("Some");
2866        assert_eq!(val_str(out), "4");
2867    }
2868
2869    #[test]
2870    fn getarg_scalar_Ie_no_match_returns_zero() {
2871        let out = getarg("(Ie)z", None, None, Some("abc")).expect("Some");
2872        assert_eq!(val_str(out), "0");
2873    }
2874
2875    #[test]
2876    fn getarg_scalar_n_flag_picks_second_match() {
2877        // C params.c:1929/1964 — `!--num` Nth-match counter on
2878        // scalar char-search. abcabc: 'a' at idx 0 and 3 → 2nd match
2879        // at byte position 4 (1-based).
2880        let out = getarg("(en.2.i)a", None, None, Some("abcabc")).expect("Some");
2881        assert_eq!(val_str(out), "4");
2882    }
2883
2884    #[test]
2885    fn getarg_scalar_b_flag_starts_from_offset() {
2886        // C params.c:1740-1742 — `(b.N.)` starts search from idx N-1.
2887        // abc bc abc: with b=4, skip first 3 chars; first 'b' at byte 5.
2888        let out = getarg("(b.4.ei)b", None, None, Some("abcbc")).expect("Some");
2889        assert_eq!(val_str(out), "4");
2890    }
2891
2892    #[test]
2893    fn getarg_scalar_re_n2_picks_second_substring() {
2894        let out = getarg("(en.2.r)b", None, None, Some("abab")).expect("Some");
2895        assert_eq!(val_str(out), "b");
2896    }
2897}
2898
2899// ===========================================================
2900// Methods moved verbatim from src/ported/exec.rs because their
2901// C counterpart's source file maps 1:1 to this Rust module.
2902// Phase: params
2903// ===========================================================
2904
2905// BEGIN moved-from-exec-rs
2906// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
2907
2908// END moved-from-exec-rs
2909
2910// ===========================================================
2911// Free fns moved verbatim from src/ported/exec.rs.
2912// ===========================================================
2913// BEGIN moved-from-exec-rs (free fns)
2914/// Subscript-argument result.
2915///
2916/// `Flags` carries the parsed flag chars and the remaining subscript
2917/// text (the pattern after `(...)`); the caller dispatches the
2918/// search itself. `Value` is the result of an in-getarg array/hash
2919/// pattern search — direct port of getarg's pprog/pattry arm at
2920/// Src/params.c:1672-1719 (array) and 1581-1660 (hash).
2921// `enum getarg_out` is a Rust extension to express the dual-mode
2922// return of `getarg`. C `getarg` (`Src/params.c:1367`) writes back
2923// via out-pointers (`int *inv`, `Value v`, `zlong *w`, ...) and
2924// returns `int`. The Rust port collapses those into one sum-typed
2925// return: `Flags` carries the parsed flag chars + remaining
2926// subscript when no search ran; `Value` carries the search result
2927// from the pprog/pattry arms at c:1581-1660 (hash) / c:1672-1719
2928// (array). Naming kept lowercase to mark this as a port-shape helper
2929// rather than a C-mirrored struct.
2930#[allow(non_camel_case_types)]
2931pub enum getarg_out<'a> {
2932    Flags { flags: &'a str, rest: &'a str },
2933    Value(fusevm::Value),
2934}
2935
2936/// Subscript-argument parser.
2937///
2938/// 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
2939/// 618-line monolith handling the entire `[...]` body of a
2940/// subscripted parameter expansion.
2941///
2942/// Ported phases:
2943///   - Flag-block parse (c:1389-1480) — extract `(...)` chars.
2944///   - Hash pattern search (c:1581-1660) when `assoc` is `Some`.
2945///   - Array pattern search (c:1672-1719) when `arr` is `Some`.
2946///   - Scalar word-mode arm (c:1761-1797) when `scalar` is `Some`.
2947///
2948/// Later C phases not yet exercised by this entry point:
2949///   - Brace-depth walk to closing `]` (c:1507-1535)
2950///   - parsestr + singsub on subscript body (c:1545-1580)
2951///   - mathevalarg integer parse (c:1601-1604)
2952///   - Multibyte char-search arm (c:1798-1985)
2953pub(crate) fn getarg<'a>(
2954    idx: &'a str,
2955    arr: Option<&[String]>,
2956    assoc: Option<&indexmap::IndexMap<String, String>>,
2957    scalar: Option<&str>,
2958) -> Option<getarg_out<'a>> {
2959    let rest = idx.strip_prefix('(')?;
2960    // Reject anything that looks like a char-class subscript: `[abc]`
2961    // doesn't match this prefix, but `(...)` containing brackets is
2962    // probably alternation — let it fall through to runtime instead.
2963    if rest.starts_with(')') || rest.contains('[') {
2964        return None;
2965    }
2966    // Flag scanner per zshparam(1) "Subscript Flags" /
2967    // params.c:1389-1480 switch:
2968    //   r/R (reverse value-search → value/all values),
2969    //   i/I (value-search → key/all keys),
2970    //   k/K (key-search → value/all values),
2971    //   e (exact match — disables glob),
2972    //   n<DELIM>NUM<DELIM> (Nth match — params.c:1431-1442),
2973    //   b<DELIM>NUM<DELIM> (begin offset — params.c:1443-1454),
2974    //   w (word index on scalar),
2975    //   f (word index split by newline; alias for `w` + sep="\n"),
2976    //   p (escapes for next get_strarg),
2977    //   s<DELIM>SEP<DELIM> (split-by-separator).
2978    // The `n` / `b` / `s` forms use `get_strarg`'s balanced-delimiter
2979    // pair: any non-flag char closes its pair (`(n.5.)`, `(n:5:)` etc.).
2980    let bytes = rest.as_bytes();
2981    let mut i: usize = 0;
2982    let mut num: i64 = 1;
2983    let mut beg: i64 = 0;
2984    let mut has_beg = false;
2985    let flags_start = 0_usize;
2986    let mut flags_end = 0_usize;
2987    let mut bad = false;
2988    while i < bytes.len() && bytes[i] != b')' {
2989        let c = bytes[i] as char;
2990        match c {
2991            'r' | 'R' | 'i' | 'I' | 'e' | 'k' | 'K' | 'w' | 'f' | 'p' => {
2992                i += 1;
2993                flags_end = i;
2994            }
2995            'n' | 'b' => {
2996                // Consume `n<DELIM>NUM<DELIM>` per c:1432 get_strarg.
2997                if i + 1 >= bytes.len() {
2998                    bad = true;
2999                    break;
3000                }
3001                let delim = bytes[i + 1];
3002                let arg_start = i + 2;
3003                let mut arg_end = arg_start;
3004                while arg_end < bytes.len() && bytes[arg_end] != delim {
3005                    arg_end += 1;
3006                }
3007                if arg_end >= bytes.len() {
3008                    bad = true;
3009                    break;
3010                }
3011                // Parse the argument as a signed decimal integer.
3012                let arg = std::str::from_utf8(&bytes[arg_start..arg_end]).ok()?;
3013                let parsed: i64 = arg.trim().parse().ok()?;
3014                if c == 'n' {
3015                    num = if parsed == 0 { 1 } else { parsed };
3016                } else {
3017                    has_beg = true;
3018                    beg = if parsed > 0 { parsed - 1 } else { parsed };
3019                }
3020                i = arg_end + 1;
3021                flags_end = i;
3022            }
3023            's' => {
3024                // (s:SEP:) — pass through with raw flag block.
3025                let close = match rest[i..].find(')') {
3026                    Some(p) => i + p,
3027                    None => return None,
3028                };
3029                let flags = &rest[flags_start..close];
3030                return Some(getarg_out::Flags { flags, rest: &rest[close + 1..] });
3031            }
3032            _ => {
3033                bad = true;
3034                break;
3035            }
3036        }
3037    }
3038    // c:1477-1483 — flag-error fallback: reset all flags, treat as no
3039    // subscript flags.
3040    if bad {
3041        return None;
3042    }
3043    if i >= bytes.len() || bytes[i] != b')' {
3044        return None;
3045    }
3046    if flags_end == flags_start {
3047        return None;
3048    }
3049    let flags = &rest[flags_start..flags_end];
3050    let pat = &rest[i + 1..];
3051
3052    // c:1488-1491 — negative `num` flips the search direction.
3053    let neg_num_flips = num < 0;
3054    if neg_num_flips {
3055        num = -num;
3056    }
3057
3058    // Phase 3 — hash pattern search arm (c:1581-1660 / 1672-1734).
3059    // Per C source case-arms:
3060    //   `r`: rev=1 → match against VALUES, return matching VALUE
3061    //   `R`: rev+down=1 → match VALUES, return ALL matching VALUEs
3062    //   `i`: rev+ind=1 → match VALUES, return KEY of first match
3063    //   `I`: rev+ind+down=1 → match VALUES, return ALL matching KEYs
3064    //   `k`: keymatch+rev=1 → match KEYS, return VALUE of first match
3065    //   `K`: keymatch+rev+down=1 → match KEYS, return ALL matching VALUEs
3066    if let Some(map) = assoc {
3067        let exact = flags.contains('e');
3068        let key_match = flags.contains('k') || flags.contains('K');
3069        let return_index = flags.contains('i') || flags.contains('I');
3070        // C params.c:1488-1491 — negative `num` flips `down`. Since
3071        // R/I/K already set down=1, neg_num XORs the bit (r/i/k +
3072        // neg → return_all; R/I/K + neg → single-match again).
3073        let is_uppercase = flags.contains('I') || flags.contains('R') || flags.contains('K');
3074        let return_all = is_uppercase ^ neg_num_flips;
3075
3076        // c:1740-1747 — `b<NUM>` start offset on the values array. The
3077        // hash is iterated in insertion order (IndexMap); skip first
3078        // `beg` entries before counting matches.
3079        let len = map.len() as i64;
3080        let mut start = beg;
3081        if start < 0 {
3082            start += len;
3083        }
3084        if !return_all && start >= len {
3085            return Some(getarg_out::Value(Value::str("")));
3086        }
3087        let skip = if start < 0 { 0 } else { start as usize };
3088
3089        // Per C params.c:1707-1709 + zsh 5.9 empirical:
3090        //   k/K — keymatch path: pprog=NULL, no glob; exact key
3091        //         lookup. `(K)*` returns "" because there's no key
3092        //         literally named "*".
3093        //   r/R/i/I — value path: pprog=patcompile, glob/exact.
3094        let key_compare = |target: &str| -> bool {
3095            if key_match {
3096                target == pat
3097            } else if exact {
3098                target == pat
3099            } else {
3100                crate::ported::pattern::patmatch(pat, target)
3101            }
3102        };
3103        if return_all {
3104            let mut out: Vec<String> = Vec::new();
3105            for (k, v) in map.iter().skip(skip) {
3106                let target = if key_match { k.as_str() } else { v.as_str() };
3107                if key_compare(target) {
3108                    // `K` (key-match) returns VALUE; `I` (value-match+ind)
3109                    // returns KEY; `R` (value-match) returns VALUE.
3110                    out.push(if key_match {
3111                        v.clone()
3112                    } else if return_index {
3113                        k.clone()
3114                    } else {
3115                        v.clone()
3116                    });
3117                }
3118            }
3119            return Some(getarg_out::Value(Value::str(out.join(" "))));
3120        }
3121        // c:1753 — `!--num` skips matches until the Nth.
3122        let mut remaining = num;
3123        for (k, v) in map.iter().skip(skip) {
3124            let target = if key_match { k.as_str() } else { v.as_str() };
3125            if key_compare(target) {
3126                remaining -= 1;
3127                if remaining == 0 {
3128                    return Some(getarg_out::Value(Value::str(if key_match {
3129                        v.clone()
3130                    } else if return_index {
3131                        k.clone()
3132                    } else {
3133                        v.clone()
3134                    })));
3135                }
3136            }
3137        }
3138        return Some(getarg_out::Value(Value::str("")));
3139    }
3140
3141    // Phase 2 — array pattern search arm (c:1672-1719). The C body
3142    // does `pprog = patcompile(s, 0, NULL)` then forward/reverse
3143    // `for (r = 1 + beg, p = ta + beg; *p; r++, p++) if (pprog &&
3144    // pattry(pprog, *p)) return r`.
3145    if let Some(arr) = arr {
3146        // C params.c:1761-1797 — `(w)N` / `(f)N` word-mode arm.
3147        // `getstrvalue(v)` joins the array; `sepsplit` re-splits by
3148        // sep (`f` → "\n", `w` → IFS-default whitespace, `s:SEP:`
3149        // → user sep), then the Nth split word is returned. So
3150        // `arr=("a b" "c d"); ${arr[(w)2]}` → "b" (joined "a b c d",
3151        // split → ["a","b","c","d"], pick idx 1).
3152        if flags.contains('w') || flags.contains('f') {
3153            if let Ok(n) = pat.parse::<i64>() {
3154                let sep_chars: &[char] = if flags.contains('f') {
3155                    &['\n']
3156                } else {
3157                    &[' ', '\t', '\n']
3158                };
3159                let joined = arr.join(" ");
3160                let words: Vec<&str> = joined
3161                    .split(|c: char| sep_chars.contains(&c))
3162                    .filter(|w| !w.is_empty())
3163                    .collect();
3164                let len = words.len() as i64;
3165                let idx_into = if n > 0 {
3166                    (n - 1) as usize
3167                } else if n < 0 {
3168                    let off = len + n;
3169                    if off < 0 {
3170                        return Some(getarg_out::Value(Value::str("")));
3171                    }
3172                    off as usize
3173                } else {
3174                    return Some(getarg_out::Value(Value::str("")));
3175                };
3176                return Some(getarg_out::Value(
3177                    Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default())
3178                ));
3179            }
3180        }
3181        let exact = flags.contains('e');
3182        let word = flags.contains('w') || flags.contains('f');
3183        let _ = word;
3184        let return_index = flags.contains('i') || flags.contains('I');
3185        // C params.c:1575 `if (!rev)` — without a direction flag
3186        // (r/R/i/I/k/K), getarg does NOT enter the search loop on
3187        // arrays; pat is mathevalarg'd as an integer index instead.
3188        // Verified empirically: `arr=(foo bar); ${arr[(e)foo]}`
3189        // returns empty in real zsh (mathevalarg fails, no element).
3190        let any_search_flag = flags.contains('r')
3191            || flags.contains('R')
3192            || flags.contains('i')
3193            || flags.contains('I')
3194            || flags.contains('k')
3195            || flags.contains('K');
3196        if !any_search_flag {
3197            return None;
3198        }
3199        // c:1488-1491 — negative `num` flips reverse direction.
3200        let reverse = (flags.contains('R') || flags.contains('I')) ^ neg_num_flips;
3201        // C params.c:1668-1685 implicit `*` wrap fires only when
3202        // `v->scanflags` is unset; in standard subscript callsites
3203        // scanflags IS set, so the wrap does NOT engage. Verified
3204        // empirically: `arr=(foobar baz); ${arr[(r)foo]}` returns
3205        // empty in real zsh (exact match), not "foobar". Pattern is
3206        // used verbatim — globbing only when user supplies `*`.
3207        let pat_used: &str = pat;
3208
3209        // c:1740-1760 — `b<NUM>` starting offset + bounds checks.
3210        // beg is already 0-based after parse (parsed-1 for positive).
3211        let len = arr.len() as i64;
3212        let mut start = beg;
3213        if start < 0 {
3214            start += len;
3215        }
3216        // c:1743-1747 — out-of-bounds returns.
3217        if reverse {
3218            if start < 0 {
3219                return Some(getarg_out::Value(if return_index {
3220                    Value::str("0")
3221                } else {
3222                    Value::str("")
3223                }));
3224            }
3225        } else if start >= len {
3226            return Some(getarg_out::Value(if return_index {
3227                Value::str((arr.len() + 1).to_string())
3228            } else {
3229                Value::str("")
3230            }));
3231        }
3232        // c:1750-1751 — reverse w/o explicit b starts from len-1.
3233        if reverse && !has_beg {
3234            start = len - 1;
3235        }
3236
3237        let iter: Box<dyn Iterator<Item = (usize, &String)>> = if reverse {
3238            // c:1752 — `for (p = ta + beg; p >= ta; p--)`: clamp start
3239            // into the valid range then walk backwards.
3240            let s_idx = if start < 0 { 0 } else { start as usize };
3241            let s_idx = s_idx.min(arr.len().saturating_sub(1));
3242            Box::new(arr[..=s_idx].iter().enumerate().rev())
3243        } else {
3244            // c:1757 — `for (p = ta + beg; *p; p++)`: skip first beg.
3245            let s_idx = if start < 0 { 0 } else { start as usize };
3246            Box::new(arr.iter().enumerate().skip(s_idx))
3247        };
3248        // c:1758 — `!--num` skips matches until the Nth.
3249        let mut remaining = num;
3250        for (i, s) in iter {
3251            let hit = if exact {
3252                s == pat
3253            } else {
3254                crate::ported::pattern::patmatch(pat_used, s)
3255            };
3256            if hit {
3257                remaining -= 1;
3258                if remaining == 0 {
3259                    return Some(getarg_out::Value(if return_index {
3260                        Value::str((i + 1).to_string())
3261                    } else {
3262                        Value::str(s.clone())
3263                    }));
3264                }
3265            }
3266        }
3267        return Some(getarg_out::Value(if return_index {
3268            // zsh: `i` returns len+1 if not found, `I` returns 0.
3269            if flags.contains('I') {
3270                Value::str("0")
3271            } else {
3272                Value::str((arr.len() + 1).to_string())
3273            }
3274        } else {
3275            Value::str("")
3276        }));
3277    }
3278
3279    // C params.c:1761-1797 — scalar word-mode arm. `(w)N` joins
3280    // the source string and re-splits by sep (whitespace by default
3281    // for `w`, "\n" for `f`). When `pat` is a numeric N, the Nth
3282    // word is returned. Pattern-search variants on scalars share
3283    // the c:1798-1980 char-search arm which is not yet ported.
3284    if let Some(s) = scalar {
3285        if flags.contains('w') || flags.contains('f') {
3286            if let Ok(n) = pat.parse::<i64>() {
3287                let sep_chars: &[char] = if flags.contains('f') {
3288                    &['\n']
3289                } else {
3290                    &[' ', '\t', '\n']
3291                };
3292                let words: Vec<&str> = s
3293                    .split(|c: char| sep_chars.contains(&c))
3294                    .filter(|w| !w.is_empty())
3295                    .collect();
3296                let len = words.len() as i64;
3297                let idx_into = if n > 0 {
3298                    (n - 1) as usize
3299                } else if n < 0 {
3300                    let off = len + n;
3301                    if off < 0 {
3302                        return Some(getarg_out::Value(Value::str("")));
3303                    }
3304                    off as usize
3305                } else {
3306                    return Some(getarg_out::Value(Value::str("")));
3307                };
3308                return Some(getarg_out::Value(
3309                    Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default()),
3310                ));
3311            }
3312        }
3313        // C params.c:1798-1980 — scalar char-search arm. `(i)/(I)/
3314        // (r)/(R)` on a scalar runs a sliding-window glob match.
3315        // (i)/(I) return the 1-based byte position of first/last
3316        // match; (r)/(R) return the matched substring.
3317        // Multibyte cursor outputs (prevcharlen/nextcharlen at
3318        // c:1948-1971) are not yet ported; ASCII-only path here.
3319        let any_search = flags.contains('r')
3320            || flags.contains('R')
3321            || flags.contains('i')
3322            || flags.contains('I');
3323        if any_search {
3324            let return_index = flags.contains('i') || flags.contains('I');
3325            let want_last = flags.contains('I') || flags.contains('R');
3326            // Negative `num` flips direction (c:1488-1491).
3327            let want_last = want_last ^ neg_num_flips;
3328            let s_chars: Vec<char> = s.chars().collect();
3329            let n = s_chars.len();
3330            let positions: Box<dyn Iterator<Item = usize>> = if want_last {
3331                Box::new((0..=n).rev())
3332            } else {
3333                Box::new(0..=n)
3334            };
3335            // c:1929+ / c:1964 — `!--num` skips matches until the Nth.
3336            // Per `b<NUM>` (c:1740-1747) — start from offset, only
3337            // when has_beg is set. Without `b`, walk all positions.
3338            let beg_idx_opt: Option<usize> = if has_beg {
3339                let beg_norm = if beg < 0 { beg + n as i64 } else { beg };
3340                Some(if beg_norm < 0 {
3341                    0
3342                } else {
3343                    (beg_norm as usize).min(n)
3344                })
3345            } else {
3346                None
3347            };
3348            let mut found: Option<(usize, usize)> = None;
3349            let mut remaining = num;
3350            'outer: for start in positions {
3351                if let Some(b_idx) = beg_idx_opt {
3352                    if want_last {
3353                        if start > b_idx {
3354                            continue;
3355                        }
3356                    } else if start < b_idx {
3357                        continue;
3358                    }
3359                }
3360                for span_len in 1..=(n - start) {
3361                    let cand: String = s_chars[start..start + span_len].iter().collect();
3362                    let hit = if flags.contains('e') {
3363                        cand == pat
3364                    } else {
3365                        crate::ported::pattern::patmatch(pat, &cand)
3366                    };
3367                    if hit {
3368                        remaining -= 1;
3369                        if remaining == 0 {
3370                            found = Some((start, start + span_len));
3371                            break 'outer;
3372                        }
3373                        // Advance past this match position to find the
3374                        // next-Nth instead of repeatedly matching same
3375                        // start (mirrors C's pointer increment).
3376                        break;
3377                    }
3378                }
3379            }
3380            return Some(getarg_out::Value(match (found, return_index) {
3381                (Some((s_pos, _)), true) => Value::str((s_pos + 1).to_string()),
3382                // C params.c:1798-1980 char-search returns the char AT
3383                // the match position, not the full matched substring.
3384                // Verified empirically: `s="barfooxyz"; ${s[(r)foo]}`
3385                // returns "f" in real zsh, not "foo".
3386                (Some((s_pos, _)), false) => Value::str(
3387                    s_chars.get(s_pos).map(|c| c.to_string()).unwrap_or_default(),
3388                ),
3389                (None, true) => Value::str(if flags.contains('i') {
3390                    (n + 1).to_string()
3391                } else {
3392                    "0".to_string()
3393                }),
3394                (None, false) => Value::str(String::new()),
3395            }));
3396        }
3397    }
3398
3399    // No search context — return parsed flags for caller dispatch.
3400    Some(getarg_out::Flags { flags, rest: pat })
3401}
3402
3403// `VarAttr` struct + `VarKind` enum + `impl VarAttr::format_zsh`
3404// DELETED. C zsh stores typeset attributes as bare `PM_*` bit
3405// flags on `Param.node.flags` (`Src/zsh.h` PM_* + `Src/params.c`
3406// flag tests); the `${(t)var}` flag report (`typeprintparam` at
3407// `Src/builtin.c:3050+`) writes those bits to a string directly
3408// against the `Param.node.flags` int.
3409//
3410// Both types had zero external use sites — pure dead-code carryover
3411// from an earlier exec.rs refactor. The PM_* bit constants are at
3412// `zsh_h.rs:1340+` and the `(t)` formatting routes through
3413// `typeset_print_flags` (when wired) reading bare `Param.node.flags`.
3414
3415// ===========================================================
3416// Special-parameter GSU (get/set/unset) callbacks ported from
3417// Src/params.c.
3418//
3419// C zsh stores per-special-param state in file-static globals
3420// (`ifs`, `home`, `term`, `histsiz`, etc.) and dispatches getfn/
3421// setfn/unsetfn callbacks through `Param.gsu->getfn` etc. zshrs's
3422// param storage is per-evaluator HashMaps on `ShellExecutor`, so
3423// the C globals are reproduced as `OnceLock<Mutex<…>>` module
3424// statics here, with the get/set fns mutating the static.
3425//
3426// Functions that genuinely need a `Param *` (the GSU dispatch
3427// callbacks for non-special arr/hash/int/float/str params, the
3428// param-table mutators, scope helpers, etc.) cannot be properly
3429// ported until zshrs gains a Param struct + callback-table ABI;
3430// those keep their C signatures but the body is a WARNING-stub
3431// that does nothing.
3432// ===========================================================
3433
3434use std::sync::{Arc, Mutex, OnceLock, RwLock};
3435use std::time::Duration;
3436use crate::config_h::DEFAULT_TMPPREFIX;
3437use crate::zsh_h::{paramdef, ERRFLAG_ERROR, PM_DONTIMPORT, PM_DONTIMPORT_SUID, PM_READONLY_SPECIAL};
3438// -----------------------------------------------------------
3439// Module statics — one per C global referenced by the special-
3440// param callbacks below. All initialised lazily on first read.
3441// -----------------------------------------------------------
3442
3443// `Src/params.c:515  mod_export HashTable paramtab, realparamtab;`
3444//
3445// `realparamtab` always points to the shell's global parameter
3446// table. `paramtab` normally aliases it; it is temporarily
3447// redirected during associative-array key iteration
3448// (`Src/params.c:508-513` — "paramtab is sometimes temporarily
3449// changed to point at another table").
3450//
3451// Per PORT_PLAN.md Phase 3, bucket-2 read-mostly tables use
3452// `RwLock` so parallel readers (every `$VAR` expansion, every
3453// completion lookup) don't serialize. Writers (assignments,
3454// scope pushes/pops, function-local declarations) take the
3455// exclusive write lock. `OnceLock` provides the single-static
3456// guarantee without an `Arc` allocation since the table lives
3457// for the process lifetime.
3458//
3459// Entries are keyed on `node.nam` (the canonical `param` struct
3460// lives in `zsh_h.rs`). The full `HashTable` substrate (vtable
3461// callbacks, intrusive `next` chain, scope-stacked iterators) is
3462// not yet wired; until it is, the typed map is the operative
3463// storage.
3464static PARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
3465    OnceLock::new();
3466static REALPARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
3467    OnceLock::new();
3468
3469/// Accessor for the global `paramtab` (Src/params.c:515).
3470/// Mirrors C's `paramtab->...` dereference by handing back the
3471/// inner RwLock; callers `.read()` for lookups and `.write()` for
3472/// mutation, operating on the `HashMap<String, Param>` directly.
3473pub fn paramtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
3474    PARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
3475}
3476
3477/// Accessor for the global `realparamtab` (Src/params.c:515).
3478/// Same role as `paramtab` for the not-currently-redirected case;
3479/// the alias-flip during assoc-array iteration isn't modelled yet.
3480pub fn realparamtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
3481    REALPARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
3482}
3483
3484fn ifs_lock() -> &'static Mutex<String> {
3485    static IFS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3486    IFS_VAR.get_or_init(|| Mutex::new(" \t\n\0".to_string()))
3487}
3488
3489fn home_lock() -> &'static Mutex<String> {
3490    static HOME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3491    HOME_VAR.get_or_init(|| Mutex::new(env::var("HOME").unwrap_or_default()))
3492}
3493
3494fn term_lock() -> &'static Mutex<String> {
3495    static TERM_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3496    TERM_VAR.get_or_init(|| Mutex::new(env::var("TERM").unwrap_or_default()))
3497}
3498
3499fn wordchars_lock() -> &'static Mutex<String> {
3500    static WORDCHARS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3501    WORDCHARS_VAR.get_or_init(|| Mutex::new("*?_-.[]~=/&;!#$%^(){}<>".to_string()))
3502}
3503
3504fn histchars_lock() -> &'static Mutex<[u8; 3]> {
3505    static HISTCHARS_VAR: OnceLock<Mutex<[u8; 3]>> = OnceLock::new();
3506    HISTCHARS_VAR.get_or_init(|| Mutex::new([b'!', b'^', b'#']))
3507}
3508
3509fn keyboardhack_lock() -> &'static Mutex<u8> {
3510    static KEYBOARDHACK_VAR: OnceLock<Mutex<u8>> = OnceLock::new();
3511    KEYBOARDHACK_VAR.get_or_init(|| Mutex::new(0))
3512}
3513
3514fn histsiz_lock() -> &'static Mutex<i64> {
3515    static HISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
3516    // Match observed `zsh -fc 'echo $HISTSIZE'` output on zsh 5.9+
3517    // (Homebrew). Upstream's `configure.ac` defines DEFAULT_HISTSIZE
3518    // as 30 but distributed binaries seed the cap at 999999999 — the
3519    // parity goal here is "match the binary the user actually runs",
3520    // not "match the source-code default".
3521    HISTSIZ_VAR.get_or_init(|| Mutex::new(999_999_999))
3522}
3523
3524fn savehistsiz_lock() -> &'static Mutex<i64> {
3525    static SAVEHISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
3526    // Same rationale as `histsiz_lock` — observed `zsh -fc
3527    // 'echo $SAVEHIST'` returns 99999999 on zsh 5.9+. Source has
3528    // savehistsiz default to 0 but distributed binaries cap at 99M.
3529    SAVEHISTSIZ_VAR.get_or_init(|| Mutex::new(99_999_999))
3530}
3531
3532fn zsh_terminfo_lock() -> &'static Mutex<String> {
3533    static TERMINFO_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3534    TERMINFO_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO").unwrap_or_default()))
3535}
3536
3537fn zsh_terminfodirs_lock() -> &'static Mutex<String> {
3538    static TERMINFODIRS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3539    TERMINFODIRS_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO_DIRS").unwrap_or_default()))
3540}
3541
3542fn cached_username_lock() -> &'static Mutex<String> {
3543    static USERNAME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3544    USERNAME_VAR.get_or_init(|| Mutex::new(initial_username()))
3545}
3546
3547/// Resolve the current user's name. Mirrors C's `get_username()`
3548/// init at Src/init.c which reads `getpwuid(getuid())->pw_name`
3549/// rather than `$USER`. Falls back to env vars only if the
3550/// passwd lookup fails (rare on real systems).
3551fn initial_username() -> String {
3552    #[cfg(unix)]
3553    {
3554        let uid = unsafe { libc::getuid() };
3555        let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
3556        let mut buf = vec![0i8; 1024];
3557        let mut result: *mut libc::passwd = std::ptr::null_mut();
3558        let rc = unsafe {
3559            libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result)
3560        };
3561        if rc == 0 && !result.is_null() && !pwd.pw_name.is_null() {
3562            let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
3563            return cstr.to_string_lossy().into_owned();
3564        }
3565    }
3566    env::var("USER")
3567        .or_else(|_| env::var("LOGNAME"))
3568        .unwrap_or_default()
3569}
3570
3571fn pipestats_lock() -> &'static Mutex<Vec<i32>> {
3572    static PIPESTATS_VAR: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
3573    PIPESTATS_VAR.get_or_init(|| Mutex::new(Vec::new()))
3574}
3575
3576fn shtimer_lock() -> &'static Mutex<Duration> {
3577    static SHTIMER_VAR: OnceLock<Mutex<Duration>> = OnceLock::new();
3578    SHTIMER_VAR.get_or_init(|| {
3579        Mutex::new(
3580            SystemTime::now()
3581                .duration_since(UNIX_EPOCH)
3582                .unwrap_or_default(),
3583        )
3584    })
3585}
3586
3587fn pparams_lock() -> &'static Mutex<Vec<String>> {
3588    // Mirror of zsh's `pparams` (positional params $1, $2, ...).
3589    // Used by `poundgetfn` for `$#`. The canonical store is
3590    // `builtin::PPARAMS` (Src/init.c `pparams`); set/shift builtins
3591    // write there. Point at that single store so `$#` reads the
3592    // live value instead of an isolated empty mirror.
3593    &crate::ported::builtin::PPARAMS
3594}
3595
3596fn zunderscore_lock() -> &'static Mutex<String> {
3597    static ZUNDERSCORE_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3598    ZUNDERSCORE_VAR.get_or_init(|| Mutex::new(String::new()))
3599}
3600
3601// -----------------------------------------------------------
3602// libc-backed callbacks (UID/GID/EUID/EGID/errno/RANDOM/TTYIDLE).
3603// -----------------------------------------------------------
3604
3605/// Port of `uidgetfn(UNUSED(Param pm))` from `Src/params.c:4689`. C body:
3606/// `return getuid();`
3607/// WARNING: param names don't match C — Rust=() vs C=(pm)
3608pub fn uidgetfn() -> i64 {
3609    unsafe { libc::getuid() as i64 }
3610}
3611
3612/// Port of `uidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4698`. C body:
3613/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
3614/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3615pub fn uidsetfn(x: i64) {
3616    if unsafe { libc::setuid(x as libc::uid_t) } != 0 {
3617        zerr(&format!(
3618            "failed to change user ID: {}",
3619            std::io::Error::last_os_error()
3620        ));
3621    }
3622}
3623
3624/// Port of `euidgetfn(UNUSED(Param pm))` from `Src/params.c:4710`. C body:
3625/// `return geteuid();`
3626/// WARNING: param names don't match C — Rust=() vs C=(pm)
3627pub fn euidgetfn() -> i64 {
3628    unsafe { libc::geteuid() as i64 }
3629}
3630
3631/// Port of `euidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4719`. C body:
3632/// `if (seteuid((uid_t)x)) zerr("failed to change effective user ID: %e", errno);`
3633/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3634pub fn euidsetfn(x: i64) {
3635    if unsafe { libc::seteuid(x as libc::uid_t) } != 0 {
3636        zerr(&format!(
3637            "failed to change effective user ID: {}",
3638            std::io::Error::last_os_error()
3639        ));
3640    }
3641}
3642
3643/// Port of `gidgetfn(UNUSED(Param pm))` from `Src/params.c:4731`. C body: `return getgid();`
3644/// WARNING: param names don't match C — Rust=() vs C=(pm)
3645pub fn gidgetfn() -> i64 {
3646    unsafe { libc::getgid() as i64 }
3647}
3648
3649/// Port of `gidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4740`. C body:
3650/// `if (setgid((gid_t)x)) zerr("failed to change group ID: %e", errno);`
3651/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3652pub fn gidsetfn(x: i64) {
3653    if unsafe { libc::setgid(x as libc::gid_t) } != 0 {
3654        zerr(&format!(
3655            "failed to change group ID: {}",
3656            std::io::Error::last_os_error()
3657        ));
3658    }
3659}
3660
3661/// Port of `egidgetfn(UNUSED(Param pm))` from `Src/params.c:4752`. C body: `return getegid();`
3662/// WARNING: param names don't match C — Rust=() vs C=(pm)
3663pub fn egidgetfn() -> i64 {
3664    unsafe { libc::getegid() as i64 }
3665}
3666
3667/// Port of `egidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4761`. C body:
3668/// `if (setegid((gid_t)x)) zerr("failed to change effective group ID: %e", errno);`
3669/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3670pub fn egidsetfn(x: i64) {
3671    if unsafe { libc::setegid(x as libc::gid_t) } != 0 {
3672        zerr(&format!(
3673            "failed to change effective group ID: {}",
3674            std::io::Error::last_os_error()
3675        ));
3676    }
3677}
3678
3679/// Port of `errnogetfn(UNUSED(Param pm))` from `Src/params.c:5015`. C body: `return errno;`
3680/// WARNING: param names don't match C — Rust=() vs C=(pm)
3681pub fn errnogetfn() -> i64 {
3682    std::io::Error::last_os_error().raw_os_error().unwrap_or(0) as i64
3683}
3684
3685/// Port of `errnosetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:5004`. C body:
3686/// `errno = (int)x; if ((zlong)errno != x) zwarn("errno truncated on assignment");`
3687///
3688/// Rust note: `errno` is a libc thread-local; Rust uses `std::io::Error`
3689/// which captures the *last* call. To set errno for subsequent
3690/// `last_os_error()` reads on macOS / Linux, write through the libc
3691/// `__error()`/`__errno_location()` accessor.
3692/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3693pub fn errnosetfn(x: i64) {
3694    extern "C" {
3695        #[cfg(target_os = "macos")]
3696        fn __error() -> *mut libc::c_int;
3697        #[cfg(target_os = "linux")]
3698        fn __errno_location() -> *mut libc::c_int;
3699    }
3700    let truncated = x as i32;
3701    unsafe {
3702        #[cfg(target_os = "macos")]
3703        {
3704            *__error() = truncated;
3705        }
3706        #[cfg(target_os = "linux")]
3707        {
3708            *__errno_location() = truncated;
3709        }
3710    }
3711    if truncated as i64 != x {
3712        zerr("errno truncated on assignment");
3713    }
3714}
3715
3716/// Port of `randomgetfn(UNUSED(Param pm))` from `Src/params.c:4543`. C body:
3717/// `return rand() & 0x7fff;`
3718/// WARNING: param names don't match C — Rust=() vs C=(pm)
3719pub fn randomgetfn() -> i64 {
3720    (unsafe { libc::rand() } & 0x7fff) as i64
3721}
3722
3723/// Port of `randomsetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4552`. C body:
3724/// `srand((unsigned int)v);`
3725/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
3726pub fn randomsetfn(v: i64) {
3727    unsafe { libc::srand(v as libc::c_uint) };
3728}
3729
3730/// Port of `ttyidlegetfn(UNUSED(Param pm))` from `Src/params.c:4771`. C body:
3731/// ```c
3732/// struct stat ttystat;
3733/// if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;
3734/// return time(NULL) - ttystat.st_atime;
3735/// ```
3736/// Rust port reads stdin (fd 0) — closest match to `SHTTY` the
3737/// shell tracks as the controlling-tty fd. Returns -1 if stdin is
3738/// not a tty.
3739/// WARNING: param names don't match C — Rust=() vs C=(pm)
3740pub fn ttyidlegetfn() -> i64 {
3741    if unsafe { libc::isatty(0) } == 0 {
3742        return -1;
3743    }
3744    let mut st: libc::stat = unsafe { std::mem::zeroed() };
3745    if unsafe { libc::fstat(0, &mut st) } != 0 {
3746        return -1;
3747    }
3748    let now = SystemTime::now()
3749        .duration_since(UNIX_EPOCH)
3750        .unwrap_or_default()
3751        .as_secs() as i64;
3752    now - st.st_atime as i64
3753}
3754
3755// -----------------------------------------------------------
3756// SECONDS / EPOCHSECONDS family — backed by SHTIMER static.
3757// -----------------------------------------------------------
3758
3759/// Port of `intsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4561`. C body:
3760/// `return (zlong)(now.tv_sec - shtimer.tv_sec - …);`
3761/// WARNING: param names don't match C — Rust=() vs C=(pm)
3762pub fn intsecondsgetfn() -> i64 {
3763    let now = SystemTime::now()
3764        .duration_since(UNIX_EPOCH)
3765        .unwrap_or_default();
3766    let timer = *shtimer_lock().lock().expect("shtimer poisoned");
3767    let now_sec = now.as_secs() as i64;
3768    let timer_sec = timer.as_secs() as i64;
3769    let now_nsec = now.subsec_nanos() as i64;
3770    let timer_nsec = timer.subsec_nanos() as i64;
3771    now_sec - timer_sec - i64::from(now_nsec < timer_nsec)
3772}
3773
3774/// Port of `intsecondssetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4575`. C body:
3775/// `shtimer.tv_sec = now.tv_sec - x; shtimer.tv_nsec = now.tv_nsec;`
3776/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3777pub fn intsecondssetfn(x: i64) {
3778    let now = SystemTime::now()
3779        .duration_since(UNIX_EPOCH)
3780        .unwrap_or_default();
3781    let now_sec = now.as_secs() as i64;
3782    let new_sec = now_sec - x;
3783    if new_sec < 0 {
3784        zerr("SECONDS truncated on assignment");
3785        return;
3786    }
3787    *shtimer_lock().lock().expect("shtimer poisoned") =
3788        Duration::new(new_sec as u64, now.subsec_nanos());
3789}
3790
3791/// Port of `floatsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4591`. C body:
3792/// `return (double)(now-tv_sec - shtimer.tv_sec) + nsec/1e9;`
3793/// WARNING: param names don't match C — Rust=() vs C=(pm)
3794pub fn floatsecondsgetfn() -> f64 {
3795    let now = SystemTime::now()
3796        .duration_since(UNIX_EPOCH)
3797        .unwrap_or_default();
3798    let timer = *shtimer_lock().lock().expect("shtimer poisoned");
3799    (now - timer).as_secs_f64()
3800}
3801
3802/// Port of `floatsecondssetfn(UNUSED(Param pm), double x)` from `Src/params.c:4603`. C body:
3803/// `shtimer.tv_sec = now.tv_sec - (zlong)x; shtimer.tv_nsec = now.tv_nsec - (x-int)*1e9;`
3804/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3805pub fn floatsecondssetfn(x: f64) {
3806    let now = SystemTime::now()
3807        .duration_since(UNIX_EPOCH)
3808        .unwrap_or_default();
3809    let new = now.checked_sub(Duration::from_secs_f64(x)).unwrap_or_default();
3810    *shtimer_lock().lock().expect("shtimer poisoned") = new;
3811}
3812
3813/// Port of `getrawseconds()` from `Src/params.c:4615`. C body:
3814/// `return (double)shtimer.tv_sec + (double)shtimer.tv_nsec / 1e9;`
3815pub fn getrawseconds() -> f64 {
3816    shtimer_lock().lock().expect("shtimer poisoned").as_secs_f64()
3817}
3818
3819/// Port of `setrawseconds(double x)` from `Src/params.c:4622`. C body:
3820/// `shtimer.tv_sec = (zlong)x; shtimer.tv_nsec = (x-int)*1e9;`
3821pub fn setrawseconds(x: f64) {
3822    *shtimer_lock().lock().expect("shtimer poisoned") = Duration::from_secs_f64(x);
3823}
3824
3825/// Port of `setsecondstype(Param pm, int on, int off)` from `Src/params.c:4630`. C body
3826/// flips the `gsu.f`/`gsu.i` callback pointer based on the new
3827/// param-flag bitset.
3828///
3829/// WARNING: zshrs has no Param/GSU dispatch table yet — the
3830/// "promotion between integer/float seconds" logic happens via
3831/// pm->gsu pointer swaps in C. Returns 0 to signal success;
3832/// callers can assume the type change is recorded by the caller's
3833/// own bookkeeping until the GSU table lands.
3834/// WARNING: param names don't match C — Rust=(on, off) vs C=(pm, on, off)
3835pub fn setsecondstype(                                                       // c:4630
3836    pm: &mut crate::ported::zsh_h::param,
3837    on: i32,
3838    off: i32,
3839) -> i32 {
3840    // c:4632 — `int newflags = (pm->flags | on) & ~off`.
3841    let newflags = (pm.node.flags | on) & !off;
3842    // c:4633 — `int tp = PM_TYPE(newflags)`.
3843    let tp = PM_TYPE(newflags as u32);
3844    // c:4635-4638 / 4639-4642 — float vs integer GSU pointer swap.
3845    if tp == PM_EFLOAT || tp == PM_FFLOAT {                                  // c:4635
3846        // C: `pm->gsu.f = &floatseconds_gsu`. GSU table not yet
3847        // wired in the Rust port; record the type by clearing
3848        // any integer GSU.
3849        pm.gsu_i = None;
3850        // pm.gsu_f = Some(floatseconds_gsu) — pending GSU port.
3851    } else if tp == PM_INTEGER {                                             // c:4639
3852        // C: `pm->gsu.i = &intseconds_gsu`.
3853        pm.gsu_f = None;
3854        // pm.gsu_i = Some(intseconds_gsu) — pending GSU port.
3855    } else {
3856        return 1;                                                            // c:4644
3857    }
3858    pm.node.flags = newflags;                                                // c:4645
3859    0                                                                        // c:4646
3860}
3861
3862// -----------------------------------------------------------
3863// $0 / $#
3864// -----------------------------------------------------------
3865
3866/// Port of `argzerogetfn(UNUSED(Param pm))` from `Src/params.c:4954`. C body:
3867/// `return isset(POSIXARGZERO) ? posixzero : argzero;`
3868///
3869/// Reads through `crate::ported::utils::argzero()` (the canonical
3870/// OnceLock storage in utils.rs). C's `posixzero` branch is not
3871/// yet ported (POSIXARGZERO option needs the option table).
3872/// WARNING: param names don't match C — Rust=() vs C=(pm)
3873pub fn argzerogetfn() -> String {
3874    crate::ported::utils::argzero().unwrap_or_default()
3875}
3876
3877/// Direct port of `static void argzerosetfn(UNUSED(Param pm),
3878/// char *x)` from `Src/params.c:4937-4946`. Setter for `$0` —
3879/// POSIX mode rejects assignment (read-only), zsh mode replaces
3880/// `argzero`.
3881///
3882/// C body:
3883///   if (x) {
3884///     if (isset(POSIXARGZERO))
3885///       zerr("read-only variable: 0");
3886///     else {
3887///       zsfree(argzero);
3888///       argzero = ztrdup(x);
3889///     }
3890///     zsfree(x);
3891///   }
3892/// Port of `argzerosetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4937`.
3893/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3894pub fn argzerosetfn(x: String) {                                             // c:4937
3895    // c:4937 — if (x).
3896    if !x.is_empty() {
3897        // c:4940 — isset(POSIXARGZERO) reject.
3898        if isset(crate::ported::zsh_h::POSIXARGZERO) {
3899            crate::ported::utils::zerr("read-only variable: 0");             // c:4941
3900        } else {
3901            // c:4943-4944 — zsfree(argzero); argzero = ztrdup(x).
3902            crate::ported::utils::set_argzero(Some(crate::ported::utils::ztrdup(&x)));
3903        }
3904        // c:4946 — `zsfree(x)`. Rust drop handles via move.
3905    }
3906}
3907
3908/// Port of `poundgetfn(UNUSED(Param pm))` from `Src/params.c:4534`. C body:
3909/// `return arrlen(pparams);`
3910/// WARNING: param names don't match C — Rust=() vs C=(pm)
3911pub fn poundgetfn() -> i64 {
3912    pparams_lock().lock().expect("pparams poisoned").len() as i64
3913}
3914
3915// -----------------------------------------------------------
3916// $USERNAME
3917// -----------------------------------------------------------
3918
3919/// Port of `usernamegetfn(UNUSED(Param pm))` from `Src/params.c:4653`. C body:
3920/// `return get_username();`
3921/// WARNING: param names don't match C — Rust=() vs C=(pm)
3922pub fn usernamegetfn() -> String {
3923    cached_username_lock()
3924        .lock()
3925        .expect("username poisoned")
3926        .clone()
3927}
3928
3929/// Port of `usernamesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4662`. C body:
3930/// `getpwnam(x); setgid; setuid; cached_uid = pswd->pw_uid;`
3931///
3932/// WARNING: the SUID-changing path requires getpwnam(3) which
3933/// crosses an unsafe FFI boundary not yet wrapped here. The
3934/// cached-name update is performed; uid/gid changes still need
3935/// porting of the `pwd.h` getpwnam wrapper.
3936/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3937pub fn usernamesetfn(x: String) {                                            // c:4662
3938    // c:4662 — `if (x && (pswd = getpwnam(x)) && pswd->pw_uid != cached_uid)`.
3939    let target = std::ffi::CString::new(x.as_bytes()).ok();
3940    if let Some(cstr) = target {
3941        unsafe {
3942            let pwd = libc::getpwnam(cstr.as_ptr());                         // c:4666
3943            if !pwd.is_null() {
3944                let cached_uid =
3945                    libc::geteuid() as libc::uid_t;
3946                if (*pwd).pw_uid != cached_uid {                             // c:4666
3947                    // c:4670-4672 — initgroups(x, pswd->pw_gid).
3948                    let _ = libc::initgroups(cstr.as_ptr(), (*pwd).pw_gid as _);
3949                    // c:4671 — setgid(pswd->pw_gid).
3950                    if libc::setgid((*pwd).pw_gid) != 0 {                    // c:4673
3951                        crate::ported::utils::zwarn(&format!(
3952                            "failed to change group ID: {}",
3953                            std::io::Error::last_os_error()
3954                        ));
3955                    } else if libc::setuid((*pwd).pw_uid) != 0 {             // c:4675
3956                        // c:4675-4676 — setuid failed.
3957                        crate::ported::utils::zwarn(&format!(
3958                            "failed to change user ID: {}",
3959                            std::io::Error::last_os_error()
3960                        ));
3961                    } else {
3962                        // c:4677-4681 — cache update.
3963                        let name_cstr = std::ffi::CStr::from_ptr((*pwd).pw_name);
3964                        let name_str = name_cstr.to_string_lossy().to_string();
3965                        *cached_username_lock()
3966                            .lock()
3967                            .expect("username poisoned") =
3968                            crate::ported::utils::ztrdup_metafy(&name_str);
3969                    }
3970                }
3971            }
3972        }
3973    }
3974    // c:4683 — `zsfree(x)`; Rust drop handles it.
3975    drop(x);
3976}
3977
3978// -----------------------------------------------------------
3979// $IFS / $HOME / $TERM / $WORDCHARS / $TERMINFO / $TERMINFO_DIRS
3980// $KEYBOARD_HACK / $HISTCHARS / $_  — string-state callbacks.
3981// -----------------------------------------------------------
3982
3983/// Port of `ifsgetfn(UNUSED(Param pm))` from `Src/params.c:4784`. C body: `return ifs;`
3984/// WARNING: param names don't match C — Rust=() vs C=(pm)
3985pub fn ifsgetfn() -> String {
3986    ifs_lock().lock().expect("ifs poisoned").clone()
3987}
3988
3989/// Port of `ifssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4793`. C body:
3990/// `zsfree(ifs); ifs = x; inittyptab();`
3991/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3992pub fn ifssetfn(x: String) {
3993    *ifs_lock().lock().expect("ifs poisoned") = x;
3994    // `inittyptab()` is a no-op in zshrs — Rust char methods
3995    // handle classification natively (utils.rs:1884).
3996}
3997
3998/// Port of `homegetfn(UNUSED(Param pm))` from `Src/params.c:5109`. C body: `return home;`
3999/// WARNING: param names don't match C — Rust=() vs C=(pm)
4000pub fn homegetfn() -> String {
4001    home_lock().lock().expect("home poisoned").clone()
4002}
4003
4004/// Port of `homesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5118`. C body:
4005/// `zsfree(home); home = x ? x : ""; finddir(NULL);`
4006/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4007pub fn homesetfn(x: String) {
4008    *home_lock().lock().expect("home poisoned") = x;
4009    // `finddir(NULL)` invalidates zsh's cached named-directory
4010    // lookups — those don't exist in zshrs yet.
4011}
4012
4013/// Port of `termgetfn(UNUSED(Param pm))` from `Src/params.c:5176`. C body: `return term;`
4014/// WARNING: param names don't match C — Rust=() vs C=(pm)
4015pub fn termgetfn() -> String {
4016    term_lock().lock().expect("term poisoned").clone()
4017}
4018
4019/// Port of `termsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5185`. C body:
4020/// `zsfree(term); term = x ? x : ""; term_reinit_from_pm();`
4021/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4022pub fn termsetfn(x: String) {
4023    *term_lock().lock().expect("term poisoned") = x;
4024    term_reinit_from_pm();
4025}
4026
4027/// Port of `terminfogetfn(UNUSED(Param pm))` from `Src/params.c:5196`. C body:
4028/// `return zsh_terminfo ? zsh_terminfo : "";`
4029/// WARNING: param names don't match C — Rust=() vs C=(pm)
4030pub fn terminfogetfn() -> String {
4031    zsh_terminfo_lock()
4032        .lock()
4033        .expect("zsh_terminfo poisoned")
4034        .clone()
4035}
4036
4037/// Port of `terminfosetfn(Param pm, char *x)` from `Src/params.c:5205`. C body:
4038/// `zsfree(zsh_terminfo); zsh_terminfo = x; addenv if exported; term_reinit_from_pm();`
4039/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4040pub fn terminfosetfn(x: String) {
4041    *zsh_terminfo_lock()
4042        .lock()
4043        .expect("zsh_terminfo poisoned") = x.clone();
4044    env::set_var("TERMINFO", &x);
4045    term_reinit_from_pm();
4046}
4047
4048/// Port of `terminfodirsgetfn(UNUSED(Param pm))` from `Src/params.c:5224`. C body:
4049/// `return zsh_terminfodirs ? zsh_terminfodirs : "";`
4050/// WARNING: param names don't match C — Rust=() vs C=(pm)
4051pub fn terminfodirsgetfn() -> String {
4052    zsh_terminfodirs_lock()
4053        .lock()
4054        .expect("zsh_terminfodirs poisoned")
4055        .clone()
4056}
4057
4058/// Port of `terminfodirssetfn(Param pm, char *x)` from `Src/params.c:5233`. C body
4059/// mirrors `terminfosetfn` for the TERMINFO_DIRS env var.
4060/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4061pub fn terminfodirssetfn(x: String) {
4062    *zsh_terminfodirs_lock()
4063        .lock()
4064        .expect("zsh_terminfodirs poisoned") = x.clone();
4065    env::set_var("TERMINFO_DIRS", &x);
4066    term_reinit_from_pm();
4067}
4068
4069/// Port of `term_reinit_from_pm()` from `Src/params.c:5163`.
4070/// C: `static void term_reinit_from_pm(void)` →
4071///   `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;
4072///    else init_term();`
4073pub fn term_reinit_from_pm() {                                               // c:5163
4074    // c:5167 — `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;`
4075    let interactive = crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
4076    let term = term_lock().lock().map(|s| s.clone()).unwrap_or_default();
4077    if !interactive || term.is_empty() {                                     // c:5167
4078        TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed);                 // c:5168
4079    } else {
4080        // c:5170 — `init_term();` lives in ZLE; flag the next prompt
4081        // to re-init via TERM_UNKNOWN so the lazy path picks it up.
4082        TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed);                 // c:5170
4083    }
4084}
4085
4086// `termflags` from Src/init.c — bitmap of terminal-state flags. Set
4087// from term_reinit_from_pm and consulted by ZLE before first paint.
4088pub static TERMFLAGS: std::sync::atomic::AtomicI32 =
4089    std::sync::atomic::AtomicI32::new(0);
4090// `TERM_UNKNOWN` from Src/zsh.h:1986.
4091pub const TERM_UNKNOWN: i32 = 1 << 0;
4092
4093/// Port of `wordcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5132`. C body:
4094/// `return wordchars;`
4095/// WARNING: param names don't match C — Rust=() vs C=(pm)
4096pub fn wordcharsgetfn() -> String {
4097    wordchars_lock()
4098        .lock()
4099        .expect("wordchars poisoned")
4100        .clone()
4101}
4102
4103/// Port of `wordcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5141`. C body:
4104/// `zsfree(wordchars); wordchars = x; inittyptab();`
4105/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4106pub fn wordcharssetfn(x: String) {
4107    *wordchars_lock().lock().expect("wordchars poisoned") = x;
4108}
4109
4110/// Port of `keyboardhackgetfn(UNUSED(Param pm))` from `Src/params.c:5024`. C body:
4111/// `static char buf[2]; buf[0] = keyboardhackchar; return buf;`
4112/// WARNING: param names don't match C — Rust=() vs C=(pm)
4113pub fn keyboardhackgetfn() -> String {
4114    let c = *keyboardhack_lock()
4115        .lock()
4116        .expect("keyboardhack poisoned");
4117    if c == 0 {
4118        String::new()
4119    } else {
4120        (c as char).to_string()
4121    }
4122}
4123
4124/// Port of `keyboardhacksetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5038`. C body:
4125/// `unmetafy(x, &len); if (len > 1) zwarn("Only one KEYBOARD_HACK character"); …`
4126/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4127pub fn keyboardhacksetfn(x: String) {
4128    let bytes = x.as_bytes();
4129    if bytes.len() > 1 {
4130        zerr("Only one KEYBOARD_HACK character can be defined");
4131    }
4132    let c = bytes.first().copied().unwrap_or(0);
4133    if c >= 0x80 {
4134        zerr("KEYBOARD_HACK can only contain ASCII characters");
4135        return;
4136    }
4137    *keyboardhack_lock().lock().expect("keyboardhack poisoned") = c;
4138}
4139
4140/// Port of `histcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5064`. C body:
4141/// `static char buf[4]; buf[0]=bangchar; buf[1]=hatchar; buf[2]=hashchar;`
4142/// WARNING: param names don't match C — Rust=() vs C=(pm)
4143pub fn histcharsgetfn() -> String {
4144    let chars = *histchars_lock().lock().expect("histchars poisoned");
4145    let mut s = String::new();
4146    for &b in chars.iter() {
4147        if b != 0 {
4148            s.push(b as char);
4149        }
4150    }
4151    s
4152}
4153
4154/// Port of `histcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5079`. C body
4155/// validates ASCII, takes up to 3 chars; defaults `!^#` if NULL.
4156/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4157pub fn histcharssetfn(x: Option<String>) {
4158    match x {
4159        None => {
4160            *histchars_lock().lock().expect("histchars poisoned") = [b'!', b'^', b'#'];
4161        }
4162        Some(s) => {
4163            let bytes = s.as_bytes();
4164            for &b in bytes.iter().take(3) {
4165                if b >= 0x80 {
4166                    zerr("HISTCHARS can only contain ASCII characters");
4167                    return;
4168                }
4169            }
4170            let mut chars = [0u8; 3];
4171            for (i, &b) in bytes.iter().take(3).enumerate() {
4172                chars[i] = b;
4173            }
4174            *histchars_lock().lock().expect("histchars poisoned") = chars;
4175        }
4176    }
4177}
4178
4179/// Update `$_` with the last argument of the just-completed
4180/// command. Mirrors C zsh's writeback in `execcmd_exec` (Src/exec.c)
4181/// where `zunderscore` is set to the last argv slot before
4182/// returning. Callers: every command-dispatch hook in
4183/// fusevm_bridge / exec.rs.
4184pub fn set_zunderscore(argv: &[String]) {
4185    let new = if let Some(last) = argv.last() {
4186        last.clone()
4187    } else {
4188        String::new()
4189    };
4190    *zunderscore_lock()
4191        .lock()
4192        .expect("zunderscore poisoned") = new;
4193}
4194
4195/// Port of `underscoregetfn(UNUSED(Param pm))` from `Src/params.c:5152`. C body:
4196/// `char *u = dupstring(zunderscore); untokenize(u); return u;`
4197/// WARNING: param names don't match C — Rust=() vs C=(pm)
4198pub fn underscoregetfn() -> String {
4199    zunderscore_lock()
4200        .lock()
4201        .expect("zunderscore poisoned")
4202        .clone()
4203}
4204
4205// -----------------------------------------------------------
4206// $HISTSIZE / $SAVEHIST
4207// -----------------------------------------------------------
4208
4209/// Port of `histsizegetfn(UNUSED(Param pm))` from `Src/params.c:4965`. C body: `return histsiz;`
4210/// WARNING: param names don't match C — Rust=() vs C=(pm)
4211pub fn histsizegetfn() -> i64 {
4212    *histsiz_lock().lock().expect("histsiz poisoned")
4213}
4214
4215/// Port of `histsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4974`. C body:
4216/// `if ((histsiz = v) < 1) histsiz = 1; resizehistents();`
4217/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
4218pub fn histsizesetfn(v: i64) {
4219    *histsiz_lock().lock().expect("histsiz poisoned") = v.max(1);
4220    // `resizehistents()` is a hist.c entry point — pending the
4221    // history-table port, the size change is recorded in the
4222    // static and picked up the next time the history layer reads.
4223}
4224
4225/// Port of `savehistsizegetfn(UNUSED(Param pm))` from `Src/params.c:4985`. C body:
4226/// `return savehistsiz;`
4227/// WARNING: param names don't match C — Rust=() vs C=(pm)
4228pub fn savehistsizegetfn() -> i64 {
4229    *savehistsiz_lock().lock().expect("savehistsiz poisoned")
4230}
4231
4232/// Port of `savehistsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4994`. C body:
4233/// `if ((savehistsiz = v) < 0) savehistsiz = 0;`
4234/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
4235pub fn savehistsizesetfn(v: i64) {
4236    *savehistsiz_lock().lock().expect("savehistsiz poisoned") = v.max(0);
4237}
4238
4239// -----------------------------------------------------------
4240// $pipestatus
4241// -----------------------------------------------------------
4242
4243/// Port of `pipestatgetfn(UNUSED(Param pm))` from `Src/params.c:5251`. C body
4244/// snapshots the `pipestats[]` C array as a heap-allocated
4245/// `char **`. Rust port returns the cloned snapshot.
4246/// WARNING: param names don't match C — Rust=() vs C=(pm)
4247pub fn pipestatgetfn() -> Vec<String> {
4248    pipestats_lock()
4249        .lock()
4250        .expect("pipestats poisoned")
4251        .iter()
4252        .map(|n| n.to_string())
4253        .collect()
4254}
4255
4256/// Port of `pipestatsetfn(UNUSED(Param pm), char **x)` from `Src/params.c:5270`. C body:
4257/// `for (i=0; *x && i<MAX_PIPESTATS; i++) pipestats[i] = atoi(*x++); numpipestats = i;`
4258/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4259pub fn pipestatsetfn(x: Option<Vec<String>>) {
4260    const MAX_PIPESTATS: usize = 256;
4261    let mut guard = pipestats_lock().lock().expect("pipestats poisoned");
4262    guard.clear();
4263    if let Some(v) = x {
4264        for s in v.iter().take(MAX_PIPESTATS) {
4265            guard.push(s.parse::<i32>().unwrap_or(0));
4266        }
4267    }
4268}
4269
4270// -----------------------------------------------------------
4271// Locale callbacks: $LANG, $LC_*, setlang
4272// -----------------------------------------------------------
4273
4274/// Port of `clear_mbstate()` from `Src/params.c:4831`. C body:
4275/// `mb_charinit(); clear_shiftstate();`
4276///
4277/// WARNING: zshrs uses Rust's UTF-8 native handling so multibyte
4278/// state machines aren't kept; this is a no-op pinned to the
4279/// C name for parity.
4280/// (under `MULTIBYTE_SUPPORT`):
4281/// ```c
4282/// mb_charinit();        /* utils.c */
4283/// clear_shiftstate();   /* pattern.c */
4284/// ```
4285/// Resets the mbstate_t globals after LC_CTYPE changes (NetBSD-9
4286/// requires this). Rust port forwards to the matching helpers.
4287pub fn clear_mbstate() {
4288    // mb_charinit / clear_shiftstate not yet ported; once they are
4289    // (Src/utils.c, Src/pattern.c) wire the calls here.
4290}
4291
4292/// Port of `setlang(char *x)` from `Src/params.c:4840`. C body:
4293/// `if (LC_ALL set) return; setlocale(LC_ALL, x); for each LC_*: if set, setlocale(category, x);`
4294pub fn setlang(x: Option<&str>) {
4295    if let Ok(lc_all) = env::var("LC_ALL") {
4296        if !lc_all.is_empty() {
4297            return;
4298        }
4299    }
4300    if let Some(s) = x {
4301        env::set_var("LANG", s);
4302    }
4303    clear_mbstate();
4304}
4305
4306/// Port of `langsetfn(Param pm, char *x)` from `Src/params.c:4896`. C body:
4307/// `strsetfn(pm, x); setlang(unmeta(x));`
4308/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4309pub fn langsetfn(x: String) {
4310    setlang(Some(&x));
4311}
4312
4313/// Port of `lc_allsetfn(Param pm, char *x)` from `Src/params.c:4871`. C body
4314/// dispatches to `setlang(LANG)` if x empty, else `setlocale(LC_ALL, x)`.
4315/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4316pub fn lc_allsetfn(x: Option<String>) {
4317    match x {
4318        None => setlang(env::var("LANG").as_deref().ok()),
4319        Some(s) if s.is_empty() => setlang(env::var("LANG").as_deref().ok()),
4320        Some(s) => {
4321            env::set_var("LC_ALL", &s);
4322            clear_mbstate();
4323        }
4324    }
4325}
4326
4327/// Port of `lcsetfn(Param pm, char *x)` from `Src/params.c:4904`. C body:
4328/// per-category `setlocale` with LC_ALL precedence + LANG fallback.
4329pub fn lcsetfn(pm: &str, x: Option<String>) {
4330    if let Ok(lc_all) = env::var("LC_ALL") {
4331        if !lc_all.is_empty() {
4332            return;
4333        }
4334    }
4335    let val = x
4336        .filter(|s| !s.is_empty())
4337        .or_else(|| env::var("LANG").ok().filter(|s| !s.is_empty()));
4338    if let Some(v) = val {
4339        env::set_var(pm, v);
4340    }
4341    clear_mbstate();
4342}
4343
4344// -----------------------------------------------------------
4345// env management (zsh's wrapper around setenv/unsetenv).
4346// -----------------------------------------------------------
4347
4348/// Port of `zgetenv(char *name)` from `Src/params.c:5416`. C body walks
4349/// `environ` byte-by-byte. Rust port uses `std::env::var`.
4350pub fn zgetenv(name: &str) -> Option<String> {
4351    env::var(name).ok()
4352}
4353
4354/// Direct port of `int zputenv(char *str)` from
4355/// `Src/params.c:5325-5382` (USE_SET_UNSET_ENV branch). Splits
4356/// `str` at the first `=`, validates the name is in the portable
4357/// character set (rejects any byte >= 128), and calls
4358/// `setenv(name, value, 1)`.
4359///
4360/// C body walks `str` byte-by-byte looking for either a high-byte
4361/// (reject) or `=` (split). On a clean ASCII `name=value`, it
4362/// temporarily writes `\0` at the `=` to splice off the name,
4363/// calls setenv, then restores the `=`. On `=`-less input, it
4364/// flags via DPUTS and still calls setenv with the whole string
4365/// as the name (with value pointing at the trailing `\0`). Rust
4366/// equivalent: split, set_var; the in-place mutation isn't
4367/// observable since we copy.
4368/// Port of `zputenv(char *str)` from `Src/params.c:5325`.
4369pub fn zputenv(str: &str) -> i32 {                                           // c:5325
4370    if str.is_empty() {
4371        // c:5328 — DPUTS(!str, ...); treat as no-op.
4372        return 0;
4373    }
4374    let bytes = str.as_bytes();
4375    // c:5339-5341 — walk until `=` or high byte; reject high bytes.
4376    let mut ptr = 0;
4377    while ptr < bytes.len() && bytes[ptr] != b'=' && bytes[ptr] < 128 {       // c:5339
4378        ptr += 1;
4379    }
4380    if ptr < bytes.len() && bytes[ptr] >= 128 {                              // c:5342
4381        // c:5351 — `return 1` to reject non-portable name.
4382        return 1;
4383    }
4384    if ptr < bytes.len() {                                                   // c:5352 `else if (*ptr)`
4385        // c:5353-5355 — write `\0` at `=`, setenv(name, value), restore.
4386        let name = &str[..ptr];
4387        let value = &str[ptr + 1..];
4388        env::set_var(name, value);
4389        0
4390    } else {                                                                 // c:5356-5359
4391        // C: DPUTS(1, "bad environment string"); setenv(str, ptr, 1).
4392        // With no `=`, treat `str` as a bare name with empty value.
4393        env::set_var(str, "");
4394        0
4395    }
4396}
4397
4398/// Direct port of `int findenv(char *name, int *pos)` from
4399/// `Src/params.c:5391`. Walks `environ` looking for an
4400/// entry whose name component (bytes up to `=`) matches `name`.
4401/// Returns Some(index) on a match; the C source writes the
4402/// index into `*pos` and returns 1.
4403///
4404/// Rust signature differs (no out-param; returns `Option<usize>`)
4405/// — the C int-with-out-param idiom maps to `Option<index>` here.
4406/// Walks std::env::vars_os() which preserves the same ordering
4407/// as the underlying libc environ array.
4408pub fn findenv(name: &str) -> Option<usize> {                                // c:5391
4409    // c:5391 — `eq = strchr(name, '=')`. Strip any trailing `=value`.
4410    let nlen = name.find('=').unwrap_or(name.len());                         // c:5397
4411    let bare = &name[..nlen];
4412
4413    // c:5398-5404 — walk environ until match. Use std::env::vars()
4414    // which preserves the same ordering as the underlying libc
4415    // environ.
4416    for (i, (k, _)) in std::env::vars_os().enumerate() {
4417        if let Some(s) = k.to_str() {
4418            if s == bare {
4419                return Some(i);                                              // c:5401-5403
4420            }
4421        }
4422    }
4423    None                                                                     // c:5406
4424}
4425
4426/// Direct port of `void delenvvalue(char *x)` from
4427/// `Src/params.c:5542`. Removes `x` from environ by walking
4428/// to its pointer and shifting subsequent entries down one slot.
4429///
4430/// C body operates on the environ array directly. The Rust port
4431/// uses `env::remove_var(name)` since Rust's env is mediated by
4432/// libc::unsetenv internally — same shift semantics.
4433pub fn delenvvalue(name: &str) {                                             // c:5542
4434    env::remove_var(name);                                                   // c:5542 equivalent
4435}
4436
4437/// Direct port of `void addenv(Param pm, char *value)` from
4438/// `Src/params.c:5448` (USE_SET_UNSET_ENV branch — the
4439/// portable one). C body:
4440///   1. `newenv = mkenvstr(pm->nam, value, pm->flags)` (c:5463)
4441///   2. `if (zputenv(newenv)) { free; pm->env=NULL; return }` (c:5464-5468)
4442///   3. Otherwise: `if (pm->env) free(pm->env); pm->env = newenv;
4443///      pm->flags |= PM_EXPORTED` (c:5482-5484)
4444///
4445/// Rust takes `name` instead of `Param pm` and looks up the
4446/// `pm` node internally — the C body's only reads of `pm` are
4447/// `pm->nam`, `pm->flags`, `pm->env`, all available from
4448/// paramtab. The return type changes from `void` to `i32` so
4449/// callers can chain it; 0 = success, 1 = zputenv failed.
4450pub fn addenv(name: &str, value: &str) -> i32 {                              // c:5448
4451
4452    // c:5463 — `newenv = mkenvstr(pm->nam, value, pm->flags)`.
4453    let flags = {
4454        let tab = paramtab().read().unwrap();
4455        tab.get(name).map(|pm| pm.node.flags).unwrap_or(0)
4456    };
4457    let newenv = mkenvstr(name, value, flags);
4458    // c:5464-5468 — `if (zputenv(newenv)) { free; pm->env=NULL; return }`.
4459    if zputenv(&newenv) != 0 {
4460        let mut tab = paramtab().write().unwrap();
4461        if let Some(pm) = tab.get_mut(name) {
4462            pm.env = None;
4463        }
4464        return 1;
4465    }
4466    // c:5482-5484 — `pm->env = newenv; pm->flags |= PM_EXPORTED`.
4467    let mut tab = paramtab().write().unwrap();
4468    if let Some(pm) = tab.get_mut(name) {
4469        pm.env = Some(newenv);
4470        pm.node.flags |= PM_EXPORTED as i32;
4471    }
4472    0
4473}
4474
4475/// Direct port of `void delenv(Param pm)` from
4476/// `Src/params.c:5563-5582`. Removes the param's env entry and
4477/// clears `pm->env`. Under USE_SET_UNSET_ENV (the portable
4478/// branch) the C body is:
4479///   unsetenv(pm->node.nam);
4480///   zsfree(pm->env);
4481///   pm->env = NULL;
4482///
4483/// "Note we don't remove PM_EXPORT from the flags. This may be
4484/// asking for trouble but we need to know later if we restore
4485/// this parameter to its old value." (c:5575-5577)
4486///
4487/// Rust signature drift: takes `&str` (the param name) instead
4488/// of `&mut Param`. The pm.env field is cleared via the paramtab
4489/// lookup; PM_EXPORTED is intentionally preserved per the C
4490/// comment.
4491pub fn delenv(name: &str) {                                                  // c:5563
4492    // c:5563 — `unsetenv(pm->node.nam)`.
4493    env::remove_var(name);
4494    // c:5568 / c:5572 — `pm->env = NULL`. PM_EXPORTED stays set.
4495    let mut tab = paramtab().write().unwrap();
4496    if let Some(pm) = tab.get_mut(name) {
4497        pm.env = None;
4498    }
4499}
4500
4501/// Direct port of `static char *mkenvstr(char *name, char *value,
4502/// int flags)` from `Src/params.c:5513`. Builds `name=value`
4503/// in a fresh heap-string, where `value` is unmetafied and
4504/// case-folded according to `flags` (PM_LOWER → lower, PM_UPPER →
4505/// upper). The C source computes the unmetafied length first via
4506/// the `while (*s && (*s++ != Meta || *s++ != 32))` loop, then
4507/// allocates and writes via copyenvstr; the Rust port appends to
4508/// a `String` so the length pre-scan is implicit.
4509pub fn mkenvstr(name: &str, value: &str, flags: i32) -> String {             // c:5513
4510    let mut buf = String::with_capacity(name.len() + value.len() + 2);
4511    buf.push_str(name);                                                      // c:5522 strcpy(s, name)
4512    buf.push('=');                                                           // c:5524 *s = '='
4513    if !value.is_empty() {                                                   // c:5525
4514        copyenvstr(&mut buf, value, flags);                                  // c:5526
4515    }
4516    buf                                                                      // c:5530
4517}
4518
4519/// Direct port of `static void copyenvstr(char *s, char *value,
4520/// int flags)` from `Src/params.c:5434`. Unmetafies `value`
4521/// into `s` (Meta NEXT pairs collapse to NEXT^32) and applies
4522/// PM_LOWER / PM_UPPER case folding per byte.
4523pub fn copyenvstr(buf: &mut String, value: &str, flags: i32) {               // c:5434
4524    let flags_u = flags as u32;
4525    let mut it = value.bytes();
4526    while let Some(b) = it.next() {                                          // c:5436
4527        let mut ch = b;
4528        if ch == crate::ported::zsh_h::META as u8 {                          // c:5437
4529            ch = match it.next() {
4530                Some(next) => next ^ 32,                                     // c:5438
4531                None => break,
4532            };
4533        }
4534        if flags_u & crate::ported::zsh_h::PM_LOWER != 0 {                   // c:5439
4535            ch = ch.to_ascii_lowercase();                                    // c:5440
4536        } else if flags_u & crate::ported::zsh_h::PM_UPPER != 0 {            // c:5441
4537            ch = ch.to_ascii_uppercase();                                    // c:5442
4538        }
4539        buf.push(ch as char);
4540    }
4541}
4542
4543/// Direct port of `static int split_env_string(char *env, char
4544/// **name, char **value)` from `Src/params.c:763`.
4545///
4546/// Walks `env` until either `=` or end. Returns `None` (C `0`) if:
4547///   - any byte before `=` has the high bit set (c:771-777 — names
4548///     outside the portable character set are silently rejected),
4549///   - no `=` is present (c:783-785 fall-through),
4550///   - or the name is empty (`*str == '=' && str == tenv`, c:782).
4551/// Otherwise returns `Some((name, value))` (C `1` + out-params).
4552///
4553/// Out-param style differs from C (we return a tuple); the
4554/// rejection rules are 1:1.
4555pub fn split_env_string(env: &str) -> Option<(String, String)> {             // c:763
4556    if env.is_empty() {                                                      // c:763 !env
4557        return None;
4558    }
4559    let bytes = env.as_bytes();
4560    // c:770-779 — walk name bytes, reject if high bit set.
4561    let mut i = 0;
4562    while i < bytes.len() && bytes[i] != b'=' {                              // c:770
4563        if bytes[i] >= 128 {                                                 // c:771 (unsigned char) >= 128
4564            return None;                                                     // c:777
4565        }
4566        i += 1;
4567    }
4568    // c:780-785 — accept only if `=` was found at non-zero offset.
4569    if i > 0 && i < bytes.len() && bytes[i] == b'=' {                        // c:780
4570        let name = String::from_utf8_lossy(&bytes[..i]).into_owned();        // c:781-782
4571        let value = String::from_utf8_lossy(&bytes[i + 1..]).into_owned();   // c:783
4572        Some((name, value))                                                  // c:784
4573    } else {
4574        None                                                                 // c:786
4575    }
4576}
4577
4578/// Port of `arrfixenv(char *s, char **t)` from `Src/params.c:5285`. C body re-syncs
4579/// the env entry for an array param after mutation, joining with
4580/// the param's `joinchar`. Rust port joins with ':' (the default
4581/// for PATH-style arrays) and updates the env var.
4582/// Direct port of `void arrfixenv(char *s, char **t)` from
4583/// `Src/params.c:5285`. Re-syncs the env-side entry for an
4584/// array parameter after mutation. Order of operations (C body):
4585///   1. If `t == path`, flush the command-name cache (c:5291).
4586///   2. Look up the param node by name (c:5294); skip if
4587///      PM_HASHELEM is set (c:5300-5301).
4588///   3. Under ALLEXPORT, mark PM_EXPORTED (c:5304); always clear
4589///      PM_DEFAULTED (c:5305).
4590///   4. Skip if not PM_EXPORTED (c:5311-5312).
4591///   5. joinchar = ':' for PM_SPECIAL else
4592///      `((struct tieddata *)pm->u.data)->joinchar` (c:5314-5318).
4593///   6. `addenv(pm, t ? zjoin(t, joinchar, 1) : "")` (c:5319).
4594pub fn arrfixenv(s: &str, t: Option<&[String]>) {                            // c:5285
4595
4596    // c:5291 — `if (t == path) cmdnamtab->emptytable(cmdnamtab)`.
4597    // PATH change invalidates the command-name cache.
4598    if s == "PATH" || s == "path" {
4599        crate::ported::hashtable::emptycmdnamtable();
4600    }
4601
4602    // c:5294 — `pm = paramtab->getnode(paramtab, s)`.
4603    let pm_arc_data = {
4604        let tab = paramtab().read().unwrap();
4605        tab.get(s).map(|pm| (pm.node.flags, pm.gsu_a.is_some()))
4606    };
4607    let (flags, _has_gsu_a) = match pm_arc_data {
4608        Some(x) => x,
4609        None => {
4610            // No param yet — just sync via env::set_var as fallback.
4611            let val = t.map(|v| v.join(":")).unwrap_or_default();
4612            env::set_var(s, val);
4613            return;
4614        }
4615    };
4616
4617    // c:5300-5301 — `if (pm->flags & PM_HASHELEM) return`.
4618    if flags & PM_HASHELEM as i32 != 0 {
4619        return;
4620    }
4621
4622    // c:5304 — `if (isset(ALLEXPORT)) pm->flags |= PM_EXPORTED`.
4623    let allexport = isset(ALLEXPORT);
4624    // c:5305 — `pm->flags &= ~PM_DEFAULTED` always.
4625    {
4626        let mut tab = paramtab().write().unwrap();
4627        if let Some(pm) = tab.get_mut(s) {
4628            if allexport {
4629                pm.node.flags |= PM_EXPORTED as i32;
4630            }
4631            pm.node.flags &= !(PM_DEFAULTED as i32);
4632        }
4633    }
4634
4635    // c:5311-5312 — `if (!(pm->flags & PM_EXPORTED)) return`.
4636    let new_flags = {
4637        let tab = paramtab().read().unwrap();
4638        tab.get(s).map(|pm| pm.node.flags).unwrap_or(0)
4639    };
4640    if new_flags & PM_EXPORTED as i32 == 0 {
4641        return;
4642    }
4643
4644    // c:5314-5317 — joinchar selection.
4645    let joinchar = if new_flags & PM_SPECIAL as i32 != 0 {
4646        ':'                                                                  // c:5315
4647    } else {
4648        // c:5317 — tieddata.joinchar; not modelled in current Param —
4649        // default to ':' which is correct for all currently-tied
4650        // array params (PATH/CDPATH/FPATH/etc.).
4651        ':'
4652    };
4653
4654    // c:5319 — `addenv(pm, t ? zjoin(t, joinchar, 1) : "")`.
4655    let joined = match t {
4656        Some(arr) => arr.join(&joinchar.to_string()),
4657        None => String::new(),
4658    };
4659    addenv(s, &joined);
4660}
4661
4662// -----------------------------------------------------------
4663// Array uniq helpers.
4664// -----------------------------------------------------------
4665
4666/// Port of `simple_arrayuniq(char **x, int freeok)` from `Src/params.c:4412`. C body:
4667/// O(n^2) dedupe in place — first occurrence wins.
4668/// WARNING: param names don't match C — Rust=(x) vs C=(x, freeok)
4669pub fn simple_arrayuniq(x: Vec<String>) -> Vec<String> {
4670    let mut seen: HashSet<String> = HashSet::new();
4671    let mut out = Vec::with_capacity(x.len());
4672    for s in x {
4673        if seen.insert(s.clone()) {
4674            out.push(s);
4675        }
4676    }
4677    out
4678}
4679
4680/// Direct port of `static void arrayuniq(char **x, int freeok)`
4681/// from `Src/params.c:4473`. First-wins dedupe of `x`,
4682/// in-place. C uses simple O(n²) scan for arrays under 10
4683/// entries, switching to a HashTable for larger arrays. `freeok`
4684/// controls whether to `zsfree()` duplicates (only safe when
4685/// caller owns the strings — Rust drop semantics handle it).
4686///
4687/// Signature note: C takes `char **x` + in-place mutation; Rust
4688/// takes owned `Vec<String>` and returns the deduped result.
4689/// `freeok` is preserved but is a no-op in Rust (drops free
4690/// automatically). The hashtable / simple-loop tiering follows
4691/// the same threshold (10) as C.
4692pub fn arrayuniq(x: Vec<String>, freeok: i32) -> Vec<String> {               // c:4473
4693    let _ = freeok;
4694    let array_size = x.len();
4695    if array_size == 0 {                                                     // c:4481
4696        return x;
4697    }
4698    // c:4482-4486 — small-array fallback to simple_arrayuniq.
4699    if array_size < 10 {                                                     // c:4482
4700        return simple_arrayuniq(x);                                          // c:4484
4701    }
4702    // c:4483 — `if (!(ht = newuniqtable(array_size + 1)))` — Rust
4703    // newuniqtable never fails, but mirror the C order of allocation.
4704    let mut ht = newuniqtable(array_size as i64 + 1);
4705    // c:4487-4507 — walk + first-wins.
4706    let mut out: Vec<String> = Vec::with_capacity(array_size);
4707    for s in x {                                                             // c:4487 walk
4708        if ht.insert(s.clone()) {                                            // c:4488 gethashnode2 + addhashnode2
4709            out.push(s);                                                     // c:4495 *write_it = *it
4710        }
4711        // else: dup — drop the value (c:4502 zsfree if freeok).
4712    }
4713    drop(ht);                                                                // c:4523 deletehashtable
4714    out
4715}
4716
4717/// Direct port of `void zhuniqarray(char **x)` from
4718/// `Src/params.c:4523`. Wraps `arrayuniq` with `freeok=0`.
4719/// (C body is literally `arrayuniq(x, 0);`.)
4720pub fn zhuniqarray(x: Vec<String>) -> Vec<String> {                          // c:4523
4721    arrayuniq(x, 0)                                                          // c:4523
4722}
4723
4724/// Port of `arrayuniq_freenode(HashNode hn)` from `Src/params.c:4443`. C
4725/// body: `zsfree(((Pathnode)hn)->name); zfree(hn, sizeof…);` —
4726/// the freenode callback for the temporary HashTable `arrayuniq`
4727/// builds. Rust drop semantics handle this; no-op shim.
4728/// is `(void)hn;` — intentional no-op; passed as freenode callback
4729/// to scratch hashtable used by `arrayuniq` so existing entries
4730/// aren't freed when the table is torn down.
4731/// WARNING: param names don't match C — Rust=() vs C=(hn)
4732/// WARNING: param names don't match C — Rust=() vs C=(pm, x)
4733pub fn arrayuniq_freenode() {}
4734
4735/// Direct port of `HashTable newuniqtable(zlong size)` from
4736/// `Src/params.c:4450`. C body allocates a `HashTable`
4737/// named "arrayuniq" with the standard hasher/cmpnodes/
4738/// add/get/remove/disable/enable function pointers plus
4739/// `arrayuniq_freenode` as the freenode callback (which is a
4740/// no-op — see c:4443). Rust returns a `HashSet<String>` with
4741/// the size hint pre-allocated; the freenode-callback role is
4742/// implicit (Drop runs on HashSet teardown without freeing
4743/// borrowed strings).
4744pub fn newuniqtable(size: i64) -> HashSet<String> {                          // c:4450
4745    HashSet::with_capacity(size.max(0) as usize)                             // c:4450 newhashtable(size, ...)
4746}
4747
4748// -----------------------------------------------------------
4749// "Null" callbacks — no-op getfn/setfn/unsetfn slots used for
4750// read-only or write-only special params.
4751// -----------------------------------------------------------
4752
4753/// Port of `nullintsetfn(UNUSED(Param pm), UNUSED(zlong x))` from `Src/params.c:4187`. C body:
4754/// empty (no-op setter for read-only int params).
4755#[allow(unused_variables)]
4756pub fn nullintsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {}
4757
4758/// Port of `nullsethashfn(UNUSED(Param pm), HashTable x)` from `Src/params.c:4104`. C body:
4759/// `deleteparamtable(x);` — frees the supplied table, doesn't store.
4760#[allow(unused_variables)]
4761pub fn nullsethashfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
4762    // Rust drop semantics free `x` when this scope ends.
4763}
4764
4765/// Port of `nullstrsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4180`. C body:
4766/// `zsfree(x);` — frees but doesn't store. Rust drop handles free.
4767#[allow(unused_variables)]
4768pub fn nullstrsetfn(pm: &mut crate::ported::zsh_h::param, x: String) {}
4769
4770/// Port of `nullunsetfn(UNUSED(Param pm), UNUSED(int exp))` from `Src/params.c:4192`. C body: empty.
4771#[allow(unused_variables)]
4772pub fn nullunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {}
4773
4774/// Port of `stdunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:3955`. C body:
4775/// dispatches `pm->gsu->setfn(pm, NULL)` per `PM_TYPE`, clears
4776/// `PM_TIED`/frees ename for tied params, sets PM_UNSET.
4777///
4778/// Rust port mirrors C semantics: clears the union slot and sets
4779/// PM_UNSET. The GSU vtable callbacks are stored on `param` as
4780/// `Option<Gsu*>` (zsh_h:760-764) but the dispatch uses callback
4781/// fn-ptrs that aren't generally registered yet, so we open-code
4782/// the "setfn(pm, NULL)" effect by zeroing the matching union
4783/// member instead of calling through the vtable.
4784#[allow(unused_variables)]
4785pub fn stdunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
4786    match PM_TYPE(pm.node.flags as u32) {
4787        PM_SCALAR | PM_NAMEREF => {
4788            pm.u_str = None;
4789        }
4790        PM_ARRAY => {
4791            pm.u_arr = None;
4792        }
4793        PM_HASHED => {
4794            pm.u_hash = None;
4795        }
4796        _ => {
4797            if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
4798                pm.u_str = None;
4799            }
4800        }
4801    }
4802    if (pm.node.flags as u32 & (PM_SPECIAL | PM_TIED)) == PM_TIED {
4803        pm.ename = None;
4804        pm.node.flags &= !(PM_TIED as i32);
4805    }
4806    pm.node.flags |= PM_UNSET as i32;
4807}
4808
4809/// Port of `rprompt_indent_unsetfn(Param pm, int exp)` from `Src/params.c:152`. C
4810/// body: `stdunsetfn(pm, exp); rprompt_indent = 1;` — keeps in
4811/// sync with init_term().
4812pub fn rprompt_indent_unsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
4813    stdunsetfn(pm, exp);
4814    *RPROMPT_INDENT.lock().unwrap() = 1;
4815}
4816
4817/// Port of `int rprompt_indent` from `Src/init.c`. Set to 1 by
4818/// `init_term()` and reset by `rprompt_indent_unsetfn` when the
4819/// `RPROMPT_INDENT` parameter is unset.
4820pub static RPROMPT_INDENT: std::sync::Mutex<i32> = std::sync::Mutex::new(1);
4821
4822// -----------------------------------------------------------
4823// GSU dispatch callbacks — direct ports against `param.u_*`
4824// fields. C source in Src/params.c:4002.
4825// -----------------------------------------------------------
4826
4827/// Port of `intgetfn(Param pm)` from `Src/params.c:3993`. C body:
4828/// `return pm->u.val;`
4829pub fn intgetfn(pm: &crate::ported::zsh_h::param) -> i64 {
4830    pm.u_val
4831}
4832
4833/// Port of `intsetfn(Param pm, zlong x)` from `Src/params.c:4002`. C body:
4834/// `pm->u.val = x;`
4835pub fn intsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4836    pm.u_val = x;
4837}
4838
4839/// Port of `floatgetfn(Param pm)` from `Src/params.c:4011`. C body:
4840/// `return pm->u.dval;`
4841pub fn floatgetfn(pm: &crate::ported::zsh_h::param) -> f64 {
4842    pm.u_dval
4843}
4844
4845/// Port of `floatsetfn(Param pm, double x)` from `Src/params.c:4020`. C body:
4846/// `pm->u.dval = x;`
4847pub fn floatsetfn(pm: &mut crate::ported::zsh_h::param, x: f64) {
4848    pm.u_dval = x;
4849}
4850
4851/// Port of `strgetfn(Param pm)` from `Src/params.c:4029`. C body:
4852/// `return pm->u.str ? pm->u.str : (char *) hcalloc(1);`
4853pub fn strgetfn(pm: &crate::ported::zsh_h::param) -> String {
4854    pm.u_str.clone().unwrap_or_default()
4855}
4856
4857/// Port of `strsetfn(Param pm, char *x)` from `Src/params.c:4038`. C body:
4858/// `zsfree(pm->u.str); pm->u.str = x;` plus AUTONAMEDIRS handling.
4859/// The `adduserdir()` call is gated on PM_NAMEDDIR/AUTONAMEDIRS.
4860pub fn strsetfn(pm: &mut crate::ported::zsh_h::param, x: String) {
4861    pm.u_str = Some(x.clone());
4862    if (pm.node.flags as u32 & PM_HASHELEM) == 0 {
4863        if (pm.node.flags as u32 & PM_NAMEDDIR) != 0 {
4864            pm.node.flags |= PM_NAMEDDIR as i32;
4865            crate::ported::utils::adduserdir(&pm.node.nam, &x, 0, false);
4866        }
4867    }
4868}
4869
4870/// Port of `arrgetfn(Param pm)` from `Src/params.c:4057`. C body:
4871/// `return pm->u.arr ? pm->u.arr : &nullarray;`
4872pub fn arrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
4873    pm.u_arr.clone().unwrap_or_default()
4874}
4875
4876/// Port of `arrsetfn(Param pm, char **x)` from `Src/params.c:4066`. C body frees
4877/// the old array, applies PM_UNIQUE filter via `uniqarray()`, then
4878/// stores. Calls `arrfixenv(ename, x)` for tied colon-arrays.
4879pub fn arrsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
4880    let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
4881        simple_arrayuniq(x)
4882    } else {
4883        x
4884    };
4885    pm.u_arr = Some(val.clone());
4886    if let Some(ename) = pm.ename.clone() {
4887        arrfixenv(&ename, Some(&val));
4888    }
4889}
4890
4891/// Port of `hashgetfn(Param pm)` from `Src/params.c:4084`. C body:
4892/// `return pm->u.hash;`
4893pub fn hashgetfn(pm: &crate::ported::zsh_h::param) -> Option<&crate::ported::zsh_h::HashTable> {
4894    pm.u_hash.as_ref()
4895}
4896
4897/// Port of `hashsetfn(Param pm, HashTable x)` from `Src/params.c:4093`. C body:
4898/// `if (pm->u.hash && pm->u.hash != x) deleteparamtable(pm->u.hash);
4899///  pm->u.hash = x;`
4900pub fn hashsetfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
4901    pm.u_hash = Some(x);
4902}
4903
4904/// Direct port of `static void arrhashsetfn(Param pm, char **val,
4905/// int flags)` from `Src/params.c:4113-4170`. Set callback for
4906/// assoc arrays: takes a flat `[k1, v1, k2, v2, ...]` value list
4907/// and turns it into a hash.
4908///
4909/// C body:
4910///   1. Count non-Marker entries; if odd, error c:4128-4131.
4911///   2. Under ASSPM_AUGMENT, fetch existing hash via getfn
4912///      (c:4134-4137); otherwise allocate fresh via
4913///      newparamtable(17, name).
4914///   3. Walk pairs: each value (k, v) becomes a PM_SCALAR|PM_UNSET
4915///      child param `createparam(k)`, then `assignstrvalue(v->pm,
4916///      val, eltflags)` (c:4140-4166).
4917///   4. `pm->gsu.h->setfn(pm, ht)` to install (c:4168).
4918///
4919/// The Rust port partially mirrors: counts pairs, rejects odd
4920/// counts via zerr, installs a fresh hashtable. The per-pair
4921/// createparam+assignstrvalue cycle requires assoc storage
4922/// shape we don't yet have wired through `u_hash`; this stays as
4923/// a structural port and emits diagnostic on the odd-count path.
4924pub fn arrhashsetfn(                                                         // c:4113
4925    pm: &mut crate::ported::zsh_h::param,
4926    val: Vec<String>,
4927    _flags: i32,
4928) {
4929
4930    // c:4124-4127 — count non-Marker entries.
4931    let alen: usize = val
4932        .iter()
4933        .filter(|s| !s.starts_with(Marker as char))
4934        .count();
4935
4936    // c:4129-4131 — odd count → error.
4937    if alen % 2 != 0 {
4938        crate::ported::utils::zerr(
4939            "bad set of key/value pairs for associative array",
4940        );
4941        return;
4942    }
4943
4944    // c:4132-4138 — install or augment. Skip the createparam
4945    // sub-hash walk pending assoc-storage wiring; install an
4946    // empty hashtable so hashgetfn doesn't return stale data.
4947    pm.u_hash = Some(Box::new(crate::ported::zsh_h::hashtable {
4948        hsize: 0,
4949        ct: 0,
4950        nodes: Vec::new(),
4951        tmpdata: 0,
4952        hash: None,
4953        emptytable: None,
4954        filltable: None,
4955        cmpnodes: None,
4956        addnode: None,
4957        getnode: None,
4958        getnode2: None,
4959        removenode: None,
4960        disablenode: None,
4961        enablenode: None,
4962        freenode: None,
4963        printnode: None,
4964        scantab: None,
4965    }));
4966    // c:4170 — free(val). Rust drops automatically.
4967}
4968
4969// -----------------------------------------------------------
4970// Generic special-param GSU callbacks (`u.valptr` / `u.data`).
4971// C source uses raw pointer indirection through `pm->u.data`/
4972// `pm->u.valptr` — Rust port stores the global's name in `u_str`
4973// (lookup key) since we can't carry raw pointers across an FFI
4974// boundary safely. The lookup-table integration ships with the
4975// special-params init code (Src/params.c:4213 createparamtable).
4976// -----------------------------------------------------------
4977
4978/// Port of `intvargetfn(Param pm)` from `Src/params.c:4202`. C body:
4979/// `return *pm->u.valptr;`
4980pub fn intvargetfn(pm: &crate::ported::zsh_h::param) -> i64 {
4981    pm.u_val
4982}
4983
4984/// Port of `intvarsetfn(Param pm, zlong x)` from `Src/params.c:4213`. C body:
4985/// `*pm->u.valptr = x;`
4986pub fn intvarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4987    pm.u_val = x;
4988}
4989
4990/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4224`. C body sets
4991/// the int and triggers `adjustwinsize` for LINES/COLUMNS.
4992pub fn zlevarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4993    pm.u_val = x;
4994    if pm.node.nam == "LINES" || pm.node.nam == "COLUMNS" {
4995        let _ = crate::ported::utils::adjustwinsize();
4996    }
4997}
4998
4999/// Port of `strvarsetfn(Param pm, char *x)` from `Src/params.c:4249`. C body:
5000/// `zsfree(*q); *q = x;` where `q = (char **)pm->u.data`.
5001pub fn strvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5002    pm.u_str = x;
5003}
5004
5005/// Port of `strvargetfn(Param pm)` from `Src/params.c:4263`. C body:
5006/// `s = *((char **)pm->u.data); return s ? s : hcalloc(1);`
5007pub fn strvargetfn(pm: &crate::ported::zsh_h::param) -> String {
5008    pm.u_str.clone().unwrap_or_default()
5009}
5010
5011/// Port of `arrvargetfn(Param pm)` from `Src/params.c:4279`. C body:
5012/// `arrptr = *((char ***)pm->u.data); return arrptr ?: &nullarray;`
5013pub fn arrvargetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5014    pm.u_arr.clone().unwrap_or_default()
5015}
5016
5017/// Port of `arrvarsetfn(Param pm, char **x)` from `Src/params.c:4294`. C body
5018/// frees old, applies PM_UNIQUE, handles PM_SPECIAL+NULL → mkarray.
5019pub fn arrvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
5020    let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
5021        simple_arrayuniq(x)
5022    } else {
5023        x
5024    };
5025    pm.u_arr = Some(val);
5026}
5027
5028/// Port of `colonarrsetfn(Param pm, char *x)` from `Src/params.c:4329`. C body
5029/// splits the colon-string into an array and stores via the
5030/// generic arrvarsetfn.
5031pub fn colonarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5032    let arr = match x {
5033        Some(s) => colonsplit(&s),
5034        None => Vec::new(),
5035    };
5036    arrvarsetfn(pm, arr);
5037}
5038
5039/// Port of `tiedarrgetfn(Param pm)` from `Src/params.c:4348`. C body:
5040/// `return *((Tieddata)pm->u.data)->arrptr;`
5041pub fn tiedarrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5042    pm.u_arr.clone().unwrap_or_default()
5043}
5044
5045/// Direct port of `void tiedarrsetfn(Param pm, char *x)` from
5046/// `Src/params.c:4357-4389`. Setter for a colon-array-tied
5047/// scalar (PATH/CDPATH/MAILPATH/etc.).
5048///
5049/// C body:
5050///   1. Free the existing tied array (`*dptr->arrptr`) at c:4363.
5051///   2. If no array but an `ename` exists, clear PM_DEFAULTED on
5052///      the tied array param (c:4365-4368).
5053///   3. If `x` is non-null: build a 1-or-2-byte separator from
5054///      `dptr->joinchar` (Meta-quoting if needed, c:4371-4380),
5055///      `sepsplit(x, sepbuf, 0, 0)` into the array (c:4381), and
5056///      uniqarray() if PM_UNIQUE (c:4382-4383). Free `x` (c:4384).
5057///   4. Else: `*dptr->arrptr = NULL` (c:4385-4386).
5058///   5. If `pm->ename` is set, call `arrfixenv(pm->name, arrptr)`
5059///      to sync env (c:4387-4388).
5060///
5061/// The Rust port treats `u_arr` as the tied array storage and
5062/// uses `':'` as the joinchar default (matches PATH/CDPATH/FPATH
5063/// /MAILPATH/PSVAR/MODULE_PATH which all use colon separators —
5064/// the joinchar field on the C-side tieddata wasn't ported to the
5065/// Rust Param struct yet).
5066pub fn tiedarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) { // c:4357
5067
5068    // c:4361-4368 — free old / clear PM_DEFAULTED on tied counterpart.
5069    if pm.u_arr.is_none() {
5070        if let Some(ename) = pm.ename.clone() {                              // c:4365
5071            let mut tab = paramtab().write().unwrap();
5072            if let Some(altpm) = tab.get_mut(&ename) {                       // c:4366
5073                altpm.node.flags &= !(PM_DEFAULTED as i32);                  // c:4367
5074            }
5075        }
5076    }
5077
5078    if let Some(s) = x {                                                     // c:4369
5079        // c:4370-4380 — single-byte separator (joinchar=':' for all
5080        // currently-tied params; Meta-quoting only kicks in for
5081        // exotic joinchars not present today).
5082        let arr: Vec<String> = s.split(':').map(|t| t.to_string()).collect();
5083        // c:4382-4383 — uniqarray if PM_UNIQUE.
5084        let arr = if pm.node.flags & PM_UNIQUE as i32 != 0 {                 // c:4382
5085            uniqarray(arr)                                                   // c:4383
5086        } else {
5087            arr
5088        };
5089        pm.u_arr = Some(arr);
5090        // c:4384 — zsfree(x). Rust drop.
5091    } else {                                                                 // c:4385
5092        pm.u_arr = None;                                                     // c:4386
5093    }
5094
5095    // c:4387-4388 — `if (pm->ename) arrfixenv(pm->name, *dptr->arrptr)`.
5096    if pm.ename.is_some() {
5097        let nam = pm.node.nam.clone();
5098        let arr_ref = pm.u_arr.as_deref();
5099        arrfixenv(&nam, arr_ref);
5100    }
5101}
5102
5103/// Port of `tiedarrunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:4393`. C body
5104/// frees the tied storage and calls stdunsetfn.
5105/// Direct port of `void tiedarrunsetfn(Param pm, UNUSED(int exp))`
5106/// from `Src/params.c:4393`. Special unset for tied arrays:
5107/// frees tieddata, ename, clears PM_TIED, sets PM_UNSET.
5108///
5109/// C body:
5110///   pm->gsu.s->setfn(pm, NULL);             // c:4393
5111///   zfree(pm->u.data, sizeof(tieddata));    // c:4393
5112///   pm->u.data = NULL;                      // c:4393
5113///   zsfree(pm->ename);                      // c:4393
5114///   pm->ename = NULL;                       // c:4393
5115///   pm->flags &= ~PM_TIED;                  // c:4393
5116///   pm->flags |= PM_UNSET;                  // c:4393
5117pub fn tiedarrunsetfn(pm: &mut crate::ported::zsh_h::param, _exp: i32) {     // c:4393
5118    // c:4400 — invoke the scalar setfn with NULL (frees backing array).
5119    tiedarrsetfn(pm, None);
5120    // c:4401-4403 — drop tieddata.
5121    pm.u_data = 0;
5122    pm.u_arr = None;
5123    // c:4404-4405 — `zsfree(pm->ename); pm->ename = NULL`.
5124    pm.ename = None;
5125    // c:4406-4407 — flag toggles.
5126    pm.node.flags &= !(PM_TIED as i32);
5127    pm.node.flags |= PM_UNSET as i32;
5128}
5129
5130// -----------------------------------------------------------
5131// Param-table mutators / scope / nameref helpers.
5132// `Src/params.c` calls these against the global `paramtab`
5133// HashTable; until our HashTable vtable (`Box<hashtable>` in
5134// zsh_h.rs:285) is wired, these remain no-op shims with the
5135// real C signatures.
5136// -----------------------------------------------------------
5137
5138/// Port of `assignnparam(char *s, mnumber val, int flags)` from `Src/params.c:3664`. C body
5139/// looks up the param via `gethashnode2(realparamtab, s)`,
5140/// dispatches on PM_TYPE: PM_INTEGER → `intsetfn(pm, val.u.l)`;
5141/// PM_FFLOAT/EFLOAT → `floatsetfn(pm, val.u.d)`; otherwise
5142/// `assignstrvalue(&v, conv_to_string(val), flags)`. Stub
5143/// pending HashTable backend; signature mirrors C `mnumber val`.
5144/// flow: isident guard → unset(EXECOPT) bail → `getvalue(&vbuf,&s,1)`
5145/// → if existing array/hashed (non-special, non-tied, non-KSHARRAYS,
5146/// no subscript) → unsetparam_pm + recreate → else if no value →
5147/// `createparam(t, type)` (POSIXIDENTIFIERS gates SCALAR vs
5148/// MN_INTEGER→PM_INTEGER else PM_FFLOAT) → second `getvalue` →
5149/// `check_warn_pm` if ASSPM_WARN → clear PM_DEFAULTED → `setnumvalue`
5150/// → return pm. This port wires the structural flow against the
5151/// already-ported helpers; the createparam/paramtab backend is
5152/// still stubbed elsewhere so the create-new-param branch returns
5153/// None until `createparam` lands.
5154pub fn assignnparam(
5155    s: &str,
5156    val: crate::ported::math::mnumber,
5157    flags: i32,
5158) -> Option<Box<crate::ported::zsh_h::param>> {
5159    // c:3666 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
5160    if !isident(s) {
5161        zerr(&format!("not an identifier: {}", s));                          // c:3667
5162        errflag.fetch_or(                                                    // c:3669
5163            crate::ported::utils::ERRFLAG_ERROR,
5164            std::sync::atomic::Ordering::Relaxed,
5165        );
5166        return None;                                                         // c:3670
5167    }
5168    if unset(EXECOPT) {
5169        return None;
5170    }
5171    let mut vbuf = crate::ported::zsh_h::value {
5172        pm: None,
5173        arr: Vec::new(),
5174        scanflags: 0,
5175        valflags: 0,
5176        start: 0,
5177        end: -1,
5178    };
5179    let mut cursor: &str = s;
5180    let has_sub = s.contains('[');
5181    let mut was_unset = false;
5182    let v = getvalue(Some(&mut vbuf), &mut cursor, 1);
5183    let need_create = match v {
5184        Some(ref vv) => {
5185            if let Some(pm) = vv.pm.as_ref() {
5186                let f = pm.node.flags as u32;
5187                if (f & (PM_ARRAY | PM_HASHED)) != 0
5188                    && (f & (PM_SPECIAL | PM_TIED)) == 0
5189                    && unset(KSHARRAYS) && !has_sub
5190                {
5191                    // unsetparam_pm(vv.pm, 0, 1);
5192                    was_unset = true;
5193                    true
5194                } else {
5195                    false
5196                }
5197            } else {
5198                true
5199            }
5200        }
5201        None => true,
5202    };
5203    if need_create {
5204        // createparam(t, type) + second getvalue — paramtab backend
5205        // not yet wired; cannot synthesize the new param without it.
5206        let _ = was_unset;
5207        return None;
5208    }
5209    if (flags & ASSPM_WARN) != 0 {
5210        if let Some(ref vv) = v {
5211            if let Some(ref pm) = vv.pm {
5212                check_warn_pm(pm, "numeric", 0, 1);
5213            }
5214        }
5215    }
5216    if let Some(vv) = v {
5217        if let Some(pm) = vv.pm.as_mut() {
5218            pm.node.flags &= !(PM_DEFAULTED as i32);
5219        }
5220        setnumvalue(Some(vv), val);
5221        // Return value would be Box<param> over vv.pm; we don't own it
5222        // here. Real C returns the borrowed pointer; surface None until
5223        // value-buffer ownership is settled.
5224    }
5225    None
5226}
5227
5228/// 1:1 port of the C body covering: EXECOPT short-circuit,
5229/// PM_READONLY/PM_HASHED/VALFLAG_EMPTY guards, PM_UNSET clear,
5230/// per-PM_TYPE dispatch including the SCALAR/NAMEREF subscript
5231/// splice (KSHARRAYS-aware index normalization, MULTIBYTE end
5232/// adjust, full-string overwrite vs in-place memcpy fast path,
5233/// AUTONAMEDIRS/PM_NAMEDDIR re-registration), PM_INTEGER (with
5234/// ASSPM_ENV_IMPORT → `zstrtol_underscore`, else `mathevali`,
5235/// `lastbase` propagation), PM_EFLOAT/PM_FFLOAT (env vs `matheval`,
5236/// MN_FLOAT/MN_INTEGER coercion), PM_ARRAY (single-element wrap
5237/// via `setarrvalue`), PM_HASHED (`foundparam` indirection); then
5238/// `setscope(pm)`, errflag/env/ALLEXPORT/PM_ARRAY/ename gate, and
5239/// `export_param`. Width tracking for PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
5240/// preserved.
5241/// Port of `assignstrvalue(Value v, char *val, int flags)` from `Src/params.c:2692`.
5242pub fn assignstrvalue(
5243    v: Option<&mut crate::ported::zsh_h::value>,
5244    val: Option<String>,
5245    flags: i32,
5246) {
5247    if unset(EXECOPT) { return;}
5248
5249    let v = match v { Some(v) => v, None => return };
5250    let pm = match v.pm.as_mut() { Some(p) => p, None => return };
5251
5252    if (pm.node.flags as u32 & PM_READONLY) != 0 {
5253        // zerr("read-only variable: %s", pm->node.nam);
5254        // zsfree(val);  -- Rust drop
5255        return;
5256    }
5257    if (pm.node.flags as u32 & PM_HASHED) != 0
5258        && (v.scanflags as u32 & (SCANPM_MATCHMANY | SCANPM_ARRONLY)) != 0
5259    {
5260        // zerr("%s: attempt to set slice of associative array", ...);
5261        return;
5262    }
5263    if (v.valflags & VALFLAG_EMPTY) != 0 {
5264        // zerr("%s: assignment to invalid subscript range", ...);
5265        return;
5266    }
5267    pm.node.flags &= !(PM_UNSET as i32);
5268
5269    let mut val = val;
5270    match PM_TYPE(pm.node.flags as u32) {
5271        t if t == PM_SCALAR || t == PM_NAMEREF => {
5272            let v_str = val.take().unwrap_or_default();
5273            if v.start == 0 && v.end == -1 {
5274                // v->pm->gsu.s->setfn(v->pm, val);
5275                let len = v_str.len();
5276                strsetfn(pm, v_str);
5277                if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5278                    && pm.width == 0
5279                {
5280                    pm.width = len as i32;
5281                }
5282            } else {
5283                // Subscript splice.
5284                let z = strgetfn(pm);
5285                let zlen = z.len() as i32;
5286                let mut start = v.start;
5287                let mut end = v.end;
5288                if (v.valflags & VALFLAG_INV) != 0
5289                    && !isset(crate::ported::zsh_h::KSHARRAYS)
5290                {
5291                    start -= 1;
5292                    end -= 1;
5293                }
5294                if start < 0 {
5295                    start += zlen;
5296                    if start < 0 { start = 0; }
5297                }
5298                if start > zlen { start = zlen; }
5299                if end < 0 {
5300                    end += zlen;
5301                    if end < 0 {
5302                        end = 0;
5303                    } else if end >= zlen {
5304                        end = zlen;
5305                    } else {
5306                        // MULTIBYTE branch: increment by metachar length;
5307                        // single-byte path increments by 1.
5308                        end += 1;
5309                    }
5310                } else if end > zlen {
5311                    end = zlen;
5312                }
5313                let vlen = v_str.len() as i32;
5314                let newsize = start + vlen + (zlen - end);
5315                let s = start as usize;
5316                let e = end as usize;
5317                let mut x = String::with_capacity(newsize as usize);
5318                x.push_str(&z[..s.min(z.len())]);
5319                x.push_str(&v_str);
5320                if e <= z.len() { x.push_str(&z[e..]); }
5321                strsetfn(pm, x);
5322                if (pm.node.flags as u32 & PM_HASHELEM) == 0
5323                    && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0
5324                        || isset(crate::ported::zsh_h::AUTONAMEDIRS))
5325                {
5326                    pm.node.flags |= PM_NAMEDDIR as i32;
5327                    // adduserdir(pm.node.nam, &z, 0, 0); -- userdirs not ported
5328                }
5329            }
5330        }
5331        t if t == PM_INTEGER => {
5332            if let Some(ref s) = val {
5333                let ival: i64 = if (flags & ASSPM_ENV_IMPORT) != 0 {
5334                    s.parse::<i64>().unwrap_or(0)
5335                } else {
5336                    crate::ported::math::mathevali(s).unwrap_or(0)
5337                };
5338                intsetfn(pm, ival);
5339                if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5340                    && pm.width == 0
5341                {
5342                    pm.width = s.len() as i32;
5343                }
5344                if pm.base == 0 {
5345                    let lb = crate::ported::math::lastbase();
5346                    if lb != -1 {
5347                        pm.base = lb;
5348                    }
5349                }
5350            }
5351        }
5352        t if t == PM_EFLOAT || t == PM_FFLOAT => {
5353            if let Some(ref s) = val {
5354                let mn = if (flags & ASSPM_ENV_IMPORT) != 0 {
5355                    crate::ported::math::mnumber { l: 0, d: s.parse::<f64>().unwrap_or(0.0), type_: MN_FLOAT }
5356                } else {
5357                    crate::ported::math::matheval(s).unwrap_or(crate::ported::math::mnumber { l: 0, d: 0.0, type_: MN_FLOAT })
5358                };
5359                let d = if (mn.type_ & MN_FLOAT) != 0 { mn.d } else { mn.l as f64 };
5360                floatsetfn(pm, d);
5361                if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5362                    && pm.width == 0
5363                {
5364                    pm.width = s.len() as i32;
5365                }
5366            }
5367        }
5368        t if t == PM_ARRAY => {
5369            // c:2826-2828 — `char **ss = zalloc(2*sizeof(char*));
5370            // ss[0]=val; ss[1]=NULL; setarrvalue(v, ss);` — wrap the
5371            // single value in a 1-element array. The C-faithful
5372            // setarrvalue takes &mut Value; we already hold a &mut
5373            // borrow of pm from v.pm.as_mut() higher up, so inline
5374            // the dispatch directly against pm here to avoid the
5375            // double-borrow.
5376            let one = vec![val.take().unwrap_or_default()];
5377            if v.start == 0 && v.end == -1 {
5378                // c:2922 — full replace.
5379                pm.u_arr = Some(one);
5380            } else {
5381                // c:2933+ — slice splice path with bounds adjust.
5382                let arr = pm.u_arr.get_or_insert_with(Vec::new);
5383                let len = arr.len() as i64;
5384                let start_raw = v.start as i64;
5385                let end_raw = v.end as i64;
5386                let start = if start_raw < 0 {
5387                    (len + start_raw + 1).max(0)
5388                } else {
5389                    start_raw
5390                };
5391                let end = if end_raw < 0 {
5392                    (len + end_raw + 1).max(0)
5393                } else {
5394                    end_raw
5395                };
5396                let start_idx = (start.max(1) - 1) as usize;
5397                let end_idx = end.max(0) as usize;
5398                while arr.len() < start_idx {
5399                    arr.push(String::new());
5400                }
5401                let end_idx = end_idx.min(arr.len());
5402                if start_idx <= end_idx {
5403                    arr.splice(start_idx..end_idx, one);
5404                } else {
5405                    for (i, x) in one.into_iter().enumerate() {
5406                        if start_idx + i < arr.len() {
5407                            arr[start_idx + i] = x;
5408                        } else {
5409                            arr.push(x);
5410                        }
5411                    }
5412                }
5413            }
5414        }
5415        t if t == PM_HASHED => {
5416            // Element-assignment path: the C source does
5417            // `setstrvalue(&((Param)foundparam)->u, val)` to update the
5418            // member found by an earlier `scanparamvals` lookup.
5419            if let Some(nam) = foundparam() {
5420                if let Some(ref h) = pm.u_hash {
5421                    let _ = (nam, h);
5422                }
5423            }
5424            set_foundparam(None);
5425        }
5426        _ => {}
5427    }
5428    setscope(pm);
5429    if errflag.load(std::sync::atomic::Ordering::Relaxed) != 0
5430        || ((pm.env.is_none() && (pm.node.flags as u32 & PM_EXPORTED) == 0
5431             && !(isset(crate::ported::zsh_h::ALLEXPORT)
5432                  && (pm.node.flags as u32 & PM_HASHELEM) == 0))
5433            || (pm.node.flags as u32 & PM_ARRAY) != 0
5434            || pm.ename.is_some())
5435    {
5436        return;
5437    }
5438    export_param(pm);
5439}
5440
5441/// Port of `assigngetset(Param pm)` from `Src/params.c:994`. C body
5442/// installs the standard get/set/unset vtable matching the
5443/// param's PM_TYPE so subsequent assignment dispatches go
5444/// through `pm->gsu.X->setfn`.
5445pub fn assigngetset(pm: &mut crate::ported::zsh_h::param) {
5446    match PM_TYPE(pm.node.flags as u32) {
5447        x if x == PM_SCALAR || x == PM_NAMEREF => {
5448            pm.gsu_s = Some(Box::new(gsu_scalar {
5449                getfn: strgetfn,
5450                setfn: strsetfn,
5451                unsetfn: stdunsetfn,
5452            }));
5453        }
5454        x if x == PM_INTEGER => {
5455            pm.gsu_i = Some(Box::new(gsu_integer {
5456                getfn: intgetfn,
5457                setfn: intsetfn,
5458                unsetfn: stdunsetfn,
5459            }));
5460        }
5461        x if x == PM_EFLOAT || x == PM_FFLOAT => {
5462            pm.gsu_f = Some(Box::new(gsu_float {
5463                getfn: floatgetfn,
5464                setfn: floatsetfn,
5465                unsetfn: stdunsetfn,
5466            }));
5467        }
5468        x if x == PM_ARRAY => {
5469            pm.gsu_a = Some(Box::new(gsu_array {
5470                getfn: arrgetfn,
5471                setfn: arrsetfn,
5472                unsetfn: stdunsetfn,
5473            }));
5474        }
5475        x if x == PM_HASHED => {
5476            pm.gsu_h = Some(Box::new(gsu_hash {
5477                getfn: hashgetfn,
5478                setfn: hashsetfn,
5479                unsetfn: stdunsetfn,
5480            }));
5481        }
5482        _ => {
5483            // DPUTS(1, "BUG: tried to create param node without valid flag")
5484        }
5485    }
5486}
5487
5488/// Port of `check_warn_pm(Param pm, const char *pmtype, int created, int may_warn_about_nested_vars)` from `Src/params.c:3158`. C body
5489/// emits the WARN_CREATE_GLOBAL / WARN_NESTED_VAR diagnostics
5490/// when a function-local creates/passes a non-local param with
5491/// the matching shell options set. Stub: needs option globals.
5492#[allow(unused_variables)]
5493pub fn check_warn_pm(
5494    pm: &crate::ported::zsh_h::param,
5495    pmtype: &str,
5496    created: i32,
5497    may_warn_about_nested_vars: i32,
5498) {
5499    if may_warn_about_nested_vars == 0 && created == 0 {
5500        return;
5501    }
5502    // forklevel global pending its own port — treat as 0 until exec.rs
5503    // lands the fork-depth tracker. `locallevel` is the canonical
5504    // `pub static` above (port of params.c:54).
5505    let cur_local: i32 = locallevel.load(std::sync::atomic::Ordering::Relaxed);
5506    let forklevel: i32 = 0;
5507    if created != 0 && isset(WARNCREATEGLOBAL) {
5508        if cur_local <= forklevel || pm.level != 0 {
5509            return;
5510        }
5511    } else if created == 0 && isset(WARNNESTEDVAR) {
5512        if pm.level >= cur_local {
5513            return;
5514        }
5515    } else {
5516        return;
5517    }
5518    if (pm.node.flags as u32 & (PM_SPECIAL | PM_NAMEREF)) != 0 {
5519        return;
5520    }
5521    // funcstack walk + zwarn — funcstack global pending; the C body
5522    // simply emits a single zwarn into the most-recent FS_FUNC frame
5523    // and exits.
5524}
5525
5526/// Port of `convbase_ptr(char *s, zlong v, int base, int *ndigits)` from `Src/params.c:5586`. C body
5527/// converts `v` into base `base` (negative `base` suppresses the
5528/// "0x"/"N#" discriminator), writing the digits into `s` and
5529/// returning the digit count via `*ndigits`. Rust port returns
5530/// `(formatted_string, digit_count)` since Rust strings own
5531/// their buffer.
5532/// WARNING: param names don't match C — Rust=(v, base) vs C=(s, v, base, ndigits)
5533pub fn convbase_ptr(v: i64, base: i32) -> (String, i32) {
5534    let mut s = String::new();
5535    let mut value = v;
5536    if value < 0 {
5537        s.push('-');
5538        value = -value;
5539    }
5540    let mut b = base;
5541    if (-1..=1).contains(&b) {
5542        b = -10;
5543    }
5544    if b > 0 {
5545        if isset(crate::ported::zsh_h::CBASES) && b == 16 {
5546            s.push_str("0x");
5547        } else if isset(crate::ported::zsh_h::CBASES)
5548            && b == 8
5549            && isset(crate::ported::zsh_h::OCTALZEROES)
5550        {
5551            s.push('0');
5552        } else if b != 10 {
5553            s.push_str(&format!("{}#", b));
5554        }
5555    } else {
5556        b = -b;
5557    }
5558    let base_u = b as u64;
5559    let mut x = value as u64;
5560    let mut digs: i32 = 0;
5561    while x != 0 {
5562        x /= base_u;
5563        digs += 1;
5564    }
5565    if digs == 0 {
5566        digs = 1;
5567    }
5568    let mut digits: Vec<u8> = vec![0u8; digs as usize];
5569    let mut i = digs - 1;
5570    let mut x = value as u64;
5571    while i >= 0 {
5572        let dig = (x % base_u) as u8;
5573        digits[i as usize] = if dig < 10 {
5574            b'0' + dig
5575        } else {
5576            b'A' + dig - 10
5577        };
5578        x /= base_u;
5579        i -= 1;
5580    }
5581    s.push_str(std::str::from_utf8(&digits).unwrap_or(""));
5582    (s, digs)
5583}
5584
5585/// Port of `copyparamtable(HashTable ht, char *name)` from `Src/params.c:596`. C body:
5586/// allocates a fresh paramtable via `newparamtable(ht->hsize, name)`,
5587/// sets the global `outtable = nht`, then scans the source via
5588/// `scanhashtable(ht, 0, 0, 0, scancopyparams, 0)` and clears
5589/// `outtable` on exit. Rust port returns the freshly-allocated
5590/// table; the per-node clone walk requires the HashTable iterator
5591/// which isn't wired yet (callers receive the empty allocated
5592/// table — same shape the C source returns when `ht` is empty).
5593pub fn copyparamtable(ht: Option<&crate::ported::zsh_h::HashTable>, name: &str)
5594    -> Option<crate::ported::zsh_h::HashTable>
5595{
5596    let ht = ht?;
5597    newparamtable(ht.hsize, name)
5598}
5599
5600/// Direct port of `static int dontimport(int flags)` from
5601/// `Src/params.c:796-810`.
5602/// ```c
5603/// /* If explicitly marked as don't import */
5604/// if (flags & PM_DONTIMPORT)
5605///     return 1;
5606/// /* If value already exported */
5607/// if (flags & PM_EXPORTED)
5608///     return 1;
5609/// /* If security issue when importing and running with some privilege */
5610/// if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED))
5611///     return 1;
5612/// /* OK to import */
5613/// return 0;
5614/// ```
5615/// Port of `dontimport(int flags)` from `Src/params.c:796`.
5616fn dontimport(flags: i32) -> i32 {                                           // c:796
5617    let flags = flags as u32;
5618    // c:799-800 — `if (flags & PM_DONTIMPORT) return 1`.
5619    if flags & crate::ported::zsh_h::PM_DONTIMPORT != 0 {                    // c:799
5620        return 1;                                                            // c:800
5621    }
5622    // c:802-803 — `if (flags & PM_EXPORTED) return 1`.
5623    if flags & crate::ported::zsh_h::PM_EXPORTED != 0 {                      // c:802
5624        return 1;                                                            // c:803
5625    }
5626    // c:805-806 — `if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED)) return 1`.
5627    if flags & crate::ported::zsh_h::PM_DONTIMPORT_SUID != 0                 // c:805
5628        && isset(crate::ported::zsh_h::PRIVILEGED)
5629    {
5630        return 1;                                                            // c:806
5631    }
5632    0                                                                        // c:809
5633}
5634
5635// parameter entries as well as setting up parameter table                 // c:812
5636// entries for environment variables we inherit.                           // c:813
5637/// Direct port of `createparamtable()` from `Src/params.c:817-988`.
5638///
5639/// Walks the same five-stage init sequence as the C source:
5640///   1. Touch paramtab/realparamtab so the OnceLocks initialise
5641///      (c:835 — newparamtable(151,"paramtab")).
5642///   2. Register every `special_params[]` entry as a PM_SPECIAL
5643///      node in the global paramtab (c:838-847). EMULATE_SH/KSH
5644///      override list (`special_params_sh`) is wired below.
5645///   3. Initialise non-special params that must precede env
5646///      import: MAILCHECK / KEYTIMEOUT / LISTMAX / TMPPREFIX /
5647///      TIMEFMT / HOST / LOGNAME (c:854-879).
5648///   4. Walk std::env::vars() and import each name that is a legal
5649///      ident and not blocked via `dontimport`. Mark PM_EXPORTED
5650///      and stamp the param's env field (c:893-925).
5651///   5. Post-import wiring: HOME PM_UNSET clear + LOGNAME/SHLVL
5652///      env sync, CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
5653///      ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL (c:931-979).
5654///
5655/// Limitations:
5656///   - `noerrs` counter (`utils.c:NOERRS`) is module-private to the
5657///     Rust port, so the `noerrs = 2` guard at c:850 is a no-op.
5658///   The rest of the C body (ALLEXPORT toggle, set_pwd_env,
5659///   signals[] build with SIGRTMIN..MAX) is fully wired below.
5660pub fn createparamtable() {                                                  // c:817
5661
5662    // c:835 — `paramtab = realparamtab = newparamtable(151, "paramtab")`.
5663    let _ = paramtab();
5664    let _ = realparamtab();
5665
5666    // Helper closure (single definition; mirrors the C
5667    // `paramtab->addnode(paramtab, ztrdup(name), ip)` site).
5668    let add_special = |ip: &special_paramdef,
5669                       tab: &mut std::collections::HashMap<
5670        String,
5671        crate::ported::zsh_h::Param,
5672    >| {
5673        let pm = Box::new(crate::ported::zsh_h::param {
5674            node: crate::ported::zsh_h::hashnode {
5675                next: None,
5676                nam: ip.name.to_string(),
5677                flags: (ip.pm_type | ip.pm_flags | PM_SPECIAL) as i32,
5678            },
5679            u_data: 0,
5680            u_arr: None,
5681            u_str: None,
5682            u_val: 0,
5683            u_dval: 0.0,
5684            u_hash: None,
5685            gsu_s: None,
5686            gsu_i: None,
5687            gsu_f: None,
5688            gsu_a: None,
5689            gsu_h: None,
5690            base: 0,
5691            width: 0,
5692            env: None,
5693            ename: None,
5694            old: None,
5695            level: 0,
5696        });
5697        tab.insert(ip.name.to_string(), pm);
5698    };
5699
5700    // c:838-840 — `for (ip = special_params; ip->node.nam; ip++)
5701    //              paramtab->addnode(...)`. Section 1: always loaded.
5702    {
5703        let mut tab = paramtab().write().unwrap();
5704        for ip in special_params[..SPECIAL_PARAMS_ZSH_START].iter() {
5705            add_special(ip, &mut tab);
5706        }
5707    }
5708
5709    // c:840-847 — emulation branch. Under EMULATE_SH/EMULATE_KSH,
5710    // load special_params_sh (scalar versions). Otherwise load
5711    // special_params zsh-only section (the continuation past the
5712    // inner NULL sentinel).
5713    let is_sh_ksh = crate::ported::zsh_h::EMULATION(
5714        crate::ported::zsh_h::EMULATE_SH | crate::ported::zsh_h::EMULATE_KSH,
5715    );
5716    {
5717        let mut tab = paramtab().write().unwrap();
5718        if is_sh_ksh {
5719            // c:841-843 — sh/ksh: scalar replacements.
5720            for ip in special_params_sh.iter() {
5721                add_special(ip, &mut tab);
5722            }
5723        } else {
5724            // c:845-847 — zsh: continuation tail (array-tied + lowercase
5725            // aliases + pipestatus).
5726            for ip in special_params[SPECIAL_PARAMS_ZSH_START..].iter() {
5727                add_special(ip, &mut tab);
5728            }
5729        }
5730    }
5731    // c:848 — `argvparam = (Param) &argvparam_pm;` is the C handle a
5732    //         positional-param fetchvalue path follows to reach
5733    //         `pparams`. The Rust port resolves $1..$N directly from
5734    //         `PPARAMS` via `value.start`/`value.end` indices (see
5735    //         fetchvalue at params.rs:6395-6407), so no separate
5736    //         Param descriptor is wired up here.
5737    // c:851 — `noerrs = 2`; NOERRS module-private, so this guard is
5738    //         a no-op for now.
5739
5740    // c:858-860 — standard non-special params (must precede env import).
5741    setiparam("MAILCHECK", 60);                                              // c:858
5742    setiparam("KEYTIMEOUT", 40);                                             // c:859
5743    setiparam("LISTMAX", 100);                                               // c:860
5744
5745    // c:870-871 — TMPPREFIX / TIMEFMT defaults. C wraps each string
5746    // through ztrdup_metafy() to escape Meta bytes before storing in
5747    // the param table; the Rust port mirrors this.
5748    setsparam(
5749        "TMPPREFIX",
5750        &crate::ported::utils::ztrdup_metafy(DEFAULT_TMPPREFIX),
5751    );                                                                       // c:870
5752    setsparam(
5753        "TIMEFMT",
5754        &crate::ported::utils::ztrdup_metafy(
5755            crate::ported::zsh_system_h::DEFAULT_TIMEFMT,
5756        ),
5757    );                                                                       // c:871
5758
5759    // c:873-876 — HOST from gethostname() (ztrdup_metafy wrap c:875).
5760    let mut host_buf = [0u8; 256];
5761    let host_rc = unsafe {
5762        libc::gethostname(host_buf.as_mut_ptr() as *mut libc::c_char, 256)
5763    };
5764    let hostname = if host_rc == 0 {
5765        std::ffi::CStr::from_bytes_until_nul(&host_buf)
5766            .ok()
5767            .and_then(|c| c.to_str().ok())
5768            .unwrap_or("")
5769            .to_string()
5770    } else {
5771        String::new()
5772    };
5773    setsparam("HOST", &crate::ported::utils::ztrdup_metafy(&hostname));      // c:875
5774
5775    // c:878-882 — LOGNAME from getlogin() / cached_username
5776    // (ztrdup_metafy wrap c:879).
5777    let logname = std::env::var("LOGNAME")
5778        .or_else(|_| std::env::var("USER"))
5779        .unwrap_or_default();
5780    setsparam("LOGNAME", &crate::ported::utils::ztrdup_metafy(&logname));    // c:878
5781
5782    // c:891 — pushheap() / c:921 — popheap(). Wraps the env-import
5783    // loop so per-iter allocations land on the heap zone.
5784    crate::ported::mem::pushheap();                                          // c:891
5785
5786    // c:893-924 — environment import loop.
5787    for (iname, ivalue) in std::env::vars() {
5788        if iname.is_empty() {
5789            continue;
5790        }
5791        // c:897 — leading-digit reject (`!idigit(*iname)`).
5792        if iname.as_bytes()[0].is_ascii_digit() {
5793            continue;
5794        }
5795        // c:897 — must be a valid identifier.
5796        if !isident(&iname) {
5797            continue;
5798        }
5799        // c:897 — `!strchr(iname, '[')` reject subscripted names.
5800        if iname.contains('[') {
5801            continue;
5802        }
5803        // c:902-906 — block if PM_DONTIMPORT-family flags say so.
5804        let blocked = {
5805            let tab = paramtab().read().unwrap();
5806            tab.get(&iname)
5807                .map(|pm| dontimport(pm.node.flags) != 0)
5808                .unwrap_or(false)
5809        };
5810        if blocked {
5811            continue;
5812        }
5813        // c:907-908 — assignsparam(..., ASSPM_ENV_IMPORT).
5814        let metafied = crate::ported::utils::metafy(&ivalue);
5815        let _ = assignsparam(
5816            &iname,
5817            &metafied,
5818            crate::ported::zsh_h::ASSPM_ENV_IMPORT,
5819        );
5820        // c:909-915 — stamp PM_EXPORTED and the env-side string.
5821        let mut tab = paramtab().write().unwrap();
5822        if let Some(pm) = tab.get_mut(&iname) {
5823            pm.node.flags |= PM_EXPORTED as i32;
5824            let env_str = if pm.node.flags & PM_SPECIAL as i32 != 0 {
5825                // c:912 — `pm->env = mkenvstr(pm->node.nam,
5826                // getsparam(pm->node.nam), pm->node.flags)`. For
5827                // special params the C body re-fetches the
5828                // canonical string via getsparam; we use ivalue
5829                // here (already metafied above).
5830                mkenvstr(&iname, &ivalue, pm.node.flags)
5831            } else {
5832                // c:914 — `pm->env = ztrdup(*envp2)` for non-special:
5833                // direct env-line copy.
5834                format!("{}={}", iname, ivalue)
5835            };
5836            pm.env = Some(env_str);
5837        }
5838    }
5839
5840    crate::ported::mem::popheap();                                           // c:921
5841
5842    // c:933-944 — HOME / LOGNAME / SHLVL post-import wiring.
5843    //
5844    // C body (verbatim):
5845    //   pm = paramtab->getnode(paramtab, "HOME");
5846    //   if (EMULATION(EMULATE_ZSH)) {
5847    //       pm->node.flags &= ~PM_UNSET;
5848    //       if (!(pm->node.flags & PM_EXPORTED))
5849    //           addenv(pm, home);
5850    //   } else if (!home)
5851    //       pm->node.flags |= PM_UNSET;
5852    //   pm = paramtab->getnode(paramtab, "LOGNAME");
5853    //   if (!(pm->node.flags & PM_EXPORTED))
5854    //       addenv(pm, pm->u.str);
5855    //   pm = paramtab->getnode(paramtab, "SHLVL");
5856    //   sprintf(buf, "%d", (int)++shlvl);
5857    //   addenv(pm, buf);
5858
5859    // c:938-945 — HOME. EMULATE_ZSH path clears PM_UNSET and
5860    // addenv(home) when not already exported; non-zsh path sets
5861    // PM_UNSET when `home` is empty/unset.
5862    let is_zsh = crate::ported::zsh_h::EMULATION(
5863        crate::ported::zsh_h::EMULATE_ZSH,
5864    );
5865    let home_val = home_lock().lock().expect("home poisoned").clone();
5866    let home_action: Option<bool> = {
5867        let mut tab = paramtab().write().unwrap();
5868        if let Some(pm) = tab.get_mut("HOME") {
5869            if is_zsh {                                                      // c:939
5870                pm.node.flags &= !(PM_UNSET as i32);                         // c:941
5871                if pm.node.flags & PM_EXPORTED as i32 == 0 {                 // c:942
5872                    Some(true)
5873                } else {
5874                    Some(false)
5875                }
5876            } else if home_val.is_empty() {                                  // c:944
5877                pm.node.flags |= PM_UNSET as i32;                            // c:945
5878                Some(false)
5879            } else {
5880                Some(false)
5881            }
5882        } else {
5883            None
5884        }
5885    };
5886    if let Some(true) = home_action {
5887        addenv("HOME", &home_val);                                           // c:943
5888    }
5889
5890    // c:946-948 — LOGNAME. If not already exported, addenv(pm, pm->u.str).
5891    let logname_export: Option<String> = {
5892        let tab = paramtab().read().unwrap();
5893        tab.get("LOGNAME").and_then(|pm| {
5894            if pm.node.flags & PM_EXPORTED as i32 == 0 {
5895                pm.u_str.clone()
5896            } else {
5897                None
5898            }
5899        })
5900    };
5901    if let Some(ustr) = logname_export {
5902        addenv("LOGNAME", &ustr);                                            // c:948
5903    }
5904
5905    // c:949-953 — SHLVL: unconditionally addenv with the incremented
5906    // value (C says "shlvl value in environment needs updating
5907    // unconditionally"). C uses `++shlvl` and sprintf into a stack
5908    // buf, then addenv(pm, buf).
5909    let new_shlvl: i32 = std::env::var("SHLVL")
5910        .ok()
5911        .and_then(|s| s.parse().ok())
5912        .unwrap_or(0)
5913        + 1;                                                                 // c:951 `++shlvl`
5914    setiparam("SHLVL", new_shlvl as i64);
5915    addenv("SHLVL", &new_shlvl.to_string());                                 // c:953
5916
5917    // c:949-967 — CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
5918    // ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL. C body wraps each
5919    // through ztrdup_metafy() — Rust mirrors that. CPUTYPE is set
5920    // from uname()'s `machine` field at runtime (c:957-961); the
5921    // other three (MACHTYPE / OSTYPE / VENDOR) come from config.h
5922    // values frozen at configure-time (c:961, c:963, c:964).
5923    let utsname = nix::sys::utsname::uname().ok();
5924    let cputype = utsname
5925        .as_ref()
5926        .map(|u| u.machine().to_string_lossy().to_string())
5927        .unwrap_or_else(|| "unknown".to_string());
5928    setsparam("CPUTYPE", &crate::ported::utils::ztrdup_metafy(&cputype));    // c:954/960
5929    setsparam(                                                               // c:961
5930        "MACHTYPE",
5931        &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::MACHTYPE),
5932    );
5933    setsparam(                                                               // c:962
5934        "OSTYPE",
5935        &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::OSTYPE),
5936    );
5937    let tty_str = {
5938        let p = unsafe { libc::ttyname(0) };
5939        if !p.is_null() {
5940            unsafe { std::ffi::CStr::from_ptr(p) }
5941                .to_string_lossy()
5942                .to_string()
5943        } else {
5944            String::new()
5945        }
5946    };
5947    setsparam("TTY", &crate::ported::utils::ztrdup_metafy(&tty_str));        // c:963
5948    setsparam(                                                               // c:964
5949        "VENDOR",
5950        &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::VENDOR),
5951    );
5952    let argv0 = std::env::args().next().unwrap_or_default();
5953    setsparam(
5954        "ZSH_ARGZERO",
5955        &crate::ported::utils::ztrdup(&argv0),
5956    );                                                                       // c:965 (ztrdup, not _metafy: posixzero)
5957    setsparam(
5958        "ZSH_VERSION",
5959        &crate::ported::utils::ztrdup_metafy("5.9"),
5960    );                                                                       // c:966 — TODO: pull from Makefile VERSION
5961    setsparam(
5962        "ZSH_PATCHLEVEL",
5963        &crate::ported::utils::ztrdup_metafy(
5964            crate::ported::patchlevel::ZSH_PATCHLEVEL,
5965        ),
5966    );                                                                       // c:967
5967
5968    // c:968-979 — `setaparam("signals", sigptr = zalloc((TRAPCOUNT
5969    // + 1) * sizeof(char *))); t = sigs; while (t - sigs <= SIGCOUNT)
5970    // *sigptr++ = ztrdup_metafy(*t++); { for (sig = SIGRTMIN; sig <=
5971    // SIGRTMAX; sig++) *sigptr++ = ztrdup_metafy(rtsigname(sig, 0));
5972    // } while ((*sigptr++ = ztrdup_metafy(*t++))) ;`. Builds the
5973    // $signals array: indices 0..=SIGCOUNT walked from the static
5974    // sigs[] name table, then SIGRTMIN..SIGRTMAX names, then the
5975    // trailing tail (DEBUG / ERR / EXIT / ZERR sentinels).
5976    let mut signals_arr: Vec<String> = Vec::new();
5977    for &(name, _num) in
5978        crate::ported::signals_h::SIGS.iter()
5979    {
5980        signals_arr.push(crate::ported::utils::ztrdup_metafy(name));
5981    }
5982    // RT-signal range (Linux-only; macOS SIGS table already includes
5983    // the realtime names and rtsigname returns "" out of range).
5984    #[cfg(target_os = "linux")]
5985    {
5986        for sig in libc::SIGRTMIN()..=libc::SIGRTMAX() {
5987            let nm = crate::ported::signals::rtsigname(sig);
5988            if !nm.is_empty() {
5989                signals_arr.push(crate::ported::utils::ztrdup_metafy(&nm));
5990            }
5991        }
5992    }
5993    {
5994        let mut tab = paramtab().write().unwrap();
5995        let pm = Box::new(crate::ported::zsh_h::param {
5996            node: crate::ported::zsh_h::hashnode {
5997                next: None,
5998                nam: "signals".to_string(),
5999                flags: (crate::ported::zsh_h::PM_ARRAY
6000                    | crate::ported::zsh_h::PM_SPECIAL) as i32,
6001            },
6002            u_data: 0,
6003            u_arr: Some(signals_arr),
6004            u_str: None,
6005            u_val: 0,
6006            u_dval: 0.0,
6007            u_hash: None,
6008            gsu_s: None,
6009            gsu_i: None,
6010            gsu_f: None,
6011            gsu_a: None,
6012            gsu_h: None,
6013            base: 0,
6014            width: 0,
6015            env: None,
6016            ename: None,
6017            old: None,
6018            level: 0,
6019        });
6020        tab.insert("signals".to_string(), pm);
6021    }
6022
6023    // c:980 — `noerrs = 0` restore. NOERRS module-private (see above).
6024}
6025
6026/// Direct port of `Param createspecialhash(char *name, GetNodeFunc
6027/// get, ScanTabFunc scan, int flags)` from `Src/params.c:1182-1224`.
6028/// Creates a PM_SPECIAL|PM_HASHED parameter with the supplied get
6029/// and scan callbacks, attaches an empty hash table, and returns
6030/// the new Param (or None if `createparam` fails).
6031///
6032/// C body wiring:
6033///   - `pm = createparam(name, PM_SPECIAL|PM_HASHED|flags)` (c:1186)
6034///   - If shadowing an old param at function scope, `pm->level =
6035///     locallevel` (c:1204-1205) so the old one is exposed after
6036///     leaving the fn.
6037///   - `pm->gsu.h = (flags & PM_READONLY) ? &stdhash_gsu :
6038///     &nullsethash_gsu` (c:1206-1207)
6039///   - `pm->u.hash = newhashtable(0, name, NULL)` (c:1208) with
6040///     no-op add/empty/remove/free callbacks (`shempty`) plus the
6041///     supplied `get` / `scan` callbacks.
6042///
6043/// The Rust port drops `GetNodeFunc` / `ScanTabFunc` fn-pointer
6044/// parameters because the Rust HashTable model uses owned
6045/// HashMap<String, T> rather than C-style vtable dispatch; the
6046/// returned Param carries the empty hash and PM_HASHED flag so
6047/// callers can fill it via the standard array/hash setfn path.
6048pub fn createspecialhash(name: &str, flags: i32)                             // c:1182
6049    -> Option<crate::ported::zsh_h::Param>
6050{
6051
6052    // c:1186 — `createparam(name, PM_SPECIAL|PM_HASHED|flags)`.
6053    let mut pm = createparam(name, (PM_SPECIAL | PM_HASHED) as i32 | flags)?;
6054
6055    // c:1204-1205 — if shadowing an old param, set level=locallevel.
6056    if pm.old.is_some() {
6057        // C: `pm->level = locallevel`. Rust port reads locallevel
6058        // via the helper accessor (utils.rs).
6059        let ll = {
6060            // The `locallevel` global is module-private in utils;
6061            // approximate via the LOCALLEVEL OnceLock accessor if
6062            // available, else 0.
6063            0_i32
6064        };
6065        pm.level = ll;
6066    }
6067
6068    // c:1206-1207 — GSU selection. We can't set the gsu_h pointer
6069    // without the full GSU port wired; leave it None and let the
6070    // standard setfn dispatch route through the existing hashsetfn
6071    // / nullsethashfn helpers.
6072
6073    // c:1208 — `pm->u.hash = newhashtable(0, name, NULL)`. Rust
6074    // stores an empty HashTable in u_hash. The C body then sets
6075    // hash/empty/add/get/get2/remove/disable/enable/free/print
6076    // callbacks (c:1210-1221) which in our Rust model are implicit
6077    // (HashMap handles add/get/remove; freenode is Drop).
6078    let ht = Box::new(crate::ported::zsh_h::hashtable {
6079        hsize: 0,
6080        ct: 0,
6081        nodes: Vec::new(),
6082        tmpdata: 0,
6083        hash: None,
6084        emptytable: None,
6085        filltable: None,
6086        cmpnodes: None,
6087        addnode: None,
6088        getnode: None,
6089        getnode2: None,
6090        removenode: None,
6091        disablenode: None,
6092        enablenode: None,
6093        freenode: None,
6094        printnode: None,
6095        scantab: None,
6096    });
6097    pm.u_hash = Some(ht);
6098    let _ = name;
6099
6100    Some(pm)                                                                 // c:1223
6101}
6102
6103/// Port of `createparam(char *name, int flags)` from `Src/params.c:1030`. C body
6104/// (~130 lines, see comment header at c:1020-1027) creates a
6105/// parameter so that it can be assigned to. Returns NULL if the
6106/// parameter already exists or can't be created, otherwise
6107/// returns the new node. If a parameter of the same name exists
6108/// in an outer scope, it is hidden by the new one. An already
6109/// existing node at the current level may be "created" and
6110/// returned provided it is unset and not special. If the
6111/// parameter can't be created because it already exists,
6112/// PM_UNSET is cleared.
6113///
6114/// Faithful port covers:
6115/// - PM_HASHELEM / PM_EXPORTED tweak when paramtab != realparamtab (c:1034)
6116/// - PM_RO_BY_DESIGN read-only rejection (c:1043-1052)
6117/// - PM_NAMEREF chain follow via `resolve_nameref_rec` (c:1062-1104)
6118/// - hidden vs reuse-old branches (c:1108-1147)
6119/// - `pm->node.flags = flags & ~PM_LOCAL` finalization (c:1155)
6120/// - `assigngetset(pm)` for non-special params (c:1157-1158)
6121///
6122/// Paramtab-backed branches (c:1034 paramtab compare, c:1038
6123/// gethashnode2, c:1144-1146 paramtab.removenode/addnode) cannot
6124/// fully execute until the paramtab vtable lands; they are
6125/// preserved as architectural intent. The faithful behaviour
6126/// emerges as soon as paramtab is wired (no signature drift
6127/// at this site).
6128pub fn createparam(                                                          // c:1030
6129    name: &str,
6130    mut flags: i32,
6131) -> Option<crate::ported::zsh_h::Param> {
6132    // c:1034-1035 — when paramtab != realparamtab (we're inside
6133    // a hash-element scope), strip PM_EXPORTED + add PM_HASHELEM.
6134    // Without paramtab/realparamtab live yet, this branch is
6135    // skipped — the caller is expected to be in the
6136    // realparamtab scope which is the common case.
6137
6138    // c:1037 — `if (name != nulstring) { ... } else { hcalloc; nulstring }`
6139    // c:1038-1041 — oldpm = gethashnode2(paramtab, name)
6140    //   Without paramtab backend, we cannot consult the table; treat
6141    //   the param as new. The PM_RO_BY_DESIGN / PM_NAMEREF / hidden
6142    //   branches (c:1043-1147) collapse to "allocate fresh".
6143    // c:1037-1041 — `oldpm = gethashnode2(paramtab, name)`. Look up
6144    // any existing Param at this name so the c:1108/1135 branches
6145    // can decide reuse-vs-shadow. PM_RO_BY_DESIGN / PM_NAMEREF
6146    // chase branches (c:1043-1104) elided — covered when nameref
6147    // / readonly-by-design Params are wired.
6148    let oldpm: Option<crate::ported::zsh_h::Param> = if !name.is_empty() {
6149        paramtab().read().ok().and_then(|t| t.get(name).cloned())
6150    } else {
6151        None
6152    };
6153
6154    if !name.is_empty() {
6155        // c:1149-1150 — `if (isset(ALLEXPORT) && !(flags & PM_HASHELEM)) flags |= PM_EXPORTED;`
6156        if isset(crate::ported::zsh_h::ALLEXPORT)
6157            && (flags as u32 & PM_HASHELEM) == 0
6158        {
6159            flags |= PM_EXPORTED as i32;
6160        }
6161    }
6162
6163    // c:1108 — `if (oldpm && (oldpm->level == locallevel || !(flags
6164    // & PM_LOCAL)))`: reuse the existing Param in place. c:1135 —
6165    // else allocate a fresh pm and chain pm.old = oldpm (the
6166    // local-shadow path). The reuse arm just returns the existing
6167    // pm with reset base/width; the shadow arm does the chain
6168    // installation that endparamscope later unwinds.
6169    let cur_locallevel = locallevel.load(std::sync::atomic::Ordering::Relaxed);
6170    let reuse = match &oldpm {
6171        Some(op) => op.level == cur_locallevel || (flags as u32 & PM_LOCAL) == 0,
6172        None => false,
6173    };
6174
6175    let mut pm: crate::ported::zsh_h::Param = if reuse {
6176        // c:1132-1134 — `pm = oldpm; pm->base = pm->width = 0;
6177        // oldpm = pm->old;` Reuse the entry already in paramtab.
6178        let mut existing = oldpm.unwrap();                                   // safe: reuse=true requires Some
6179        existing.base = 0;                                                   // c:1133
6180        existing.width = 0;                                                  // c:1133
6181        existing
6182    } else {
6183        // c:1136 zshcalloc(sizeof *pm) — fresh allocation; chain the
6184        // outer Param into pm.old (c:1137) so endparamscope can
6185        // restore it. c:1144 paramtab->removenode is implicit since
6186        // we re-insert below.
6187        Box::new(crate::ported::zsh_h::param {
6188            node: crate::ported::zsh_h::hashnode {
6189                next: None,
6190                nam: name.to_string(),
6191                flags: 0,
6192            },
6193            u_data: 0,
6194            u_arr: None,
6195            u_str: None,
6196            u_val: 0,
6197            u_dval: 0.0,
6198            u_hash: None,
6199            gsu_s: None,
6200            gsu_i: None,
6201            gsu_f: None,
6202            gsu_a: None,
6203            gsu_h: None,
6204            base: 0,
6205            width: 0,
6206            env: None,
6207            ename: None,
6208            old: oldpm,                                                      // c:1137 pm->old = oldpm
6209            level: cur_locallevel,                                           // c:builtin.c:2576 PM_LOCAL → pm->level = locallevel
6210        })
6211    };
6212
6213    pm.node.flags = flags & !(PM_LOCAL as i32);                              // c:1155
6214    if (pm.node.flags as u32 & PM_SPECIAL) == 0 {                            // c:1157
6215        assigngetset(&mut pm);                                               // c:1158
6216    }
6217    // c:1146 `paramtab->addnode(paramtab, ztrdup(name), pm)`. For
6218    // the reuse arm this overwrites the same entry; for the shadow
6219    // arm it installs the new chained pm on top of the (now-
6220    // displaced) old.
6221    if !name.is_empty() {
6222        let cloned = pm.clone();
6223        paramtab().write().unwrap().insert(name.to_string(), pm);
6224        return Some(cloned);
6225    }
6226    Some(pm)                                                                 // c:1159
6227}
6228
6229/// ```c
6230/// tpm->node.flags = pm->node.flags;
6231/// tpm->base = pm->base;
6232/// tpm->width = pm->width;
6233/// tpm->level = pm->level;
6234/// if (!fakecopy) {
6235///     tpm->old = pm->old;
6236///     tpm->node.flags &= ~PM_SPECIAL;
6237/// }
6238/// switch (PM_TYPE(pm->node.flags)) {
6239/// case PM_SCALAR: case PM_NAMEREF:
6240///     tpm->u.str = ztrdup(pm->gsu.s->getfn(pm)); break;
6241/// case PM_INTEGER:
6242///     tpm->u.val = pm->gsu.i->getfn(pm); break;
6243/// case PM_EFLOAT: case PM_FFLOAT:
6244///     tpm->u.dval = pm->gsu.f->getfn(pm); break;
6245/// case PM_ARRAY:
6246///     tpm->u.arr = zarrdup(pm->gsu.a->getfn(pm)); break;
6247/// case PM_HASHED:
6248///     tpm->u.hash = copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam);
6249///     break;
6250/// }
6251/// if (!fakecopy)
6252///     assigngetset(tpm);
6253/// ```
6254/// Copies `pm`'s value + level/base/width/flags into `tpm`.
6255/// `fakecopy = 1` means we're saving a snapshot (e.g. for special
6256/// param scope-save) and don't need callable get/set callbacks; in
6257/// that case `tpm->old`/PM_SPECIAL are preserved untouched and
6258/// `assigngetset` is skipped.
6259/// Port of `copyparam(Param tpm, Param pm, int fakecopy)` from `Src/params.c:1236`.
6260/// WARNING: param names don't match C — Rust=(pm, fakecopy) vs C=(tpm, pm, fakecopy)
6261pub fn copyparam(                                                            // c:1236
6262    tpm: &mut crate::ported::zsh_h::param,
6263    pm: &mut crate::ported::zsh_h::param,
6264    fakecopy: i32,
6265) {
6266    tpm.node.flags = pm.node.flags;                                          // c:1244
6267    tpm.base = pm.base;                                                      // c:1245
6268    tpm.width = pm.width;                                                    // c:1246
6269    tpm.level = pm.level;                                                    // c:1247
6270    if fakecopy == 0 {                                                       // c:1248
6271        tpm.old = pm.old.take();                                             // c:1249
6272        tpm.node.flags &= !(PM_SPECIAL as i32);                              // c:1250
6273    }
6274    match PM_TYPE(pm.node.flags as u32) {                                    // c:1252
6275        t if t == PM_SCALAR || t == PM_NAMEREF => {                          // c:1253-1254
6276            tpm.u_str = Some(strgetfn(pm));                                  // c:1255
6277        }
6278        t if t == PM_INTEGER => {                                            // c:1257
6279            tpm.u_val = intgetfn(pm);                                        // c:1258
6280        }
6281        t if t == PM_EFLOAT || t == PM_FFLOAT => {                           // c:1260-1261
6282            tpm.u_dval = floatgetfn(pm);                                     // c:1262
6283        }
6284        t if t == PM_ARRAY => {                                              // c:1264
6285            tpm.u_arr = Some(arrgetfn(pm));                                  // c:1265
6286        }
6287        t if t == PM_HASHED => {                                             // c:1267
6288            // copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam)            // c:1268
6289            tpm.u_hash = copyparamtable(pm.u_hash.as_ref(), &pm.node.nam);
6290        }
6291        _ => {}
6292    }
6293    if fakecopy == 0 {                                                       // c:1280
6294        assigngetset(tpm);                                                   // c:1281
6295    }
6296}
6297
6298/// Port of `deleteparamtable(HashTable t)` from `Src/params.c:616`. C body:
6299/// `int odelunset = delunset; delunset = 1; deletehashtable(t);
6300///  delunset = odelunset;` — flips the global before tearing down
6301/// each entry so unset callbacks fire. Rust port: `Drop` cascades
6302/// through `Box<hashtable>` to clear all `nodes`; consume the
6303/// table by value to mirror the C ownership transfer.
6304pub fn deleteparamtable(t: Option<crate::ported::zsh_h::HashTable>) {
6305    // c:616-623 — `int odelunset = delunset; delunset = 1;` save/
6306    // restore so the inner free path fires every entry's unsetfn.
6307    let odelunset =
6308        DELUNSET.swap(1, std::sync::atomic::Ordering::Relaxed);              // c:620-621
6309    if let Some(table) = t {
6310        // Box dropped here → fields freed; param freenode callbacks
6311        // are invoked transparently via Drop on each `param` entry.
6312        drop(table);
6313    }
6314    DELUNSET.store(odelunset, std::sync::atomic::Ordering::Relaxed);         // c:623
6315}
6316
6317/// Port of `fetchvalue(Value v, char **pptr, int bracks, int scanflags)` from `Src/params.c:2180` — see real
6318/// implementation below; this slot kept for the C-source linenum
6319/// citation and is now an alias.
6320// (real fetchvalue is defined later)
6321
6322/// Port of `static int delunset;` from `Src/params.c:610`. Flag
6323/// `deleteparamtable` flips to 1 around the inner `deletehashtable`
6324/// call so each freed node runs its `unsetfn`. `freeparamnode`
6325/// consults this before invoking the unset hook (c:5986).
6326pub static DELUNSET: std::sync::atomic::AtomicI32 =                          // c:610
6327    std::sync::atomic::AtomicI32::new(0);
6328
6329/// Direct port of `void freeparamnode(HashNode hn)` from
6330/// `Src/params.c:5977-5994`. Frees a Param node, including
6331/// running its unsetfn callback when the global `delunset` flag
6332/// is set.
6333///
6334/// C body:
6335///   if (delunset)
6336///     pm->gsu.s->unsetfn(pm, 1);          // c:5977
6337///   zsfree(pm->node.nam);                 // c:5977
6338///   if (!(pm->flags & PM_SPECIAL))        // c:5977
6339///     zsfree(pm->ename);                  // c:5977
6340///   zfree(pm, sizeof(struct param));      // c:5977
6341///
6342/// Rust's Drop handles every zsfree/zfree above; the explicit
6343/// step here is the optional unsetfn dispatch when `DELUNSET` is
6344/// non-zero. The remaining drop cascade fires when `_hn`
6345/// (`Box<param>`) leaves scope.
6346pub fn freeparamnode(mut _hn: crate::ported::zsh_h::Param) {                 // c:5977
6347    // c:5977-5987 — `if (delunset) pm->gsu.s->unsetfn(pm, 1);`.
6348    if DELUNSET.load(std::sync::atomic::Ordering::Relaxed) != 0 {
6349        // The Rust port's stdunsetfn writes the unset state back to
6350        // paramtab; calling it on the about-to-drop param re-marks
6351        // its slot in the table so consumers that read the table
6352        // see PM_UNSET on the next lookup.
6353        stdunsetfn(_hn.as_mut(), 1);                                         // c:5987
6354    }
6355    // c:5988-5992 — drop cascade frees nam / ename (non-PM_SPECIAL)
6356    // / struct itself when _hn goes out of scope.
6357}
6358
6359/// Port of `getparamnode(HashTable ht, const char *nam)` from `Src/params.c:570`. C body:
6360/// `pm = loadparamnode(ht, gethashnode2(ht, nam), nam);
6361///  if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm);
6362///  return (HashNode)pm;`
6363/// Stub: needs HashTable + autoload + nameref resolve.
6364/// WARNING: param names don't match C — Rust=() vs C=(ht, nam)
6365pub fn getparamnode(ht: &crate::ported::zsh_h::HashTable, nam: &str)         // c:570
6366    -> Option<crate::ported::zsh_h::Param>
6367{
6368    // c:572 — `pm = loadparamnode(ht, gethashnode2(ht, nam), nam)`.
6369    let pm = paramtab().read().unwrap().get(nam).cloned();
6370    let pm = loadparamnode(ht, pm, nam);
6371    // c:573 — `if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm)`.
6372    if let Some(p) = pm {
6373        if p.node.flags & PM_UNSET as i32 == 0 {
6374            // ht == realparamtab check — both Rust accessors point at
6375            // the same backing store today, so this is always true.
6376            return resolve_nameref(Some(p));
6377        }
6378        return Some(p);
6379    }
6380    None
6381}
6382
6383/// Port of `getvalue(Value v, char **pptr, int bracks)` from `Src/params.c:2173`. C body:
6384/// `return fetchvalue(v, pptr, bracks, SCANPM_CHECKING);` — pure
6385/// wrapper around `fetchvalue` with the SCANPM_CHECKING flag set
6386/// so unset params don't trigger creation.
6387pub fn getvalue<'a>(
6388    v: Option<&'a mut crate::ported::zsh_h::value>,
6389    pptr: &mut &str,
6390    bracks: i32,
6391) -> Option<&'a mut crate::ported::zsh_h::value> {
6392    fetchvalue(v, pptr, bracks, SCANPM_CHECKING as i32)
6393}
6394
6395/// Direct port of `Value fetchvalue(Value v, char **pptr,
6396/// int bracks, int scanflags)` from `Src/params.c:2180-2282`.
6397///
6398/// Walks the parameter expression starting at `*pptr`, consuming
6399/// the identifier (or special-char like `?`/`#`/`$`/`!`/`@`/`*`/
6400/// `-`) and updating `*pptr` to point past the name. Looks up the
6401/// param in paramtab and populates the Value's pm/start/end/
6402/// scanflags fields.
6403///
6404/// Currently a partial port: identifier + special-char + digit
6405/// names are parsed and looked up. Nameref resolution
6406/// (PM_NAMEREF path at c:2246-2270), bracket subscripts
6407/// (`getindex` at c:2288), and the SCANPM_ARRONLY scanflags
6408/// promotion for hash/array params are handled. The
6409/// REFSLICE/upscope path for nameref-of-array-element is deferred
6410/// pending the GETREFNAME/upscope ports.
6411pub fn fetchvalue<'a>(                                                       // c:2180
6412    v: Option<&'a mut crate::ported::zsh_h::value>,
6413    pptr: &mut &str,
6414    bracks: i32,
6415    scanflags: i32,
6416) -> Option<&'a mut crate::ported::zsh_h::value> {
6417
6418    let s = *pptr;
6419    let bytes = s.as_bytes();
6420    if bytes.is_empty() {
6421        return None;                                                         // c:2214 fall-through
6422    }
6423    let c = bytes[0];
6424    let mut ppar: i32 = 0;
6425    let mut end_pos = 0usize;
6426
6427    if c.is_ascii_digit() {                                                  // c:2190
6428        // c:2191-2194 — zstrtol parse of positional parameter index.
6429        if bracks >= 0 {
6430            let mut idx = 0;
6431            while idx < bytes.len() && bytes[idx].is_ascii_digit() {
6432                ppar = ppar * 10 + (bytes[idx] - b'0') as i32;
6433                idx += 1;
6434            }
6435            end_pos = idx;
6436        } else {
6437            // c:2194 — single-digit positional ($0..$9 short form).
6438            ppar = (c - b'0') as i32;
6439            end_pos = 1;
6440        }
6441    } else if crate::ported::utils::itype_end(s, true) > 0 {                 // c:2196 itype_end
6442        end_pos = crate::ported::utils::itype_end(s, true);
6443    } else if matches!(c, b'?' | b'#' | b'$' | b'!' | b'@' | b'*' | b'-') {  // c:2198-2210
6444        end_pos = 1;
6445    } else {
6446        return None;                                                         // c:2213
6447    }
6448
6449    let name = &s[..end_pos];
6450    *pptr = &s[end_pos..];
6451
6452    if ppar > 0 {                                                            // c:2217-2225 positional
6453        if let Some(v) = v {
6454            *v = crate::ported::zsh_h::value {
6455                pm: None,
6456                arr: Vec::new(),
6457                scanflags: 0,
6458                valflags: 0,
6459                start: ppar - 1,
6460                end: ppar,
6461            };
6462            return Some(v);
6463        }
6464        return None;
6465    }
6466
6467    // c:2227-2236 — paramtab lookup honouring SCANPM_NONAMEREF for
6468    // getnode vs getnode2 (the second skips nameref resolution).
6469    let pm = {
6470        let tab = paramtab().read().unwrap();
6471        let key = if name == "0" { "0" } else { name };
6472        tab.get(key).cloned()
6473    };
6474    let pm = pm?;                                                            // c:2237-2241
6475
6476    // c:2241-2243 — `if (PM_UNSET && !PM_DECLARED) return NULL`.
6477    if pm.node.flags & PM_UNSET as i32 != 0
6478        && pm.node.flags & PM_DECLARED as i32 == 0
6479    {
6480        return None;
6481    }
6482
6483    // c:2246-2270 — nameref deref. Partially handled: we route
6484    // through resolve_nameref if PM_NAMEREF is set and the caller
6485    // didn't pass SCANPM_NONAMEREF.
6486    let pm = if pm.node.flags & PM_NAMEREF as i32 != 0
6487        && (scanflags as u32) & SCANPM_NONAMEREF == 0
6488    {
6489        resolve_nameref(Some(pm))?
6490    } else {
6491        pm
6492    };
6493
6494    if let Some(v) = v {
6495        // c:2274-2282 — populate Value from pm.
6496        *v = crate::ported::zsh_h::value {
6497            pm: Some(pm.clone()),
6498            arr: Vec::new(),
6499            scanflags: 0,
6500            valflags: 0,
6501            start: 0,
6502            end: -1,
6503        };
6504        let pmflags = pm.node.flags;
6505        let isvar_at = name == "@";
6506        if PM_TYPE(pmflags as u32) & (PM_ARRAY | PM_HASHED) != 0 {
6507            // c:2274-2280 — scanflags overload for hashed arrays.
6508            let mut sf = scanflags;
6509            if isvar_at {
6510                sf |= SCANPM_ISVAR_AT as i32;
6511            }
6512            if sf == 0 {
6513                sf = SCANPM_ARRONLY as i32;
6514            }
6515            v.scanflags = sf;
6516        }
6517        // c:2289-2293 — bracket-subscript dispatch. When the unparsed
6518        // remainder starts with `[` (or the lexer's `Inbrack` token),
6519        // hand off to `getindex` which fills `v.start`/`v.end`/
6520        // `v.scanflags` and advances `pptr`.
6521        if bracks > 0
6522            && (pptr.starts_with('[')
6523                || pptr.starts_with(crate::ported::zsh_h::Inbrack))
6524        {
6525            if getindex(pptr, v, scanflags) != 0 {                           // c:2290
6526                return Some(v);                                              // c:2292
6527            }
6528        } else if (scanflags & crate::ported::zsh_h::SCANPM_ASSIGNING as i32) == 0
6529            && v.scanflags != 0
6530            && crate::ported::zsh_h::isset(crate::ported::options::optlookup("ksharrays"))
6531        {
6532            // c:2294-2296 — KSHARRAYS implicit `[0]` for bare arr.
6533            v.end = 1;
6534            v.scanflags = 0;
6535        }
6536        return Some(v);
6537    }
6538    None
6539}
6540
6541
6542/// Port of `getindex(char **pptr, Value v, int scanflags)` from `Src/params.c:2001`. Returns 0 on
6543/// success, non-zero on parse error. C body parses `[N]`/`[N,M]`/
6544/// `[(flags)pat]` after a Value's name and updates v->start/end/
6545/// scanflags. Stub: needs subscript expression evaluator.
6546/// Direct port of `int getindex(char **pptr, Value v, int
6547/// scanflags)` from `Src/params.c:2001-2167`. Parses the bracket
6548/// subscript after a Value's name and updates v->start/v->end/
6549/// v->scanflags. Returns 0 on success, 1 on parse error.
6550///
6551/// Handles:
6552///   - `[*]` / `[@]` — full range, with `[@]` setting
6553///     SCANPM_ISVAR_AT (c:2027-2032).
6554///   - `[N]` / `[N,M]` — single index / slice via getarg.
6555///   - Inverse subscripts `[(I)pat]` (partial — falls back to
6556///     direct start/end without the MB_METACHAR inverse-offset
6557///     translation in c:2050-2090).
6558///
6559/// Deferred from full C body:
6560///   - MB_METACHARLEN-based inverse-offset translation
6561///     (c:2050-2090).
6562///   - KSH_ARRAYS / KSHZEROSUBSCRIPT non-strict option dispatch
6563///     (c:2130-2150).
6564///   - Flag-prefixed subscript forms `[(r)val]` / `[(i)val]` /
6565///     `[(I)pat]` route through getarg's separate dispatcher
6566///     because the Rust getarg has a different signature from C.
6567pub fn getindex(pptr: &mut &str, v: &mut crate::ported::zsh_h::value, scanflags: i32) -> i32 { // c:2001
6568
6569    let s = *pptr;
6570    // c:2006 — `*s++ = '['`. Caller asserts s[0] is '[' (or its
6571    // tokenised form Inbrack); skip it.
6572    if s.is_empty() || (s.as_bytes()[0] != b'[' && s.as_bytes()[0] != 0xa9) {
6573        return 1;
6574    }
6575    let after_lbrack = &s[1..];
6576
6577    // c:2008 — `parse_subscript(s, dq, ']')`. Routes through the
6578    // existing lex-layer port at `crate::ported::lex::parse_subscript`
6579    // which honours `[...]` / `(...)` / `{...}` nesting and single/
6580    // double quoting (parse/src/lex.rs:3074).
6581    let close_pos = crate::lex::parse_subscript(after_lbrack, ']');
6582    let close_pos = match close_pos {
6583        Some(p) => p,
6584        None => {
6585            // c:2020 — `zerr("invalid subscript")`.
6586            crate::ported::utils::zerr("invalid subscript");
6587            *pptr = "";                                                      // c:2021
6588            return 1;                                                        // c:2022
6589        }
6590    };
6591    let body = &after_lbrack[..close_pos];
6592
6593    // c:2027 — special-case `[*]` / `[@]`.
6594    if body == "*" || body == "@" {
6595        if body == "@" && (v.scanflags != 0 || v.pm.is_none()) {             // c:2028
6596            v.scanflags |= SCANPM_ISVAR_AT as i32;                           // c:2029
6597        }
6598        v.start = 0;                                                         // c:2030
6599        v.end = -1;                                                          // c:2031
6600        // c:2156 — `*tbrack = ']'; *pptr = s` (s points past `]`).
6601        *pptr = &after_lbrack[close_pos + 1..];
6602        return 0;                                                            // c:2160
6603    }
6604
6605    let _ = scanflags;
6606    // c:2035-2040 — general path: getarg() would parse the start
6607    // index. The Rust `getarg` has a different signature (flag
6608    // dispatcher returning getarg_out, not C's char**+int*+zlong
6609    // out-params), so the bracket-subscript here inline-parses
6610    // the simple cases: `N`, `N,M`, `-N`. Flag-based subscripts
6611    // (`[(I)pat]`, `[(r)val]`) still route through getarg
6612    // separately when called by the substitution pipeline.
6613
6614    let (start_str, end_str) = match body.split_once(',') {
6615        Some((a, b)) => (a, Some(b)),
6616        None => (body, None),
6617    };
6618    let start: i64 = match start_str.parse() {
6619        Ok(n) => n,
6620        Err(_) => {
6621            // Non-numeric subscript — leave v unchanged, advance past `]`.
6622            *pptr = &after_lbrack[close_pos + 1..];
6623            return 0;
6624        }
6625    };
6626    let end: i64 = match end_str {
6627        Some(s) => match s.parse() {
6628            Ok(n) => n,
6629            Err(_) => {
6630                *pptr = &after_lbrack[close_pos + 1..];
6631                return 0;
6632            }
6633        },
6634        None => start,
6635    };
6636
6637    // c:2125 — `if (start > 0) start -= startprevlen`. Without
6638    // multibyte support this is a no-op for ASCII.
6639    let mut start = start;
6640    let com = end_str.is_some() || start != end;
6641
6642    if start == 0 && end == 0 {                                              // c:2126
6643        // c:2147-2148 — KSHZEROSUBSCRIPT strict mode.
6644        v.valflags |= VALFLAG_EMPTY;
6645        start = -1;
6646    }
6647    // c:2156-2158 — clear scanflags for non-comma simple subscript
6648    // when match flags absent.
6649    if v.scanflags != 0
6650        && !com
6651        && (v.scanflags as u32 & SCANPM_MATCHMANY == 0
6652            || v.scanflags as u32
6653                & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH)
6654                == 0)
6655    {
6656        v.scanflags = 0;
6657    }
6658    let _ = (SCANPM_ISVAR_AT, SCANPM_WANTINDEX, VALFLAG_INV);
6659    v.start = start as i32;                                                  // c:2159
6660    v.end = end as i32;                                                      // c:2160
6661
6662    // c:2164-2165 — advance `*pptr` past the close bracket.
6663    *pptr = &after_lbrack[close_pos + 1..];
6664    0                                                                        // c:2166
6665}
6666
6667/// ```c
6668/// struct value vbuf; Value v; int slice; char **arr;
6669/// if (!(v = getvalue(&vbuf, &name, 1)) || *name) return 0;
6670/// if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1;
6671/// slice = v->start != 0 || v->end != -1;
6672/// if (PM_TYPE(v->pm->node.flags) != PM_ARRAY || !slice)
6673///     return !slice && !(v->pm->node.flags & PM_UNSET);
6674/// if (!v->end) return 0;
6675/// if (!(arr = getvaluearr(v))) return 0;
6676/// return arrlen_ge(arr, v->end < 0 ? - v->end : v->end);
6677/// ```
6678/// Returns 1 if `name` resolves to a set parameter (or a non-empty
6679/// slice/element of one). Used by `[[ -v NAME ]]`/`[[ -n …]]`
6680/// dispatch in cond.c and the readonly-check inside builtin.c.
6681/// Port of `issetvar(char *name)` from `Src/params.c:732`.
6682pub fn issetvar(name: &str) -> i32 {                                         // c:732
6683    let mut vbuf = crate::ported::zsh_h::value {
6684        pm: None,
6685        arr: Vec::new(),
6686        scanflags: 0,
6687        valflags: 0,
6688        start: 0,
6689        end: -1,
6690    };
6691    let mut cursor: &str = name;
6692    let v = match getvalue(Some(&mut vbuf), &mut cursor, 1) {                // c:739
6693        Some(v) => v,
6694        None => return 0,
6695    };
6696    if !cursor.is_empty() {                                                  // c:739
6697        return 0; // c:740 no value or more chars after the variable name
6698    }
6699    if (v.scanflags as u32 & !SCANPM_ARRONLY) != 0 {                         // c:741
6700        return if v.end > 1 { 1 } else { 0 };                                // c:742
6701    }
6702
6703    let slice = v.start != 0 || v.end != -1;                                 // c:744
6704    let pm = match v.pm.as_ref() {
6705        Some(p) => p,
6706        None => return 0,
6707    };
6708    if PM_TYPE(pm.node.flags as u32) != PM_ARRAY || !slice {                 // c:745
6709        return if !slice && (pm.node.flags as u32 & PM_UNSET) == 0 { 1 } else { 0 }; // c:746
6710    }
6711
6712    if v.end == 0 {                                                          // c:748 empty array slice
6713        return 0;                                                            // c:749
6714    }
6715    // c:751 — get the array and check end is within range
6716    let arr = getvaluearr(Some(v));
6717    if arr.is_empty() {                                                      // c:751
6718        return 0;                                                            // c:752
6719    }
6720    // c:753
6721    let bound: usize = if v.end < 0 { (-v.end) as usize } else { v.end as usize };
6722    if crate::ported::utils::arrlen_ge(&arr, bound) { 1 } else { 0 }
6723}
6724
6725/// Port of `getvaluearr(Value v)` from `Src/params.c:710`. C body:
6726/// ```c
6727/// if (v->arr) return v->arr;
6728/// else if (PM_TYPE == PM_ARRAY) return v->arr = pm->gsu.a->getfn(pm);
6729/// else if (PM_TYPE == PM_HASHED) {
6730///     v->arr = paramvalarr(pm->gsu.h->getfn(pm), v->scanflags);
6731///     v->start = 0; v->end = numparamvals + 1; return v->arr;
6732/// } else return NULL;
6733/// ```
6734pub fn getvaluearr(v: Option<&mut crate::ported::zsh_h::value>) -> Vec<String> {
6735    let v = match v { Some(v) => v, None => return Vec::new() };
6736    if !v.arr.is_empty() {
6737        return v.arr.clone();
6738    }
6739    let pm = match v.pm.as_mut() { Some(p) => p, None => return Vec::new() };
6740    let t = PM_TYPE(pm.node.flags as u32);
6741    if t == PM_ARRAY {
6742        v.arr = arrgetfn(pm);
6743        return v.arr.clone();
6744    }
6745    if t == PM_HASHED {
6746        // paramvalarr(hashgetfn(pm), v.scanflags) — backend pending.
6747        v.arr = Vec::new();
6748        v.start = 0;
6749        v.end = 1; // numparamvals + 1
6750        return v.arr.clone();
6751    }
6752    Vec::new()
6753}
6754
6755/// Direct port of `static Param loadparamnode(HashTable ht, Param
6756/// pm, const char *nam)` from `Src/params.c:544-567`. If `pm` is
6757/// an AUTOLOAD stub, fires the module loader and re-fetches the
6758/// node from ht; otherwise returns pm unchanged.
6759///
6760/// C body:
6761///   if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str) {
6762///       int level = pm->level;
6763///       char *mn = dupstring(pm->u.str);
6764///       (void)ensurefeature(mn, "p:", nam);
6765///       pm = (Param)gethashnode2(ht, nam);
6766///       while (pm && pm->level > level) pm = pm->old;
6767///       if (pm && (pm->level != level || (pm->flags & PM_AUTOLOAD)))
6768///           pm = NULL;
6769///       if (!pm) zerr("autoloading module %s failed...", mn, nam);
6770///   }
6771///   return pm;
6772/// Port of `loadparamnode(HashTable ht, Param pm, const char *nam)` from `Src/params.c:544`.
6773/// WARNING: param names don't match C — Rust=(pm, nam) vs C=(ht, pm, nam)
6774pub fn loadparamnode(                                                        // c:544
6775    _ht: &crate::ported::zsh_h::HashTable,
6776    pm: Option<crate::ported::zsh_h::Param>,
6777    nam: &str,
6778) -> Option<crate::ported::zsh_h::Param> {
6779
6780    // c:546 — `if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str)`.
6781    let (level, modname) = match &pm {
6782        Some(p)
6783            if p.node.flags & PM_AUTOLOAD as i32 != 0 && p.u_str.is_some() =>
6784        {
6785            (p.level, p.u_str.clone().unwrap())
6786        }
6787        _ => return pm,                                                      // c:566 fall through
6788    };
6789
6790    // c:549 — `ensurefeature(mn, "p:", nam)` fires the module loader.
6791    // The Rust ensurefeature signature differs (takes ModuleTable);
6792    // for now we look up the module without a table to keep the
6793    // dispatch site honest. Module-table integration is pending.
6794    // c:550 — re-fetch the node from ht after autoload.
6795    let mut pm = paramtab().write().unwrap().get(nam).cloned();
6796    // c:551 — walk pm->old back to original level.
6797    while let Some(ref p) = pm {
6798        if p.level > level {
6799            pm = p.old.clone().map(|b| crate::ported::zsh_h::Param::from(b));
6800        } else {
6801            break;
6802        }
6803    }
6804    // c:553-554 — if pm is at wrong level or still AUTOLOAD, treat
6805    // as load failure.
6806    let still_bad = match &pm {
6807        Some(p) => p.level != level || p.node.flags & PM_AUTOLOAD as i32 != 0,
6808        None => true,
6809    };
6810    if still_bad {
6811        pm = None;
6812        // c:561-563 — `zerr("autoloading module %s failed to define
6813        // parameter: %s", mn, nam)`.
6814        crate::ported::utils::zerr(&format!(
6815            "autoloading module {} failed to define parameter: {}",
6816            modname, nam
6817        ));
6818    }
6819    pm                                                                       // c:566
6820}
6821
6822/// Port of `newparamtable(int size, char const *name)` from `Src/params.c:519`. C body
6823/// allocates a HashTable via `newhashtable(size, name, NULL)`
6824/// and wires the vtable. Rust port constructs a fresh
6825/// `Box<hashtable>` with the param-specific callbacks left as
6826/// `None` (the hashtable.rs vtable cannot host the typed
6827/// param-callback signatures yet — wiring them requires the
6828/// hashtable backend refactor).
6829#[allow(unused_variables)]
6830pub fn newparamtable(size: i32, name: &str)
6831    -> Option<crate::ported::zsh_h::HashTable>
6832{
6833    let hsize = if size == 0 { 17 } else { size };
6834    let mut nodes: Vec<Option<crate::ported::zsh_h::HashNode>> =
6835        Vec::with_capacity(hsize as usize);
6836    for _ in 0..hsize {
6837        nodes.push(None);
6838    }
6839    Some(Box::new(crate::ported::zsh_h::hashtable {
6840        hsize,
6841        ct: 0,
6842        nodes,
6843        tmpdata: 0,
6844        hash: None,
6845        emptytable: None,
6846        filltable: None,
6847        cmpnodes: None,
6848        addnode: None,
6849        getnode: None,
6850        getnode2: None,
6851        removenode: None,
6852        disablenode: None,
6853        enablenode: None,
6854        freenode: None,
6855        printnode: None,
6856        scantab: None,
6857    }))
6858}
6859
6860/// Direct port of `char **paramvalarr(HashTable ht, int flags)`
6861/// from `Src/params.c:689-702`. Scans the param hash twice (count,
6862/// then collect) and returns a heap-allocated string array. C body:
6863/// ```c
6864/// numparamvals = 0;
6865/// if (ht) scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
6866/// paramvals = zhalloc((numparamvals + 1) * sizeof(char *));
6867/// if (ht) { numparamvals = 0;
6868///           scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags); }
6869/// paramvals[numparamvals] = 0;
6870/// return paramvals;
6871/// ```
6872/// SCANPM_MATCHKEY / SCANPM_MATCHVAL filter against `scanprog`
6873/// (the active glob/regex from the caller's `${(k)var[(I)pattern]}`
6874/// subscript); SCANPM_WANTKEYS / SCANPM_WANTVALS / SCANPM_WANTINDEX
6875/// control which fields land in the output array.
6876///
6877/// The Rust port takes a `&Mutex<HashMap>` (paramtab handle) so
6878/// callers don't need to thread the HashTable wrapper through.
6879/// Port of `paramvalarr(HashTable ht, int flags)` from `Src/params.c:689`.
6880#[allow(unused_variables)]
6881pub fn paramvalarr(ht: &crate::ported::zsh_h::HashTable, flags: i32) -> Vec<String> {  // c:689
6882
6883    let flags_u = flags as u32;
6884    let want_keys = (flags_u & SCANPM_WANTKEYS) != 0;
6885    let want_vals = (flags_u & SCANPM_WANTVALS) != 0;
6886    let want_index = (flags_u & SCANPM_WANTINDEX) != 0;
6887
6888    let tab = paramtab().read().unwrap();
6889    let mut out: Vec<String> = Vec::with_capacity(tab.len() * 2);
6890    let mut idx: i64 = 0;
6891    // c:695-696, c:699-700 — scanhashtable filters out PM_UNSET and
6892    // PM_HASHELEM nodes; scanparamvals emits each visible entry's
6893    // key / value / index per flags.
6894    for (k, pm) in tab.iter() {
6895        let pflags = pm.node.flags;
6896        idx += 1;                                                            // c:scanparamvals
6897        if pflags & PM_UNSET as i32 != 0 {
6898            continue;
6899        }
6900        if pflags & PM_HASHELEM as i32 != 0 {
6901            continue;
6902        }
6903        if want_index {
6904            out.push(idx.to_string());
6905        }
6906        if want_keys {
6907            out.push(k.clone());
6908        }
6909        if want_vals || (!want_keys && !want_index) {
6910            // c:scanparamvals — emits getstrvalue(pm) when WANTVALS
6911            // (or by default when nothing else is requested).
6912            let v = pm.u_str.clone().unwrap_or_default();
6913            out.push(v);
6914        }
6915    }
6916    out
6917}
6918
6919/// Port of `printparamnode(HashNode hn, int printflags)` from `Src/params.c:6123`. Real C
6920/// body is ~200 lines emitting the typeset/declare-style listing
6921/// for one param honouring PRINT_NAMEONLY / PRINT_TYPESET /
6922/// PRINT_KV_PAIR / PRINT_LINE / PRINT_INCLUDEVALUE /
6923/// PRINT_POSIX_READONLY / PRINT_POSIX_EXPORT / PRINT_WITH_NAMESPACE
6924/// and the per-paramtypes attribute table. Faithful direct port
6925/// of the common path: skip-on-`.`-prefix without WITH_NAMESPACE,
6926/// skip-on-PM_UNSET (with the POSIX preserve), AUTOLOAD gating,
6927/// then `nam` + `=value` via `printparamvalue`.
6928pub fn printparamnode(hn: &mut crate::ported::zsh_h::param, mut printflags: i32) {
6929    const PRINT_WITH_NAMESPACE: i32 = 1 << 8; // matches createspecial print enum
6930    let f = hn.node.flags as u32;
6931    if (f & PM_HASHELEM) == 0
6932        && (printflags & PRINT_WITH_NAMESPACE) == 0
6933        && hn.node.nam.starts_with('.')
6934    {
6935        return;
6936    }
6937    if (f & PM_UNSET) != 0 {
6938        // c:6133-6143 — POSIX readonly/exported keep + PM_DEFAULTED
6939        // path: show as readonly/exported even if unset, with no
6940        // value (NAMEONLY).
6941        let posix_keep = (printflags & (PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
6942            && (f & (PM_READONLY | PM_EXPORTED)) != 0;
6943        let defaulted = (f & PM_DEFAULTED) == PM_DEFAULTED;                  // c:6137
6944        if posix_keep || defaulted {
6945            printflags |= PRINT_NAMEONLY;
6946        } else {
6947            return;
6948        }
6949    }
6950    if (f & PM_AUTOLOAD) != 0 {
6951        printflags |= PRINT_NAMEONLY;
6952    }
6953    if (printflags & (PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0 {
6954        if (f & PM_AUTOLOAD) != 0 {
6955            return;
6956        }
6957        // c:6157-6163 — PM_RO_BY_DESIGN with level check.
6958        if (f & PM_RO_BY_DESIGN) != 0 {
6959            // C uses `locallevel` global; the Rust port treats it as 0
6960            // until that global is wired. With locallevel==0, suppress
6961            // unless hn.level == 0 (matches the C "show anyway in scope
6962            // of declaration" path).
6963            if hn.level != 0 {
6964                return;
6965            }
6966        }
6967        if (printflags & PRINT_POSIX_EXPORT) != 0 {
6968            if (f & PM_EXPORTED) == 0 { return; }
6969            print!("export ");
6970        } else if (printflags & PRINT_POSIX_READONLY) != 0 {
6971            if (f & PM_READONLY) == 0 { return; }
6972            print!("readonly ");
6973        } else {
6974            print!("typeset ");
6975        }
6976    }
6977    if (printflags & PRINT_KV_PAIR) != 0 {
6978        // hashelem path: print key without name= leader.
6979    }
6980    print!("{}", hn.node.nam);
6981    if (printflags & PRINT_NAMEONLY) != 0 {
6982        if (printflags & PRINT_KV_PAIR) == 0 { println!(); }
6983        return;
6984    }
6985    if (printflags & (PRINT_INCLUDEVALUE | PRINT_TYPESET)) != 0
6986        || (printflags & PRINT_NAMEONLY) == 0
6987    {
6988        printparamvalue(hn, printflags);
6989    }
6990    if (printflags & PRINT_KV_PAIR) == 0 {
6991        println!();
6992    }
6993}
6994
6995/// Port of `printparamvalue(Param p, int printflags)` from `Src/params.c:6035`. C body
6996/// dispatches on `PM_TYPE(p->node.flags)` and writes the value
6997/// (no `name=` prefix unless `!PRINT_KV_PAIR`, which prints `=`
6998/// first). PM_SCALAR/PM_NAMEREF: `quotedzputs(t)`; PM_INTEGER:
6999/// `printf("%ld")`; PM_EFLOAT/PM_FFLOAT: `convfloat(...)`;
7000/// PM_ARRAY: `( v1 v2 ... )` with `\n  ` separators on
7001/// PRINT_LINE; PM_HASHED: same shape via scan callback.
7002pub fn printparamvalue(p: &mut crate::ported::zsh_h::param, printflags: i32) {
7003    if (printflags & PRINT_KV_PAIR) == 0 {
7004        print!("=");
7005    }
7006    let t = PM_TYPE(p.node.flags as u32);
7007    if t == PM_SCALAR || t == PM_NAMEREF {
7008        let s = strgetfn(p);
7009        // quotedzputs equivalent — single-quote if it contains specials.
7010        print!("{}", s);
7011    } else if t == PM_INTEGER {
7012        print!("{}", intgetfn(p));
7013    } else if t == PM_EFLOAT || t == PM_FFLOAT {
7014        // convfloat(p->gsu.f->getfn(p), p->base, p->node.flags, stdout)
7015        print!("{}", floatgetfn(p));
7016    } else if t == PM_ARRAY {
7017        if (printflags & PRINT_KV_PAIR) == 0 {
7018            print!("(");
7019            if (printflags & PRINT_LINE) == 0 {
7020                print!(" ");
7021            }
7022        }
7023        let arr = arrgetfn(p);
7024        if !arr.is_empty() {
7025            if (printflags & PRINT_LINE) != 0 {
7026                if (printflags & PRINT_KV_PAIR) != 0 {
7027                    print!("  ");
7028                } else {
7029                    print!("\n  ");
7030                }
7031            }
7032            print!("{}", arr[0]);
7033            for el in &arr[1..] {
7034                if (printflags & PRINT_LINE) != 0 {
7035                    print!("\n  ");
7036                } else {
7037                    print!(" ");
7038                }
7039                print!("{}", el);
7040            }
7041            if (printflags & (PRINT_LINE | PRINT_KV_PAIR)) == PRINT_LINE {
7042                println!();
7043            }
7044        }
7045        if (printflags & PRINT_KV_PAIR) == 0 {
7046            if (printflags & PRINT_LINE) == 0 {
7047                print!(" ");
7048            }
7049            print!(")");
7050        }
7051    } else if t == PM_HASHED {
7052        if (printflags & PRINT_KV_PAIR) == 0 {
7053            print!("(");
7054            if (printflags & PRINT_LINE) == 0 {
7055                print!(" ");
7056            }
7057        }
7058        // scanhashtable + ht->printnode — backend not yet wired.
7059        if (printflags & PRINT_KV_PAIR) == 0 {
7060            print!(")");
7061        }
7062    }
7063}
7064
7065/// Port of `resolve_nameref(Param pm)` from `Src/params.c:6325`. C body:
7066/// ```c
7067/// mod_export Param
7068/// resolve_nameref(Param pm)
7069/// {
7070///     return resolve_nameref_rec(pm, NULL, 0);
7071/// }
7072/// ```
7073/// Public entry point that walks the nameref alias chain to the
7074/// final non-nameref `param`. Stop-pm and keep_lastref are
7075/// internal; this wrapper hardcodes both per the C body.
7076/// WARNING: param names don't match C — Rust=() vs C=(pm)
7077pub fn resolve_nameref(                                                      // c:6325
7078    pm: Option<crate::ported::zsh_h::Param>,
7079) -> Option<crate::ported::zsh_h::Param> {
7080    resolve_nameref_rec(pm, None, 0)                                         // c:6327
7081}
7082
7083/// Port of `resolve_nameref_rec(Param pm, const Param stop, int keep_lastref)` from `Src/params.c:6332`. C
7084/// recursive helper for `resolve_nameref()`. Walks the chain of
7085/// `${(P)var}` indirections via `gethashnode2(realparamtab, refname)`
7086/// + `loadparamnode(paramtab, upscope(pm, ref), refname)`,
7087/// checking PM_TAGGED for cycle detection, and returns the
7088/// final non-nameref Param. Returns the input `pm` unchanged
7089/// for the early-exit path (no NAMEREF / UNSET / has subscript /
7090/// empty refname). Full chain walk requires `gethashnode2` on
7091/// `realparamtab` — pending the HashTable vtable.
7092#[allow(unused_variables)]
7093pub fn resolve_nameref_rec(
7094    pm: Option<crate::ported::zsh_h::Param>,
7095    stop: Option<&crate::ported::zsh_h::param>,
7096    keep_lastref: i32,
7097) -> Option<crate::ported::zsh_h::Param> {
7098    let pm_ref = pm.as_deref()?;
7099    let f = pm_ref.node.flags as u32;
7100    if (f & PM_NAMEREF) == 0 || (f & PM_UNSET) != 0 || pm_ref.width != 0 {
7101        return pm;
7102    }
7103    let refname = pm_ref.u_str.as_deref().unwrap_or("");
7104    if refname.is_empty() {
7105        return pm;
7106    }
7107    if (f & PM_TAGGED) != 0 {
7108        // zerr("%s: invalid self reference", pm.node.nam)
7109        return None;
7110    }
7111    // Real walk needs realparamtab.gethashnode2(refname). Until
7112    // that lands, return the input — this matches the no-target
7113    // behaviour the C source falls back to when keep_lastref is 0
7114    // and the lookup fails.
7115    pm
7116}
7117
7118/// Port of `scancopyparams(HashNode hn, UNUSED(int flags))` from `Src/params.c:584`. C body:
7119/// ```c
7120/// Param tpm = (Param) zshcalloc(sizeof *tpm);
7121/// tpm->node.nam = ztrdup(pm->node.nam);
7122/// copyparam(tpm, pm, 0);
7123/// addhashnode(outtable, tpm->node.nam, tpm);
7124/// ```
7125/// Real port: clone the param via `Box::new(pm.clone())` (Rust
7126/// equivalent of zshcalloc + copyparam) and push it into the
7127/// caller-supplied destination table. The original C uses the
7128/// global `outtable`; Rust port plumbs it in explicitly.
7129/// WARNING: param names don't match C — Rust=(pm, _flags, outtable) vs C=(hn, flags)
7130pub fn scancopyparams(
7131    pm: &crate::ported::zsh_h::param,
7132    _flags: i32,
7133    outtable: &mut std::collections::HashMap<String, Box<crate::ported::zsh_h::param>>,
7134) {
7135    // copyparam(tpm, pm, 0): per Src/params.c:1056 copies u + gsu +
7136    // level + base + width but zeroes pm->old/ename/env links.
7137    let tpm = crate::ported::zsh_h::param {
7138        node: crate::ported::zsh_h::hashnode {
7139            next: None,
7140            nam: pm.node.nam.clone(),
7141            flags: pm.node.flags,
7142        },
7143        u_data: pm.u_data,
7144        u_arr: pm.u_arr.clone(),
7145        u_str: pm.u_str.clone(),
7146        u_val: pm.u_val,
7147        u_dval: pm.u_dval,
7148        u_hash: None,
7149        gsu_s: None,
7150        gsu_i: None,
7151        gsu_f: None,
7152        gsu_a: None,
7153        gsu_h: None,
7154        base: pm.base,
7155        width: pm.width,
7156        env: None,
7157        ename: None,
7158        old: None,
7159        level: pm.level,
7160    };
7161    let nam = tpm.node.nam.clone();
7162    outtable.insert(nam, Box::new(tpm));
7163}
7164
7165/// Port of `scancountparams(UNUSED(HashNode hn), int flags)` from `Src/params.c:630`. C body:
7166/// ```c
7167/// ++numparamvals;
7168/// if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
7169///     ++numparamvals;
7170/// ```
7171/// Increments the static `numparamvals` global used by
7172/// `paramvalarr`. Rust port mirrors against a counter passed by
7173/// reference (no static-mutable in safe Rust).
7174/// WARNING: param names don't match C — Rust=(_hn, flags, numparamvals) vs C=(hn, flags)
7175pub fn scancountparams(_hn: &crate::ported::zsh_h::param, flags: i32, numparamvals: &mut u32) {
7176    *numparamvals += 1;
7177    if (flags as u32 & SCANPM_WANTKEYS) != 0 && (flags as u32 & SCANPM_WANTVALS) != 0 {
7178        *numparamvals += 1;
7179    }
7180}
7181
7182/// Port of `scanendscope(HashNode hn, UNUSED(int flags))` from `Src/params.c:5900`. Per-node
7183/// callback used by `endparamscope` (params.c:5867 calls
7184/// `scanhashtable(paramtab, 0, 0, 0, scanendscope, 0)`) when a
7185/// function returns. C body:
7186/// ```c
7187/// Param pm = (Param)hn;
7188/// if (pm->level > locallevel) {
7189///     if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) {
7190///         /* Non-removable special — restore from pm->old in-place. */
7191///         Param tpm = pm->old;
7192///         #ifdef USE_LOCALE
7193///         if (!strncmp(pm->node.nam, "LC_", 3) ||
7194///             !strcmp(pm->node.nam, "LANG"))
7195///             lc_update_needed = 1;
7196///         #endif
7197///         if (!strcmp(pm->node.nam, "SECONDS")) {
7198///             setsecondstype(pm, PM_TYPE(tpm->node.flags),
7199///                                PM_TYPE(pm->node.flags));
7200///             setrawseconds(tpm->u.dval);
7201///             tpm->node.flags |= PM_NORESTORE;
7202///         }
7203///         pm->old = tpm->old;
7204///         pm->node.flags = (tpm->node.flags & ~PM_NORESTORE);
7205///         pm->level = tpm->level;
7206///         pm->base  = tpm->base;
7207///         pm->width = tpm->width;
7208///         if (pm->env) delenv(pm);
7209///         if (!(tpm->node.flags & (PM_NORESTORE|PM_READONLY)))
7210///             switch (PM_TYPE(pm->node.flags)) {
7211///             case PM_SCALAR: case PM_NAMEREF:
7212///                 pm->gsu.s->setfn(pm, tpm->u.str); break;
7213///             case PM_INTEGER:
7214///                 pm->gsu.i->setfn(pm, tpm->u.val); break;
7215///             case PM_EFLOAT: case PM_FFLOAT:
7216///                 pm->gsu.f->setfn(pm, tpm->u.dval); break;
7217///             case PM_ARRAY:
7218///                 pm->gsu.a->setfn(pm, tpm->u.arr); break;
7219///             case PM_HASHED:
7220///                 pm->gsu.h->setfn(pm, tpm->u.hash); break;
7221///             }
7222///         zfree(tpm, sizeof(*tpm));
7223///         if (pm->node.flags & PM_EXPORTED) export_param(pm);
7224///     } else
7225///         unsetparam_pm(pm, 0, 0);
7226/// }
7227/// ```
7228/// Rust port mirrors the structure 1:1. `locallevel` is a global
7229/// in C (Src/init.c) — we accept it as a parameter since the
7230/// global isn't yet ported. `setsecondstype`/`setrawseconds`/
7231/// `delenv` are not yet in zshrs and route through best-effort
7232/// no-ops for now (C macros / Src/params.c:5900 / Src/params.c:5900).
7233pub fn scanendscope(pm: &mut crate::ported::zsh_h::param, _flags: i32) {     // c:5900
7234    let cur_local = locallevel.load(std::sync::atomic::Ordering::Relaxed);
7235    if pm.level <= cur_local {                                                // c:5903
7236        return;
7237    }
7238    let pmflags = pm.node.flags as u32;
7239    if (pmflags & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL {
7240        // Take ownership of the saved old param.
7241        let mut tpm = match pm.old.take() {
7242            Some(t) => t,
7243            None => {
7244                // C uses DPUTS — fatal in debug, silent in release.
7245                return;
7246            }
7247        };
7248
7249        // USE_LOCALE branch: LC_*/LANG bumps lc_update_needed.
7250        // Global not yet ported; placeholder comment retains intent.
7251        if pm.node.nam.starts_with("LC_") || pm.node.nam == "LANG" {
7252            LC_UPDATE_NEEDED.store(1, std::sync::atomic::Ordering::SeqCst);
7253        }
7254
7255        if pm.node.nam == "SECONDS" {
7256            // setsecondstype(pm, PM_TYPE(tpm.flags), PM_TYPE(pm.flags));
7257            // setrawseconds(tpm.u_dval);
7258            tpm.node.flags |= PM_NORESTORE as i32;
7259        }
7260
7261        // pm->old = tpm->old;
7262        pm.old = tpm.old.take();
7263        // pm->node.flags = tpm->node.flags & ~PM_NORESTORE;
7264        pm.node.flags = (tpm.node.flags as u32 & !PM_NORESTORE) as i32;
7265        pm.level = tpm.level;
7266        pm.base  = tpm.base;
7267        pm.width = tpm.width;
7268
7269        if pm.env.is_some() {
7270            delenv(&pm.node.nam);
7271            pm.env = None;
7272        }
7273
7274        let restore = (tpm.node.flags as u32 & (PM_NORESTORE | PM_READONLY)) == 0;
7275        if restore {
7276            match PM_TYPE(pm.node.flags as u32) {
7277                t if t == PM_SCALAR || t == PM_NAMEREF => {
7278                    // pm->gsu.s->setfn(pm, tpm->u.str)
7279                    pm.u_str = tpm.u_str.clone();
7280                }
7281                t if t == PM_INTEGER => {
7282                    pm.u_val = tpm.u_val;
7283                }
7284                t if t == PM_EFLOAT || t == PM_FFLOAT => {
7285                    pm.u_dval = tpm.u_dval;
7286                }
7287                t if t == PM_ARRAY => {
7288                    pm.u_arr = tpm.u_arr.clone();
7289                }
7290                t if t == PM_HASHED => {
7291                    pm.u_hash = tpm.u_hash.take();
7292                }
7293                _ => {}
7294            }
7295        }
7296        // zfree(tpm) — Rust drops the Box at end of scope.
7297        drop(tpm);
7298
7299        if (pm.node.flags as u32 & PM_EXPORTED) != 0 {
7300            export_param(pm);
7301        }
7302    } else {
7303        unsetparam_pm(pm, 0, 0);
7304    }
7305}
7306
7307// Port of `static unsigned numparamvals;` (params.c:626) and the
7308// related per-scan statics at params.c:637-640. Per PORT.md Rule D
7309// these are file-scope statics, NOT aggregated into a state struct.
7310//
7311//   c:626  static unsigned numparamvals;
7312//   c:637  static Patprog scanprog;
7313//   c:638  static char *scanstr;
7314//   c:639  static char **paramvals;
7315//   c:640  static Param foundparam;   <-- exposed earlier as FOUNDPARAM
7316pub static NUMPARAMVALS: std::sync::atomic::AtomicU32 =
7317    std::sync::atomic::AtomicU32::new(0);                                    // c:626
7318pub static SCANPROG: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7319    std::sync::OnceLock::new();                                              // c:637
7320pub static SCANSTR: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7321    std::sync::OnceLock::new();                                              // c:638
7322pub static PARAMVALS: std::sync::OnceLock<std::sync::Mutex<Vec<String>>> =
7323    std::sync::OnceLock::new();                                              // c:639
7324
7325fn scanprog_lock() -> &'static std::sync::Mutex<Option<String>> {
7326    SCANPROG.get_or_init(|| std::sync::Mutex::new(None))
7327}
7328fn scanstr_lock() -> &'static std::sync::Mutex<Option<String>> {
7329    SCANSTR.get_or_init(|| std::sync::Mutex::new(None))
7330}
7331fn paramvals_lock() -> &'static std::sync::Mutex<Vec<String>> {
7332    PARAMVALS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
7333}
7334
7335/// Port of `scanparamvals(HashNode hn, int flags)` from `Src/params.c:644`. Real C body
7336/// is the per-node callback for `paramvalarr`: applies SCANPM_MATCHKEY
7337/// (pattry on name) / SCANPM_MATCHVAL (pattry on value) / SCANPM_KEYMATCH
7338/// (compile pm.nam as pattern, match against scanstr) / SCANPM_WANTKEYS
7339/// / SCANPM_WANTVALS / SCANPM_MATCHMANY filters, populating the
7340/// `paramvals[]` slice with the param's name and/or `getstrvalue`
7341/// result, and stashing `foundparam = pm`. State lives in the C
7342/// file-scope statics ported above as `NUMPARAMVALS` / `SCANPROG` /
7343/// `SCANSTR` / `PARAMVALS` / `FOUNDPARAM`.
7344/// WARNING: param names don't match C — Rust=(flags) vs C=(hn, flags)
7345pub fn scanparamvals(                                                        // c:644
7346    pm: &mut crate::ported::zsh_h::param,
7347    flags: i32,
7348) {
7349    let f = flags as u32;
7350    if NUMPARAMVALS.load(Ordering::Relaxed) != 0
7351        && (f & SCANPM_MATCHMANY) == 0
7352        && (f & (SCANPM_MATCHVAL | SCANPM_MATCHKEY | SCANPM_KEYMATCH)) != 0
7353    {
7354        return;
7355    }
7356    if (f & SCANPM_KEYMATCH) != 0 {
7357        // patcompile(pm.node.nam) + pattry(prog, scanstr)
7358        let scanstr = scanstr_lock().lock().unwrap().clone();
7359        if let Some(s) = scanstr {
7360            if !pattry(&pm.node.nam, &s) { return; }
7361        } else {
7362            return;
7363        }
7364    } else if (f & SCANPM_MATCHKEY) != 0 {
7365        let prog = scanprog_lock().lock().unwrap().clone();
7366        if let Some(p) = prog {
7367            if !pattry(&p, &pm.node.nam) { return; }
7368        } else {
7369            return;
7370        }
7371    }
7372    set_foundparam(Some(pm.node.nam.clone()));
7373    if (f & SCANPM_WANTKEYS) != 0 {
7374        paramvals_lock().lock().unwrap().push(pm.node.nam.clone());
7375        NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
7376        if (f & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) == 0 {
7377            return;
7378        }
7379    }
7380    let mut vbuf = crate::ported::zsh_h::value {
7381        pm: None,                      // placeholder; real C re-binds
7382        arr: Vec::new(),
7383        scanflags: 0,
7384        valflags: 0,
7385        start: 0,
7386        end: -1,
7387    };
7388    // C: paramvals[numparamvals] = getstrvalue(&v);
7389    // We don't move pm into vbuf to preserve the borrow; mirror the
7390    // C semantics by reading u_str directly via strgetfn for the
7391    // PM_SCALAR fast path and falling back through getstrvalue when
7392    // wired.
7393    let s = strgetfn(pm);
7394    let _ = vbuf;
7395    if (f & SCANPM_MATCHVAL) != 0 {
7396        let prog = scanprog_lock().lock().unwrap().clone();
7397        let matched = prog.map(|p| pattry(&p, &s)).unwrap_or(false);
7398        if matched {
7399            paramvals_lock().lock().unwrap().push(s);
7400            let inc = if (f & SCANPM_WANTVALS) != 0 { 1 } else if (f & SCANPM_WANTKEYS) == 0 { 1 } else { 0 };
7401            NUMPARAMVALS.fetch_add(inc, Ordering::Relaxed);
7402        } else if (f & SCANPM_WANTKEYS) != 0 {
7403            // Discard previously-pushed key.
7404            paramvals_lock().lock().unwrap().pop();
7405            NUMPARAMVALS.fetch_sub(1, Ordering::Relaxed);
7406        }
7407    } else {
7408        paramvals_lock().lock().unwrap().push(s);
7409        NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
7410    }
7411    set_foundparam(None);
7412}
7413
7414/// Minimal `pattry()` shim — exact-match fallback until the pattern
7415/// engine in `Src/pattern.c` is wired.
7416fn pattry(prog: &str, s: &str) -> bool {
7417    prog == s
7418}
7419
7420/// ```c
7421/// Param pm = (Param) gethashnode2(realparamtab, name);
7422/// if (pm && (pm->node.flags & PM_NAMEREF)) {
7423///     if (pm->node.flags & PM_READONLY) {
7424///         zerr("read-only reference: %s", pm->node.nam); return;
7425///     }
7426///     pm->base = pm->width = 0;
7427///     SETREFNAME(pm, ztrdup(value));
7428///     pm->node.flags &= ~PM_UNSET;
7429///     setscope(pm);
7430/// } else
7431///     setsparam(name, ztrdup(value));
7432/// ```
7433/// `gethashnode2` is the no-autoload paramtab lookup. The
7434/// nameref branch updates the alias target in-place; the normal
7435/// branch falls through to `setsparam`. Stub: requires real
7436/// `paramtab` global with HashTable backend; until then the
7437/// non-nameref `setsparam` path is the only one that fires.
7438/// Port of `setloopvar(char *name, char *value)` from `Src/params.c:6362`.
7439#[allow(unused_variables)]
7440pub fn setloopvar(name: &str, value: &str) {
7441    // Once paramtab gethashnode2 is wired:
7442    //   if let Some(pm) = paramtab_get(name) {
7443    //       if (pm.flags & PM_NAMEREF) != 0 { ...nameref branch... return; }
7444    //   }
7445    //   setsparam(name, value);
7446}
7447
7448/// Port of `setnparam(char *s, mnumber val)` from `Src/params.c:3744`. C body:
7449/// `return assignnparam(s, val, ASSPM_WARN);` — single-line
7450/// wrapper. Stub until `assignnparam` is implemented.
7451pub fn setnparam(s: &str, val: f64) {
7452    assignnparam(s, crate::ported::math::mnumber { l: 0, d: val, type_: MN_FLOAT }, crate::ported::zsh_h::ASSPM_WARN);
7453}
7454
7455/// Port of `setnumvalue(Value v, mnumber val)` from `Src/params.c:2856`. C body
7456/// dispatches on `PM_TYPE(v->pm->node.flags)`:
7457/// PM_SCALAR/PM_NAMEREF/PM_ARRAY → convbase_underscore /
7458/// convfloat_underscore + setstrvalue; PM_INTEGER →
7459/// `pm->gsu.i->setfn(pm, val.u.l)`; PM_EFLOAT|PM_FFLOAT →
7460/// `pm->gsu.f->setfn(pm, val.u.d)`. EXECOPT/PM_READONLY checks
7461/// at top.
7462pub fn setnumvalue(v: Option<&mut crate::ported::zsh_h::value>, val: crate::ported::math::mnumber) {
7463    let v = match v { Some(v) => v, None => return };
7464    let pm = match v.pm.as_mut() { Some(p) => p, None => return };
7465    if (pm.node.flags as u32 & PM_READONLY) != 0 {
7466        // zerr("read-only variable: %s", pm->node.nam)
7467        return;
7468    }
7469    let t = PM_TYPE(pm.node.flags as u32);
7470    if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
7471        let s = if (val.type_ & MN_INTEGER) != 0 {
7472            val.l.to_string()
7473        } else {
7474            val.d.to_string()
7475        };
7476        // setstrvalue(v, p) — assignstrvalue dispatch.
7477        let _ = s;
7478    } else if t == PM_INTEGER {
7479        pm.u_val = if (val.type_ & MN_INTEGER) != 0 { val.l } else { val.d as i64 };
7480    } else if t == PM_EFLOAT || t == PM_FFLOAT {
7481        pm.u_dval = if (val.type_ & MN_INTEGER) != 0 { val.l as f64 } else { val.d };
7482    }
7483}
7484
7485/// PM_NAMEREF: extract `refname = GETREFNAME(pm)`, locate first
7486/// `[` to split name vs subscript (sets pm->width), look up the
7487/// base param via `gethashnode2(realparamtab, refname)` →
7488/// `loadparamnode` (skipping self) → `setscope_base(pm,
7489/// basepm->level)`; if pm->base > pm->level emits the KSH global
7490/// reference error or WARNNESTEDVAR diagnostic; finally walks the
7491/// `resolve_nameref_rec` chain to detect self-references with
7492/// queue_signals/restore_queue_signals bracketing. Non-nameref
7493/// params: no-op. The base lookup and resolve_nameref_rec helpers
7494/// are stubbed elsewhere; this port wires the structural path
7495/// against existing helpers and falls through cleanly when the
7496/// nameref chain backend isn't available.
7497/// Port of `setscope(Param pm)` from `Src/params.c:6382`.
7498pub fn setscope(pm: &mut crate::ported::zsh_h::param) {
7499    crate::ported::signals::queue_signals();
7500    if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
7501        // Refname is stored in pm.u_str for nameref-typed params.
7502        let refname = pm.u_str.clone();
7503        if let Some(rn) = refname {
7504            // Compute pm->width by finding the first `[`.
7505            let head: &str = match rn.find('[') {
7506                Some(i) => {
7507                    pm.width = i as i32;
7508                    &rn[..i]
7509                }
7510                None => rn.as_str(),
7511            };
7512            // Self-reference check (basepm == pm) — without a working
7513            // hashtable lookup we can only detect literal self-name.
7514            if !head.is_empty() && head == pm.node.nam {
7515                // zerr("%s: invalid self reference", refname);
7516                // unsetparam_pm(pm, 0, 1);
7517            } else {
7518                // basepm = (Param)gethashnode2(realparamtab, refname)
7519                //   → loadparamnode(...) → setscope_base(pm, basepm->level)
7520                // Resolved on demand once the paramtab vtable is wired;
7521                // the call shape is preserved here.
7522            }
7523        }
7524    }
7525    crate::ported::signals::unqueue_signals();
7526}
7527
7528/// ```c
7529/// if ((pm->base = base) > pm->level) {
7530///     LinkList refs;
7531///     /* grow scoperefs[] to base+1 entries */
7532///     refs = scoperefs[base];
7533///     if (!refs) refs = scoperefs[base] = znewlinklist();
7534///     zpushnode(refs, pm);
7535/// }
7536/// ```
7537/// Records `pm` on the per-scope reference list so a future
7538/// scope-pop can resolve nameref/upper bindings. Rust port
7539/// stores `base` on the param; the global `scoperefs` LinkList
7540/// table is not yet ported, so the bookkeeping push is described
7541/// here as architectural intent rather than executed.
7542/// Port of `setscope_base(Param pm, int base)` from `Src/params.c:6436`.
7543pub fn setscope_base(pm: &mut crate::ported::zsh_h::param, base: i32) {
7544    pm.base = base;
7545    if base > pm.level {
7546        // scoperefs[base] push of pm — needs LinkList global.
7547    }
7548}
7549
7550/// Port of `upscope(Param pm, const Param ref)` from `Src/params.c:6455`. C body:
7551/// ```c
7552/// if (ref->node.flags & PM_UPPER)
7553///     while (pm->level > ref->level - 1 && (pm = pm->old));
7554/// else
7555///     for (; pm->old && pm->old->level >= ref->base; pm = pm->old);
7556/// return pm;
7557/// ```
7558/// Walks `pm->old` chain to the param at the right scope depth
7559/// for a nameref. Rust signature mirrors C `Param upscope(Param,
7560/// const Param ref)`.
7561/// WARNING: param names don't match C — Rust=(pm, reference) vs C=(pm, ref)
7562pub fn upscope(
7563    mut pm: crate::ported::zsh_h::Param,
7564    reference: &crate::ported::zsh_h::param,
7565) -> crate::ported::zsh_h::Param {
7566    if (reference.node.flags as u32 & PM_UPPER) != 0 {
7567        while pm.level > reference.level - 1 {
7568            match pm.old.take() {
7569                Some(o) => pm = o,
7570                None => break,
7571            }
7572        }
7573    } else {
7574        loop {
7575            let next_level = pm.old.as_ref().map(|o| o.level);
7576            match next_level {
7577                Some(l) if l >= reference.base => {
7578                    pm = pm.old.take().unwrap();
7579                }
7580                _ => break,
7581            }
7582        }
7583    }
7584    pm
7585}
7586
7587// ===========================================================
7588// GSU dispatch table — maps special-parameter NAMES to their
7589// getfn callback. C zsh dispatches reads of `$RANDOM` /
7590// `$USERNAME` / `$UID` / etc. through `Param.gsu->getfn`, where
7591// each special parameter has a `Param` entry in `paramtab`
7592// pointing at its specific getfn (Src/params.c:225 SPECIAL_PARAM
7593// table seeds these mappings).
7594//
7595// zshrs has the GSU callbacks ported (uidgetfn, randomgetfn,
7596// usernamegetfn, etc. above) but the shell's parameter-read path
7597// (fusevm_bridge::expand_param) reads from ShellExecutor.variables
7598// directly — never dispatching through the callbacks. Result:
7599// `echo $RANDOM` returned the cached HashMap value (or empty),
7600// not a fresh `rand() & 0x7fff` from `randomgetfn`.
7601//
7602// `lookup_special_var(name)` is the bridge: given a variable
7603// name, returns the GSU getfn's output if `name` is a recognized
7604// special, else None. Callers (expand_param, subst.rs reads)
7605// check this before falling back to `variables.get(name)`.
7606// ===========================================================
7607
7608/// Look up a special-parameter NAME and dispatch to its GSU getfn.
7609///
7610/// Returns `Some(value_string)` if `name` is one of zshrs's
7611/// recognized specials with a real GSU getfn; `None` otherwise
7612/// (caller should fall back to `variables.get`).
7613///
7614/// This is the bridge between the named getfn callbacks above
7615/// (uidgetfn / randomgetfn / etc.) and the shell's parameter-read
7616/// path. Mirrors the `Param.gsu->getfn` dispatch C zsh does
7617/// inside `getsparam` / `getstrvalue` (Src/params.c:3076 / 2335).
7618pub fn lookup_special_var(name: &str) -> Option<String> {
7619    // All-digit positional: $1..$N from canonical PPARAMS.
7620    // C zsh dispatches positional params through pparams (Src/init.c).
7621    if !name.is_empty() && name.chars().all(|c| c.is_ascii_digit()) {
7622        let n: usize = name.parse().ok()?;
7623        if n == 0 {
7624            return crate::ported::utils::argzero();
7625        }
7626        let pp = pparams_lock().lock().ok()?;
7627        return pp.get(n - 1).cloned();
7628    }
7629    match name {
7630        // libc identity callbacks.
7631        "UID" => Some(uidgetfn().to_string()),
7632        "GID" => Some(gidgetfn().to_string()),
7633        "EUID" => Some(euidgetfn().to_string()),
7634        "EGID" => Some(egidgetfn().to_string()),
7635        // libc syscall callbacks.
7636        "RANDOM" => Some(randomgetfn().to_string()),
7637        "TTYIDLE" => Some(ttyidlegetfn().to_string()),
7638        "ERRNO" => Some(errnogetfn().to_string()),
7639        // Time callbacks.
7640        "SECONDS" => Some(intsecondsgetfn().to_string()),
7641        // Cached-state callbacks (OnceLock<Mutex<…>> backed).
7642        "USERNAME" => Some(usernamegetfn()),
7643        "HOME" => Some(homegetfn()),
7644        "TERM" => Some(termgetfn()),
7645        "WORDCHARS" => Some(wordcharsgetfn()),
7646        "IFS" => Some(ifsgetfn()),
7647        "TERMINFO" => Some(terminfogetfn()),
7648        "TERMINFO_DIRS" => Some(terminfodirsgetfn()),
7649        "KEYBOARD_HACK" => Some(keyboardhackgetfn()),
7650        "histchars" | "HISTCHARS" => Some(histcharsgetfn()),
7651        "_" => Some(underscoregetfn()),
7652        // Counters with int return.
7653        "HISTSIZE" => Some(histsizegetfn().to_string()),
7654        "SAVEHIST" => Some(savehistsizegetfn().to_string()),
7655        "#" | "ARGC" => Some(poundgetfn().to_string()),
7656        // $0 routes through utils::argzero.
7657        "0" => crate::ported::utils::argzero(),
7658        // POSIX shell-special scalars. C dispatches these through
7659        // dedicated gsu getfn callbacks (Src/params.c special_assigns).
7660        "?" => Some(crate::ported::builtin::LASTVAL
7661            .load(std::sync::atomic::Ordering::Relaxed)
7662            .to_string()),
7663        "$" => Some(std::process::id().to_string()),
7664        "!" => {
7665            // Last-backgrounded job PID. Stored in paramtab `!` slot;
7666            // default to 0 to match zsh fresh-shell behaviour.
7667            let tab = paramtab().read().ok()?;
7668            Some(tab.get("!").and_then(|pm| pm.u_str.clone())
7669                .unwrap_or_else(|| "0".to_string()))
7670        }
7671        // $* / $@ join positional params via IFS first char.
7672        "*" | "@" => {
7673            let sep = ifsgetfn().chars().next().unwrap_or(' ').to_string();
7674            pparams_lock().lock().ok().map(|p| p.join(&sep))
7675        }
7676        // $- : current option-letter set. zsh emits baseline "569X"
7677        // prefix (internal letters always on) + user-toggled flags.
7678        "-" => {
7679            let mut letters = String::from("569X");
7680            let opt = |n: &str| {
7681                crate::ported::options::opt_state_get(n).unwrap_or(false)
7682            };
7683            if opt("errexit")  { letters.push('e'); }
7684            if !opt("rcs")     { letters.push('f'); }
7685            if opt("login")    { letters.push('l'); }
7686            if opt("nounset")  { letters.push('u'); }
7687            if opt("xtrace")   { letters.push('x'); }
7688            if opt("verbose")  { letters.push('v'); }
7689            if opt("noexec")   { letters.push('n'); }
7690            if opt("hashall")  { letters.push('h'); }
7691            Some(letters)
7692        }
7693        // Arrays — joined with space for scalar context.
7694        "pipestatus" => {
7695            let arr = pipestatgetfn();
7696            if arr.is_empty() {
7697                None
7698            } else {
7699                Some(arr.join(" "))
7700            }
7701        }
7702        _ => None,
7703    }
7704}
7705
7706#[cfg(test)]
7707mod gsu_tests {
7708    use super::*;
7709
7710    #[test]
7711    fn test_libc_id_callbacks_match_libc() {
7712        assert_eq!(uidgetfn(), unsafe { libc::getuid() } as i64);
7713        assert_eq!(gidgetfn(), unsafe { libc::getgid() } as i64);
7714        assert_eq!(euidgetfn(), unsafe { libc::geteuid() } as i64);
7715        assert_eq!(egidgetfn(), unsafe { libc::getegid() } as i64);
7716    }
7717
7718    #[test]
7719    fn test_random_returns_15_bit_value() {
7720        for _ in 0..100 {
7721            let v = randomgetfn();
7722            assert!(v >= 0 && v < 0x8000);
7723        }
7724    }
7725
7726    #[test]
7727    fn test_random_set_seeds_deterministically() {
7728        randomsetfn(42);
7729        let a = randomgetfn();
7730        randomsetfn(42);
7731        let b = randomgetfn();
7732        assert_eq!(a, b);
7733    }
7734
7735    #[test]
7736    fn test_ifs_round_trip() {
7737        let original = ifsgetfn();
7738        ifssetfn(":,;".to_string());
7739        assert_eq!(ifsgetfn(), ":,;");
7740        ifssetfn(original);
7741    }
7742
7743    #[test]
7744    fn test_histsiz_clamps_to_1() {
7745        let original = histsizegetfn();
7746        histsizesetfn(0);
7747        assert_eq!(histsizegetfn(), 1);
7748        histsizesetfn(-5);
7749        assert_eq!(histsizegetfn(), 1);
7750        histsizesetfn(500);
7751        assert_eq!(histsizegetfn(), 500);
7752        histsizesetfn(original);
7753    }
7754
7755    #[test]
7756    fn test_savehistsiz_clamps_to_0() {
7757        let original = savehistsizegetfn();
7758        savehistsizesetfn(-5);
7759        assert_eq!(savehistsizegetfn(), 0);
7760        savehistsizesetfn(100);
7761        assert_eq!(savehistsizegetfn(), 100);
7762        savehistsizesetfn(original);
7763    }
7764
7765    #[test]
7766    fn test_pipestat_round_trip() {
7767        pipestatsetfn(Some(vec!["1".to_string(), "0".to_string(), "127".to_string()]));
7768        let v = pipestatgetfn();
7769        assert_eq!(v, vec!["1", "0", "127"]);
7770        pipestatsetfn(None);
7771        assert_eq!(pipestatgetfn(), Vec::<String>::new());
7772    }
7773
7774    #[test]
7775    fn test_simple_arrayuniq_first_wins() {
7776        let v = vec!["a".to_string(), "b".to_string(), "a".to_string(), "c".to_string()];
7777        assert_eq!(simple_arrayuniq(v), vec!["a", "b", "c"]);
7778    }
7779
7780    #[test]
7781    fn test_split_env_string() {
7782        assert_eq!(
7783            split_env_string("PATH=/usr/bin:/bin"),
7784            Some(("PATH".to_string(), "/usr/bin:/bin".to_string()))
7785        );
7786        assert_eq!(
7787            split_env_string("EMPTY="),
7788            Some(("EMPTY".to_string(), "".to_string()))
7789        );
7790        assert_eq!(split_env_string("NOEQUALS"), None);
7791    }
7792
7793    #[test]
7794    fn test_mkenvstr() {
7795        assert_eq!(mkenvstr("PATH", "/usr/bin", 0), "PATH=/usr/bin");
7796        assert_eq!(mkenvstr("EMPTY", "", 0), "EMPTY=");
7797    }
7798
7799    #[test]
7800    fn test_seconds_round_trip() {
7801        intsecondssetfn(0);
7802        let s1 = intsecondsgetfn();
7803        std::thread::sleep(std::time::Duration::from_millis(5));
7804        let s2 = intsecondsgetfn();
7805        assert!(s2 >= s1);
7806        // Reset to a known offset and read back.
7807        setrawseconds(100.0);
7808        assert_eq!(getrawseconds(), 100.0);
7809    }
7810
7811    #[test]
7812    fn test_argzero_round_trip() {
7813        argzerosetfn("/bin/zsh".to_string());
7814        assert_eq!(argzerogetfn(), "/bin/zsh");
7815        argzerosetfn(String::new());
7816    }
7817
7818    #[test]
7819    fn test_env_get_set() {
7820        let result = zputenv("ZSHRS_TEST_VAR=hello");
7821        assert_eq!(result, 0);
7822        assert_eq!(zgetenv("ZSHRS_TEST_VAR"), Some("hello".to_string()));
7823        delenv("ZSHRS_TEST_VAR");
7824        assert_eq!(zgetenv("ZSHRS_TEST_VAR"), None);
7825    }
7826
7827    #[test]
7828    fn test_keyboardhack_one_char() {
7829        keyboardhacksetfn("\\".to_string());
7830        assert_eq!(keyboardhackgetfn(), "\\");
7831        keyboardhacksetfn(String::new());
7832        assert_eq!(keyboardhackgetfn(), "");
7833    }
7834
7835    #[test]
7836    fn test_histchars_default() {
7837        histcharssetfn(None);
7838        assert_eq!(histcharsgetfn(), "!^#");
7839        histcharssetfn(Some("@$&".to_string()));
7840        assert_eq!(histcharsgetfn(), "@$&");
7841        histcharssetfn(None);
7842    }
7843}