Skip to main content

zsh/ported/modules/
parameter.rs

1//! Parameter interface to shell internals - port of Modules/parameter.c
2//!
3//! Functions for the parameters special parameter.                          // c:37
4//! Return a string describing the type of a parameter.                      // c:39
5//! Functions for the commands special parameter.                            // c:147
6//! Functions for the functions special parameter.                           // c:280
7//! Functions for the builtins special parameter.                            // c:771
8//! Functions for the options special parameter.                             // c:922
9//! Functions for the modules special parameter.                             // c:1036
10//! Functions for the history special parameter.                             // c:1152
11//! Table for defined parameters.                                            // c:2177
12//!
13//! Provides special parameters: $commands, $functions, $aliases, $builtins,
14//! $modules, $dirstack, $history, $historywords, $options, $nameddirs, $userdirs
15
16use crate::ported::builtin::BUILTINS;
17use crate::ported::hashnameddir::{addnameddirnode, nameddirtab};
18use crate::ported::hashtable::{
19    aliastab_lock, cmdnam_hashed, cmdnamtab_lock, createaliasnode, shfunctab_lock, sufaliastab_lock,
20};
21use crate::ported::hist::hist_ring;
22use crate::ported::jobs::{getjob, selectjobtab, sigmsg};
23use crate::ported::module::MODULESTAB;
24use crate::ported::options::{dosetopt, opt_state_set, optlookup};
25use crate::ported::params::{deleteparamtable, getsparam, getstrvalue, realparamtab};
26use crate::ported::utils::zwarn;
27use crate::ported::zsh_h::{
28    hashnode, hashtable, isset, module, nameddir, opt_name, param, value, HashNode, HashTable,
29    Param, ParamScanFunc, ALIAS_GLOBAL, ALIAS_SUFFIX, DISABLED, FS_EVAL, FS_SOURCE, INTERACTIVE,
30    ND_USERNAME, PM_ARRAY, PM_AUTOLOAD, PM_EFLOAT, PM_EXPORTED, PM_FFLOAT, PM_HASHED, PM_HIDE,
31    PM_HIDEVAL, PM_INTEGER, PM_LEFT, PM_LOWER, PM_NAMEREF, PM_READONLY, PM_RIGHT_B, PM_RIGHT_Z,
32    PM_RO_BY_DESIGN, PM_SCALAR, PM_SPECIAL, PM_TAGGED, PM_TIED, PM_TYPE, PM_UNALIASED, PM_UNIQUE,
33    PM_UNSET, PM_UPPER, SCANPM_MATCHVAL, SCANPM_WANTKEYS, SCANPM_WANTVALS, SP_RUNNING, STAT_DONE,
34    STAT_NOPRINT, STAT_STOPPED,
35};
36use crate::zsh_h::{shfunc, HASHED};
37use crate::DPUTS;
38use std::collections::HashMap;
39use std::path::PathBuf;
40use std::sync::{Mutex, OnceLock};
41// Bag-of-globals `ParamType`/`ParamFlags` enum + `*Table` structs
42// deleted (PORT_PLAN.md Phase 2 anti-pattern #1): C has no
43// counterpart — paramtypestr now reads `PM_TYPE(pm->node.flags)`
44// directly, mirroring Src/Modules/parameter.c:43.
45
46// Return a string describing the type of a parameter.                      // c:43
47/// Port of `paramtypestr()` from `Src/Modules/parameter.c:43` — C decl `paramtypestr(Param pm)`.
48/// C: `static char *paramtypestr(Param pm)` — render a parameter's
49/// type and modifier flags as the `typeset -p` flag string.
50pub fn paramtypestr(pm: &param) -> String {
51    // c:43
52
53    let f: u32 = pm.node.flags as u32; // c:46
54
55    if (f & PM_UNSET) != 0 {
56        // c:48 (else branch c:91)
57        return String::new(); // c:92 dupstring("")
58    }
59    if (f & PM_AUTOLOAD) != 0 {
60        // c:49
61        return "undefined".to_string(); // c:50
62    }
63
64    let mut val: String = match PM_TYPE(f) {
65        // c:52
66        PM_SCALAR => "scalar".to_string(),            // c:53
67        PM_NAMEREF => "nameref".to_string(),          // c:54
68        PM_ARRAY => "array".to_string(),              // c:55
69        PM_INTEGER => "integer".to_string(),          // c:56
70        PM_EFLOAT | PM_FFLOAT => "float".to_string(), // c:57-58
71        PM_HASHED => "association".to_string(),       // c:59
72        _ => {
73            // c:60 default
74            DPUTS!(true, "BUG: type not handled in parameter"); // c:61
75            String::new() // c:62
76        }
77    };
78
79    if pm.level != 0 {
80        val.push_str("-local");
81    } // c:63-64
82    if (f & PM_LEFT) != 0 {
83        val.push_str("-left");
84    } // c:65-66
85    if (f & PM_RIGHT_B) != 0 {
86        val.push_str("-right_blanks");
87    } // c:67-68
88    if (f & PM_RIGHT_Z) != 0 {
89        val.push_str("-right_zeros");
90    } // c:69-70
91    if (f & PM_LOWER) != 0 {
92        val.push_str("-lower");
93    } // c:71-72
94    if (f & PM_UPPER) != 0 {
95        val.push_str("-upper");
96    } // c:73-74
97    if (f & PM_READONLY) != 0 {
98        val.push_str("-readonly");
99    } // c:75-76
100    if (f & PM_TAGGED) != 0 {
101        val.push_str("-tag");
102    } // c:77-78
103    if (f & PM_TIED) != 0 {
104        val.push_str("-tied");
105    } // c:79-80
106    if (f & PM_EXPORTED) != 0 {
107        val.push_str("-export");
108    } // c:81-82
109    if (f & PM_UNIQUE) != 0 {
110        val.push_str("-unique");
111    } // c:83-84
112    if (f & PM_HIDE) != 0 {
113        val.push_str("-hide");
114    } // c:85-86
115    if (f & PM_HIDEVAL) != 0 {
116        val.push_str("-hideval");
117    } // c:87-88
118    if (f & PM_SPECIAL) != 0 {
119        val.push_str("-special");
120    } // c:89-90
121
122    val // c:94
123}
124
125/// Port of `getpmparameter()` from `Src/Modules/parameter.c:99` — C decl `getpmparameter(UNUSED(HashTable ht), const char *name)`.
126/// C body (c:102-210): `paramtab[name]` lookup; emit a scalar Param
127/// whose value is the type-letter encoding (`scalar`, `array`,
128/// `association`, `integer`, `float`, plus `-readonly`/`-export`/
129/// etc. modifiers per PM_* flags).
130#[allow(non_snake_case)]
131#[allow(unused_variables)]
132pub fn getpmparameter(ht: *mut HashTable, name: &str) -> Option<Param> {
133    // c:99
134    // c:99-140 — `if ((pm = (Param)paramtab->getnode2(paramtab, name)))`
135    //              then dispatch on `PM_TYPE(pm->node.flags)` for the
136    //              type-letter and append all PM_* modifier suffixes
137    //              (-readonly, -export, -tied, -special, -local, etc.)
138    //              per c:120-200. The full encoding is what
139    //              `paramtypestr` (this module, line 51) produces;
140    //              previous getpmparameter inlined a stripped-down
141    //              dispatch that returned just the base type letter,
142    //              dropping the modifier suffixes entirely. Mirror C
143    //              by routing through paramtypestr.
144    //
145    // Symptom of the previous form:
146    //   typeset -gx VAR=val; echo "${parameters[VAR]}"
147    //   zshrs (before): scalar        zsh: scalar-export
148    // c:107-114 — `rpm = getnode2(name)` is the bare param (the nameref
149    // itself); for a nameref with a non-empty refname, `getnode(name)`
150    // (getparamnode, c:Src/params.c:570-575) resolves the chain from that
151    // same node and its type is appended after a `-`. A reference to an
152    // element ends the chain at itself (c:6339), so it reads
153    // `nameref-…-nameref-…`.
154    let rpm = crate::ported::params::paramtab().read().unwrap().get(name).cloned();
155    let value = match rpm {
156        Some(pm) if (pm.node.flags as u32 & PM_UNSET) == 0 => {
157            let base = paramtypestr(&pm);
158            let has_refname = (pm.node.flags as u32 & PM_NAMEREF) != 0
159                && pm.u_str.as_deref().is_some_and(|s| !s.is_empty());
160            match has_refname
161                .then(|| crate::ported::params::resolve_nameref(Some(pm)))
162                .flatten()
163            {
164                // c:112-113
165                Some(t) if (t.node.flags as u32 & PM_UNSET) == 0 => {
166                    format!("{}-{}", base, paramtypestr(&t))
167                }
168                _ => base,
169            }
170        }
171        _ => String::new(),
172    };
173    let found = !value.is_empty();
174    let pm = Box::new(param {
175        // c:103 hcalloc
176        node: hashnode {
177            next: None,
178            nam: name.to_string(), // c:104
179            flags: if found {
180                (PM_SCALAR | PM_READONLY) as i32
181            } else {
182                (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32
183            }, // c:209
184        },
185        u_data: 0,
186        u_tied: None,
187        u_arr: None,
188        u_str: Some(value), // c:208
189        u_val: 0,
190        u_dval: 0.0,
191        u_hash: None,
192        gsu_s: None, // c:106 pmparam_gsu
193        gsu_i: None,
194        gsu_f: None,
195        gsu_a: None,
196        gsu_h: None,
197        base: 0,
198        width: 0,
199        env: None,
200        ename: None,
201        old: None,
202        level: 0,
203    });
204    Some(pm) // c:210
205}
206
207#[cfg(test)]
208mod paramtypestr_tests {
209    use super::*;
210    use crate::ported::zsh_h::param;
211
212    fn make_pm(flags: u32, level: i32) -> param {
213        param {
214            node: hashnode {
215                next: None,
216                nam: String::new(),
217                flags: flags as i32,
218            },
219            u_data: 0,
220            u_tied: None,
221            u_arr: None,
222            u_str: None,
223            u_val: 0,
224            u_dval: 0.0,
225            u_hash: None,
226            gsu_s: None,
227            gsu_i: None,
228            gsu_f: None,
229            gsu_a: None,
230            gsu_h: None,
231            base: 0,
232            width: 0,
233            env: None,
234            ename: None,
235            old: None,
236            level,
237        }
238    }
239
240    /// Mirrors Src/Modules/parameter.c:43-95 — switch on
241    /// `PM_TYPE(pm->node.flags)` then dyncat'd modifier chain.
242    #[test]
243    fn paramtypestr_matches_c_dispatch() {
244        let _g = crate::test_util::global_state_lock();
245        // c:53 — plain scalar.
246        assert_eq!(paramtypestr(&make_pm(PM_SCALAR, 0)), "scalar");
247        // c:55,63-64,81-82 — array + level=1 + PM_EXPORTED.
248        assert_eq!(
249            paramtypestr(&make_pm(PM_ARRAY | PM_EXPORTED, 1)),
250            "array-local-export",
251        );
252        // c:91-92 — PM_UNSET short-circuits to "".
253        assert_eq!(paramtypestr(&make_pm(PM_UNSET, 0)), "");
254    }
255
256    // ═══════════════════════════════════════════════════════════════════
257    // Additional C-parity tests for Src/Modules/parameter.c:43-95
258    // paramtypestr modifier chain (every PM_* branch independently).
259    // ═══════════════════════════════════════════════════════════════════
260
261    /// c:49 — PM_AUTOLOAD short-circuits to "undefined" regardless
262    /// of any other flags or type.
263    #[test]
264    fn paramtypestr_autoload_returns_undefined() {
265        let _g = crate::test_util::global_state_lock();
266        assert_eq!(paramtypestr(&make_pm(PM_AUTOLOAD, 0)), "undefined");
267        // Even combined with other flags — autoload wins.
268        assert_eq!(
269            paramtypestr(&make_pm(PM_AUTOLOAD | PM_READONLY, 5)),
270            "undefined"
271        );
272        assert_eq!(
273            paramtypestr(&make_pm(PM_AUTOLOAD | PM_SCALAR, 0)),
274            "undefined"
275        );
276    }
277
278    /// c:55 — PM_ARRAY alone → "array".
279    #[test]
280    fn paramtypestr_plain_array() {
281        let _g = crate::test_util::global_state_lock();
282        assert_eq!(paramtypestr(&make_pm(PM_ARRAY, 0)), "array");
283    }
284
285    /// c:56 — PM_INTEGER alone → "integer".
286    #[test]
287    fn paramtypestr_plain_integer() {
288        let _g = crate::test_util::global_state_lock();
289        assert_eq!(paramtypestr(&make_pm(PM_INTEGER, 0)), "integer");
290    }
291
292    /// c:57-58 — PM_EFLOAT and PM_FFLOAT both → "float".
293    #[test]
294    fn paramtypestr_both_float_types_render_as_float() {
295        let _g = crate::test_util::global_state_lock();
296        assert_eq!(paramtypestr(&make_pm(PM_EFLOAT, 0)), "float");
297        assert_eq!(paramtypestr(&make_pm(PM_FFLOAT, 0)), "float");
298    }
299
300    /// c:59 — PM_HASHED → "association".
301    #[test]
302    fn paramtypestr_hashed_renders_as_association() {
303        let _g = crate::test_util::global_state_lock();
304        assert_eq!(paramtypestr(&make_pm(PM_HASHED, 0)), "association");
305    }
306
307    /// c:65-66 — PM_LEFT modifier appends "-left".
308    #[test]
309    fn paramtypestr_left_modifier() {
310        let _g = crate::test_util::global_state_lock();
311        assert_eq!(
312            paramtypestr(&make_pm(PM_SCALAR | PM_LEFT, 0)),
313            "scalar-left"
314        );
315    }
316
317    /// c:67-68 — PM_RIGHT_B modifier appends "-right_blanks".
318    #[test]
319    fn paramtypestr_right_blanks_modifier() {
320        let _g = crate::test_util::global_state_lock();
321        assert_eq!(
322            paramtypestr(&make_pm(PM_SCALAR | PM_RIGHT_B, 0)),
323            "scalar-right_blanks"
324        );
325    }
326
327    /// c:69-70 — PM_RIGHT_Z modifier appends "-right_zeros".
328    #[test]
329    fn paramtypestr_right_zeros_modifier() {
330        let _g = crate::test_util::global_state_lock();
331        assert_eq!(
332            paramtypestr(&make_pm(PM_SCALAR | PM_RIGHT_Z, 0)),
333            "scalar-right_zeros"
334        );
335    }
336
337    /// c:71-72 — PM_LOWER modifier appends "-lower".
338    #[test]
339    fn paramtypestr_lower_modifier() {
340        let _g = crate::test_util::global_state_lock();
341        assert_eq!(
342            paramtypestr(&make_pm(PM_SCALAR | PM_LOWER, 0)),
343            "scalar-lower"
344        );
345    }
346
347    /// c:73-74 — PM_UPPER modifier appends "-upper".
348    #[test]
349    fn paramtypestr_upper_modifier() {
350        let _g = crate::test_util::global_state_lock();
351        assert_eq!(
352            paramtypestr(&make_pm(PM_SCALAR | PM_UPPER, 0)),
353            "scalar-upper"
354        );
355    }
356
357    /// c:75-76 — PM_READONLY modifier appends "-readonly".
358    #[test]
359    fn paramtypestr_readonly_modifier() {
360        let _g = crate::test_util::global_state_lock();
361        assert_eq!(
362            paramtypestr(&make_pm(PM_SCALAR | PM_READONLY, 0)),
363            "scalar-readonly"
364        );
365    }
366
367    /// c:77-78 — PM_TAGGED modifier appends "-tag".
368    #[test]
369    fn paramtypestr_tagged_modifier() {
370        let _g = crate::test_util::global_state_lock();
371        assert_eq!(
372            paramtypestr(&make_pm(PM_SCALAR | PM_TAGGED, 0)),
373            "scalar-tag"
374        );
375    }
376
377    /// c:79-80 — PM_TIED modifier appends "-tied".
378    #[test]
379    fn paramtypestr_tied_modifier() {
380        let _g = crate::test_util::global_state_lock();
381        assert_eq!(
382            paramtypestr(&make_pm(PM_SCALAR | PM_TIED, 0)),
383            "scalar-tied"
384        );
385    }
386
387    /// c:81-82 — PM_EXPORTED modifier appends "-export".
388    #[test]
389    fn paramtypestr_exported_modifier() {
390        let _g = crate::test_util::global_state_lock();
391        assert_eq!(
392            paramtypestr(&make_pm(PM_SCALAR | PM_EXPORTED, 0)),
393            "scalar-export"
394        );
395    }
396
397    /// c:83-84 — PM_UNIQUE modifier appends "-unique".
398    #[test]
399    fn paramtypestr_unique_modifier() {
400        let _g = crate::test_util::global_state_lock();
401        assert_eq!(
402            paramtypestr(&make_pm(PM_ARRAY | PM_UNIQUE, 0)),
403            "array-unique"
404        );
405    }
406
407    /// c:85-86 — PM_HIDE modifier appends "-hide".
408    #[test]
409    fn paramtypestr_hide_modifier() {
410        let _g = crate::test_util::global_state_lock();
411        assert_eq!(
412            paramtypestr(&make_pm(PM_SCALAR | PM_HIDE, 0)),
413            "scalar-hide"
414        );
415    }
416
417    /// c:87-88 — PM_HIDEVAL modifier appends "-hideval".
418    #[test]
419    fn paramtypestr_hideval_modifier() {
420        let _g = crate::test_util::global_state_lock();
421        assert_eq!(
422            paramtypestr(&make_pm(PM_SCALAR | PM_HIDEVAL, 0)),
423            "scalar-hideval"
424        );
425    }
426
427    /// c:89-90 — PM_SPECIAL modifier appends "-special".
428    #[test]
429    fn paramtypestr_special_modifier() {
430        let _g = crate::test_util::global_state_lock();
431        assert_eq!(
432            paramtypestr(&make_pm(PM_SCALAR | PM_SPECIAL, 0)),
433            "scalar-special"
434        );
435    }
436
437    /// c:63-64 — pm.level > 0 appends "-local" before other modifiers.
438    /// Pin order: local-first to match C's modifier-chain order.
439    #[test]
440    fn paramtypestr_level_one_adds_local() {
441        let _g = crate::test_util::global_state_lock();
442        assert_eq!(paramtypestr(&make_pm(PM_SCALAR, 1)), "scalar-local");
443        assert_eq!(paramtypestr(&make_pm(PM_INTEGER, 3)), "integer-local");
444    }
445
446    /// c:63-64 — pm.level = 0 does NOT add "-local". Pin to catch
447    /// off-by-one (would label every var as "-local" silently).
448    #[test]
449    fn paramtypestr_level_zero_does_not_add_local() {
450        let _g = crate::test_util::global_state_lock();
451        let r = paramtypestr(&make_pm(PM_SCALAR, 0));
452        assert!(
453            !r.contains("local"),
454            "level=0 must not include 'local', got: {}",
455            r
456        );
457    }
458
459    /// Combined modifier chain order: local → flags appear in C's
460    /// declaration order (left/right_b/right_z/lower/upper/readonly/
461    /// tag/tied/export/unique/hide/hideval/special).
462    /// Pin: PM_LEFT + PM_READONLY + PM_EXPORTED = "scalar-left-readonly-export".
463    #[test]
464    fn paramtypestr_modifier_chain_order_matches_c() {
465        let _g = crate::test_util::global_state_lock();
466        let pm = make_pm(PM_SCALAR | PM_LEFT | PM_READONLY | PM_EXPORTED, 0);
467        assert_eq!(paramtypestr(&pm), "scalar-left-readonly-export");
468    }
469
470    /// c:48 — PM_UNSET wins over any modifier — short-circuits to "".
471    /// Pin: even if PM_READONLY etc. are set, PM_UNSET → "".
472    #[test]
473    fn paramtypestr_unset_wins_over_modifiers() {
474        let _g = crate::test_util::global_state_lock();
475        let pm = make_pm(PM_UNSET | PM_SCALAR | PM_READONLY, 0);
476        assert_eq!(paramtypestr(&pm), "", "PM_UNSET wins over modifiers");
477    }
478}
479
480// =====================================================================
481// static struct features module_features                            c:2300 (parameter.c)
482// =====================================================================
483
484/// Port of `scanpmparameters()` from `Src/Modules/parameter.c:124` — C decl `scanpmparameters(UNUSED(HashTable ht), ScanFunc func, int flags)`.
485///
486/// C iterates `realparamtab` invoking `func(&pm.node, flags)` for each
487/// non-PM_UNSET entry. The zshrs special-parameter hashparam-node
488/// integration isn't wired up yet — `${(@k)parameters}` reads
489/// through `paramtab()` directly, which is the Rust idiom
490/// replacement for the C iteration callback. No Rust callers of
491/// this fn; structural pass-through retained for C name parity.
492#[allow(non_snake_case)]
493/// WARNING: param names don't match C — Rust=(_ht, _func) vs C=(ht, func, flags)
494pub fn scanpmparameters(
495    _ht: *mut HashTable,
496    func: Option<ParamScanFunc>, // c:124
497    flags: i32,
498) {
499    let func = match func {
500        Some(f) => f,
501        None => return,
502    }; // c:131-141 no-op without func
503       // Reading `$parameters` IS an access to `parameters`, so it stops
504       // being an autoload stub here (C: the module load that answers this
505       // read defines the node). Its siblings stay stubs until touched.
506    crate::vm_helper::mark_module_param_used("parameters");
507    // Snapshot names + per-entry data under read lock so func() can
508    // re-enter paramtab without deadlock — C is single-threaded so
509    // walks the live table directly.
510    let entries: Vec<(String, u32, String)> = {
511        // c:135 — C `realparamtab` walk. Rust port keeps a separate
512        // `realparamtab` static that's never been wired to the live
513        // shell param storage; the actual shell paramtab is the
514        // canonical source. Walk that instead so
515        // `${(k)parameters}` / `${parameters[(i)PAT]}` see the
516        // shell's actual params (PATH, USER, IFS, etc.). Without
517        // this redirect, every scanpmparameters call returned empty.
518        let tab = crate::ported::params::paramtab()
519            .read()
520            .expect("paramtab poisoned");
521        tab.iter()
522            .filter(|(_, p)| (p.node.flags as u32 & PM_UNSET) == 0) // c:138 PM_UNSET skip
523            .map(|(name, p)| {
524                let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
525                    || (flags as u32 & SCANPM_WANTKEYS) == 0; // c:140-142
526                                                              // c:49-50 — `if (pm->node.flags & PM_AUTOLOAD) return
527                                                              // dupstring("undefined");`. In C the `zsh/parameter` params
528                                                              // sit in realparamtab as PM_AUTOLOAD stubs and materialize
529                                                              // ONE AT A TIME as they are touched (loading the module for
530                                                              // `$parameters` does not define its siblings), so an
531                                                              // enumeration reports "undefined" for the untouched ones.
532                                                              // zshrs seeds them all eagerly, so the stub state is tracked
533                                                              // separately; without this every one reported its real type
534                                                              // and `_parameters -g '^a*'` / `-g 'a*'` bucketed them the
535                                                              // opposite way from zsh.
536                let val = if want_val {
537                    // c:Src/params.c:1157-1166 createparam — a `local NAME`
538                    // pushes the PM_AUTOLOAD stub onto `pm->old` and makes a
539                    // fresh plain node visible; c:144 types the node it
540                    // ITERATED, so a shadowed name never reports "undefined".
541                    if crate::vm_helper::module_param_is_autoload_stub(name)
542                        && !crate::vm_helper::magic_special_shadowed(name)
543                    {
544                        "undefined".to_string() // c:50
545                    } else {
546                        paramtypestr(p)
547                    }
548                } else {
549                    String::new()
550                };
551                (name.clone(), p.node.flags as u32, val)
552            })
553            .collect()
554    };
555    // c:126-129 — `struct param pm;` declared ONCE outside the walk,
556    // `memset` ONCE, `pm.node.flags` set ONCE; the loop only rebinds
557    // `pm.node.nam` / `pm.u.str` per entry. (c:130 `pm.gsu.s =
558    // &nullsetscalar_gsu` — vtable not modelled.)
559    let mut pm = param {
560        node: hashnode {
561            next: None,
562            nam: String::new(),
563            flags: (PM_SCALAR | PM_READONLY) as i32, // c:129
564        },
565        ..Default::default()
566    };
567    for (name, _orig_flags, val) in entries {
568        // c:135-145
569        pm.node.nam = name; // c:137 pm.node.nam = hn->nam
570        pm.u_str = Some(val); // c:144 pm.u.str
571        func(&pm, flags); // c:145 func(&pm.node, flags)
572    }
573}
574
575/// Port of `setpmcommand()` from `Src/Modules/parameter.c:151` — C decl `setpmcommand(Param pm, char *value)`.
576/// C: `static void setpmcommand(Param pm, char *value)` — register a path
577/// alias in cmdnamtab for the named command.
578#[allow(non_snake_case)]
579pub fn setpmcommand(pm: Param, value: String) {
580    // c:151
581    // c:151-158 — `cn = zshcalloc(...); cn->node.flags = HASHED;
582    //   cn->u.cmd = ztrdup(value); cmdnamtab->addnode(...)`. The
583    //   helper bundles the hashnode literal so the call-site stays
584    //   one line.
585    let cn = cmdnam_hashed(&pm.node.nam, &value); // c:173-156
586    if let Ok(mut tab) = cmdnamtab_lock().write() {
587        tab.add(cn); // c:173 addnode
588    }
589}
590
591/// Port of `unsetpmcommand()` from `Src/Modules/parameter.c:163` — C decl `unsetpmcommand(Param pm, UNUSED(int exp))`.
592/// C: `static void unsetpmcommand(Param pm, UNUSED(int exp))` — remove the
593/// named entry from `cmdnamtab`.
594#[allow(non_snake_case)]
595#[allow(unused_variables)]
596pub fn unsetpmcommand(pm: Param, exp: i32) {
597    // c:163
598    if let Ok(mut tab) = cmdnamtab_lock().write() {
599        // c:165 — HashNode hn = cmdnamtab->removenode(cmdnamtab, pm->node.nam);
600        let _hn = tab.remove(&pm.node.nam);
601        // c:167-168 — if (hn) cmdnamtab->freenode(hn); — Rust Drop on scope exit.
602    }
603}
604
605/// Port of `setpmcommands()` from `Src/Modules/parameter.c:173` — C decl `setpmcommands(Param pm, HashTable ht)`.
606/// C: `static void setpmcommands(Param pm, HashTable ht)` — bulk install.
607#[allow(non_snake_case)]
608#[allow(unused_variables)]
609/// WARNING: param shape doesn't match C — C passes the temporary
610/// HashTable built by `arrhashsetfn` (`Src/params.c:4113`) whose nodes
611/// are child Params carrying values in `u.str`; zshrs's `hashnode` has
612/// no value slot, so the (key, value) pairs are passed directly. The
613/// iteration semantics are identical: one install per pair, additive
614/// (existing `cmdnamtab` entries are NOT flushed).
615pub fn setpmcommands(pm: Param, ht: &[(String, String)]) {
616    // c:173
617    // c:178-179 — if (!ht) return; (an empty pair list is the
618    // equivalent no-op; the loop below simply doesn't run).
619    //
620    // c:181-194 — for each node: `cn = zshcalloc(...);
621    //   cn->node.flags = HASHED; cn->u.cmd = ztrdup(getstrvalue(&v));
622    //   cmdnamtab->addnode(cmdnamtab, ztrdup(hn->nam), &cn->node);`
623    for (nam, path) in ht {
624        let cn = cmdnam_hashed(nam, path); // c:183/191/192
625        if let Ok(mut tab) = cmdnamtab_lock().write() {
626            tab.add(cn); // c:194 addnode
627        }
628    }
629    // c:196-205 — `if (ht != pm->u.hash) deleteparamtable(ht);` —
630    // temp-table teardown; Rust drops the borrowed slice's owner.
631    let _ = pm;
632}
633
634/// Port of `getpmcommand()` from `Src/Modules/parameter.c:213` — C decl `getpmcommand(UNUSED(HashTable ht), const char *name)`.
635/// C body (c:216-241):
636/// ```c
637/// cmd = cmdnamtab->getnode(cmdnamtab, name);
638/// if (!cmd && isset(HASHLISTALL)) cmdnamtab->filltable(...); cmd = ...;
639/// pm.node.nam = name; pm.node.flags = PM_SCALAR; pm.gsu.s = &pmcommand_gsu;
640/// if (cmd) {
641///     if (cmd->node.flags & HASHED) pm->u.str = cmd->u.cmd;
642///     else                          pm->u.str = path/name;
643/// } else {
644///     pm->u.str = ""; pm->node.flags |= (PM_UNSET|PM_SPECIAL);
645/// }
646/// ```
647#[allow(non_snake_case)]
648#[allow(unused_variables)]
649pub fn getpmcommand(ht: *mut HashTable, name: &str) -> Option<Param> {
650    // c:213
651    // c:218-222 — `if (!cmdnamtab->getnode(cmdnamtab, name) &&
652    //              isset(HASHLISTALL)) { cmdnamtab->filltable(...);
653    //              cmd = cmdnamtab->getnode(...); }`
654    // Walk cmdnamtab; if miss AND HASHLISTALL set (default ON in
655    // zsh), fill the table from $PATH and retry.
656    //
657    // p10k / zinit hit `${commands[name]}` for every command-check
658    // they do (`whence -p`-equivalent without a fork). Without the
659    // HASHLISTALL-driven filltable, lookups returned empty for any
660    // command the shell hadn't explicitly `hash`'d yet.
661    let entry_exists = cmdnamtab_lock()
662        .read()
663        .ok()
664        .and_then(|g| g.get(name).cloned())
665        .is_some();
666    if !entry_exists && crate::ported::zsh_h::isset(crate::ported::zsh_h::HASHLISTALL) {
667        // c:220
668        if let Some(path) = crate::ported::params::getsparam("PATH") {
669            let path_arr: Vec<String> = path.split(':').map(|s| s.to_string()).collect();
670            crate::ported::hashtable::fillcmdnamtable(&path_arr); // c:220
671        }
672    }
673    // c:224-232 — `else { char *found = findcmd((char*)name, 1, 0); if (found)
674    // { cmd = hcalloc(...); cmd->u.cmd = found; cmd->node.flags = HASHED; } }`.
675    // Without HASH_LIST_ALL the table is not filled; the lookup searches
676    // $path directly ("this will return the path even if hashcmds is
677    // disabled"). The port had no such branch, so `$commands[ls]` read
678    // whatever stale entry the table held (or nothing).
679    if !entry_exists && !crate::ported::zsh_h::isset(crate::ported::zsh_h::HASHLISTALL) {
680        if let Some(found) = crate::ported::exec::findcmd(name, 1, 0) {
681            // c:227
682            return Some(Box::new(param {
683                node: hashnode {
684                    next: None,
685                    nam: name.to_string(),   // c:236
686                    flags: PM_SCALAR as i32, // c:237
687                },
688                u_data: 0,
689                u_tied: None,
690                u_arr: None,
691                u_str: Some(found), // c:241 `pm->u.str = cmd->u.cmd`
692                u_val: 0,
693                u_dval: 0.0,
694                u_hash: None,
695                gsu_s: None,
696                gsu_i: None,
697                gsu_f: None,
698                gsu_a: None,
699                gsu_h: None,
700                base: 0,
701                width: 0,
702                env: None,
703                ename: None,
704                old: None,
705                level: 0,
706            }));
707        }
708    }
709    let g = cmdnamtab_lock().read().ok()?;
710    let entry = g.get(name); // c:218/221 cmdnamtab->getnode
711    let (value, found) = if let Some(cmd) = entry {
712        // c:227
713        let v = if (cmd.node.flags & HASHED as i32) != 0 {
714            // c:229 HASHED
715            cmd.cmd.clone().unwrap_or_default() // c:230 cn->u.cmd
716        } else {
717            let dir = cmd
718                .name
719                .as_ref()
720                .and_then(|v| v.first().cloned()) // c:232 *(cmd->u.name)
721                // C: `*(cmd->u.name)` reads first entry of $path array.
722                //     paramtab read; was OS env split.
723                .unwrap_or_else(|| {
724                    getsparam("PATH")
725                        .and_then(|p| p.split(':').next().map(|s| s.to_string()))
726                        .unwrap_or_default()
727                });
728            format!("{}/{}", dir, name) // c:233-235 strcat
729        };
730        (v, true)
731    } else {
732        (String::new(), false) // c:238
733    };
734    let mut pm = Box::new(param {
735        // c:223 hcalloc
736        node: hashnode {
737            next: None,
738            nam: name.to_string(), // c:224
739            flags: if found {
740                PM_SCALAR as i32
741            } else {
742                (PM_SCALAR | PM_UNSET | PM_SPECIAL) as i32
743            }, // c:226 / c:239
744        },
745        u_data: 0,
746        u_tied: None,
747        u_arr: None,
748        u_str: Some(value), // c:230 / c:233 / c:238
749        u_val: 0,
750        u_dval: 0.0,
751        u_hash: None,
752        // c:209-210 — `pmcommand_gsu = { strgetfn, setpmcommand, unsetpmcommand }`
753        // c:238 — `pm->gsu.s = &pmcommand_gsu;`
754        gsu_s: Some(Box::new(crate::ported::zsh_h::gsu_scalar {
755            getfn: crate::ported::params::strgetfn,
756            setfn: |pm, val| setpmcommand(Box::new(pm.clone()), val),
757            unsetfn: |pm, exp| unsetpmcommand(Box::new(pm.clone()), exp),
758        })),
759        gsu_i: None,
760        gsu_f: None,
761        gsu_a: None,
762        gsu_h: None,
763        base: 0,
764        width: 0,
765        env: None,
766        ename: None,
767        old: None,
768        level: 0,
769    });
770    let _ = &mut pm;
771    Some(pm) // c:241 return &pm->node
772}
773
774/// Port of `scanpmcommands()` from `Src/Modules/parameter.c:245` — C decl `scanpmcommands(UNUSED(HashTable ht), ScanFunc func, int flags)`.
775/// C body (c:248-280):
776/// ```c
777/// if (isset(HASHLISTALL)) cmdnamtab->filltable(cmdnamtab);
778/// pm.node.flags = PM_SCALAR; pm.gsu.s = &pmcommand_gsu;
779/// for each hn in cmdnamtab:
780///     pm.node.nam = hn->nam;
781///     if non-counting && wantvals:
782///         pm.u.str = HASHED ? cmd->u.cmd : path/name
783///     func(&pm.node, flags);
784/// ```
785#[allow(non_snake_case)]
786/// WARNING: param names don't match C — Rust=(_ht, func) vs C=(ht, func, flags)
787pub fn scanpmcommands(
788    _ht: *mut HashTable,
789    func: Option<ParamScanFunc>, // c:245
790    flags: i32,
791) {
792    // c:253 — `if (isset(HASHLISTALL)) cmdnamtab->filltable(...)`. The
793    // filltable variant scans $PATH and inserts every executable into
794    // cmdnamtab; without HASHLISTALL only previously-hashed entries
795    // appear.
796    //
797    // Mirror the C HASHLISTALL gate: when set (default in interactive
798    // and -fc modes), fill cmdnamtab from $PATH before walking it.
799    // Same fix the bin_hash -m branch already does (builtin.rs:5180+).
800    // Without this, `${commands[(i)pat]}` and `${(k)commands}` return
801    // empty for any binary the shell hasn't already `hash`'d.
802    if crate::ported::zsh_h::isset(crate::ported::zsh_h::HASHLISTALL) {
803        if let Some(path) = getsparam("PATH") {
804            let path_arr: Vec<String> = path.split(':').map(|s| s.to_string()).collect();
805            crate::ported::hashtable::fillcmdnamtable(&path_arr); // c:253
806        }
807    }
808    // c:275-277 — `if (func != scancountparams && ((flags &
809    // (SCANPM_WANTVALS|SCANPM_MATCHVAL)) || !(flags & SCANPM_WANTKEYS)))`:
810    // the value side is built ONLY when the caller asked for values. This
811    // port used to build it unconditionally, so every keys-only scan
812    // (`${(k)commands}`, `compadd -k commands` via `gethkparam`) still
813    // formatted a `dir/name` path for all of cmdnamtab and threw it away.
814    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
815        || (flags as u32 & SCANPM_WANTKEYS) == 0;
816    let cmds: Vec<(String, String)> = {
817        let g = cmdnamtab_lock().read().unwrap();
818        g.iter()
819            .map(|(name, cmd)| {
820                // c:259-260
821                let hashed = (cmd.node.flags & HASHED as i32) != 0;
822                // c:266-274 — pm.u.str: HASHED → cmd->u.cmd (real path);
823                // unhashed → first $PATH dir + "/" + name.
824                let value = if !want_val {
825                    String::new()
826                } else if hashed {
827                    cmd.cmd.clone().unwrap_or_default() // c:267 cn->u.cmd
828                } else {
829                    let dir = cmd
830                        .name
831                        .as_ref()
832                        .and_then(|v| v.first().cloned()) // c:269 *(cmd->u.name)
833                        // C: `*(cmd->u.name)` — first entry of $path array.
834                        //     Read shell-side $PATH from paramtab (was OS env).
835                        .unwrap_or_else(|| {
836                            getsparam("PATH")
837                                .and_then(|p| p.split(':').next().map(|s| s.to_string()))
838                                .unwrap_or_default()
839                        });
840                    format!("{}/{}", dir, name) // c:271-273 strcat
841                };
842                (name.clone(), value)
843            })
844            .collect()
845    };
846    if let Some(f) = func {
847        // c:259-269 — `struct param pm;` declared ONCE outside the walk;
848        // the loop only rebinds `pm.node.nam` (c:273) and `pm.u.str`
849        // (c:279/281) before each `func(&pm.node, flags)`.
850        let mut pm = param {
851            node: hashnode {
852                next: None,
853                nam: String::new(),
854                flags: PM_SCALAR as i32, // c:268
855            },
856            ..Default::default()
857        };
858        for (name, value) in cmds {
859            pm.node.nam = name; // c:273
860            pm.u_str = Some(value); // c:279 / c:281-286
861            f(&pm, flags); // c:290 func(&pm.node, flags)
862        }
863    }
864}
865
866/// Port of `setfunction()` from `Src/Modules/parameter.c:284` — C decl `setfunction(char *name, char *val, int dis)`.
867/// C: `static void setfunction(char *name, char *val, int dis)` — install
868/// a shell function from text source.
869#[allow(non_snake_case)]
870pub fn setfunction(name: &str, mut val: String, dis: i32) {
871    // c:284
872    // c:284-289 — declarations at function top (PORT.md Rule 5: same
873    // names, same order, same scope as C).
874    let value: String; // c:286 char *value
875    let mut shf: shfunc; // c:287 Shfunc shf
876    let prog: Option<crate::ported::zsh_h::eprog>; // c:288 Eprog prog
877                                                   // c:289 — int sn (used inside the TRAP branch only)
878
879    // c:286 — char *value = dupstring(val);
880    value = val.clone();
881    // c:291 — val = metafy(val, strlen(val), META_REALLOC);
882    val = crate::ported::utils::metafy(&val);
883    // c:293 — prog = parse_string(val, 1);
884    // parse_string ported at crate::ported::exec::parse_string (c:283
885    // in Src/exec.c). Returns None on parse error → matches the C
886    // !prog guard at c:295.
887    prog = crate::ported::exec::parse_string(&val, 1); // c:293
888    if prog.is_none() {
889        // c:295 !prog
890        zwarn(
891            // c:296
892            &format!("invalid function definition: {}", value),
893        );
894        return; // c:298
895    }
896    // c:300 — shf = zshcalloc(sizeof(*shf));
897    // c:301 — shf->funcdef = dupeprog(prog, 0);
898    // c:302 — shf->node.flags = dis;
899    shf = shfunc {
900        node: hashnode {
901            next: None,
902            nam: name.to_string(),
903            flags: dis, // c:302
904        },
905        filename: None,
906        lineno: 0,
907        funcdef: prog.clone().map(Box::new), // c:301 — dupeprog(prog, 0)
908        redir: None,
909        sticky: None,
910        body: Some(val.clone()), // body source retained for deferred-recompile flows
911        redir_text: None,
912    };
913    // c:303 — `shfunc_set_sticky(shf);` (EXTERN exec.c). Stamps the
914    // pending sticky-emulation snapshot onto the new function.
915    crate::ported::exec::shfunc_set_sticky(&mut shf);
916
917    // c:305-313 — TRAP* handling: a function named TRAPINT / TRAPHUP /
918    // TRAPCHLD / etc. is also installed as the signal trap for the
919    // matching signal. settrap(sn, NULL, ZSIG_FUNC) tells the signal
920    // subsystem "the handler is the same-named shell function — look
921    // it up by name at delivery time," so we don't pass the Eprog.
922    //
923    // C body (verbatim):
924    //   if (!strncmp(name, "TRAP", 4) &&
925    //       (sn = getsigidx(name + 4)) != -1) {
926    //       if (settrap(sn, NULL, ZSIG_FUNC)) {
927    //           freeeprog(shf->funcdef);
928    //           zfree(shf, sizeof(*shf));
929    //           zsfree(val);
930    //           return;
931    //       }
932    //   }
933    //
934    // settrap returns non-zero on rejection (invalid sig, MONITOR
935    // can't-trap, etc.); on rejection C frees the half-built shfunc
936    // and returns WITHOUT calling shfunctab->addnode — i.e. defining
937    // TRAPTTOU under `setopt MONITOR` is a no-op that doesn't
938    // register the function either. Prior Rust port just commented
939    // the dispatch out, so TRAP* functions were never wired as
940    // signal handlers — `TRAPINT() { ... }` defined via
941    // `functions[TRAPINT]=...` never fired on SIGINT.
942    if name.len() >= 4 && &name[..4] == "TRAP" {
943        // c:305
944        if let Some(sn) = crate::ported::signals::getsigidx(&name[4..]) {
945            // c:306 sn = getsigidx(name + 4)
946            if crate::ported::signals::settrap(sn, None, crate::ported::zsh_h::ZSIG_FUNC) != 0 {
947                // c:307-312 — settrap rejected the install; don't
948                // register the function either.
949                return; // c:311
950            }
951        }
952    }
953
954    // c:314 — shfunctab->addnode(shfunctab, ztrdup(name), shf);
955    if let Ok(mut tab) = shfunctab_lock().write() {
956        tab.add(shf);
957    }
958    // Invalidate the executor's compiled-function cache so the next
959    // call recompiles from the new body. Without this, re-defining
960    // an existing function via `functions[name]=...` updates
961    // shfunctab but the executor keeps dispatching to the cached
962    // Eprog from the original definition (resulting in old-body
963    // behavior). Bug #323 in docs/BUGS.md.
964    // Invalidate via the exec accessors channel (drops the executor's
965    // compiled-chunk + source caches) instead of reaching into the
966    // ShellExecutor directly from the ported tree.
967    let _ = crate::ported::exec::unregister_function(name);
968    // c:315 — zsfree(val); — Rust drops on scope exit.
969}
970
971/// Port of `setpmfunction()` from `Src/Modules/parameter.c:320` — C decl `setpmfunction(Param pm, char *value)`.
972/// C: `setfunction(pm->node.nam, value, 0);`
973#[allow(non_snake_case)]
974pub fn setpmfunction(pm: Param, value: String) {
975    // c:320
976    let nam = pm.node.nam.clone();
977    setfunction(&nam, value, 0) // c:323
978}
979
980/// Port of `setpmdisfunction()` from `Src/Modules/parameter.c:327` — C decl `setpmdisfunction(Param pm, char *value)`.
981/// C: `setfunction(pm->node.nam, value, DISABLED);`
982#[allow(non_snake_case)]
983pub fn setpmdisfunction(pm: Param, value: String) {
984    // c:327
985    let nam = pm.node.nam.clone();
986    setfunction(&nam, value, DISABLED) // c:330
987}
988
989/// Port of `unsetpmfunction()` from `Src/Modules/parameter.c:334` — C decl `unsetpmfunction(Param pm, UNUSED(int exp))`.
990/// C: `static void unsetpmfunction(Param pm, UNUSED(int exp))` — remove the
991/// named function from `shfunctab`.
992#[allow(non_snake_case)]
993#[allow(unused_variables)]
994pub fn unsetpmfunction(pm: Param, exp: i32) {
995    // c:334
996    if let Ok(mut tab) = shfunctab_lock().write() {
997        // c:336 — HashNode hn = shfunctab->removenode(shfunctab, pm->node.nam);
998        let _hn = tab.remove(&pm.node.nam);
999        // c:338-339 — if (hn) shfunctab->freenode(hn); — Rust Drop on scope exit.
1000    }
1001}
1002
1003/// Port of `setfunctions()` from `Src/Modules/parameter.c:344` — C decl `setfunctions(Param pm, HashTable ht, int dis)`.
1004/// C: `static void setfunctions(Param pm, HashTable ht, int dis)` — install
1005/// all functions in `ht`.
1006#[allow(non_snake_case)]
1007#[allow(unused_variables)]
1008/// WARNING: param shape doesn't match C — C passes the temporary
1009/// HashTable of value-carrying child Params (see setpmcommands);
1010/// zshrs passes the (key, value) pairs directly. Additive: existing
1011/// functions are NOT flushed.
1012pub fn setfunctions(pm: Param, ht: &[(String, String)], dis: i32) {
1013    // c:344
1014    // c:349-350 — if (!ht) return; (empty pair list = same no-op).
1015    //
1016    // c:352-362 — for each node:
1017    //   `setfunction(hn->nam, ztrdup(getstrvalue(&v)), dis);`
1018    for (nam, body) in ht {
1019        setfunction(nam, body.clone(), dis); // c:361
1020    }
1021    // c:364-365 — if (ht != pm->u.hash) deleteparamtable(ht);
1022    let _ = pm;
1023}
1024
1025/// Port of `setpmfunctions()` from `Src/Modules/parameter.c:370` — C decl `setpmfunctions(Param pm, HashTable ht)`.
1026#[allow(non_snake_case)]
1027pub fn setpmfunctions(pm: Param, ht: &[(String, String)]) {
1028    // c:370
1029    setfunctions(pm, ht, 0) // c:370
1030}
1031
1032/// Port of `setpmdisfunctions()` from `Src/Modules/parameter.c:377` — C decl `setpmdisfunctions(Param pm, HashTable ht)`.
1033/// C: `setfunctions(pm, ht, DISABLED);`
1034#[allow(non_snake_case)]
1035pub fn setpmdisfunctions(pm: Param, ht: &[(String, String)]) {
1036    // c:377
1037    setfunctions(pm, ht, DISABLED) // c:377
1038}
1039
1040/// Memo for the `getpermtext` deparse of a function body.
1041///
1042/// C keeps every function as a compiled `Eprog` (`shf->funcdef`) and deparses
1043/// it on demand at c:Src/Modules/parameter.c:419 and again at :493, so a
1044/// re-read costs one walk of already-parsed wordcode. zshrs stores the body as
1045/// SOURCE TEXT, so reproducing C's output means re-LEXING and re-parsing it
1046/// first — far more than C pays. Keyed by name, holding `(body, deparsed)`: a hit
1047/// requires the stored body to equal the current one, so redefining a function
1048/// invalidates its entry. The name-only key keeps a probe allocation-free —
1049/// `${(P)i}` with `i=functions` (zpwr `_parameters`) probes every function on
1050/// every TAB, and a `name\0body` key copied and hashed all of their bodies.
1051///
1052/// Module-level rather than per-function because BOTH C copies of the value
1053/// construction (`getfunction` c:401-443 and `scanfunctions` c:487-521) are
1054/// ported, and a single memo keeps a whole-hash scan and a per-key read from
1055/// each re-deparsing what the other already did.
1056thread_local! {
1057    static FN_DEPARSE_CACHE: std::cell::RefCell<
1058        std::collections::HashMap<String, (String, String)>,
1059    > = std::cell::RefCell::new(std::collections::HashMap::new());
1060}
1061
1062/// Port of `getfunction()` from `Src/Modules/parameter.c:389` — C decl `getfunction(UNUSED(HashTable ht), const char *name, int dis)`.
1063/// C body (c:392-441):
1064/// ```c
1065/// pm.node.nam = name; pm.node.flags = PM_SCALAR;
1066/// pm.gsu.s = dis ? &pmdisfunction_gsu : &pmfunction_gsu;
1067/// if (shf = shfunctab[name]; shf matches dis) {
1068///     if (PM_UNDEFINED) pm.u.str = "builtin autoload -X" + flags;
1069///     else { build "{\n\t<body>\n\t<name> "$@"" if EF_RUN; getpermtext };
1070/// } else { pm.u.str = ""; flags |= PM_UNSET|PM_SPECIAL; }
1071/// ```
1072#[allow(non_snake_case)]
1073/// WARNING: param names don't match C — Rust=() vs C=(ht, name, dis)
1074pub fn getfunction(_ht: *mut HashTable, name: &str, dis: i32) -> Option<Param> {
1075    // c:388
1076    // Faithful port of c:399-438:
1077    //   if ((shf = shfunctab->getnode2(shfunctab, name)) &&
1078    //       (dis ? (shf->node.flags & DISABLED)
1079    //            : !(shf->node.flags & DISABLED))) {
1080    //       if (shf->node.flags & PM_UNDEFINED) {
1081    //           pm->u.str = dyncat('builtin autoload -X', ...);
1082    //       } else { /* build pretty body */ }
1083    //   } else {
1084    //       pm->u.str = ''; flags |= PM_UNSET|PM_SPECIAL;
1085    //   }
1086    //
1087    // C uses getnode2 (no DISABLED filter) so the entry is visible
1088    // regardless of state; the dis-parity check decides visibility.
1089    // The Rust equivalent is get_including_disabled — get() filters
1090    // DISABLED out automatically.
1091    //
1092    // Prior port called shfunctab.get(name) which already drops the
1093    // disabled entries, then ignored the `dis` parameter entirely.
1094    // That meant:
1095    //   - \${(k)functions[(I)f]}     listed disabled fns (wrong)
1096    //   - \${(k)dis_functions[(I)f]} returned nothing (wrong: should
1097    //                                list ONLY disabled fns)
1098    let g = shfunctab_lock().read().ok()?;
1099    let entry = g.get_including_disabled(name); // c:399 shfunctab->getnode2
1100    let (value, found) = if let Some(shf) = entry {
1101        // c:400 — DISABLED parity check.
1102        let is_disabled = (shf.node.flags & DISABLED as i32) != 0;
1103        let dis_match = if dis != 0 { is_disabled } else { !is_disabled };
1104        if dis_match {
1105            // c:401-407 — PM_UNDEFINED autoload form. C builds the
1106            // suffix from PM_UNALIASED + PM_TAGGED:
1107            //   pm->u.str = dyncat("builtin autoload -X",
1108            //       ((shf->node.flags & PM_UNALIASED) ?
1109            //        ((shf->node.flags & PM_TAGGED) ? "Ut" : "U") :
1110            //        ((shf->node.flags & PM_TAGGED) ? "t" : "")));
1111            //
1112            // So autoload -X with the four-state suffix tells the user
1113            // (via ${functions[NAME]}) which `autoload` flags were
1114            // used: "U" = `autoload -U` (no alias expansion),
1115            // "t" = `autoload -t` (tracing on entry), "Ut" = both.
1116            // Prior Rust port always emitted bare "builtin autoload -X"
1117            // — `${functions[my_autoload_U]}` returned the same string
1118            // whether the function was loaded with `autoload -U` or
1119            // `autoload`, dropping the metaprogramming signal that
1120            // many prompt frameworks (powerlevel10k, starship)
1121            // consult when deciding whether to re-source.
1122            //
1123            // Static-link path also doesn't yet expose PM_UNDEFINED on
1124            // ShFunc; route via body.is_none() as the autoload signal,
1125            // then inspect node.flags for the U/t letters.
1126            let body = shf.body.as_deref();
1127            let v = match body {
1128                None => {
1129                    let f = shf.node.flags as u32;
1130                    let unaliased = (f & PM_UNALIASED) != 0;
1131                    let tagged = (f & PM_TAGGED) != 0;
1132                    let suffix = match (unaliased, tagged) {
1133                        // c:402-405
1134                        (true, true) => "Ut",
1135                        (true, false) => "U",
1136                        (false, true) => "t",
1137                        (false, false) => "",
1138                    };
1139                    format!("builtin autoload -X{}", suffix)
1140                }
1141                Some(text) => {
1142                    // c:Src/Modules/parameter.c:419 — `getpermtext(shf->funcdef,
1143                    // NULL, 1)`: C re-deparses the parsed body with tnewlins=1
1144                    // so a multi-statement body renders one statement per line,
1145                    // tab-indented (`print a; print b` → two `\t`-prefixed
1146                    // lines). zshrs stores the body as source text rather than
1147                    // the Eprog, so re-parse it and run the same getpermtext
1148                    // deparser to recover the line breaks. On a parse failure
1149                    // (should not happen — it parsed at definition time) fall
1150                    // back to the raw single-line text.
1151                    //
1152                    // Memoize: the deparse of a given body TEXT is
1153                    // deterministic, and whole-assoc consumers ($functions
1154                    // enumerations, e.g. zinit's `.zinit-diff-functions`)
1155                    // call this per key, repeatedly, across a plugin load.
1156                    // Re-parsing + re-deparsing every function body on every
1157                    // read made a p10k load take seconds. Cache keyed by the
1158                    // raw body text (self-invalidating: a redefinition
1159                    // changes the text → new key). C never pays this because
1160                    // it keeps the compiled Eprog and deparses on demand.
1161                    // The re-parse above is a LEX, so it resolves quotes
1162                    // against whatever options are live NOW — but C deparses
1163                    // wordcode baked at definition time (c:Src/exec.c:5389
1164                    // execfuncdef), which cannot be revisited. Pin the lexer
1165                    // to the state the definition was installed under, the
1166                    // same way `printshfuncnode` does for `functions NAME`.
1167                    // Without it `${functions[f]}` and `whence -f f`
1168                    // disagreed with EACH OTHER for one function.
1169                    // docs/BUGS.md #1105.
1170                    let deparsed = if let Some(hit) = FN_DEPARSE_CACHE.with(|c| {
1171                        c.borrow()
1172                            .get(&shf.node.nam)
1173                            .filter(|(body, _)| body == text)
1174                            .map(|(_, d)| d.clone())
1175                    })
1176                    {
1177                        hit
1178                    } else {
1179                        let _pin = crate::vm_helper::funcdef_lex_pin(&shf.node.nam, text);
1180                        // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1181                        // The cross-process deparse cache (extensions/deparse_cache.rs);
1182                        // the key is taken under the pin.
1183                        let key = crate::deparse_cache::key(&shf.node.nam, text);
1184                        let out = match key.and_then(|k| crate::deparse_cache::lookup(&k)) {
1185                            Some(hit) => hit,
1186                            None => {
1187                                let out = match crate::ported::exec::parse_string(text, 0) {
1188                                    Some(prog) => crate::ported::text::getpermtext(Box::new(prog), None, 1),
1189                                    None => text.to_string(),
1190                                };
1191                                if let Some(k) = key {
1192                                    crate::deparse_cache::record(k, &out);
1193                                }
1194                                out
1195                            }
1196                        };
1197                        FN_DEPARSE_CACHE.with(|c| {
1198                            c.borrow_mut()
1199                                .insert(shf.node.nam.clone(), (text.to_string(), out.clone()))
1200                        });
1201                        out
1202                    };
1203                    // c:422-425 — `if (shf->redir) start = "{\n\t"; else
1204                    // start = "\t";`. A definition that carried trailing
1205                    // redirections prints its body BRACED, because the
1206                    // redirections have to hang off the closing brace.
1207                    // `redir_text` is zshrs's rendered stand-in for
1208                    // `shf->redir` (see shfunc::redir_text).
1209                    // c:440 — `getpermtext(shf->redir, NULL, 1)`. zshrs's
1210                    // fusevm definition path stores the already-rendered text
1211                    // in the Rust-only `redir_text` twin instead of a second
1212                    // Eprog (see `shfunc::redir_text`), so consult both.
1213                    let redir: Option<String> = shf
1214                        .redir
1215                        .as_ref()
1216                        .map(|r| crate::ported::text::getpermtext(r.clone(), None, 1)) // c:440
1217                        .filter(|t| !t.is_empty())
1218                        .or_else(|| shf.redir_text.clone());
1219                    let start = if redir.is_some() {
1220                        "{\n\t" // c:423
1221                    } else {
1222                        "\t" // c:425
1223                    };
1224                    // c:436 — `h = dyncat(start, t);`
1225                    let mut h = format!("{}{}", start, deparsed);
1226                    // c:439-443 — `if (shf->redir) { t =
1227                    // getpermtext(shf->redir, NULL, 1); h = zhtricat(h,
1228                    // "\n}", t); }`
1229                    if let Some(rt) = redir {
1230                        h.push_str("\n}"); // c:441
1231                        h.push_str(&rt); // c:441
1232                    }
1233                    h
1234                }
1235            };
1236            (v, true)
1237        } else {
1238            // c:435-437 — wrong DISABLED parity: treat as not found.
1239            (String::new(), false)
1240        }
1241    } else {
1242        (String::new(), false) // c:439
1243    };
1244    let pm = Box::new(param {
1245        // c:393
1246        node: hashnode {
1247            next: None,
1248            nam: name.to_string(), // c:394
1249            flags: if found {
1250                PM_SCALAR as i32
1251            }
1252            // c:395
1253            else {
1254                (PM_SCALAR | PM_UNSET | PM_SPECIAL) as i32
1255            }, // c:440
1256        },
1257        u_data: 0,
1258        u_tied: None,
1259        u_arr: None,
1260        u_str: Some(value), // c:402/431/438
1261        u_val: 0,
1262        u_dval: 0.0,
1263        u_hash: None,
1264        gsu_s: None, // c:396 pm[dis]function_gsu
1265        gsu_i: None,
1266        gsu_f: None,
1267        gsu_a: None,
1268        gsu_h: None,
1269        base: 0,
1270        width: 0,
1271        env: None,
1272        ename: None,
1273        old: None,
1274        level: 0,
1275    });
1276    Some(pm) // c:441
1277}
1278
1279/// Port of `getpmfunction()` from `Src/Modules/parameter.c:444` — C decl `getpmfunction(HashTable ht, const char *name)`.
1280/// C: `static HashNode getpmfunction(HashTable ht, const char *name)` →
1281///   `return getfunction(ht, name, 0);`
1282#[allow(non_snake_case)]
1283pub fn getpmfunction(ht: *mut HashTable, name: &str) -> Option<Param> {
1284    // c:444
1285    getfunction(ht, name, 0) // c:444
1286}
1287
1288/// Port of `getpmdisfunction()` from `Src/Modules/parameter.c:451` — C decl `getpmdisfunction(HashTable ht, const char *name)`.
1289/// C: `static HashNode getpmdisfunction(HashTable ht, const char *name)` →
1290///   `return getfunction(ht, name, DISABLED);`
1291#[allow(non_snake_case)]
1292pub fn getpmdisfunction(ht: *mut HashTable, name: &str) -> Option<Param> {
1293    // c:451
1294    getfunction(ht, name, DISABLED) // c:451
1295}
1296
1297/// Port of `scanfunctions()` from `Src/Modules/parameter.c:458` — C decl `scanfunctions(UNUSED(HashTable ht), ScanFunc func, int flags, int dis)`.
1298/// C: `static void scanfunctions(UNUSED(HashTable ht), ScanFunc func,
1299///     int flags, int dis)` — iterate shfunctab.
1300#[allow(non_snake_case)]
1301/// WARNING: param names don't match C — Rust=(_ht, func, _dis) vs C=(ht, func, flags, dis)
1302pub fn scanfunctions(
1303    _ht: *mut HashTable,
1304    func: Option<ParamScanFunc>, // c:458
1305    flags: i32,
1306    dis: i32,
1307) {
1308    // C body (c:464-514):
1309    //   for (i = 0; i < shfunctab->hsize; i++)
1310    //       for (hn = shfunctab->nodes[i]; hn; hn = hn->next) {
1311    //           if (dis ? (hn->flags & DISABLED)
1312    //                   : !(hn->flags & DISABLED)) {
1313    //               pm.node.nam = hn->nam;
1314    //               ... build body ...
1315    //               func(&pm.node, flags);
1316    //           }
1317    //       }
1318    //
1319    // Prior Rust port iterated every shfunctab entry and emitted
1320    // unconditionally — neither the dis parameter nor the DISABLED
1321    // bit was honoured. \${(k)dis_functions} returned every function
1322    // (wrong: should list ONLY disabled), \${(k)functions} included
1323    // disabled ones too. Both diverging from zsh -fc parity.
1324    //
1325    // Now reads the DISABLED bit from each entry and matches against
1326    // dis per C's c:470 gate. shfunctab.iter() exposes all entries
1327    // including disabled ones, so the flag check is what discriminates.
1328    // c:484-486 — `if (func != scancountparams && ((flags &
1329    // (SCANPM_WANTVALS|SCANPM_MATCHVAL)) || !(flags & SCANPM_WANTKEYS)))`:
1330    // the deparsed body is built ONLY when values were asked for, so
1331    // `${(k)functions}` never pays getpermtext for every function.
1332    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
1333        || (flags as u32 & SCANPM_WANTKEYS) == 0;
1334    // c:468-472 + c:487-521 — C reads `hn->nam` AND builds `pm.u.str` from the
1335    // very `Shfunc` its bucket walk is standing on, so a whole-hash scan costs
1336    // one pass. The port used to collect names here and then call
1337    // `getfunction(name)` once per entry, which re-took this same read lock,
1338    // re-hashed the (possibly long) name for a second lookup, re-ran the
1339    // DISABLED parity test, and heap-allocated a whole `struct param` — with a
1340    // second copy of the name inside it — purely so the caller could pull
1341    // `u_str` back out and drop the box. C's `scanfunctions` writes this
1342    // construction out in full rather than calling `getfunction`, and the two C
1343    // copies (c:401-443 and c:487-521) produce byte-identical text; the port now
1344    // does the same, sharing only the `FN_DEPARSE_CACHE` memo, which exists
1345    // because zshrs stores bodies as text where C stores wordcode.
1346    //
1347    // !!! WARNING: RUST-ONLY LOCK NOTE (C has no locks here) !!!
1348    // C calls `func(&pm.node, flags)` INSIDE the bucket walk (c:523) — its
1349    // `shfunctab` is a plain pointer. The Rust walk holds an `RwLock` read
1350    // guard, and a `ScanFunc` is free to read `shfunctab` again or take the
1351    // write side, which would deadlock. Collect what the walk produces, drop
1352    // the guard, then dispatch. The callback still sees exactly the entries C
1353    // passes it, in the same order, one at a time.
1354    let scanned: Vec<(String, Option<String>)> = if let Ok(g) = shfunctab_lock().read() {
1355        let mut out = Vec::with_capacity(g.len());
1356        out.extend(g.iter().filter_map(|(n, shf)| {
1357            // c:470 — `dis ? (hn->flags & DISABLED) : !(hn->flags & DISABLED)`.
1358            let is_disabled = (shf.node.flags & DISABLED as i32) != 0;
1359            if (dis != 0) != is_disabled {
1360                return None;
1361            }
1362            if !want_val {
1363                return Some((n.clone(), None)); // c:472 only pm.node.nam
1364            }
1365            // c:487-521 — the body text, built from the Shfunc in hand. This
1366            // mirrors `getfunction`'s copy (c:401-443) line for line; see there
1367            // for the provenance of each branch.
1368            let v = match shf.body.as_deref() {
1369                None => {
1370                    // c:488-493 — `dyncat("builtin autoload -X", …)`, suffix
1371                    // from PM_UNALIASED + PM_TAGGED.
1372                    let f = shf.node.flags as u32;
1373                    let suffix = match ((f & PM_UNALIASED) != 0, (f & PM_TAGGED) != 0) {
1374                        (true, true) => "Ut",   // c:490
1375                        (true, false) => "U",   // c:490
1376                        (false, true) => "t",   // c:491
1377                        (false, false) => "",   // c:491
1378                    };
1379                    format!("builtin autoload -X{}", suffix) // c:488
1380                }
1381                Some(text) => {
1382                    // c:495 — `getpermtext(shf->funcdef, NULL, 1)`; zshrs keeps
1383                    // source text, so re-parse and deparse, memoized.
1384                    let deparsed = if let Some(hit) = FN_DEPARSE_CACHE.with(|c| {
1385                        c.borrow()
1386                            .get(&shf.node.nam)
1387                            .filter(|(body, _)| body == text)
1388                            .map(|(_, d)| d.clone())
1389                    })
1390                    {
1391                        hit
1392                    } else {
1393                        let _pin = crate::vm_helper::funcdef_lex_pin(&shf.node.nam, text);
1394                        // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1395                        // Same cross-process deparse cache as `getfunction`.
1396                        let key = crate::deparse_cache::key(&shf.node.nam, text);
1397                        let o = match key.and_then(|k| crate::deparse_cache::lookup(&k)) {
1398                            Some(hit) => hit,
1399                            None => {
1400                                let o = match crate::ported::exec::parse_string(text, 0) {
1401                                    Some(prog) => crate::ported::text::getpermtext(Box::new(prog), None, 1),
1402                                    None => text.to_string(),
1403                                };
1404                                if let Some(k) = key {
1405                                    crate::deparse_cache::record(k, &o);
1406                                }
1407                                o
1408                            }
1409                        };
1410                        FN_DEPARSE_CACHE.with(|c| {
1411                            c.borrow_mut()
1412                                .insert(shf.node.nam.clone(), (text.to_string(), o.clone()))
1413                        });
1414                        o
1415                    };
1416                    // c:497-500 — `if (shf->redir) start = "{\n\t"; else "\t";`
1417                    let redir: Option<String> = shf
1418                        .redir
1419                        .as_ref()
1420                        .map(|r| crate::ported::text::getpermtext(r.clone(), None, 1)) // c:518
1421                        .filter(|s| !s.is_empty())
1422                        .or_else(|| shf.redir_text.clone());
1423                    let start = if redir.is_some() { "{\n\t" } else { "\t" }; // c:498/500
1424                    let mut h = format!("{}{}", start, deparsed); // c:512
1425                    if let Some(rt) = redir {
1426                        // c:517-520 — `h = zhtricat(h, "\n}", t);`
1427                        h.push_str("\n}"); // c:519
1428                        h.push_str(&rt); // c:519
1429                    }
1430                    h
1431                }
1432            };
1433            Some((n.clone(), Some(v)))
1434        }));
1435        out
1436    } else {
1437        Vec::new()
1438    };
1439    if let Some(f) = func {
1440        // c:461 `struct param pm;` — ONE param for the whole walk, rebound per
1441        // entry by c:472 `pm.node.nam = hn->nam` and c:489/514 `pm.u.str`. The
1442        // port allocated a fresh `Box<hashnode>` per entry instead, i.e. one
1443        // heap allocation for every shell function on every `${functions}` read.
1444        let mut pm = param {
1445            node: hashnode {
1446                next: None,
1447                nam: String::new(),
1448                flags: PM_SCALAR as i32, // c:463
1449            },
1450            ..Default::default()
1451        };
1452        for (name, value) in scanned {
1453            pm.u_str = value; // c:489/514 pm.u.str
1454            pm.node.nam = name; // c:472 pm.node.nam = hn->nam
1455            f(&pm, flags); // c:524
1456        }
1457    }
1458}
1459
1460/// Port of `scanpmfunctions()` from `Src/Modules/parameter.c:519` — C decl `scanpmfunctions(HashTable ht, ScanFunc func, int flags)`.
1461#[allow(non_snake_case)]
1462/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
1463pub fn scanpmfunctions(
1464    ht: *mut HashTable,
1465    func: Option<ParamScanFunc>, // c:519
1466    flags: i32,
1467) {
1468    scanfunctions(ht, func, flags, 0) // c:522
1469}
1470
1471/// Port of `scanpmdisfunctions()` from `Src/Modules/parameter.c:526` — C decl `scanpmdisfunctions(HashTable ht, ScanFunc func, int flags)`.
1472/// C: `static void scanpmdisfunctions(HashTable ht, ScanFunc func, int flags)`
1473///   → `scanfunctions(ht, func, flags, DISABLED);`
1474#[allow(non_snake_case)]
1475/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
1476pub fn scanpmdisfunctions(
1477    ht: *mut HashTable,
1478    func: Option<ParamScanFunc>, // c:526
1479    flags: i32,
1480) {
1481    scanfunctions(ht, func, flags, DISABLED) // c:529
1482}
1483
1484/// Port of `getfunction_source()` from `Src/Modules/parameter.c:537` — C decl `getfunction_source(UNUSED(HashTable ht), const char *name, int dis)`.
1485/// C: `static HashNode getfunction_source(UNUSED(HashTable ht),
1486///     const char *name, int dis)` — synth a Param naming the source file.
1487#[allow(non_snake_case)]
1488/// WARNING: param names don't match C — Rust=() vs C=(ht, name, dis)
1489pub fn getfunction_source(_ht: *mut HashTable, name: &str, dis: i32) -> Option<Param> {
1490    // c:537
1491    // Faithful port of c:547-552:
1492    //   if ((shf = shfunctab->getnode2(shfunctab, name)) &&
1493    //       (dis ? (shf->node.flags & DISABLED)
1494    //            : !(shf->node.flags & DISABLED))) {
1495    //       pm->u.str = getshfuncfile(shf);
1496    //       if (!pm->u.str) pm->u.str = dupstring('');
1497    //   }
1498    //
1499    // Prior port had two bugs:
1500    //   1. shfunctab.get(name) filters out DISABLED entries
1501    //      automatically (hashtable.rs:404 .filter()), so disabled
1502    //      functions were invisible to both lookups.
1503    //   2. Ignored the dis parameter entirely.
1504    //
1505    // Use get_including_disabled and check DISABLED parity, matching
1506    // the getfunction fix in 615e408fc4.
1507    let g = shfunctab_lock().read().ok()?;
1508    let entry = g.get_including_disabled(name); // c:547 getnode2 — no DISABLED filter
1509    let (value, found) = if let Some(shf) = entry {
1510        let is_disabled = (shf.node.flags & DISABLED as i32) != 0;
1511        let dis_match = if dis != 0 { is_disabled } else { !is_disabled }; // c:548
1512        if dis_match {
1513            // c:549-551 — `pm->u.str = getshfuncfile(shf);
1514            //              if (!pm->u.str) pm->u.str = dupstring("");`
1515            //
1516            // Route through the canonical hashtable::getshfuncfile so
1517            // the PM_LOADDIR `filename/name` join lands here (matches
1518            // the c:1061 branch of getshfuncfile at hashtable.c:1059).
1519            // Previously this inlined `shf.filename.clone()` and
1520            // skipped the LOADDIR join, so autoloads loaded via fpath
1521            // dir match had `${functions_source[name]}` reporting the
1522            // dir instead of the source file path.
1523            //
1524            // C passes the NODE (`getshfuncfile(shf)`), not the name: the
1525            // lookup at c:547 already resolved it. zshrs's
1526            // `hashtable::getshfuncfile` is keyed by NAME, so routing through
1527            // it re-acquired the shfunctab lock and re-hashed the name — two
1528            // lookups per entry instead of one, i.e. 46k redundant hashes on
1529            // every whole-map `${functions_source}` read on this host. Inline
1530            // getshfuncfile's body (Src/hashtable.c:1059-1069) against the
1531            // node c:547 already gave us, which is what C's call does.
1532            let fname = match shf.filename.as_ref() {
1533                // c:1061 — PM_LOADDIR: `zhtricat(shf->filename, "/", shf->node.nam)`
1534                Some(f) if (shf.node.flags as u32 & crate::ported::zsh_h::PM_LOADDIR) != 0 => {
1535                    format!("{}/{}", f, shf.node.nam)
1536                }
1537                Some(f) => f.clone(),  // c:1063 — `dupstring(shf->filename)`
1538                None => String::new(), // c:1065 NULL → c:551 `dupstring("")`
1539            };
1540            (fname, true)
1541        } else {
1542            // c:552 — wrong DISABLED parity: pm->u.str stays NULL,
1543            // then c:553 caller emits PM_UNSET|PM_SPECIAL.
1544            (String::new(), false)
1545        }
1546    } else {
1547        (String::new(), false) // c:586
1548    };
1549    let pm = Box::new(param {
1550        // c:541
1551        node: hashnode {
1552            next: None,
1553            nam: name.to_string(), // c:542
1554            // c:555-556 — `pm->node.flags = PM_SCALAR|PM_READONLY;`, set
1555            // BEFORE the shfunctab lookup and never amended. Unlike
1556            // `getfunction` / `getpmjobstate` (Src/Modules/parameter.c:1422-1423) this getter has no
1557            // `else` arm: an unknown (or wrong-DISABLED-parity) function still
1558            // yields an ordinary node whose `u.str` was simply never assigned,
1559            // so `${+functions_source[nosuch]}` is 1 in zsh — the previous
1560            // `PM_UNSET|PM_SPECIAL` else-branch has no counterpart in C and
1561            // made it 0.
1562            flags: (PM_SCALAR | PM_READONLY) as i32, // c:555-556
1563        },
1564        u_data: 0,
1565        u_tied: None,
1566        u_arr: None,
1567        u_str: Some(value), // c:553 / c:586
1568        u_val: 0,
1569        u_dval: 0.0,
1570        u_hash: None,
1571        gsu_s: None,
1572        gsu_i: None,
1573        gsu_f: None,
1574        gsu_a: None,
1575        gsu_h: None,
1576        base: 0,
1577        width: 0,
1578        env: None,
1579        ename: None,
1580        old: None,
1581        level: 0,
1582    });
1583    Some(pm) // c:589
1584}
1585
1586/// Port of `scanfunctions_source()` from `Src/Modules/parameter.c:560` — C decl `scanfunctions_source(UNUSED(HashTable ht), ScanFunc func, int flags, int dis)`.
1587/// C: `static void scanfunctions_source(UNUSED(HashTable ht), ScanFunc func,
1588///     int flags, int dis)` — iterate shfunctab, emit source filename.
1589#[allow(non_snake_case)]
1590/// WARNING: param names don't match C — Rust=(_ht, func, _dis) vs C=(ht, func, flags, dis)
1591pub fn scanfunctions_source(
1592    _ht: *mut HashTable,
1593    func: Option<ParamScanFunc>, // c:560
1594    flags: i32,
1595    dis: i32,
1596) {
1597    // c:560
1598    // Faithful port of c:570-584:
1599    //   for (i = 0; i < shfunctab->hsize; i++) {
1600    //       for (hn = shfunctab->nodes[i]; hn; hn = hn->next) {
1601    //           if (dis ? (hn->flags & DISABLED)
1602    //                   : !(hn->flags & DISABLED)) {
1603    //               pm.node.nam = hn->nam;
1604    //               ... pm.u.str = getshfuncfile(...); ...
1605    //               func(&pm.node, flags);
1606    //           }
1607    //       }
1608    //   }
1609    //
1610    // Same fix pattern as scanfunctions (da3bce77e6): the dis
1611    // parameter wasn't read and the DISABLED bit wasn't checked.
1612    // \${(k)dis_functions_source} returned every fn, including
1613    // enabled ones (wrong). Now filters per C's c:572 gate using
1614    // the live shfunctab entry's node.flags.
1615    let names: Vec<String> = if let Ok(g) = shfunctab_lock().read() {
1616        let mut v = Vec::with_capacity(g.len());
1617        v.extend(g.iter().filter_map(|(n, shf)| {
1618            let is_disabled = (shf.node.flags & DISABLED as i32) != 0;
1619            let pass = if dis != 0 { is_disabled } else { !is_disabled };
1620            if pass {
1621                Some(n.clone())
1622            } else {
1623                None
1624            }
1625        }));
1626        v
1627    } else {
1628        Vec::new()
1629    };
1630    // c:586-588 — value side only when values were asked for.
1631    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
1632        || (flags as u32 & SCANPM_WANTKEYS) == 0;
1633    if let Some(f) = func {
1634        // c:574-580 `struct param pm;` — one param for the whole walk (see the
1635        // matching note in `scanfunctions`), rebound per entry at c:585/589.
1636        let mut pm = param {
1637            node: hashnode {
1638                next: None,
1639                nam: String::new(),
1640                flags: (PM_SCALAR | PM_READONLY) as i32, // c:579
1641            },
1642            ..Default::default()
1643        };
1644        for name in names {
1645            // c:589-591 `pm.u.str = getshfuncfile((Shfunc)hn)` — the same
1646            // lookup `getfunction_source` (c:547) performs for a single key.
1647            pm.u_str = if want_val {
1648                Some(
1649                    getfunction_source(std::ptr::null_mut(), &name, dis)
1650                        .and_then(|p| p.u_str)
1651                        .unwrap_or_default(), // c:590-591
1652                )
1653            } else {
1654                None
1655            };
1656            pm.node.nam = name; // c:585 pm.node.nam = hn->nam
1657            f(&pm, flags); // c:593
1658        }
1659    }
1660}
1661
1662/// Port of `getpmfunction_source()` from `Src/Modules/parameter.c:591` — C decl `getpmfunction_source(HashTable ht, const char *name)`.
1663/// C: `static HashNode getpmfunction_source(HashTable ht, const char *name)`
1664///   → `return getfunction_source(ht, name, 0);`
1665#[allow(non_snake_case)]
1666pub fn getpmfunction_source(ht: *mut HashTable, name: &str) -> Option<Param> {
1667    // c:591
1668    getfunction_source(ht, name, 0) // c:591
1669}
1670
1671/// Port of `getpmdisfunction_source()` from `Src/Modules/parameter.c:600` — C decl `getpmdisfunction_source(HashTable ht, const char *name)`.
1672/// C: `static HashNode getpmdisfunction_source(HashTable ht,
1673///     const char *name)` → `return getfunction_source(ht, name, 1);`
1674#[allow(non_snake_case)]
1675/// WARNING: param names don't match C — Rust=() vs C=(ht, name)
1676pub fn getpmdisfunction_source(ht: *mut HashTable, name: &str) -> Option<Param> {
1677    getfunction_source(ht, name, 1) // c:603
1678}
1679
1680/// Port of `scanpmfunction_source()` from `Src/Modules/parameter.c:609` — C decl `scanpmfunction_source(HashTable ht, ScanFunc func, int flags)`.
1681#[allow(non_snake_case)]
1682/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
1683pub fn scanpmfunction_source(
1684    ht: *mut HashTable,
1685    func: Option<ParamScanFunc>, // c:609
1686    flags: i32,
1687) {
1688    scanfunctions_source(ht, func, flags, 0) // c:612
1689}
1690
1691/// Port of `scanpmdisfunction_source()` from `Src/Modules/parameter.c:618` — C decl `scanpmdisfunction_source(HashTable ht, ScanFunc func, int flags)`.
1692/// C: `static void scanpmdisfunction_source(HashTable ht, ScanFunc func,
1693///     int flags)` → `scanfunctions_source(ht, func, flags, 1);`
1694#[allow(non_snake_case)]
1695/// WARNING: param names don't match C — Rust=(ht, flags) vs C=(ht, func, flags)
1696pub fn scanpmdisfunction_source(
1697    ht: *mut HashTable, // c:618
1698    func: Option<ParamScanFunc>,
1699    flags: i32,
1700) {
1701    scanfunctions_source(ht, func, flags, 1) // c:621
1702}
1703
1704/// Port of `funcstackgetfn()` from `Src/Modules/parameter.c:627` — C decl `funcstackgetfn(UNUSED(Param pm))`.
1705/// C: `static char **funcstackgetfn(UNUSED(Param pm))` — returns the
1706/// list of function names currently on the call stack.
1707#[allow(non_snake_case)]
1708#[allow(unused_variables)]
1709pub fn funcstackgetfn(pm: *mut param) -> Vec<String> {
1710    // c:627
1711    // c:627-643 — count frames, allocate, walk linking *p = f->name.
1712    // C walks `for (f = funcstack; f; f = f->prev)` — head of list is
1713    // most-recent frame. Rust stores frames in a Vec push-back, so the
1714    // last element is the most-recent; reverse-iterate to match C's
1715    // head-first order: innermost frame first, outermost last.
1716    let stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default();
1717    stack.iter().rev().map(|f| f.name.clone()).collect() // c:648
1718}
1719
1720/// Port of `functracegetfn()` from `Src/Modules/parameter.c:648` — C decl
1721/// `static char **functracegetfn(UNUSED(Param pm))`. Walks the `funcstack` linked
1722/// list, building `"<caller>:<lineno>"` per frame.
1723/// ```c
1724/// static char **
1725/// functracegetfn(UNUSED(Param pm))
1726/// {
1727///     Funcstack f;
1728///     int num;
1729///     char **ret, **p;
1730///     for (f = funcstack, num = 0; f; f = f->prev, num++);
1731///     ret = zhalloc((num + 1) * sizeof(char *));
1732///     for (f = funcstack, p = ret; f; f = f->prev, p++) {
1733///         char *colonpair = zhalloc(strlen(f->caller) +
1734///                                   (f->lineno > 9999 ? 24 : 6));
1735///         sprintf(colonpair, "%s:%lld", f->caller, f->lineno);
1736///         *p = colonpair;
1737///     }
1738///     *p = NULL;
1739///     return ret;
1740/// }
1741/// ```
1742#[allow(non_snake_case)]
1743#[allow(unused_variables)]
1744pub fn functracegetfn(pm: *mut param) -> Vec<String> {
1745    // c:648
1746    let f_stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default(); // c:650
1747                                                                           // c:654 — `for (f = funcstack, num = 0; f; f = f->prev, num++)`
1748    let num = f_stack.len(); // c:654
1749                             // c:656 — `ret = zhalloc((num + 1) * sizeof(char *));`
1750    let mut ret: Vec<String> = Vec::with_capacity(num + 1); // c:656
1751                                                            // c:658 — `for (f = funcstack, p = ret; f; f = f->prev, p++)`
1752                                                            // C walks from head (most recent) outward via f->prev. Rust's Vec
1753                                                            // is push-back so iterate reverse to match C head-first order.
1754    for f in f_stack.iter().rev() {
1755        // c:658
1756        // c:661 — `colonpair = zhalloc(...)`; c:663-665 — `sprintf(colonpair, "%s:%lld", f->caller, f->lineno);`
1757        let caller = f.caller.as_deref().unwrap_or(""); // c:661
1758        let colonpair = format!("{}:{}", caller, f.lineno); // c:663
1759        ret.push(colonpair); // c:668 *p = colonpair
1760    }
1761    // c:670 `*p = NULL;` — Rust Vec doesn't need a sentinel
1762    ret // c:672 return ret
1763}
1764
1765/// Port of `funcsourcetracegetfn()` from `Src/Modules/parameter.c:679` — C decl
1766/// `static char **funcsourcetracegetfn(UNUSED(Param pm))`. Same shape as `functracegetfn` but
1767/// uses `f->filename` / `f->flineno` (the source location, not the
1768/// caller location).
1769/// ```c
1770/// static char **
1771/// funcsourcetracegetfn(UNUSED(Param pm))
1772/// {
1773///     /* same as functracegetfn but with f->filename + f->flineno */
1774/// }
1775/// ```
1776#[allow(non_snake_case)]
1777#[allow(unused_variables)]
1778pub fn funcsourcetracegetfn(pm: *mut param) -> Vec<String> {
1779    // c:679
1780    let f_stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default(); // c:681
1781    let num = f_stack.len(); // c:685
1782    let mut ret: Vec<String> = Vec::with_capacity(num + 1); // c:687
1783                                                            // C walks head-first via f->prev; Rust Vec push-back stores in
1784                                                            // reverse order — reverse-iterate to match.
1785    for f in f_stack.iter().rev() {
1786        // c:689
1787        let fname = f.filename.as_deref().unwrap_or(""); // c:691
1788        let colonpair = format!("{}:{}", fname, f.flineno); // c:695
1789        ret.push(colonpair); // c:701
1790    }
1791    ret // c:705 return ret
1792}
1793
1794/// Port of `funcfiletracegetfn()` from `Src/Modules/parameter.c:711` — C decl `funcfiletracegetfn(UNUSED(Param pm))`.
1795/// Walks `funcstack` building a `"<file>:<lineno>"` pair per frame.
1796/// For function/eval frames the line number is computed against the
1797/// parent frame's source-file line.
1798#[allow(non_snake_case)]
1799#[allow(unused_variables)]
1800pub fn funcfiletracegetfn(pm: *mut param) -> Vec<String> {
1801    // c:711
1802    // c:717 — for (f = funcstack, num = 0; f; f = f->prev, num++);
1803    let stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default();
1804    let mut ret: Vec<String> = Vec::with_capacity(stack.len());
1805    // c:721 — for (f = funcstack, p = ret; f; f = f->prev, p++).
1806    // C walks head→tail via f->prev. Rust Vec is push-back order;
1807    // reverse-iterate so index 0 = most recent and `i+1` = previous
1808    // frame (the C `f->prev` link target).
1809    let n = stack.len();
1810    for i in 0..n {
1811        let f = &stack[n - 1 - i];
1812        // c:724 — if (!f->prev || f->prev->tp == FS_SOURCE) {
1813        // In reverse view: prev frame is at index `n - 1 - i - 1`
1814        // i.e. the next element in our reverse walk.
1815        let prev: Option<&crate::ported::zsh_h::funcstack> = if i + 1 < n {
1816            Some(&stack[n - 1 - i - 1])
1817        } else {
1818            None
1819        };
1820        let parent_is_source = match prev {
1821            None => true, // !f->prev
1822            Some(p) => p.tp == FS_SOURCE,
1823        };
1824        if parent_is_source {
1825            // c:731-737 — file context: "<caller>:<lineno>"
1826            ret.push(format!(
1827                "{}:{}",
1828                f.caller.as_deref().unwrap_or(""), // c:734
1829                f.lineno
1830            ));
1831        } else if let Some(prev) = prev {
1832            // c:747 — zlong flineno = f->prev->flineno + f->lineno;
1833            let mut flineno = prev.flineno + f.lineno;
1834            // c:752-753 — if (f->prev->tp == FS_EVAL) flineno--;
1835            if prev.tp == FS_EVAL {
1836                flineno -= 1;
1837            }
1838            // c:754 — fname = f->prev->filename ? f->prev->filename : "";
1839            let fname = prev.filename.as_deref().unwrap_or("");
1840            // c:756-761 — sprintf colonpair "<fname>:<flineno>"
1841            ret.push(format!("{}:{}", fname, flineno));
1842        }
1843    }
1844    // c:766 — *p = NULL;  (Rust Vec uses len, no trailing NULL needed)
1845    ret
1846}
1847
1848/// Port of `getbuiltin()` from `Src/Modules/parameter.c:775` — C decl `getbuiltin(UNUSED(HashTable ht), const char *name, int dis)`.
1849/// C body (c:778-796):
1850/// ```c
1851/// pm.node.nam = name; pm.node.flags = PM_SCALAR | PM_READONLY;
1852/// pm.gsu.s = &nullsetscalar_gsu;
1853/// if (bn = builtintab[name]; bn matches dis) {
1854///     pm.u.str = (bn->handlerfunc || (bn->flags & BINF_PREFIX))
1855///                ? "defined" : "undefined";
1856/// } else {
1857///     pm.u.str = ""; pm.node.flags |= (PM_UNSET|PM_SPECIAL);
1858/// }
1859/// ```
1860#[allow(non_snake_case)]
1861pub fn getbuiltin(_ht: *mut HashTable, name: &str, dis: i32) -> Option<Param> {
1862    // c:775
1863    // Faithful port of c:780-793:
1864    //   pm = hcalloc; pm->node.nam = dupstring(name);
1865    //   pm->node.flags = PM_SCALAR | PM_READONLY;
1866    //   pm->gsu.s = &nullsetscalar_gsu;
1867    //   if ((bn = builtintab->getnode2(builtintab, name)) &&
1868    //       (dis ? (bn->node.flags & DISABLED)
1869    //            : !(bn->node.flags & DISABLED))) {
1870    //       char *t = ((bn->handlerfunc ||
1871    //                   (bn->node.flags & BINF_PREFIX))
1872    //                  ? "defined" : "undefined");
1873    //       pm->u.str = dupstring(t);
1874    //   } else {
1875    //       pm->u.str = dupstring("");
1876    //       pm->node.flags |= (PM_UNSET|PM_SPECIAL);
1877    //   }
1878    //
1879    // Prior port ignored the `dis` parameter entirely — every lookup
1880    // collapsed to "found in BUILTINS = enabled". Now honours the
1881    // DISABLED gate:
1882    //   - dis=0          → entry visible iff NOT in BUILTINS_DISABLED
1883    //   - dis=DISABLED   → entry visible iff IS in BUILTINS_DISABLED
1884    // Without this distinction, `$builtins[ls]` reported "defined"
1885    // even after `disable ls`, and `$dis_builtins[ls]` reported "" /
1886    // PM_UNSET when ls was actually disabled — both diverging from
1887    // zsh -fc parity.
1888    // c:784 — `builtintab->getnode2(builtintab, name)`.
1889    //
1890    // In C a module's builtins are ADDED to `builtintab` by
1891    // `addbuiltins()` when the module loads (Src/module.c:551) and
1892    // removed when it unloads, so a name owned by an UNLOADED module is
1893    // simply not in the table. zshrs links every module statically and
1894    // keeps ONE flat `BUILTINS` slice, so the raw `.find()` below saw
1895    // `chmod`/`stat`/`zpty`/`clone`/`pcre_compile`/… whether or not the
1896    // owning `zmodload` ever ran: `$+builtins[chmod]` answered 1 where
1897    // `zsh -f` answers 0, across 27 names.
1898    //
1899    // zshrs's own dispatch and `whence -w` already apply this gate
1900    // unconditionally (port of Src/builtin.c:4123 at
1901    // `src/ported/builtin.rs:9420-9472`) — `zshrs -fc 'builtin chmod'`
1902    // runs /bin/chmod, and `whence -w chmod` says `command`, both
1903    // flipping to `builtin` after `zmodload zsh/files`. The `builtins`
1904    // parameter view was the only reader that disagreed.
1905    //
1906    // Surfaced by `_pick_variant`, whose sh:19
1907    // (`Completion/Base/Utility/_pick_variant:19`) reads exactly
1908    // `$+builtins[$opts[-c]]`: a false positive on `chmod` selected the
1909    // `zsh` variant of `_chmod` (and would for `_rm`/`_ln`/`_mv`/
1910    // `_stat`/`_chown`/`_mkdir`/`_rmdir`) instead of the OS one.
1911    //
1912    // c:Src/module.c:1265 `add_autobin` — a module registered for
1913    // AUTO-loading contributes STUBS to `builtintab` up front, so those
1914    // names (`zstyle`, `compadd`, `zle`, `bindkey`, `ulimit`, …) ARE
1915    // present before their module loads. `resolve_autoload_builtin` is
1916    // that registry (Src/init.c:1708 `init_bltinmods`), and it is the
1917    // same call the `defined`/`undefined` decision below already makes.
1918    // Shared with `whence`/`type` (Src/builtin.c:4123 port) so the two can
1919    // never drift again — the logic that used to live here verbatim.
1920    let in_builtintab = crate::ext_builtins::module_builtin_available(name);
1921    let entry = if in_builtintab {
1922        BUILTINS
1923            .iter() // c:784
1924            .find(|b| b.node.nam == name)
1925    } else {
1926        None
1927    };
1928    let (value, found) = if let Some(bn) = entry {
1929        // c:785 — `bn != NULL`. Check the DISABLED state.
1930        let is_disabled = {
1931            let set = crate::ported::builtin::BUILTINS_DISABLED.lock().ok();
1932            set.map(|s| s.contains(name)).unwrap_or(false)
1933        };
1934        let dis_match = if dis != 0 {
1935            is_disabled // c:785 dis ? (DISABLED) : ...
1936        } else {
1937            !is_disabled // c:785 ... : !(DISABLED)
1938        };
1939        if dis_match {
1940            // c:786-789 — `defined` if handlerfunc present OR
1941            // BINF_PREFIX set; else `undefined`.
1942            //
1943            // c:Src/module.c:1002 addbuiltin / :1265 add_autobin — an
1944            // AUTOLOADED module contributes its builtins to `builtintab`
1945            // as STUBS whose `handlerfunc` is NULL until the module is
1946            // actually loaded; that is precisely the `undefined` arm.
1947            // `zsh -fc` reports 27 such names (bindkey, compadd, zle,
1948            // ulimit, zstyle, …) — the exact contents of the
1949            // builtin→module auto-load registry at Src/init.c:1708
1950            // init_bltinmods, ported to `MODULESTAB.autoload_builtins`.
1951            // zshrs links every module statically, so its BUILTINS rows
1952            // ALWAYS carry a handler and every name read `defined`,
1953            // making `${builtins[zle]}` and `${builtins[(R)undefined]}`
1954            // diverge. Model the stub state the way C does: a name in
1955            // the auto-load registry has no handler until its module is
1956            // loaded.
1957            let has_handler = bn.handlerfunc.is_some()
1958                && crate::ported::module::MODULESTAB
1959                    .lock()
1960                    .map(|t| match t.resolve_autoload_builtin(name) {
1961                        // c:1265 — still a stub while the module is
1962                        // merely registered for auto-load. "Actually
1963                        // loaded" is C's `m->u.handle && !MOD_UNLOAD`
1964                        // (c:1055), whose static-link analog is
1965                        // MOD_INIT_B set / MOD_UNLOAD clear — the SAME
1966                        // criterion getpmmodule and scanpmmodules use to
1967                        // print `loaded` vs `autoloaded`. NOT
1968                        // `is_loaded()`, which keys off MOD_LINKED and is
1969                        // pre-seeded for every compiled-in module.
1970                        Some(m) => t.modules.get(m).is_some_and(|md| {
1971                            (md.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0
1972                                && (md.node.flags & crate::ported::zsh_h::MOD_UNLOAD) == 0
1973                        }),
1974                        // c:1002 — a real bintab row from zsh/main or an
1975                        // already-loaded module.
1976                        None => true,
1977                    })
1978                    .unwrap_or(true);
1979            let has_prefix = (bn.node.flags & crate::ported::zsh_h::BINF_PREFIX as i32) != 0;
1980            let t = if has_handler || has_prefix {
1981                "defined"
1982            } else {
1983                "undefined"
1984            };
1985            (t.to_string(), true) // c:790
1986        } else {
1987            // c:791-792 — wrong DISABLED parity: treat as not found.
1988            (String::new(), false)
1989        }
1990    } else {
1991        (String::new(), false) // c:793
1992    };
1993    // The two views of this parameter have to agree. `scanbuiltins` below
1994    // emits the zshrs extension builtins and the daemon `z*` family as
1995    // additional keys, so `${(k)builtins}` listed `cat`, `paste`, `peach`,
1996    // `zjob`, … while THIS lookup — which only ever consulted the C-port
1997    // BUILTINS table — reported them unset: `${(k)builtins}` contained the
1998    // name and `${+builtins[$name]}` was 0 at the same time. Compsys reads
1999    // the subscript form directly (`_set_command` sh:12 `$builtins[$cmd]`,
2000    // `_pick_variant` sh:19 `$+builtins[...]`), so those names looked like
2001    // external commands to completion. Same `dis == 0` / `hide_ext_builtins`
2002    // / `disable` gating the scan uses.
2003    let (value, found) = if found || dis != 0 || crate::ext_builtins::hide_ext_builtins() {
2004        (value, found)
2005    } else {
2006        let is_disabled = crate::ported::builtin::BUILTINS_DISABLED
2007            .lock()
2008            .map(|s| s.contains(name))
2009            .unwrap_or(false);
2010        // Ask exactly what `whence -w`/`type` ask (Src/builtin.c:4123 port):
2011        // the daemon `z*` family, then `is_extension_builtin` — the fusevm
2012        // registry plus the folded extension bintabs plus LOCAL_ONLY_BUILTINS.
2013        // Consulting the NAME LISTS instead left a third disagreement: names
2014        // that fusevm registers but EXT_BUILTIN_NAMES does not spell out
2015        // (`mkdir`, `sync`, `strftime`, `pcre_compile`, `ztie`, `zprof`,
2016        // `zsystem`, …) reported `builtin` from whence and 0 from
2017        // `${+builtins[...]}` in the same shell.
2018        let is_ext = !is_disabled
2019            && (crate::daemon::builtins::is_zshrs_builtin(name)
2020                || crate::extensions::ext_builtins::is_extension_builtin(name));
2021        if is_ext {
2022            // c:846 — extension builtins always dispatch in-process, so they
2023            // are unconditionally the "handlerfunc present" arm.
2024            ("defined".to_string(), true)
2025        } else {
2026            (value, found)
2027        }
2028    };
2029    let pm = Box::new(param {
2030        // c:780 hcalloc
2031        node: hashnode {
2032            next: None,
2033            nam: name.to_string(), // c:781
2034            flags: if found {
2035                (PM_SCALAR | PM_READONLY) as i32
2036            }
2037            // c:782
2038            else {
2039                (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32
2040            }, // c:794
2041        },
2042        u_data: 0,
2043        u_tied: None,
2044        u_arr: None,
2045        u_str: Some(value), // c:790 / c:793
2046        u_val: 0,
2047        u_dval: 0.0,
2048        u_hash: None,
2049        gsu_s: None, // c:783 nullsetscalar_gsu (gsu table not wired)
2050        gsu_i: None,
2051        gsu_f: None,
2052        gsu_a: None,
2053        gsu_h: None,
2054        base: 0,
2055        width: 0,
2056        env: None,
2057        ename: None,
2058        old: None,
2059        level: 0,
2060    });
2061    Some(pm) // c:796 return &pm->node
2062}
2063
2064// `getpatchars()` (c:894) ported above as a private helper —
2065// `dispatcharsgetfn` calls it directly; no separate public stub needed.
2066
2067/// Port of `getpmbuiltin()` from `Src/Modules/parameter.c:799` — C decl `getpmbuiltin(HashTable ht, const char *name)`.
2068/// C: `static HashNode getpmbuiltin(HashTable ht, const char *name)` →
2069///   `return getbuiltin(ht, name, 0);`
2070#[allow(non_snake_case)]
2071pub fn getpmbuiltin(ht: *mut HashTable, name: &str) -> Option<Param> {
2072    // c:799
2073    getbuiltin(ht, name, 0) // c:799
2074}
2075
2076/// Port of `getpmdisbuiltin()` from `Src/Modules/parameter.c:806` — C decl `getpmdisbuiltin(HashTable ht, const char *name)`.
2077/// C: `static HashNode getpmdisbuiltin(HashTable ht, const char *name)` →
2078///   `return getbuiltin(ht, name, DISABLED);`
2079#[allow(non_snake_case)]
2080pub fn getpmdisbuiltin(ht: *mut HashTable, name: &str) -> Option<Param> {
2081    // c:806
2082    getbuiltin(ht, name, DISABLED) // c:806
2083}
2084
2085/// Port of `scanbuiltins()` from `Src/Modules/parameter.c:813` — C decl `scanbuiltins(UNUSED(HashTable ht), ScanFunc func, int flags, int dis)`.
2086/// C: `static void scanbuiltins(UNUSED(HashTable ht), ScanFunc func,
2087///     int flags, int dis)` — iterate the builtin table.
2088#[allow(non_snake_case)]
2089/// WARNING: param names don't match C — Rust=(_ht, func, _dis) vs C=(ht, func, flags, dis)
2090pub fn scanbuiltins(
2091    _ht: *mut HashTable,
2092    func: Option<ParamScanFunc>, // c:813
2093    flags: i32,
2094    dis: i32,
2095) {
2096    // C body (c:816-840): loop through builtintab nodes; for each
2097    // matching DISABLED filter, emit a scalar Param via func().
2098    // Static-link path: walk BUILTINS table from src/ported/builtin.rs
2099    // (the Rust canonical source for builtin entries).
2100    //
2101    // c:Src/Modules/parameter.c:825 — `if (dis ? (hn->flags & DISABLED)
2102    // : !(hn->flags & DISABLED))`. With `dis=0` (the `builtins`
2103    // param), emit only enabled entries; with `dis=DISABLED` (the
2104    // `dis_builtins` param), emit only disabled entries. The Rust
2105    // BUILTINS table currently carries no DISABLED bit at construction
2106    // (every entry's `flags == 0`), so dis=DISABLED → no entries
2107    // (matches zsh: empty dis_builtins by default). Previously the
2108    // filter was ignored, so `${#dis_builtins}` returned 159 instead
2109    // of 0.
2110    if let Some(f) = func {
2111        // c:Src/Modules/parameter.c:816-840 — C iterates the LIVE
2112        // `builtintab` which only contains builtins from currently-
2113        // loaded modules. zshrs's `BUILTINS` slice is the static
2114        // union of every statically-linked module's bintab, so direct
2115        // iteration over-reports in --zsh parity mode where modules
2116        // aren't auto-loaded. Skip entries whose owning module isn't
2117        // loaded so `${(k)builtins}` and `${#builtins}` agree with
2118        // `zsh -fc` (e.g. zsh's count is 103, zshrs's full BUILTINS
2119        // is 159). Default zshrs mode walks the full set so user
2120        // scripts see all built-in module commands without explicit
2121        // zmodload — matching the auto-load posture.
2122        //
2123        // BUILTINS also currently contains some duplicate entries
2124        // (`fg`, `kill`, `suspend` appear in two adjacent batches,
2125        // builtin.rs:12110-12423 + 12855-12878) — `builtintab` in
2126        // C is a hash table so re-adds collapse, but Vec iteration
2127        // here visits each occurrence. Dedup by name to match the
2128        // hash-table shape `${#builtins}` exposes.
2129        let mut emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
2130        // c:825 — runtime DISABLED tracking lives in
2131        // BUILTINS_DISABLED (a HashSet maintained by `disable` /
2132        // `enable -r`). The BUILTINS slice's static `flags` field
2133        // never carries DISABLED at construction, so the prior
2134        // check `b_flags & DISABLED` always read 0. Read the live
2135        // set instead so `${(k)dis_builtins}` reflects the user's
2136        // `disable` invocations (and `${(k)builtins}` correctly
2137        // omits disabled entries).
2138        let disabled_set: std::collections::HashSet<String> =
2139            crate::ported::builtin::BUILTINS_DISABLED
2140                .lock()
2141                .ok()
2142                .map(|g| g.iter().cloned().collect())
2143                .unwrap_or_default();
2144        // c:839-841 — value side only when values were asked for.
2145        let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
2146            || (flags as u32 & SCANPM_WANTKEYS) == 0;
2147        // c:827-833 — `struct param pm;` declared ONCE, `memset` ONCE,
2148        // `pm.node.flags` set ONCE; the loop rebinds nam/u.str per entry.
2149        let mut pm = param {
2150            node: hashnode {
2151                next: None,
2152                nam: String::new(),
2153                flags: (PM_SCALAR | PM_READONLY) as i32, // c:832
2154            },
2155            ..Default::default()
2156        };
2157        // c:Src/Modules/parameter.c:822-823 —
2158        //     for (i = 0; i < builtintab->hsize; i++)
2159        //         for (hn = builtintab->nodes[i]; hn; hn = hn->next)
2160        // i.e. the BUCKET-ARRAY walk of `builtintab`, not the declaration
2161        // order of the `builtins[]` array. `builtintab_scan_order` is that
2162        // walk (builtin.rs `BUILTINTAB_NODES`, a faithful `newhashtable(85)`
2163        // + front-insert table); iterating `BUILTINS` directly printed the
2164        // array order, which does not match `zsh -f`.
2165        for (_, b) in crate::ported::builtin::BUILTINTAB_NODES.iter() {
2166            // c:823
2167            let is_disabled = disabled_set.contains(&b.node.nam); // c:825 hn->flags & DISABLED
2168            let pass = if dis != 0 {
2169                // c:825 dis ? (hn->flags & DISABLED)
2170                is_disabled
2171            } else {
2172                // c:825 !(hn->flags & DISABLED)
2173                !is_disabled
2174            };
2175            if !pass {
2176                continue;
2177            }
2178            // c:Src/Modules/parameter.c:823 — `builtintab` membership.
2179            // One predicate, shared with the compctl namespace dump:
2180            // see `builtin_in_builtintab`.
2181            if !crate::ext_builtins::builtin_in_builtintab(&b.node.nam) {
2182                // A name whose owning module is not loaded is normally absent
2183                // — but a few of them (strftime, pcre_compile, …) are ALSO
2184                // registered on the VM, so they dispatch right now and both
2185                // `whence -w` and `${+builtins[...]}` report them as builtins.
2186                // Emitting the key too is what keeps the three views in
2187                // agreement; under `--zsh` / ZSHRS_HIDE_EXT_BUILTINS the
2188                // module gate is all that applies and the name stays hidden,
2189                // matching zsh.
2190                let dispatches_anyway = dis == 0
2191                    && !crate::ext_builtins::hide_ext_builtins()
2192                    && crate::extensions::ext_builtins::is_extension_builtin(&b.node.nam);
2193                if !dispatches_anyway {
2194                    continue;
2195                }
2196            }
2197            if !emitted.insert(b.node.nam.clone()) {
2198                continue;
2199            }
2200            // c:842-846 — `pm.u.str = (handlerfunc || BINF_PREFIX) ?
2201            // "defined" : "undefined"`. The identical decision already lives
2202            // in `getbuiltin` (c:775), which is what the per-key
2203            // `getpmbuiltin` reads, so route through it rather than keeping
2204            // two copies of the stub/loaded-module test.
2205            pm.u_str = if want_val {
2206                getbuiltin(std::ptr::null_mut(), &b.node.nam, dis).and_then(|p| p.u_str)
2207            } else {
2208                None
2209            };
2210            pm.node.nam = b.node.nam.clone(); // c:838
2211            f(&pm, flags); // c:848
2212        }
2213        // zshrs extension builtins (`ext_builtins::EXT_BUILTIN_NAMES`)
2214        // dispatch in-process exactly like core builtins but have no
2215        // entry in the C-port BUILTINS table, so `${(k)builtins}` — and
2216        // therefore compsys's `_builtins` command-position completion —
2217        // would never offer names such as `doctor`, `peach`, `help`, or
2218        // `zassert_eq`. Emit them for the `builtins` param (dis == 0).
2219        //
2220        // `hide_ext_builtins()` suppresses them in two cases:
2221        //   * `--zsh` strict emulation (these names don't exist in zsh),
2222        //   * `ZSHRS_HIDE_EXT_BUILTINS` — the parity harnesses' knob, so
2223        //     a byte-for-byte `${(ko)builtins}` diff against real zsh
2224        //     isn't drowned in ~145 zshrs-original names. It is a
2225        //     MEASUREMENT flag, not a compat mode: dispatch is untouched
2226        //     (`peach` still runs, `whence -w peach` still says builtin).
2227        if dis == 0 && !crate::ext_builtins::hide_ext_builtins() {
2228            for name in crate::ext_builtins::EXT_BUILTIN_NAMES {
2229                let n = (*name).to_string();
2230                if disabled_set.contains(&n) {
2231                    continue; // c:825 honor `disable`
2232                }
2233                if !emitted.insert(n.clone()) {
2234                    continue; // already emitted from BUILTINS (e.g. coreutils drop-ins)
2235                }
2236                // Extension builtins always dispatch in-process, so they are
2237                // the c:842-844 "handlerfunc present" arm.
2238                pm.u_str = if want_val {
2239                    Some("defined".to_string()) // c:846
2240                } else {
2241                    None
2242                };
2243                pm.node.nam = n;
2244                f(&pm, flags);
2245            }
2246            // The daemon `z*` family (`daemon::builtins::ZSHRS_BUILTIN_NAMES`
2247            // — zjob, zcache, zls, zid, zping, ztag, zsend, …) is a SECOND
2248            // extension registry, separate from EXT_BUILTIN_NAMES above.
2249            // It dispatches by name through `try_dispatch` and never had a
2250            // BUILTINS entry, so every consumer had to be patched one at a
2251            // time: `builtin zjob` (fusevm_bridge.rs), `whence -w zjob`
2252            // (builtin.rs). `${(k)builtins}` was never one of them, so
2253            // `${+builtins[zjob]}` was 0 and compsys — which offers builtins
2254            // via `compadd -Qk builtins` (_command_names sh:38) — left the
2255            // whole family out of the "builtin command" group even though
2256            // `whence -v zjob` answered "zjob is a shell builtin".
2257            //
2258            // Same `dis == 0` and `hide_ext_builtins()` gating as the block
2259            // above: `--zsh` keeps zsh's exact builtin set.
2260            for name in crate::daemon::builtins::ZSHRS_BUILTIN_NAMES {
2261                let n = (*name).to_string();
2262                if disabled_set.contains(&n) {
2263                    continue; // c:825 honor `disable`
2264                }
2265                if !emitted.insert(n.clone()) {
2266                    continue;
2267                }
2268                pm.u_str = if want_val {
2269                    Some("defined".to_string()) // c:846 — dispatches in-process
2270                } else {
2271                    None
2272                };
2273                pm.node.nam = n;
2274                f(&pm, flags);
2275            }
2276            // The folded extension bintabs (`ext_builtins::
2277            // extension_builtin_defs`: znative, ztest/zassert, watch) are
2278            // another registry: they live in `createbuiltintable`'s map,
2279            // which `BUILTINTAB_NODES` (built from `BUILTINS`) never sees.
2280            // `${+builtins[znative]}` and `whence -w znative` already
2281            // answered builtin, so `${(k)builtins}` — and with it compsys's
2282            // `_command_names` — has to list them too. Same `dis == 0` /
2283            // `hide_ext_builtins()` gating as the loops above.
2284            for b in crate::extensions::ext_builtins::extension_builtin_defs() {
2285                let n = b.node.nam.clone();
2286                if disabled_set.contains(&n) {
2287                    continue; // c:825 honor `disable`
2288                }
2289                if !emitted.insert(n.clone()) {
2290                    continue;
2291                }
2292                pm.u_str = if want_val {
2293                    Some("defined".to_string()) // c:846 — dispatches in-process
2294                } else {
2295                    None
2296                };
2297                pm.node.nam = n;
2298                f(&pm, flags);
2299            }
2300            // Host-registered native commands (`extensions/native_cmds.rs`) —
2301            // a THIRD extension registry, and the only one owned by the
2302            // binary rather than by this crate: the fat `zshrs-native` build
2303            // registers `git` (zvcs), `arb` (arblang) and `stryke`
2304            // (strykelang) before the shell reads a line. They dispatch
2305            // in-process through `try_run_registered_builtin` and `whence -w`
2306            // already calls them builtins (`ext_builtins::
2307            // is_extension_builtin`), so `${(k)builtins}` has to list them or
2308            // `${+builtins[git]}` reports 0 for a name the same shell
2309            // dispatches as a builtin — the exact inconsistency the daemon
2310            // z* family had before the block above.
2311            //
2312            // Empty in the thin shell, so this loop costs one empty-Vec walk.
2313            // Same `dis == 0` / `hide_ext_builtins()` gating as its two
2314            // neighbours: under `--zsh` the builtin namespace stays zsh's.
2315            for n in crate::native_cmds::names() {
2316                if disabled_set.contains(&n) {
2317                    continue; // c:825 honor `disable`
2318                }
2319                if !emitted.insert(n.clone()) {
2320                    continue;
2321                }
2322                pm.u_str = if want_val {
2323                    Some("defined".to_string()) // c:846 — dispatches in-process
2324                } else {
2325                    None
2326                };
2327                pm.node.nam = n;
2328                f(&pm, flags);
2329            }
2330        }
2331    }
2332}
2333
2334/// Port of `scanpmbuiltins()` from `Src/Modules/parameter.c:843` — C decl `scanpmbuiltins(HashTable ht, ScanFunc func, int flags)`.
2335/// C: `static void scanpmbuiltins(HashTable ht, ScanFunc func, int flags)`
2336///   → `scanbuiltins(ht, func, flags, 0);`
2337#[allow(non_snake_case)]
2338/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
2339pub fn scanpmbuiltins(
2340    ht: *mut HashTable,
2341    func: Option<ParamScanFunc>, // c:843
2342    flags: i32,
2343) {
2344    scanbuiltins(ht, func, flags, 0) // c:846
2345}
2346
2347/// Port of `scanpmdisbuiltins()` from `Src/Modules/parameter.c:850` — C decl `scanpmdisbuiltins(HashTable ht, ScanFunc func, int flags)`.
2348/// C: `static void scanpmdisbuiltins(HashTable ht, ScanFunc func, int flags)`
2349///   → `scanbuiltins(ht, func, flags, DISABLED);`
2350#[allow(non_snake_case)]
2351/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
2352pub fn scanpmdisbuiltins(
2353    ht: *mut HashTable,
2354    func: Option<ParamScanFunc>, // c:850
2355    flags: i32,
2356) {
2357    scanbuiltins(ht, func, flags, DISABLED) // c:853
2358}
2359
2360/// Port of `getreswords()` from `Src/Modules/parameter.c:859` — C decl `getreswords(int dis)`.
2361/// C body (c:863-873):
2362/// ```c
2363/// p = ret = zhalloc((reswdtab->ct + 1) * sizeof(char *));
2364/// for (i = 0; i < reswdtab->hsize; i++)
2365///     for (hn = reswdtab->nodes[i]; hn; hn = hn->next)
2366///         if (dis ? (hn->flags & DISABLED) : !(hn->flags & DISABLED))
2367///             *p++ = dupstring(hn->nam);
2368/// *p = NULL; return ret;
2369/// ```
2370fn getreswords(dis: i32) -> Vec<String> {
2371    // c:859
2372    let g = match crate::ported::hashtable::reswdtab_lock().read() {
2373        Ok(g) => g,
2374        Err(_) => return Vec::new(),
2375    };
2376    let mut ret: Vec<String> = Vec::with_capacity(g.iter().count() + 1); // c:866
2377    for (name, node) in g.iter() {
2378        // c:868-871
2379        let disabled = (node.node.flags & DISABLED as i32) != 0;
2380        let pass = if dis != 0 { disabled } else { !disabled }; // c:870
2381        if pass {
2382            ret.push(name.clone()); // c:871 dupstring
2383        }
2384    }
2385    ret // c:874
2386}
2387
2388/// Port of `reswordsgetfn()` from `Src/Modules/parameter.c:878` — C decl `reswordsgetfn(UNUSED(Param pm))`.
2389/// C: `static char **reswordsgetfn(UNUSED(Param pm))` →
2390///   `return getreswords(0);`
2391#[allow(non_snake_case)]
2392#[allow(unused_variables)]
2393pub fn reswordsgetfn(pm: *mut param) -> Vec<String> {
2394    // c:878
2395    getreswords(0) // c:878
2396}
2397
2398/// Port of `disreswordsgetfn()` from `Src/Modules/parameter.c:885` — C decl `disreswordsgetfn(UNUSED(Param pm))`.
2399/// C: `static char **disreswordsgetfn(UNUSED(Param pm))` →
2400///   `return getreswords(DISABLED);`
2401#[allow(non_snake_case)]
2402#[allow(unused_variables)]
2403pub fn disreswordsgetfn(pm: *mut param) -> Vec<String> {
2404    // c:885
2405    getreswords(DISABLED) // c:885
2406}
2407
2408/// Port of `getpatchars()` from `Src/Modules/parameter.c:894` — C decl `getpatchars(int dis)`.
2409/// C: `static char **getpatchars(int dis)` — emits the array of
2410/// pattern-meta characters (or their disabled counterparts).
2411#[allow(non_snake_case)]
2412fn getpatchars(dis: i32) -> Vec<String> {
2413    // c:894
2414    let mut ret: Vec<String> = Vec::new();
2415    // c:898-902 — `for (i = 0; i < ZPC_COUNT; i++) if (zpc_strings[i]
2416    //   && !dis == !zpc_disables[i]) *p++ = dupstring(zpc_strings[i]);`
2417    // Walks the canonical ZPC_STRINGS table (port at pattern.rs:3065)
2418    // in lockstep with the per-slot zpc_disables byte vector
2419    // (pattern.rs:3506). dis=0 emits enabled tokens (zpc_disables[i]
2420    // == 0); dis=1 emits the disabled set ("disable -p NAME" added
2421    // them). Skips NULL-marked slots (ZPC_NULL / ZPC_BNULLKEEP /
2422    // ZPC_INPAR_PIPE / ZPC_KSHCHAR) per the `zpc_strings[i] &&` gate.
2423    let zpc_count = crate::ported::zsh_h::ZPC_COUNT as usize;
2424    let strings = crate::ported::pattern::ZPC_STRINGS;
2425    let disables = crate::ported::pattern::zpc_disables.lock().unwrap();
2426    for i in 0..zpc_count {
2427        // c:900
2428        if let Some(s) = strings[i] {
2429            // c:902 — `if (zpc_strings[i] && !dis == !zpc_disables[i])`
2430            //         The C-style boolean equality compares the LOGICAL
2431            //         truth of both sides: !dis == !disables[i] means
2432            //         "both zero" or "both non-zero".
2433            let dis_b = dis != 0;
2434            let dis_slot_b = disables[i] != 0;
2435            if dis_b == dis_slot_b {
2436                ret.push(s.to_string()); // c:903 dupstring
2437            }
2438        }
2439    }
2440    ret.shrink_to_fit();
2441    ret
2442}
2443
2444/// Port of `patcharsgetfn()` from `Src/Modules/parameter.c:911` — C decl `patcharsgetfn(UNUSED(Param pm))`.
2445/// C: `static char **patcharsgetfn(UNUSED(Param pm))` →
2446///   `return getpatchars(0);`
2447#[allow(non_snake_case)]
2448#[allow(unused_variables)]
2449pub fn patcharsgetfn(pm: *mut param) -> Vec<String> {
2450    // c:911
2451    getpatchars(0) // c:911
2452}
2453
2454/// Port of `dispatcharsgetfn()` from `Src/Modules/parameter.c:917` — C decl `dispatcharsgetfn(UNUSED(Param pm))`.
2455/// C: `static char **dispatcharsgetfn(UNUSED(Param pm))` →
2456///   `return getpatchars(1);`
2457#[allow(non_snake_case)]
2458#[allow(unused_variables)]
2459pub fn dispatcharsgetfn(pm: *mut param) -> Vec<String> {
2460    // c:917
2461    getpatchars(1) // c:917
2462}
2463
2464/// Port of `setpmoption()` from `Src/Modules/parameter.c:926` — C decl `setpmoption(Param pm, char *value)`.
2465/// C: `static void setpmoption(Param pm, char *value)` — set/unset the
2466/// shell option named by pm based on value ("on"/"off").
2467#[allow(non_snake_case)]
2468pub fn setpmoption(pm: Param, value: String) {
2469    // c:926
2470    // Faithful port of c:929-936:
2471    //   if (!value || (strcmp(value, 'on') && strcmp(value, 'off')))
2472    //       zwarn('invalid value: %s', value);
2473    //   else if (!(n = optlookup(pm->node.nam)))
2474    //       zwarn('no such option: %s', pm->node.nam);
2475    //   else if (dosetopt(n, (value && strcmp(value, 'off')), 0, opts))
2476    //       zwarn('can't change option: %s', pm->node.nam);
2477    //
2478    // Prior port checked value first and optlookup, but ignored
2479    // dosetopt's return — \`options[rcs]=off\` silently swallowed
2480    // 'cannot change option' errors (e.g. when a special-case
2481    // dosetopt path refuses the flip).
2482    let val = value.as_str();
2483    if val != "on" && val != "off" {
2484        // c:930-931
2485        zwarn(&format!("invalid value: {}", value));
2486        return;
2487    }
2488    let nam = pm.node.nam.clone();
2489    let n = optlookup(&nam); // c:932
2490    if n == 0 {
2491        zwarn(&format!("no such option: {}", nam)); // c:933
2492        return;
2493    }
2494    let on = val == "on"; // c:934 (value && strcmp(value, 'off'))
2495    if dosetopt(n, on as i32, 0) != 0 {
2496        // c:934-935 — non-zero dosetopt return signals 'can't change'.
2497        zwarn(&format!("can't change option: {}", nam));
2498    }
2499}
2500
2501/// Port of `unsetpmoption()` from `Src/Modules/parameter.c:941` — C decl `unsetpmoption(Param pm, UNUSED(int exp))`.
2502#[allow(non_snake_case)]
2503#[allow(unused_variables)]
2504pub fn unsetpmoption(pm: Param, exp: i32) {
2505    // c:941
2506    // Faithful port of c:945-948:
2507    //   if (!(n = optlookup(pm->node.nam)))
2508    //       zwarn('no such option: %s', pm->node.nam);
2509    //   else if (dosetopt(n, 0, 0, opts))
2510    //       zwarn('can't change option: %s', pm->node.nam);
2511    //
2512    // Prior port silently swallowed both error paths — \`unset
2513    // 'options[rcs]'\` returned zero status even for unknown
2514    // option names or refused flips.
2515    let n = optlookup(&pm.node.nam);
2516    if n == 0 {
2517        zwarn(&format!("no such option: {}", pm.node.nam)); // c:946
2518        return;
2519    }
2520    if dosetopt(n, 0, 0) != 0 {
2521        // c:947-948
2522        zwarn(&format!("can't change option: {}", pm.node.nam));
2523    }
2524}
2525
2526/// Port of `setpmoptions()` from `Src/Modules/parameter.c:953` — C decl `setpmoptions(Param pm, HashTable ht)`.
2527/// C: `static void setpmoptions(Param pm, HashTable ht)` — set or unset
2528/// each shell option named in `ht` based on its "on"/"off" value.
2529#[allow(non_snake_case)]
2530#[allow(unused_variables)]
2531/// WARNING: param shape doesn't match C — C passes the temporary
2532/// HashTable of value-carrying child Params (see setpmcommands);
2533/// zshrs passes the (key, value) pairs directly.
2534pub fn setpmoptions(pm: Param, ht: &[(String, String)]) {
2535    // c:953
2536    // c:958-959 — if (!ht) return; (empty pair list = same no-op).
2537    //
2538    // c:961-977 — per-pair walk.
2539    for (nam, val) in ht {
2540        if val.is_empty() || (val != "on" && val != "off") {
2541            // c:972
2542            zwarn(
2543                // c:973
2544                &format!("invalid value: {}", val),
2545            );
2546        } else {
2547            // c:974 — dosetopt(optlookup(hn->nam), (val && strcmp(val, "off")), 0, opts);
2548            let n = optlookup(nam);
2549            let on: i32 = if val != "off" { 1 } else { 0 };
2550            if n == 0 || dosetopt(n, on, 0) != 0 {
2551                // c:975-976 — failure path: can't change option.
2552                zwarn(
2553                    // c:976
2554                    &format!("can't change option: {}", nam),
2555                );
2556            }
2557        }
2558    }
2559    // c:979-980 — if (ht != pm->u.hash) deleteparamtable(ht);
2560    let _ = pm;
2561}
2562
2563/// Port of `getpmoption()` from `Src/Modules/parameter.c:988` — C decl `getpmoption(UNUSED(HashTable ht), const char *name)`.
2564/// C: `static HashNode getpmoption(UNUSED(HashTable ht), const char *name)`
2565/// — emit "on"/"off" for the named shell option.
2566#[allow(non_snake_case)]
2567#[allow(unused_variables)]
2568pub fn getpmoption(ht: *mut HashTable, name: &str) -> Option<Param> {
2569    // c:988
2570    // c:991-1010 — synth Param: u.str = (isset(opt)) ? "on" : "off".
2571    // Read the live option state via the canonical opt_state_get
2572    // accessor (Src/ported/options.rs:1623) so `\${options[NAME]}`
2573    // reads the actual runtime state, not an empty placeholder.
2574    //
2575    // optlookup returns SIGNED optno: positive means the canonical
2576    // name (e.g. "rcs" → +RCS_OPTNUM), negative means a "no…" alias
2577    // (e.g. "norcs" → -RCS_OPTNUM) which is the INVERSE — when RCS
2578    // is OFF, "norcs" is "on".
2579    let optno = optlookup(name); // c:1003
2580    let (value, found) = if optno != 0 {
2581        // c:1005 — "on" if set, "off" otherwise.
2582        // For negative optno (the "no" alias), invert the state so
2583        // `options[norcs]` reports the inverse of `options[rcs]`.
2584        let on = crate::ported::options::opt_state_get(name).unwrap_or_else(|| {
2585            // Fallback: lookup via canonical (no-prefix-stripped)
2586            // name. Necessary because opt_state_get is keyed on
2587            // the canonical option name; "norcs" misses the
2588            // direct lookup, so retry with the stripped name and
2589            // negate.
2590            let stripped = name.strip_prefix("no").unwrap_or(name);
2591            let s = crate::ported::options::opt_state_get(stripped).unwrap_or(false);
2592            if optno < 0 {
2593                !s
2594            } else {
2595                s
2596            }
2597        });
2598        (
2599            if on {
2600                "on".to_string()
2601            } else {
2602                "off".to_string()
2603            },
2604            true,
2605        )
2606    } else {
2607        (String::new(), false) // c:1009
2608    };
2609    let pm = Box::new(param {
2610        // c:993 hcalloc
2611        node: hashnode {
2612            next: None,
2613            nam: name.to_string(), // c:994
2614            // c:995 — `pm->node.flags = PM_SCALAR;` for known options;
2615            //         c:1009 — `pm->node.flags |= (PM_UNSET|PM_SPECIAL);`
2616            //         for unknown.
2617            //
2618            // Prior port added PM_READONLY in BOTH branches. C does not
2619            // set PM_READONLY — `$options` is a writable assoc (the
2620            // pmoption_gsu vtable at c:996 carries a setfn that routes
2621            // assignment through setopt). With PM_READONLY spuriously
2622            // set, `options[interactive]=on` failed with "read-only
2623            // variable: options" instead of toggling the option. This
2624            // broke the most common documented usage of the parameter
2625            // module's $options view (zsh manual SHMODULES.zsh-parameter
2626            // documents options[name] as a writable mapping).
2627            flags: if found {
2628                PM_SCALAR as i32
2629            } else {
2630                (PM_SCALAR | PM_UNSET | PM_SPECIAL) as i32
2631            },
2632        },
2633        u_data: 0,
2634        u_tied: None,
2635        u_arr: None,
2636        u_str: Some(value), // c:1005 / c:1008
2637        u_val: 0,
2638        u_dval: 0.0,
2639        u_hash: None,
2640        gsu_s: None, // c:996 pmoption_gsu — setter wiring pending
2641        gsu_i: None,
2642        gsu_f: None,
2643        gsu_a: None,
2644        gsu_h: None,
2645        base: 0,
2646        width: 0,
2647        env: None,
2648        ename: None,
2649        old: None,
2650        level: 0,
2651    });
2652    Some(pm) // c:1011
2653}
2654
2655/// Port of `scanpmoptions()` from `Src/Modules/parameter.c:1016` — C decl `scanpmoptions(UNUSED(HashTable ht), ScanFunc func, int flags)`.
2656/// C body walks the optns[] table emitting "on"/"off" for each option.
2657#[allow(non_snake_case)]
2658/// WARNING: param names don't match C — Rust=(_ht, func) vs C=(ht, func, flags)
2659pub fn scanpmoptions(
2660    _ht: *mut HashTable,
2661    func: Option<ParamScanFunc>, // c:1016
2662    flags: i32,
2663) {
2664    // c:1025-1026 — `for (i = 0; i < optiontab->hsize; i++)
2665    //   for (hn = optiontab->nodes[i]; hn; hn = hn->next)`:
2666    // the walk is in optiontab BUCKET order, not sorted/random order.
2667    // OPTIONTAB models that scan order (first element: posixargzero).
2668    let names: Vec<String> = crate::ported::options::OPTIONTAB
2669        .iter()
2670        .map(|s| s.to_string())
2671        .collect();
2672    if let Some(f) = func {
2673        // c:1032-1038 — `struct param pm;` declared ONCE outside the walk.
2674        let mut pm = param {
2675            node: hashnode {
2676                next: None,
2677                nam: String::new(),
2678                flags: PM_SCALAR as i32, // c:1037
2679            },
2680            ..Default::default()
2681        };
2682        for nm in names {
2683            // c:1024
2684            // c:1044-1045 — `ison = optno < 0 ? !opts[-optno] : opts[optno];
2685            // pm.u.str = dupstring(ison ? "on" : "off")`. The identical
2686            // decision lives in `getpmoption` (c:996), which the per-key
2687            // read uses; route through it so there is one copy.
2688            pm.u_str = getpmoption(std::ptr::null_mut(), &nm).and_then(|p| p.u_str);
2689            pm.node.nam = nm; // c:1043
2690            f(&pm, flags); // c:1046
2691        }
2692    }
2693}
2694
2695/// Port of `getpmmodule()` from `Src/Modules/parameter.c:1040` — C decl `getpmmodule(UNUSED(HashTable ht), const char *name)`.
2696/// Static-link path returns an empty PM_SPECIAL Param — modules
2697/// are statically linked in zshrs (no runtime module table).
2698#[allow(non_snake_case)]
2699#[allow(unused_variables)]
2700pub fn getpmmodule(_ht: *mut HashTable, name: &str) -> Option<Param> {
2701    // c:1040
2702    // Faithful port of c:1051-1068:
2703    //   m = modulestab->getnode2(modulestab, name);
2704    //   if (!m) return NULL;
2705    //   if (m->u.handle && !(m->node.flags & MOD_UNLOAD)) {
2706    //       type = ((m->node.flags & MOD_ALIAS) ?
2707    //               dyncat('alias:', m->u.alias) : 'loaded');
2708    //   }
2709    //   if (!type) {
2710    //       if (m->autoloads && firstnode(m->autoloads))
2711    //           type = 'autoloaded';
2712    //   }
2713    //   if (type) pm->u.str = dupstring(type);
2714    //   else { pm->u.str = ''; pm->node.flags |= (PM_UNSET|PM_SPECIAL); }
2715    //
2716    // Prior port missed the MOD_ALIAS branch entirely: alias entries
2717    // (zmodload -A foo=bar) showed up as 'loaded' instead of
2718    // 'alias:bar'. Now matches C's three-way dispatch.
2719    let modtab = MODULESTAB.lock().unwrap();
2720    let (module_present, is_alias, alias_target) = match modtab.modules.get(name) {
2721        // c:1055 gate: m->u.handle && !MOD_UNLOAD. Static-link analog
2722        // is MOD_INIT_B && !MOD_UNLOAD — the SAME criterion
2723        // scanpmmodules uses. is_loaded() checks MOD_LINKED, which
2724        // register_builtin_modules pre-seeds for every compiled-in
2725        // module, so per-key reads reported "loaded" for modules the
2726        // scan (and zsh -fc) reports "autoloaded".
2727        Some(m) => {
2728            // `m->u` is a UNION (`Src/zsh.h:1506-1508`): a loaded
2729            // module fills `handle`/`linked`, while an ALIAS node
2730            // (`zmodload -A x=zsh/main`, c:2597-2601) fills `u.alias`
2731            // — so `m->u.handle` is non-NULL for an alias too, and
2732            // c:1069-1071 takes the `alias:<target>` arm. Testing
2733            // MOD_INIT_B alone dropped every alias out of
2734            // `${modules[x]}` (it read as unset instead of
2735            // `alias:zsh/main`).
2736            let alias = (m.node.flags & crate::ported::zsh_h::MOD_ALIAS) != 0;
2737            let handle = if alias {
2738                m.alias.is_some()
2739            } else {
2740                (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0
2741            };
2742            let loaded = handle && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) == 0;
2743            (loaded, alias, m.alias.clone().unwrap_or_default())
2744        }
2745        None => (false, false, String::new()),
2746    };
2747    // c:1051-1059 — this getfn IS zsh/parameter's: it can only run
2748    // with the module loaded, so it self-reports "loaded" (matches
2749    // scanpmmodules' self-report and zsh -fc).
2750    let module_present = module_present || name == "zsh/parameter";
2751    // c:1060 autoload check: C uses per-module m->autoloads linklist.
2752    // Rust equivalent: any autoload_* map entry whose value == name,
2753    // plus the canonical .mdd autofeatures stub table.
2754    let autoload_present = modtab.autoload_builtins.values().any(|v| v == name)
2755        || modtab.autoload_conditions.values().any(|v| v == name)
2756        || modtab.autoload_params.values().any(|v| v == name)
2757        || modtab.autoload_mathfuncs.values().any(|v| v == name);
2758    drop(modtab);
2759    // After the lock drop — autoload_param_stubs re-locks MODULESTAB
2760    // for its loaded-filter; calling it under the lock deadlocks.
2761    let autoload_present = autoload_present
2762        || crate::vm_helper::autoload_param_stubs()
2763            .iter()
2764            .any(|(_, m)| *m == name);
2765    // c:1056-1063 — emit 'alias:<target>' / 'loaded' / 'autoloaded' / unset.
2766    let typ = if module_present {
2767        if is_alias {
2768            // c:1056-1057 — `alias:<target>`
2769            Some(format!("alias:{}", alias_target))
2770        } else {
2771            // c:1057 — bare 'loaded'
2772            Some("loaded".to_string())
2773        }
2774    } else if autoload_present {
2775        // c:1060-1061
2776        Some("autoloaded".to_string())
2777    } else {
2778        None // c:1062
2779    };
2780    let (val, extra_flags) = match typ {
2781        Some(s) => (s, 0),                                       // c:1066 set str
2782        None => (String::new(), (PM_UNSET | PM_SPECIAL) as i32), // c:1068-1069
2783    };
2784    Some(Box::new(param {
2785        node: hashnode {
2786            next: None,
2787            nam: name.to_string(),
2788            flags: (PM_SCALAR | PM_READONLY) as i32 | extra_flags,
2789        },
2790        u_data: 0,
2791        u_tied: None,
2792        u_arr: None,
2793        u_str: Some(val),
2794        u_val: 0,
2795        u_dval: 0.0,
2796        u_hash: None,
2797        gsu_s: None,
2798        gsu_i: None,
2799        gsu_f: None,
2800        gsu_a: None,
2801        gsu_h: None,
2802        base: 0,
2803        width: 0,
2804        env: None,
2805        ename: None,
2806        old: None,
2807        level: 0,
2808    }))
2809}
2810
2811/// Port of `scanpmmodules()` from `Src/Modules/parameter.c:1074` — C decl `scanpmmodules(UNUSED(HashTable ht), ScanFunc func, int flags)`.
2812///
2813/// Iteration callback that special-parameter scan walks use to
2814/// build an internal hash table from a Rust-side static. zshrs's
2815/// hashparam-node integration isn't wired up; the corresponding
2816/// `${(@k)foo}` queries read through the typed Rust accessor
2817/// directly. Structural pass-through retained for C name parity;
2818/// Rust idiom replacement covers the read side.
2819#[allow(non_snake_case)]
2820/// WARNING: param names don't match C — Rust=(_ht, _func) vs C=(ht, func, flags)
2821pub fn scanpmmodules(
2822    _ht: *mut HashTable,
2823    func: Option<ParamScanFunc>, // c:1074
2824    flags: i32,
2825) {
2826    let func = match func {
2827        Some(f) => f,
2828        None => return,
2829    };
2830    let mut done: std::collections::HashSet<String> = std::collections::HashSet::new(); // c:1080 done linklist
2831    let pm_flags = (PM_SCALAR | PM_READONLY) as i32; // c:1084
2832    let emit = |name: &str, val: &str| -> param {
2833        // c:1098-1100 `memset(&pm, 0); pm.node.flags = PM_SCALAR|PM_READONLY;`
2834        // then c:1106-1107 / c:1114 rebind `pm.node.nam` and `pm.u.str`.
2835        param {
2836            node: hashnode {
2837                next: None,
2838                nam: name.to_string(), // c:1106
2839                flags: pm_flags,
2840            },
2841            u_str: Some(val.to_string()), // c:1107 / c:1114
2842            ..Default::default()
2843        }
2844    };
2845    // c:1088-1099 — modulestab walk, emit each LOADED module.
2846    // C gate at c:1091:
2847    //   if (m->u.handle && !(m->node.flags & MOD_UNLOAD))
2848    // Static-link analog of `m->u.handle`: MOD_INIT_B (boot ran).
2849    // Same gate module_loaded uses post-6435a0dca2.
2850    //
2851    // c:1093 emit: `(m->node.flags & MOD_ALIAS)
2852    //              ? dyncat('alias:', m->u.alias) : 'loaded'`.
2853    //
2854    // Prior port iterated every modulestab entry — emitted entries
2855    // for register_builtin_modules-registered-but-not-loaded modules
2856    // (zsh/files, zsh/system, zsh/zftp, etc. that carry MOD_UNLOAD
2857    // at register time). \${(k)modules} listed ALL of them as
2858    // 'loaded' — diverging from \`zsh -fc\` which only reports
2859    // \`zsh/main\` until something fires explicit zmodload.
2860    let modules: Vec<(String, String)> = {
2861        let tab = MODULESTAB.lock().unwrap();
2862        tab.modules
2863            .iter()
2864            .filter_map(|(name, m)| {
2865                // `m->u` is a UNION: an ALIAS node carries `u.alias`
2866                // (c:2601), which makes C's `m->u.handle` test at
2867                // c:1091 true, so aliases DO appear in `${(k)modules}`
2868                // with an `alias:<target>` value. Gating on MOD_INIT_B
2869                // alone hid every `zmodload -A` alias from the scan.
2870                let is_alias = (m.node.flags & crate::ported::zsh_h::MOD_ALIAS) != 0;
2871                let handle = if is_alias {
2872                    m.alias.is_some()
2873                } else {
2874                    (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0
2875                };
2876                if !handle || (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0 {
2877                    return None; // c:1091 gate
2878                }
2879                // c:1093 — alias entries get 'alias:<target>', others
2880                // get 'loaded'.
2881                let val = if is_alias {
2882                    format!("alias:{}", m.alias.as_deref().unwrap_or(""))
2883                } else {
2884                    "loaded".to_string()
2885                };
2886                Some((name.clone(), val))
2887            })
2888            .collect()
2889    };
2890    for (name, val) in modules {
2891        // c:1090
2892        done.insert(name.clone()); // c:1095 addlinknode(done, ...)
2893        let pm = emit(&name, &val); // c:1093 emit value-side
2894        func(&pm, flags); // c:1096
2895    }
2896    // c:1088-1099 — this scan IS zsh/parameter's getfn: in C it can
2897    // only run with zsh/parameter loaded, so the module reports
2898    // itself "loaded" (zsh -fc 'print ${(k)modules}' includes
2899    // zsh/parameter). zshrs builds the param in statically; emit the
2900    // self-report when the modulestab walk didn't.
2901    if done.insert("zsh/parameter".to_string()) {
2902        let pm = emit("zsh/parameter", "loaded");
2903        func(&pm, flags);
2904    }
2905    // c:1102-1110 — builtintab autoloaded (BINF_ADDED clear with optstr → module).
2906    // C stores the OWNING MODULE NAME in `bn->optstr` only for
2907    // BINF_ADDED-clear autoload STUB entries (`zmodload -ab`); real
2908    // builtins' optstr is the option string. zshrs's builtintab holds
2909    // the real statically-linked builtins (BINF_ADDED clear), so
2910    // walking it emitted ~75 OPTION STRINGS as module names in
2911    // ${(k)modules}. The autoload-stub registry is
2912    // MODULESTAB.autoload_builtins (builtin -> owning module).
2913    //
2914    // c:1102-1103 is a BUCKET WALK of builtintab:
2915    //     for (i = 0; i < builtintab->hsize; i++)
2916    //         for (hn = builtintab->nodes[i]; hn; hn = hn->next)
2917    // and c:1104 emits `((Builtin) hn)->optstr` — which, for a
2918    // BINF_ADDED-clear stub, IS the owning module name. Reading the
2919    // ledger's `values()` instead walked a `HashMap` whose iteration
2920    // order is `RandomState`-seeded, so `${(k)modules}` came out in a
2921    // different order on every process (and matched `zsh -f` on no
2922    // line past the two loaded modules). Walk `builtintab` in C's
2923    // order (builtin.rs `BUILTINTAB_NODES`, the faithful
2924    // `newhashtable(85)` front-insert table) and resolve each name
2925    // through the ledger, which is zshrs's `optstr`-for-a-stub.
2926    let auto_bin_ledger: std::collections::HashMap<String, String> = {
2927        let tab = MODULESTAB.lock().unwrap();
2928        tab.autoload_builtins.clone()
2929    };
2930    for (name, _b) in crate::ported::builtin::BUILTINTAB_NODES.iter() {
2931        // c:1103
2932        // c:1104 — `!(hn->flags & BINF_ADDED)`: zshrs's builtintab holds
2933        // the statically-linked builtins, so the stub set is exactly the
2934        // ledger's key set.
2935        let Some(opt) = auto_bin_ledger.get(name) else {
2936            continue;
2937        };
2938        if done.insert(opt.clone()) {
2939            // c:1105 linknodebystring(done, optstr)
2940            let pm = emit(opt, "autoloaded"); // c:1106-1108
2941            func(&pm, flags); // c:1109
2942        }
2943    }
2944    // RUST-ONLY TAIL: `zmodload -ab NAME MODULE` at runtime records the
2945    // stub in the ledger, but zshrs's `BUILTINTAB_NODES` is a static
2946    // built from `BUILTINS`, so such a name has no node to walk above
2947    // (in C `add_autobin` calls `addbuiltin`, c:436, and the node IS in
2948    // builtintab). Emit the leftovers so they still appear; sorted by
2949    // stub name because there is no bucket order to inherit.
2950    let mut leftover: Vec<(&String, &String)> = auto_bin_ledger
2951        .iter()
2952        .filter(|(name, _)| !crate::ported::builtin::BUILTINTAB_NODES.contains_key(name.as_str()))
2953        .collect();
2954    leftover.sort();
2955    for (_name, opt) in leftover {
2956        if done.insert(opt.clone()) {
2957            let pm = emit(opt, "autoloaded"); // c:1108
2958            func(&pm, flags); // c:1109
2959        }
2960    }
2961    // c:1112-1117 — condtab autoloaded (p->module set).
2962    let cond_modules: Vec<String> = crate::ported::module::CONDTAB
2963        .lock()
2964        .unwrap()
2965        .iter()
2966        .filter_map(|p| p.module.clone())
2967        .collect();
2968    for m in cond_modules {
2969        // c:1112
2970        if done.insert(m.clone()) {
2971            let pm = emit(&m, "autoloaded");
2972            func(&pm, flags); // c:1116
2973        }
2974    }
2975    // c:1119-1124 — realparamtab PM_AUTOLOAD entries. The canonical
2976    // stub registry is vm_helper::autoload_param_stubs (the .mdd
2977    // autofeatures table filtered to unloaded owners) — MODULESTAB's
2978    // autoload_params map only carries explicitly-registered names
2979    // and missed zsh/zleparameter.
2980    for (_pname, m) in crate::vm_helper::autoload_param_stubs() {
2981        if done.insert(m.to_string()) {
2982            let pm = emit(m, "autoloaded");
2983            func(&pm, flags); // c:1124
2984        }
2985    }
2986    let auto_param_modules: Vec<String> = {
2987        let tab = MODULESTAB.lock().unwrap();
2988        tab.autoload_params.values().cloned().collect() // c:1121
2989    };
2990    for m in auto_param_modules {
2991        if done.insert(m.clone()) {
2992            let pm = emit(&m, "autoloaded");
2993            func(&pm, flags); // c:1124
2994        }
2995    }
2996}
2997
2998/// Port of `dirssetfn()` from `Src/Modules/parameter.c:1131` — C decl `dirssetfn(UNUSED(Param pm), char **x)`.
2999/// C: `static void dirssetfn(UNUSED(Param pm), char **x)` — replaces
3000/// the dirstack with the provided array (when not in cleanup).
3001#[allow(non_snake_case)]
3002#[allow(unused_variables)]
3003pub fn dirssetfn(pm: *mut param, x: Vec<String>) {
3004    // c:1131
3005    let incleanup = INCLEANUP.load(std::sync::atomic::Ordering::Relaxed); // c:1131
3006    if incleanup == 0 {
3007        // c:1136
3008        if let Ok(mut d) = DIRSTACK.lock() {
3009            // c:1137-1140
3010            d.clear(); // c:1137
3011            for entry in &x {
3012                // c:1139
3013                d.push(entry.clone()); // c:1140
3014            }
3015        }
3016    }
3017    // c:1142-1143 — freearray(ox); Rust drops `x` automatically.
3018    drop(x);
3019}
3020
3021// `getreswords()` (Src/lex.c) ported above as a private helper —
3022// `disreswordsgetfn` calls it directly; no separate public stub needed.
3023
3024/// Port of `dirsgetfn()` from `Src/Modules/parameter.c:1147` — C decl `dirsgetfn(UNUSED(Param pm))`.
3025/// C: `static char **dirsgetfn(UNUSED(Param pm))` →
3026///   `return hlinklist2array(dirstack, 1);`
3027#[allow(non_snake_case)]
3028#[allow(unused_variables)]
3029pub fn dirsgetfn(pm: *mut param) -> Vec<String> {
3030    // c:1147
3031    // c:1131 — hlinklist2array(dirstack, 1) returns the dirstack as
3032    // a heap-allocated array. Static-link path reads from the global
3033    // DIRSTACK list maintained by `dirs`/`pushd`/`popd`.
3034    DIRSTACK.lock().map(|d| d.clone()).unwrap_or_default() // c:1131
3035}
3036
3037/// Port of `getpmhistory()` from `Src/Modules/parameter.c:1156` — C decl `getpmhistory(UNUSED(HashTable ht), const char *name)`.
3038/// C body (c:1159-1206): quietgetn(name) → histnum; getHistEnt(num)
3039/// → histent; emit `pm.u.str = histent->text`.
3040#[allow(non_snake_case)]
3041#[allow(unused_variables)]
3042pub fn getpmhistory(ht: *mut HashTable, name: &str) -> Option<Param> {
3043    // c:1156
3044    // Faithful port of c:1168-1182:
3045    //   int ok = 1;
3046    //   if (*name != '0' || name[1]) {
3047    //       if (*name == '0') ok = 0;          ← leading-zero with more chars
3048    //       else {
3049    //           for (p = name; *p && idigit(*p); p++);
3050    //           if (*p) ok = 0;                ← non-digit suffix
3051    //       }
3052    //   }
3053    //   if (ok && (he = quietgethist(atoi(name))))
3054    //       pm->u.str = dupstring(he->node.nam);
3055    //   else {
3056    //       pm->u.str = dupstring('');
3057    //       pm->node.flags |= (PM_UNSET|PM_SPECIAL);
3058    //   }
3059    //
3060    // Prior port did `name.parse::<i64>().ok()?` which early-returned
3061    // None on parse failure — but C returns a valid Param with
3062    // PM_UNSET|PM_SPECIAL set, never NULL. The two paths differ:
3063    //
3064    //   - Rust None: caller sees 'no such param'
3065    //   - C valid Param + PM_UNSET: caller sees 'param exists but
3066    //     is unset'
3067    //
3068    // \${history[bogus]} should match C's 'exists but unset'
3069    // semantic so \${+history[bogus]} returns 1 (param exists).
3070    let bytes = name.as_bytes();
3071    let mut ok = true;
3072    // c:1168 — `if (*name != '0' || name[1])`.
3073    if bytes.first() != Some(&b'0') || bytes.len() > 1 {
3074        // c:1169 — `if (*name == '0') ok = 0;`
3075        if bytes.first() == Some(&b'0') {
3076            ok = false; // leading zero with more chars
3077        } else {
3078            // c:1171-1174 — walk digits; if non-digit hit, ok = 0.
3079            for b in bytes {
3080                if !b.is_ascii_digit() {
3081                    ok = false;
3082                    break;
3083                }
3084            }
3085            // Empty name also fails — for loop didn't break but
3086            // no digits means atoi(name)=0 which would match h0
3087            // wrongly. Treat empty as invalid.
3088            if bytes.is_empty() {
3089                ok = false;
3090            }
3091        }
3092    }
3093    // c:1177 — `if (ok && (he = quietgethist(atoi(name))))`.
3094    let value = if ok {
3095        let num: i64 = name.parse().unwrap_or(0);
3096        crate::ported::hist::quietgethist(num).map(|e| e.node.nam.clone())
3097    } else {
3098        None
3099    };
3100    let (val, found) = match value {
3101        Some(v) => (v, true),           // c:1178
3102        None => (String::new(), false), // c:1180-1181
3103    };
3104    let pm = Box::new(param {
3105        // c:1162 hcalloc
3106        node: hashnode {
3107            next: None,
3108            nam: name.to_string(),
3109            flags: if found {
3110                (PM_SCALAR | PM_READONLY) as i32
3111            } else {
3112                (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32
3113            },
3114        },
3115        u_data: 0,
3116        u_tied: None,
3117        u_arr: None,
3118        u_str: Some(val), // c:1188 / c:1204
3119        u_val: 0,
3120        u_dval: 0.0,
3121        u_hash: None,
3122        gsu_s: None,
3123        gsu_i: None,
3124        gsu_f: None,
3125        gsu_a: None,
3126        gsu_h: None,
3127        base: 0,
3128        width: 0,
3129        env: None,
3130        ename: None,
3131        old: None,
3132        level: 0,
3133    });
3134    Some(pm) // c:1206
3135}
3136
3137/// Port of `scanpmhistory()` from `Src/Modules/parameter.c:1188` — C decl `scanpmhistory(UNUSED(HashTable ht), ScanFunc func, int flags)`.
3138///
3139/// Iteration callback that special-parameter scan walks use to
3140/// build an internal hash table from a Rust-side static. zshrs's
3141/// hashparam-node integration isn't wired up; the corresponding
3142/// `${(@k)foo}` queries read through the typed Rust accessor
3143/// directly. Structural pass-through retained for C name parity;
3144/// Rust idiom replacement covers the read side.
3145#[allow(non_snake_case)]
3146/// WARNING: param names don't match C — Rust=(_ht, _func) vs C=(ht, func, flags)
3147pub fn scanpmhistory(
3148    _ht: *mut HashTable,
3149    func: Option<ParamScanFunc>, // c:1188
3150    flags: i32,
3151) {
3152    let func = match func {
3153        Some(f) => f,
3154        None => return,
3155    };
3156    // Lazy-history chokepoint: a WHOLE-`$history` scan (`${(u@)history}`,
3157    // `${history[(R)pat]}`, hsmw ^R) wants ALL of it — page the rest of
3158    // the HISTFILE in now. This is the on-demand full load; startup
3159    // never slurps the file (extensions/history_lazy).
3160    crate::history_lazy::page_older_until(0);
3161    // c:1203 — `if ((flags & (SCANPM_WANTVALS|SCANPM_MATCHVAL)) ||
3162    //             !(flags & SCANPM_WANTKEYS))`. The VALUE side is only
3163    // materialized when the caller asked for values; a keys-only scan
3164    // (`paramvalarr(ht, SCANPM_WANTKEYS)`, c:Src/params.c:3138 — what
3165    // `${(k)history}` / `${#history}` issue) never dups the command text.
3166    // This port used to clone every command string unconditionally, which
3167    // on a 574k-entry ring is 574k `String` allocations plus a copy of the
3168    // whole history text per scan, thrown away by every keys-only caller.
3169    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
3170        || (flags as u32 & SCANPM_WANTKEYS) == 0;
3171    // c:1195-1196 — `int i = addhistnum(curhist, -1, HIST_FOREIGN);
3172    //               Histent he = gethistent(i, GETHIST_UPWARD);`
3173    // The walk starts one event BELOW `curhist`, so the event being run is
3174    // not listed: under `-c`, `print -s x; print -s y; print ${(k)history}`
3175    // reads `1`, and a lone `print -s` leaves `$history` empty. Both calls
3176    // take the ring lock themselves, so resolve the start before the
3177    // snapshot below takes it.
3178    let start = crate::ported::hist::gethistent(
3179        crate::ported::hist::addhistnum(
3180            crate::ported::hist::curhist.load(std::sync::atomic::Ordering::SeqCst),
3181            -1,
3182            crate::ported::zsh_h::HIST_FOREIGN as i32,
3183        ),
3184        crate::ported::zsh_h::GETHIST_UPWARD,
3185    );
3186    let Some(start) = start else {
3187        return; // c:1199 `while (he)` — no event at or below the start
3188    };
3189    // Snapshot the walk so func() can re-enter without deadlocking on the
3190    // hist_ring mutex.
3191    let entries: Vec<(i64, Option<String>)> = {
3192        let ring = hist_ring.lock().unwrap(); // c:1196 walk via up_histent
3193        // hist_ring is newest-first (index 0 = highest histnum), and
3194        // `up_histent` steps to the next index (hist.rs), so index order from
3195        // the start event IS C's newest→oldest `up_histent` walk (c:1208).
3196        ring.iter()
3197            .skip_while(|h| h.histnum > start)
3198            .map(|h| {
3199                (
3200                    h.histnum,
3201                    if want_val {
3202                        Some(h.node.nam.clone()) // c:1204
3203                    } else {
3204                        None
3205                    },
3206                )
3207            })
3208            .collect()
3209    };
3210    // c:1192-1197 — `struct param pm;` is declared ONCE, `memset` ONCE, and
3211    // the loop only rewrites `pm.node.nam` / `pm.u.str` before each
3212    // `func(&pm.node, flags)`. The previous port built a fresh `param` and
3213    // then `Box::new`d its node on every iteration: one heap allocation per
3214    // history event (574k per scan) that C does not make.
3215    let mut pm = param {
3216        node: hashnode {
3217            // c:1194 memset((void *)&pm, 0, sizeof(struct param))
3218            next: None,
3219            nam: String::new(),
3220            flags: (PM_SCALAR | PM_READONLY) as i32, // c:1195
3221        },
3222        u_data: 0,
3223        u_tied: None,
3224        u_arr: None,
3225        u_str: None,
3226        u_val: 0,
3227        u_dval: 0.0,
3228        u_hash: None,
3229        gsu_s: None,
3230        gsu_i: None,
3231        gsu_f: None,
3232        gsu_a: None,
3233        gsu_h: None,
3234        base: 0,
3235        width: 0,
3236        env: None,
3237        ename: None,
3238        old: None,
3239        level: 0,
3240    };
3241    for (histnum, cmd) in entries {
3242        // c:1199-1207
3243        pm.node.nam = crate::ported::params::convbase(histnum, 10); // c:1202 convbase(buf, he->histnum, 10)
3244        pm.u_str = cmd; // c:1204 pm.u.str = he->node.nam
3245        func(&pm, flags); // c:1206
3246    }
3247}
3248
3249/// Port of `histwgetfn()` from `Src/Modules/parameter.c:1217` — C decl
3250/// `static char **histwgetfn(UNUSED(Param pm))`. The `$historywords` array getter.
3251/// C body c:1224-1226 prepends `bufferwords(NULL, NULL, NULL, 0)`
3252/// (current editor line) to the result, then walks the history
3253/// ring newest→oldest slicing each entry's words via the
3254/// `histent.words[]` (begin,end) byte-offset pairs in reverse
3255/// position order.
3256#[allow(non_snake_case)]
3257#[allow(unused_variables)]
3258pub fn histwgetfn(pm: *mut param) -> Vec<String> {
3259    // c:1217
3260    let mut out: Vec<String> = Vec::new();
3261    // c:1224 — `bufferwords(NULL, NULL, NULL, 0)` — current editor line.
3262    let zleline: String = crate::ported::zle::zle_main::ZLELINE
3263        .lock()
3264        .unwrap()
3265        .iter()
3266        .collect();
3267    let cursor = crate::ported::zle::zle_main::ZLECS.load(std::sync::atomic::Ordering::SeqCst);
3268    let (bw, _) = crate::ported::hist::bufferwords(&zleline, Some(cursor), 0);
3269    // c:1239-1241 — `for (n = firstnode(ll); n; incnode(n)) pushnode(l, getdata(n));`
3270    //
3271    // `pushnode` PREPENDS, so walking `ll` front-to-back and pushing each
3272    // element onto `l` leaves `l` holding `bufferwords` REVERSED. The ring
3273    // walk below is different and is already right: C uses `addlinknode`
3274    // there (c:1256), an APPEND, which is what `out.push` does.
3275    //
3276    // This appended, so `$historywords` came out in the wrong order. The
3277    // comment already said "pushnode" while the code did the opposite —
3278    // measured on the buffer `kill -`:
3279    //     zsh   ${(qq)historywords} = '- kill …'
3280    //     zshrs                       'kill - …'
3281    // Live cost: `_history` anywhere in the completer chain saw the wrong
3282    // leading word, returned a match where zsh returns none (hist_ret=0
3283    // nm=1 vs zsh's hist_ret=1 nm=0), and so KILLED every completer after
3284    // it in the chain.
3285    out.extend(bw.into_iter().rev());
3286                    // c:1229-1247 — walk hist_ring newest-to-oldest, slicing each
3287                    // entry's words by `histent.words[iw*2..iw*2+2]` byte offsets in
3288                    // reverse-position order.
3289    if let Ok(ring) = hist_ring.lock() {
3290        for he in ring.iter().rev() {
3291            // c:1229
3292            let hstr = he.node.nam.as_bytes();
3293            let len = hstr.len() as i32;
3294            let nwords = he.nwords as i32;
3295            // c:1232 — `for (iw = he->nwords - 1; iw >= 0; iw--)`
3296            let mut iw = nwords - 1;
3297            while iw >= 0 {
3298                let i2 = (iw as usize) * 2;
3299                if i2 + 1 >= he.words.len() {
3300                    break;
3301                }
3302                let wbegin = he.words[i2] as i32; // c:1233
3303                let wend = he.words[i2 + 1] as i32; // c:1234
3304                                                    // c:1236 — signed-short overflow bounds check.
3305                if wbegin < 0 || wbegin >= len || wend < 0 || wend > len {
3306                    break;
3307                }
3308                let slice = &hstr[wbegin as usize..wend as usize]; // c:1240-1244
3309                if let Ok(s) = std::str::from_utf8(slice) {
3310                    out.push(s.to_string()); // c:1244 addlinknode
3311                }
3312                iw -= 1;
3313            }
3314        }
3315    }
3316    out // c:1250 hlinklist2array
3317}
3318
3319/// Port of `pmjobtext()` from `Src/Modules/parameter.c:1255` — C decl `pmjobtext(Job jtab, int job)`.
3320/// C: `static char *pmjobtext(Job jtab, int job)` — emit pipeline text
3321/// joined with " | " across all procs.
3322#[allow(non_snake_case)]
3323#[allow(unused_variables)]
3324pub fn pmjobtext(_jtab: *mut std::ffi::c_void, job: i32) -> String {
3325    // c:1255
3326    // c:1257-1273 — `for (pn = jtab[job].procs; pn; pn = pn->next)
3327    //                  strcat(ret, pn->text); if (pn->next) strcat(ret, " | ")`.
3328    let (jtab, _jmax) = selectjobtab(); // c:1257 jtab[job].procs
3329    let job_idx = job as usize;
3330    if let Some(j) = jtab.get(job_idx) {
3331        // Join each proc's text with " | " — the canonical pipeline-
3332        // display format the C source emits.
3333        j.procs
3334            .iter()
3335            .map(|p| p.text.clone())
3336            .collect::<Vec<_>>()
3337            .join(" | ") // c:1273 " | " separator
3338    } else {
3339        String::new()
3340    }
3341}
3342
3343/// Port of `getpmjobtext()` from `Src/Modules/parameter.c:1277` — C decl `getpmjobtext(UNUSED(HashTable ht), const char *name)`.
3344#[allow(non_snake_case)]
3345#[allow(unused_variables)]
3346pub fn getpmjobtext(ht: *mut HashTable, name: &str) -> Option<Param> {
3347    // c:1277
3348    // c:1284-1287 — alloc PM_SCALAR|PM_READONLY param with name.
3349    // c:1289 — selectjobtab(&jtab, &jmax);
3350    let (jtab, jmax) = selectjobtab();
3351    // c:1291 — `job = strtol(name, &pend, 10);`
3352    //
3353    // libc `strtol` skips leading whitespace and an optional sign before the
3354    // digit run and points `pend` at the first unconverted byte; with NO digits
3355    // converted it returns 0 and leaves `pend == name`. `str::parse` rejects
3356    // both leading blanks and any trailing text, so `${jobtexts[ 1]}` (which
3357    // zsh answers with job 1) took the `getjob` path and reported
3358    // `job not found`. Inlined rather than factored into a helper: each of the
3359    // three C getters carries its own copy (c:1291 / c:1399 / c:1471), and
3360    // src/ported/ admits no functions without a C counterpart.
3361    let n_bytes = name.as_bytes();
3362    let mut n_i = 0usize;
3363    while n_i < n_bytes.len() && n_bytes[n_i].is_ascii_whitespace() {
3364        n_i += 1;
3365    }
3366    let n_num_start = n_i;
3367    if n_i < n_bytes.len() && (n_bytes[n_i] == b'+' || n_bytes[n_i] == b'-') {
3368        n_i += 1;
3369    }
3370    let n_digits_start = n_i;
3371    while n_i < n_bytes.len() && n_bytes[n_i].is_ascii_digit() {
3372        n_i += 1;
3373    }
3374    let (job, pend_nonempty) = if n_i == n_digits_start {
3375        // No conversion performed: strtol returns 0 and pend stays at `name`.
3376        (0i32, !name.is_empty())
3377    } else {
3378        // C truncates strtol's `long` into an `int` job number; an overflowing
3379        // key saturates to LONG_MAX, whose low 32 bits are -1 — out of range
3380        // either way.
3381        (
3382            name[n_num_start..n_i].parse::<i64>().unwrap_or(i64::MAX) as i32,
3383            n_i != n_bytes.len(),
3384        )
3385    };
3386    // c:1293-1294 — if (*pend) job = getjob(name, NULL);
3387    let job = if pend_nonempty { getjob(name, "") } else { job };
3388    // c:1295-1298 — if (job >= 1 && job <= jmax && jtab[job].stat && jtab[job].procs && !STAT_NOPRINT)
3389    //                  pm->u.str = pmjobtext(jtab, job);
3390    if job >= 1 && (job as usize) <= jmax {
3391        if let Some(j) = jtab.get(job as usize) {
3392            if j.stat != 0 && !j.procs.is_empty() && (j.stat & STAT_NOPRINT) == 0 {
3393                let text = pmjobtext(std::ptr::null_mut(), job);
3394                let mut pm = make_empty_special_pm(name);
3395                pm.node.flags = (PM_SCALAR | PM_READONLY) as i32;
3396                pm.u_str = Some(text);
3397                return Some(pm);
3398            }
3399        }
3400    }
3401    // c:1299-1302 — else { pm->u.str = ""; pm->node.flags |= PM_UNSET|PM_SPECIAL; }
3402    let mut pm = make_empty_special_pm(name);
3403    pm.node.flags = (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32;
3404    Some(pm)
3405}
3406
3407/// Port of `scanpmjobtexts()` from `Src/Modules/parameter.c:1308` — C decl `scanpmjobtexts(UNUSED(HashTable ht), ScanFunc func, int flags)`.
3408///
3409/// Iteration callback that special-parameter scan walks use to
3410/// build an internal hash table from a Rust-side static. zshrs's
3411/// hashparam-node integration isn't wired up; the corresponding
3412/// `${(@k)foo}` queries read through the typed Rust accessor
3413/// directly. Structural pass-through retained for C name parity;
3414/// Rust idiom replacement covers the read side.
3415#[allow(non_snake_case)]
3416/// WARNING: param names don't match C — Rust=(_ht, _func) vs C=(ht, func, flags)
3417pub fn scanpmjobtexts(
3418    _ht: *mut HashTable,
3419    func: Option<ParamScanFunc>, // c:1308
3420    flags: i32,
3421) {
3422    let func = match func {
3423        Some(f) => f,
3424        None => return,
3425    };
3426    let (jtab, jmax) = selectjobtab(); // c:1319
3427    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
3428        || (flags as u32 & SCANPM_WANTKEYS) == 0;
3429    for job in 1..=jmax {
3430        // c:1321
3431        if let Some(j) = jtab.get(job) {
3432            if j.stat != 0 && !j.procs.is_empty() && (j.stat & STAT_NOPRINT) == 0 {
3433                // c:1322-1323
3434                let val = if want_val {
3435                    pmjobtext(std::ptr::null_mut(), job as i32)
3436                } else {
3437                    String::new()
3438                }; // c:1330 pmjobtext
3439                let pm = param {
3440                    node: hashnode {
3441                        next: None,
3442                        nam: format!("{}", job), // c:1327
3443                        flags: (PM_SCALAR | PM_READONLY) as i32,
3444                    },
3445                    u_data: 0,
3446                    u_tied: None,
3447                    u_arr: None,
3448                    u_str: Some(val),
3449                    u_val: 0,
3450                    u_dval: 0.0,
3451                    u_hash: None,
3452                    gsu_s: None,
3453                    gsu_i: None,
3454                    gsu_f: None,
3455                    gsu_a: None,
3456                    gsu_h: None,
3457                    base: 0,
3458                    width: 0,
3459                    env: None,
3460                    ename: None,
3461                    old: None,
3462                    level: 0,
3463                };
3464                func(&pm, flags); // c:1333
3465            }
3466        }
3467    }
3468}
3469
3470/// Port of `pmjobstate()` from `Src/Modules/parameter.c:1340` — C decl `pmjobstate(Job jtab, int job)`.
3471/// C: `static char *pmjobstate(Job jtab, int job)` — emit stopped/running
3472/// state for each process in the job, joined with `:pid=state`.
3473#[allow(non_snake_case)]
3474#[allow(unused_variables)]
3475pub fn pmjobstate(_jtab: *mut std::ffi::c_void, job: i32) -> String {
3476    // c:1340
3477    let curjob = *crate::ported::jobs::CURJOB
3478        .get_or_init(|| Mutex::new(-1))
3479        .lock()
3480        .unwrap();
3481    let prevjob = *crate::ported::jobs::PREVJOB
3482        .get_or_init(|| Mutex::new(-1))
3483        .lock()
3484        .unwrap();
3485    // c:1346-1351 — current/prev marker.
3486    let cp = if job == curjob {
3487        ":+"
3488    }
3489    // c:1346
3490    else if job == prevjob {
3491        ":-"
3492    }
3493    // c:1348
3494    else {
3495        ":"
3496    }; // c:1350
3497    let (jtab, _jmax) = selectjobtab();
3498    let job_idx = job as usize;
3499    let j = match jtab.get(job_idx) {
3500        Some(j) => j,
3501        None => return String::new(),
3502    };
3503    // c:1353-1357 — top-level state from jtab[job].stat.
3504    let mut ret = if (j.stat & STAT_DONE) != 0 {
3505        // c:1353
3506        format!("done{cp}")
3507    } else if (j.stat & STAT_STOPPED) != 0 {
3508        // c:1355
3509        format!("suspended{cp}")
3510    } else {
3511        format!("running{cp}") // c:1357
3512    };
3513    // c:1359-1379 — per-proc `:<pid>=<state>` suffixes.
3514    for pn in &j.procs {
3515        // c:1359
3516        let state = if pn.status == SP_RUNNING {
3517            // c:1361
3518            "running".to_string()
3519        } else if pn.status >= 0 && (pn.status & 0xff) == 0 {
3520            // c:1363 WIFEXITED + WEXITSTATUS
3521            let code = (pn.status >> 8) & 0xff;
3522            if code != 0 {
3523                format!("exit {code}")
3524            } else {
3525                "done".to_string()
3526            }
3527        } else if (pn.status & 0xff) == 0x7f {
3528            // c:1369 WIFSTOPPED
3529            sigmsg((pn.status >> 8) & 0xff).to_string()
3530        } else if (pn.status & 0x80) != 0 {
3531            // c:1371 WCOREDUMP
3532            format!("{} (core dumped)", sigmsg(pn.status & 0x7f))
3533        } else {
3534            sigmsg(pn.status & 0x7f).to_string() // c:1374 WTERMSIG
3535        };
3536        ret.push_str(&format!(":{}={}", pn.pid, state)); // c:1376
3537    }
3538    ret
3539}
3540
3541/// Port of `getpmjobstate()` from `Src/Modules/parameter.c:1385` — C decl `getpmjobstate(UNUSED(HashTable ht), const char *name)`.
3542#[allow(non_snake_case)]
3543#[allow(unused_variables)]
3544pub fn getpmjobstate(ht: *mut HashTable, name: &str) -> Option<Param> {
3545    // c:1385
3546    let (jtab, jmax) = selectjobtab(); // c:1397
3547                                       // c:1399 — `job = strtol(name, &pend, 10);`. See getpmjobtext (c:1291) for
3548                                       // why libc strtol's leading-blank / partial-digit semantics are spelled out
3549                                       // here instead of using `str::parse`.
3550    let n_bytes = name.as_bytes();
3551    let mut n_i = 0usize;
3552    while n_i < n_bytes.len() && n_bytes[n_i].is_ascii_whitespace() {
3553        n_i += 1;
3554    }
3555    let n_num_start = n_i;
3556    if n_i < n_bytes.len() && (n_bytes[n_i] == b'+' || n_bytes[n_i] == b'-') {
3557        n_i += 1;
3558    }
3559    let n_digits_start = n_i;
3560    while n_i < n_bytes.len() && n_bytes[n_i].is_ascii_digit() {
3561        n_i += 1;
3562    }
3563    let (job, pend_nonempty) = if n_i == n_digits_start {
3564        (0i32, !name.is_empty())
3565    } else {
3566        (
3567            name[n_num_start..n_i].parse::<i64>().unwrap_or(i64::MAX) as i32,
3568            n_i != n_bytes.len(),
3569        )
3570    };
3571    let job = if pend_nonempty {
3572        // c:1400-1401
3573        getjob(name, "")
3574    } else {
3575        job
3576    };
3577    // c:1402-1405 — if (job >= 1 && job <= jmax && jtab[job].stat && jtab[job].procs && !STAT_NOPRINT)
3578    if job >= 1 && (job as usize) <= jmax {
3579        if let Some(j) = jtab.get(job as usize) {
3580            if j.stat != 0 && !j.procs.is_empty() && (j.stat & STAT_NOPRINT) == 0 {
3581                let state = pmjobstate(std::ptr::null_mut(), job);
3582                let mut pm = make_empty_special_pm(name);
3583                pm.node.flags = (PM_SCALAR | PM_READONLY) as i32;
3584                pm.u_str = Some(state);
3585                return Some(pm);
3586            }
3587        }
3588    }
3589    // c:1406-1409 — else { u.str = ""; flags |= PM_UNSET|PM_SPECIAL; }
3590    let mut pm = make_empty_special_pm(name);
3591    pm.node.flags = (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32;
3592    Some(pm)
3593}
3594
3595/// Port of `scanpmjobstates()` from `Src/Modules/parameter.c:1415` — C decl `scanpmjobstates(UNUSED(HashTable ht), ScanFunc func, int flags)`.
3596///
3597/// Iteration callback that special-parameter scan walks use to
3598/// build an internal hash table from a Rust-side static. zshrs's
3599/// hashparam-node integration isn't wired up; the corresponding
3600/// `${(@k)foo}` queries read through the typed Rust accessor
3601/// directly. Structural pass-through retained for C name parity;
3602/// Rust idiom replacement covers the read side.
3603#[allow(non_snake_case)]
3604/// WARNING: param names don't match C — Rust=(_ht, _func) vs C=(ht, func, flags)
3605pub fn scanpmjobstates(
3606    _ht: *mut HashTable,
3607    func: Option<ParamScanFunc>, // c:1415
3608    flags: i32,
3609) {
3610    let func = match func {
3611        Some(f) => f,
3612        None => return,
3613    };
3614    let (jtab, jmax) = selectjobtab(); // c:1426
3615    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
3616        || (flags as u32 & SCANPM_WANTKEYS) == 0;
3617    for job in 1..=jmax {
3618        // c:1428
3619        if let Some(j) = jtab.get(job) {
3620            if j.stat != 0 && !j.procs.is_empty() && (j.stat & STAT_NOPRINT) == 0 {
3621                // c:1429-1430
3622                let val = if want_val {
3623                    pmjobstate(std::ptr::null_mut(), job as i32)
3624                } else {
3625                    String::new()
3626                }; // c:1437 pmjobstate
3627                let pm = param {
3628                    node: hashnode {
3629                        next: None,
3630                        nam: format!("{}", job), // c:1434 sprintf(buf, "%d", job)
3631                        flags: (PM_SCALAR | PM_READONLY) as i32,
3632                    },
3633                    u_data: 0,
3634                    u_tied: None,
3635                    u_arr: None,
3636                    u_str: Some(val),
3637                    u_val: 0,
3638                    u_dval: 0.0,
3639                    u_hash: None,
3640                    gsu_s: None,
3641                    gsu_i: None,
3642                    gsu_f: None,
3643                    gsu_a: None,
3644                    gsu_h: None,
3645                    base: 0,
3646                    width: 0,
3647                    env: None,
3648                    ename: None,
3649                    old: None,
3650                    level: 0,
3651                };
3652                func(&pm, flags); // c:1440
3653            }
3654        }
3655    }
3656}
3657
3658/// Port of `pmjobdir()` from `Src/Modules/parameter.c:1447` — C decl `pmjobdir(Job jtab, int job)`.
3659/// C: `static char *pmjobdir(Job jtab, int job)` →
3660///   `return dupstring(jtab[job].pwd ? jtab[job].pwd : pwd);`
3661#[allow(non_snake_case)]
3662#[allow(unused_variables)]
3663pub fn pmjobdir(_jtab: *mut std::ffi::c_void, job: i32) -> String {
3664    // c:1447
3665    // c:1452 — `return dupstring(jtab[job].pwd ? jtab[job].pwd : pwd)`.
3666    let (jtab, _jmax) = selectjobtab();
3667    let job_idx = job as usize;
3668    if let Some(j) = jtab.get(job_idx) {
3669        if let Some(pwd) = j.pwd.as_ref() {
3670            return pwd.clone();
3671        } // c:1452 jtab[job].pwd
3672    }
3673    // Fallback to global pwd (c:1452's `: pwd` arm). C's `pwd` is the
3674    // shell-tracked LOGICAL cwd (Src/params.c:108, written by bin_cd
3675    // at Src/builtin.c:1239-1242) — read it through the canonical
3676    // getsparam("PWD") accessor, not the symlink-resolved
3677    // current_dir().
3678    crate::ported::params::getsparam("PWD").unwrap_or_else(|| {
3679        std::env::current_dir()
3680            .ok()
3681            .and_then(|p| p.to_str().map(String::from))
3682            .unwrap_or_default()
3683    })
3684}
3685
3686/// Port of `getpmjobdir()` from `Src/Modules/parameter.c:1457` — C decl `getpmjobdir(UNUSED(HashTable ht), const char *name)`.
3687#[allow(non_snake_case)]
3688#[allow(unused_variables)]
3689pub fn getpmjobdir(ht: *mut HashTable, name: &str) -> Option<Param> {
3690    // c:1457
3691    let (jtab, jmax) = selectjobtab(); // c:1469
3692                                       // c:1471 — `job = strtol(name, &pend, 10);`. See getpmjobtext (c:1291) for
3693                                       // why libc strtol's leading-blank / partial-digit semantics are spelled out
3694                                       // here instead of using `str::parse`.
3695    let n_bytes = name.as_bytes();
3696    let mut n_i = 0usize;
3697    while n_i < n_bytes.len() && n_bytes[n_i].is_ascii_whitespace() {
3698        n_i += 1;
3699    }
3700    let n_num_start = n_i;
3701    if n_i < n_bytes.len() && (n_bytes[n_i] == b'+' || n_bytes[n_i] == b'-') {
3702        n_i += 1;
3703    }
3704    let n_digits_start = n_i;
3705    while n_i < n_bytes.len() && n_bytes[n_i].is_ascii_digit() {
3706        n_i += 1;
3707    }
3708    let (job, pend_nonempty) = if n_i == n_digits_start {
3709        (0i32, !name.is_empty())
3710    } else {
3711        (
3712            name[n_num_start..n_i].parse::<i64>().unwrap_or(i64::MAX) as i32,
3713            n_i != n_bytes.len(),
3714        )
3715    };
3716    let job = if pend_nonempty {
3717        // c:1472-1473
3718        getjob(name, "")
3719    } else {
3720        job
3721    };
3722    // c:1474-1477 — if (job >= 1 && job <= jmax && jtab[job].stat && jtab[job].procs && !STAT_NOPRINT)
3723    if job >= 1 && (job as usize) <= jmax {
3724        if let Some(j) = jtab.get(job as usize) {
3725            if j.stat != 0 && !j.procs.is_empty() && (j.stat & STAT_NOPRINT) == 0 {
3726                let dir = pmjobdir(std::ptr::null_mut(), job);
3727                let mut pm = make_empty_special_pm(name);
3728                pm.node.flags = (PM_SCALAR | PM_READONLY) as i32;
3729                pm.u_str = Some(dir);
3730                return Some(pm);
3731            }
3732        }
3733    }
3734    // c:1478-1481 — else { u.str = ""; flags |= PM_UNSET|PM_SPECIAL; }
3735    let mut pm = make_empty_special_pm(name);
3736    pm.node.flags = (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32;
3737    Some(pm)
3738}
3739
3740/// Port of `scanpmjobdirs()` from `Src/Modules/parameter.c:1487` — C decl `scanpmjobdirs(UNUSED(HashTable ht), ScanFunc func, int flags)`.
3741///
3742/// Iteration callback that special-parameter scan walks use to
3743/// build an internal hash table from a Rust-side static. zshrs's
3744/// hashparam-node integration isn't wired up; the corresponding
3745/// `${(@k)foo}` queries read through the typed Rust accessor
3746/// directly. Structural pass-through retained for C name parity;
3747/// Rust idiom replacement covers the read side.
3748#[allow(non_snake_case)]
3749/// WARNING: param names don't match C — Rust=(_ht, _func) vs C=(ht, func, flags)
3750pub fn scanpmjobdirs(
3751    _ht: *mut HashTable,
3752    func: Option<ParamScanFunc>, // c:1487
3753    flags: i32,
3754) {
3755    let func = match func {
3756        Some(f) => f,
3757        None => return,
3758    };
3759    let (jtab, jmax) = selectjobtab(); // c:1500
3760    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
3761        || (flags as u32 & SCANPM_WANTKEYS) == 0;
3762    for job in 1..=jmax {
3763        // c:1502
3764        if let Some(j) = jtab.get(job) {
3765            if j.stat != 0 && !j.procs.is_empty() && (j.stat & STAT_NOPRINT) == 0 {
3766                // c:1503-1504
3767                let val = if want_val {
3768                    pmjobdir(std::ptr::null_mut(), job as i32)
3769                } else {
3770                    String::new()
3771                }; // c:1511 pmjobdir
3772                let pm = param {
3773                    node: hashnode {
3774                        next: None,
3775                        nam: format!("{}", job), // c:1508
3776                        flags: (PM_SCALAR | PM_READONLY) as i32,
3777                    },
3778                    u_data: 0,
3779                    u_tied: None,
3780                    u_arr: None,
3781                    u_str: Some(val),
3782                    u_val: 0,
3783                    u_dval: 0.0,
3784                    u_hash: None,
3785                    gsu_s: None,
3786                    gsu_i: None,
3787                    gsu_f: None,
3788                    gsu_a: None,
3789                    gsu_h: None,
3790                    base: 0,
3791                    width: 0,
3792                    env: None,
3793                    ename: None,
3794                    old: None,
3795                    level: 0,
3796                };
3797                func(&pm, flags); // c:1514
3798            }
3799        }
3800    }
3801}
3802
3803/// Port of `setpmnameddir()` from `Src/Modules/parameter.c:1519` — C decl `setpmnameddir(Param pm, char *value)`.
3804/// C: `static void setpmnameddir(Param pm, char *value)` — install a
3805/// `nameddirtab` entry mapping pm name → value path.
3806#[allow(non_snake_case)]
3807pub fn setpmnameddir(pm: Param, value: String) {
3808    // c:1519
3809    // c:1519-1522 — C `if (!value) zwarn("invalid value: ''");` — Rust
3810    // signature takes owned String so NULL is unreachable; we keep the
3811    // else branch only. Empty string still creates an entry per C
3812    // semantics (`!value` is only true for the NULL pointer).
3813    let nd = nameddir {
3814        // c:1524 zshcalloc
3815        node: hashnode {
3816            // c:1526 flags = 0
3817            next: None,
3818            nam: pm.node.nam.clone(),
3819            flags: 0,
3820        },
3821        dir: value, // c:1544
3822        diff: 0,
3823    };
3824    // c:1544 — nameddirtab->addnode(nameddirtab, ztrdup(pm->node.nam), nd);
3825    addnameddirnode(&pm.node.nam, nd);
3826}
3827
3828/// Port of `unsetpmnameddir()` from `Src/Modules/parameter.c:1534` — C decl `unsetpmnameddir(Param pm, UNUSED(int exp))`.
3829/// C: `static void unsetpmnameddir(Param pm, UNUSED(int exp))` — remove the
3830/// named directory from `nameddirtab`.
3831#[allow(non_snake_case)]
3832#[allow(unused_variables)]
3833pub fn unsetpmnameddir(pm: Param, exp: i32) {
3834    // c:1534
3835    if let Ok(mut tab) = nameddirtab().lock() {
3836        // c:1536 — HashNode hd = nameddirtab->removenode(nameddirtab, pm->node.nam);
3837        let _hd = tab.remove(&pm.node.nam);
3838        // c:1538-1539 — if (hd) nameddirtab->freenode(hd); — Rust Drop on scope exit.
3839    }
3840}
3841
3842/// Port of `setpmnameddirs()` from `Src/Modules/parameter.c:1544` — C decl `setpmnameddirs(Param pm, HashTable ht)`.
3843/// C: `static void setpmnameddirs(Param pm, HashTable ht)` — replace
3844/// `nameddirtab` (preserving ND_USERNAME entries) with `ht`'s contents.
3845#[allow(non_snake_case)]
3846#[allow(unused_variables)]
3847/// WARNING: param shape doesn't match C — C passes the temporary
3848/// HashTable of value-carrying child Params (see setpmcommands);
3849/// zshrs passes the (key, value) pairs directly. Replace semantics:
3850/// every non-ND_USERNAME named dir is flushed first (c:1552-1558).
3851pub fn setpmnameddirs(pm: Param, ht: &[(String, String)]) {
3852    // c:1544
3853    // c:1549-1550 — if (!ht) return; (empty pair list still flushes
3854    // in C? No — !ht returns BEFORE the flush; an empty-but-present
3855    // table flushes and installs nothing. The pairs slice is always
3856    // "present" here, so flush unconditionally.)
3857
3858    // c:1552-1558 — for (i = 0; i < nameddirtab->hsize; i++) flush non-ND_USERNAME.
3859    // The Rust `HashMap<String, nameddir>` doesn't expose buckets by `i`;
3860    // we walk all entries collecting keys to remove (C's combined
3861    // removenode+freenode = HashMap::remove).
3862    if let Ok(mut tab) = nameddirtab().lock() {
3863        let to_remove: Vec<String> = tab
3864            .iter()
3865            .filter(|(_, nd)| (nd.node.flags & ND_USERNAME) == 0) // c:1555 !ND_USERNAME
3866            .map(|(k, _)| k.clone())
3867            .collect();
3868        for k in to_remove {
3869            // c:1556 hd = ... removenode
3870            tab.remove(&k); // c:1557 freenode
3871        }
3872    }
3873
3874    // c:1560-1579 — second loop: install entries from `ht`.
3875    for (nam, val) in ht {
3876        if val.is_empty() {
3877            // c:1570 !val
3878            zwarn("invalid value: ''"); // c:1571
3879        } else {
3880            // c:1573 — Nameddir nd = zshcalloc(sizeof(*nd));
3881            let nd = nameddir {
3882                node: hashnode {
3883                    next: None,
3884                    nam: nam.clone(),
3885                    flags: 0, // c:1575 nd->node.flags = 0
3886                },
3887                dir: val.clone(), // c:1576 nd->dir = ztrdup(val)
3888                diff: 0,
3889            };
3890            addnameddirnode(nam, nd); // c:1577
3891        }
3892    }
3893    // c:1581-1589 — opts[INTERACTIVE] guard around deleteparamtable
3894    // (avoid removing sub-pms eagerly when an interactive shell is
3895    // watching). The temp table is the borrowed slice here; nothing
3896    // to delete, so the guard collapses.
3897    let _ = pm;
3898}
3899
3900/// Port of `getpmnameddir()` from `Src/Modules/parameter.c:1597` — C decl `getpmnameddir(UNUSED(HashTable ht), const char *name)`.
3901/// C body (c:1600-1615):
3902/// ```c
3903/// pm = (Param) hcalloc(sizeof(struct param));
3904/// pm->node.nam = dupstring(name);
3905/// pm->node.flags = PM_SCALAR;
3906/// pm->gsu.s = &pmnamedir_gsu;
3907/// if ((nd = (Nameddir) nameddirtab->getnode(nameddirtab, name)) &&
3908///     !(nd->node.flags & ND_USERNAME))
3909///     pm->u.str = dupstring(nd->dir);
3910/// else {
3911///     pm->u.str = dupstring("");
3912///     pm->node.flags |= (PM_UNSET|PM_SPECIAL);
3913/// }
3914/// ```
3915/// `nameddirs` enumerates `hash -d NAME=path` entries — entries
3916/// added at runtime via the hash builtin (Src/hashnameddir.c:104,
3917/// `addnameddirnode`). The username branch is a separate path
3918/// (`userdirs`, getpmuserdir at c:1646).
3919#[allow(non_snake_case)]
3920#[allow(unused_variables)]
3921pub fn getpmnameddir(ht: *mut HashTable, name: &str) -> Option<Param> {
3922    // c:1597
3923    let (value, found) = match crate::ported::hashnameddir::nameddirtab()
3924        .lock()
3925        .ok()
3926        .and_then(|t| t.get(name).cloned())
3927    {
3928        // c:1607 — `nd = nameddirtab->getnode(nameddirtab, name)`
3929        Some(nd) if (nd.node.flags & crate::ported::zsh_h::ND_USERNAME) == 0 => {
3930            // c:1608 — `!(nd->node.flags & ND_USERNAME)`
3931            (nd.dir.clone(), true) // c:1609 nd->dir
3932        }
3933        _ => (String::new(), false), // c:1611 ""
3934    };
3935    let pm = Box::new(param {
3936        // c:1601 hcalloc
3937        node: hashnode {
3938            next: None,
3939            nam: name.to_string(), // c:1602
3940            flags: if found {
3941                PM_SCALAR as i32 // c:1603
3942            } else {
3943                (PM_SCALAR | PM_UNSET | PM_SPECIAL) as i32 // c:1612
3944            },
3945        },
3946        u_data: 0,
3947        u_tied: None,
3948        u_arr: None,
3949        u_str: Some(value), // c:1609 / c:1611
3950        u_val: 0,
3951        u_dval: 0.0,
3952        u_hash: None,
3953        gsu_s: None, // c:1604 pmnamedir_gsu
3954        gsu_i: None,
3955        gsu_f: None,
3956        gsu_a: None,
3957        gsu_h: None,
3958        base: 0,
3959        width: 0,
3960        env: None,
3961        ename: None,
3962        old: None,
3963        level: 0,
3964    });
3965    Some(pm) // c:1614
3966}
3967
3968/// Port of `scanpmnameddirs()` from `Src/Modules/parameter.c:1618` — C decl `scanpmnameddirs(UNUSED(HashTable ht), ScanFunc func, int flags)`.
3969/// C body (c:1621-1641):
3970/// ```c
3971/// memset(&pm, 0, sizeof(struct param));
3972/// pm.node.flags = PM_SCALAR;
3973/// pm.gsu.s = &pmnamedir_gsu;
3974/// for (i = 0; i < nameddirtab->hsize; i++)
3975///     for (hn = nameddirtab->nodes[i]; hn; hn = hn->next)
3976///         if (!((nd = (Nameddir) hn)->node.flags & ND_USERNAME)) {
3977///             pm.node.nam = hn->nam;
3978///             if (func != scancountparams &&
3979///                 ((flags & (SCANPM_WANTVALS|SCANPM_MATCHVAL)) ||
3980///                  !(flags & SCANPM_WANTKEYS)))
3981///                 pm.u.str = dupstring(nd->dir);
3982///             func(&pm.node, flags);
3983///         }
3984/// ```
3985/// Walks the `hash -d` named directories table, NOT /etc/passwd.
3986/// The passwd enumeration belongs to `scanpmuserdirs` (c:1669).
3987#[allow(non_snake_case)]
3988/// WARNING: param names don't match C — Rust=(_ht, func) vs C=(ht, func, flags)
3989pub fn scanpmnameddirs(
3990    _ht: *mut HashTable,
3991    func: Option<ParamScanFunc>, // c:1618
3992    flags: i32,
3993) {
3994    // c:1648-1650 — value side only when values were asked for.
3995    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
3996        || (flags as u32 & SCANPM_WANTKEYS) == 0;
3997    if let Some(f) = func {
3998        // c:1640-1642 — one `struct param pm` for the whole walk.
3999        let mut pm = param {
4000            node: hashnode {
4001                next: None,
4002                nam: String::new(),
4003                flags: PM_SCALAR as i32, // c:1641
4004            },
4005            ..Default::default()
4006        };
4007        if let Ok(tab) = crate::ported::hashnameddir::nameddirtab().lock() {
4008            for (nam, nd) in tab.iter() {
4009                // c:1644-1645
4010                // c:1646 — `!(nd->node.flags & ND_USERNAME)`
4011                if (nd.node.flags & crate::ported::zsh_h::ND_USERNAME) != 0 {
4012                    continue;
4013                }
4014                pm.node.nam = nam.clone(); // c:1647
4015                pm.u_str = if want_val {
4016                    Some(nd.dir.clone()) // c:1651 pm.u.str = dupstring(nd->dir)
4017                } else {
4018                    None
4019                };
4020                f(&pm, flags); // c:1652
4021            }
4022        }
4023    }
4024}
4025
4026/// Port of `getpmuserdir()` from `Src/Modules/parameter.c:1646` — C decl `getpmuserdir(UNUSED(HashTable ht), const char *name)`.
4027/// C: `static HashNode getpmuserdir(UNUSED(HashTable ht), const char *name)`
4028/// — emit the home directory for `~user`.
4029#[allow(non_snake_case)]
4030#[allow(unused_variables)]
4031pub fn getpmuserdir(ht: *mut HashTable, name: &str) -> Option<Param> {
4032    // c:1646
4033    // c:1651 — `nameddirtab->filltable(nameddirtab);`. Route through the
4034    // real port of that callback (Src/hashnameddir.c:96 fillnameddirtable),
4035    // which is guarded by `allusersadded` (c:98/111) so the passwd database
4036    // is enumerated exactly ONCE per shell and every later read is a plain
4037    // hash lookup. Calling `getpwnam(3)` here instead re-entered the
4038    // platform directory service on EVERY key: on macOS that is an
4039    // opendirectoryd XPC round trip per key, so one whole-map read of
4040    // `$userdirs` (137 accounts on this host) cost 137 round trips —
4041    // 19 ms per read, and 15.7% of `subst::assoc_get` during a single TAB
4042    // completion.
4043    //
4044    // The interactive gate is preserved because it lives where C puts it:
4045    // `fillnameddirtable` feeds every entry through `adduserdir`, whose
4046    // first statement is Src/utils.c:1193-1194 `if (!interact) return;`.
4047    // So in a non-interactive shell the table stays empty and the lookup
4048    // below takes C's c:1660-1663 else-arm (empty value, PM_UNSET), which
4049    // is what `zsh -f -c 'print ${userdirs[root]}'` prints.
4050    crate::ported::hashnameddir::fillnameddirtable(); // c:1651
4051                                                      // c:1657-1658 — `nameddirtab->getnode(nameddirtab, name)` AND the
4052                                                      // node must carry ND_USERNAME; `hash -d` / PWD entries live in the
4053                                                      // same table but are NOT userdirs.
4054    let found_dir = crate::ported::hashnameddir::nameddirtab()
4055        .lock()
4056        .ok()
4057        .and_then(|t| {
4058            t.get(name)
4059                .filter(|nd| (nd.node.flags & crate::ported::zsh_h::ND_USERNAME) != 0)
4060                .map(|nd| nd.dir.clone()) // c:1659 nd->dir
4061        });
4062    let (value, found) = match found_dir {
4063        Some(dir) => (dir, true),       // c:1659
4064        None => (String::new(), false), // c:1662
4065    };
4066    let pm = Box::new(param {
4067        // c:1653 hcalloc
4068        node: hashnode {
4069            next: None,
4070            nam: name.to_string(), // c:1654
4071            flags: if found {
4072                (PM_SCALAR | PM_READONLY) as i32
4073            }
4074            // c:1655
4075            else {
4076                (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32
4077            }, // c:1663
4078        },
4079        u_data: 0,
4080        u_tied: None,
4081        u_arr: None,
4082        u_str: Some(value), // c:1659 / c:1662
4083        u_val: 0,
4084        u_dval: 0.0,
4085        u_hash: None,
4086        gsu_s: None, // c:1656 nullsetscalar_gsu
4087        gsu_i: None,
4088        gsu_f: None,
4089        gsu_a: None,
4090        gsu_h: None,
4091        base: 0,
4092        width: 0,
4093        env: None,
4094        ename: None,
4095        old: None,
4096        level: 0,
4097    });
4098    Some(pm) // c:1664
4099}
4100
4101/// Port of `scanpmuserdirs()` from `Src/Modules/parameter.c:1669` — C decl `scanpmuserdirs(UNUSED(HashTable ht), ScanFunc func, int flags)`.
4102/// C body (c:1672-1696): same nameddirtab walk filtered to entries
4103/// with ND_USERNAME set. Static-link path enumerates getpwent(3) —
4104/// every passwd entry is a "user dir" by definition.
4105#[allow(non_snake_case)]
4106/// WARNING: param names don't match C — Rust=(_ht, func) vs C=(ht, func, flags)
4107pub fn scanpmuserdirs(
4108    _ht: *mut HashTable,
4109    func: Option<ParamScanFunc>, // c:1669
4110    flags: i32,
4111) {
4112    // c:1676 `nameddirtab->filltable(nameddirtab)` →
4113    // Src/hashnameddir.c:96 fillnameddirtable, whose getpwent loop feeds
4114    // every entry through `adduserdir(pw->pw_name, pw->pw_dir,
4115    // ND_USERNAME, 1)` — and adduserdir's FIRST statement is
4116    // Src/utils.c:1193-1194 `if (!interact) return;` ("We don't maintain
4117    // a hash table in non-interactive shells"). So in a NON-interactive
4118    // shell nameddirtab never receives the passwd entries and the walk at
4119    // c:1682-1692 finds nothing: `zsh -f -c 'print ${#userdirs}'` is 0
4120    // even though `~root` still expands (that goes through getpwnam in
4121    // filesubstr, not through this table). zshrs inlines the passwd
4122    // enumeration here instead of routing it through adduserdir, so it
4123    // skipped that gate and reported every account in the directory
4124    // service — 136 entries against zsh's 0.
4125    crate::ported::hashnameddir::fillnameddirtable(); // c:1676
4126    let Some(f) = func else { return };
4127    // c:1682-1692 — walk `nameddirtab` and emit only the ND_USERNAME
4128    // entries. Enumerating `getpwent(3)` here instead re-read the whole
4129    // passwd database on every scan (and skipped the ND_USERNAME filter,
4130    // so `hash -d` names would have leaked into `$userdirs` once the walk
4131    // was over the real table). `fillnameddirtable`'s `allusersadded`
4132    // guard (c:Src/hashnameddir.c:98/111) makes the database read happen
4133    // once per shell; the interactive gate stays where C has it, inside
4134    // `adduserdir` (c:Src/utils.c:1193-1194).
4135    let Ok(tab) = crate::ported::hashnameddir::nameddirtab().lock() else {
4136        return;
4137    };
4138    // c:1701-1703 — value side only when values were asked for.
4139    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
4140        || (flags as u32 & SCANPM_WANTKEYS) == 0;
4141    // c:1693-1694 — one `struct param pm` for the whole walk.
4142    let mut pm = param {
4143        node: hashnode {
4144            next: None,
4145            nam: String::new(),
4146            flags: (PM_SCALAR | PM_READONLY) as i32, // c:1694
4147        },
4148        ..Default::default()
4149    };
4150    for (nam, nd) in tab.iter() {
4151        if (nd.node.flags & crate::ported::zsh_h::ND_USERNAME) == 0 {
4152            continue; // c:1699
4153        }
4154        pm.node.nam = nam.clone(); // c:1700
4155        pm.u_str = if want_val {
4156            Some(nd.dir.clone()) // c:1704 pm.u.str = dupstring(nd->dir)
4157        } else {
4158            None
4159        };
4160        f(&pm, flags); // c:1705
4161    }
4162}
4163
4164/// Port of `setalias()` from `Src/Modules/parameter.c:1699` — C decl `setalias(HashTable ht, Param pm, char *value, int flags)`.
4165/// C: `static void setalias(HashTable ht, Param pm, char *value, int flags)`
4166///   → `ht->addnode(ht, ztrdup(pm->node.nam), createaliasnode(value, flags));`
4167#[allow(non_snake_case)]
4168/// WARNING: param names don't match C — Rust=(_ht, _pm, _value) vs C=(ht, pm, value, flags)
4169pub fn setalias(
4170    _ht: *mut HashTable,
4171    pm: Param,
4172    value: String, // c:1699
4173    flags: i32,
4174) {
4175    // c:1701-1702 — `ht->addnode(ht, ztrdup(pm->node.nam),
4176    //                            createaliasnode(value, flags));`
4177    //
4178    // C callers:
4179    //   setpmralias    → ht=aliastab,    flags=0
4180    //   setpmdisralias → ht=aliastab,    flags=DISABLED
4181    //   setpmgalias    → ht=aliastab,    flags=ALIAS_GLOBAL
4182    //   setpmdisgalias → ht=aliastab,    flags=ALIAS_GLOBAL|DISABLED
4183    //   setpmsalias    → ht=sufaliastab, flags=ALIAS_SUFFIX
4184    //   setpmdissalias → ht=sufaliastab, flags=ALIAS_SUFFIX|DISABLED
4185    //
4186    // Rust callers pass a null ht so the dispatch reads the
4187    // ALIAS_SUFFIX bit out of flags — same shape as getalias,
4188    // scanaliases (2d2cdbaa5a), setaliases (e01ed226f0).
4189    //
4190    // Prior port always wrote to aliastab_lock(). That meant
4191    // \$saliases[X]=Y / \$dis_saliases[X]=Y silently landed in
4192    // aliastab with an ALIAS_SUFFIX flag (impossible combo for
4193    // that table) and sufaliastab stayed untouched.
4194    let name = (*pm).node.nam.clone();
4195    let node = crate::ported::hashtable::createaliasnode(&name, &value, flags as u32);
4196    if (flags & ALIAS_SUFFIX) != 0 {
4197        let mut tab = sufaliastab_lock().write().expect("sufaliastab poisoned");
4198        tab.add(node);
4199    } else {
4200        let mut tab = aliastab_lock().write().expect("aliastab poisoned");
4201        tab.add(node);
4202    }
4203}
4204
4205/// Port of `setpmralias()` from `Src/Modules/parameter.c:1707` — C decl `setpmralias(Param pm, char *value)`.
4206#[allow(non_snake_case)]
4207pub fn setpmralias(pm: Param, value: String) {
4208    // c:1707
4209    setalias(std::ptr::null_mut(), pm, value, 0) // c:1707
4210}
4211
4212/// Port of `setpmdisralias()` from `Src/Modules/parameter.c:1714` — C decl `setpmdisralias(Param pm, char *value)`.
4213/// C: `setalias(aliastab, pm, value, DISABLED);`
4214#[allow(non_snake_case)]
4215pub fn setpmdisralias(pm: Param, value: String) {
4216    // c:1714
4217    setalias(std::ptr::null_mut(), pm, value, DISABLED) // c:1714
4218}
4219
4220/// Port of `setpmgalias()` from `Src/Modules/parameter.c:1721` — C decl `setpmgalias(Param pm, char *value)`.
4221#[allow(non_snake_case)]
4222pub fn setpmgalias(pm: Param, value: String) {
4223    // c:1721
4224    setalias(std::ptr::null_mut(), pm, value, ALIAS_GLOBAL) // c:1721
4225}
4226
4227/// Port of `setpmdisgalias()` from `Src/Modules/parameter.c:1728` — C decl `setpmdisgalias(Param pm, char *value)`.
4228/// C: `setalias(aliastab, pm, value, ALIAS_GLOBAL|DISABLED);`
4229#[allow(non_snake_case)]
4230pub fn setpmdisgalias(pm: Param, value: String) {
4231    // c:1728
4232    setalias(std::ptr::null_mut(), pm, value, ALIAS_GLOBAL | DISABLED) // c:1728
4233}
4234
4235/// Port of `setpmsalias()` from `Src/Modules/parameter.c:1735` — C decl `setpmsalias(Param pm, char *value)`.
4236#[allow(non_snake_case)]
4237pub fn setpmsalias(pm: Param, value: String) {
4238    // c:1735
4239    setalias(std::ptr::null_mut(), pm, value, ALIAS_SUFFIX) // c:1735
4240}
4241
4242/// Port of `setpmdissalias()` from `Src/Modules/parameter.c:1742` — C decl `setpmdissalias(Param pm, char *value)`.
4243#[allow(non_snake_case)]
4244pub fn setpmdissalias(pm: Param, value: String) {
4245    // c:1742
4246    setalias(std::ptr::null_mut(), pm, value, ALIAS_SUFFIX | DISABLED) // c:1742
4247}
4248
4249/// Port of `unsetpmalias()` from `Src/Modules/parameter.c:1749` — C decl `unsetpmalias(Param pm, UNUSED(int exp))`.
4250/// C: `static void unsetpmalias(Param pm, UNUSED(int exp))` — remove the
4251/// named alias from `aliastab`.
4252#[allow(non_snake_case)]
4253#[allow(unused_variables)]
4254pub fn unsetpmalias(pm: Param, exp: i32) {
4255    // c:1749
4256    if let Ok(mut tab) = aliastab_lock().write() {
4257        // c:1751 — HashNode hd = aliastab->removenode(aliastab, pm->node.nam);
4258        let _hd = tab.remove(&pm.node.nam);
4259        // c:1753-1754 — if (hd) aliastab->freenode(hd); — Rust Drop on scope exit.
4260    }
4261}
4262
4263/// Port of `unsetpmsalias()` from `Src/Modules/parameter.c:1759` — C decl `unsetpmsalias(Param pm, UNUSED(int exp))`.
4264/// C: `static void unsetpmsalias(Param pm, UNUSED(int exp))` — remove the
4265/// named suffix alias from `sufaliastab`.
4266#[allow(non_snake_case)]
4267#[allow(unused_variables)]
4268pub fn unsetpmsalias(pm: Param, exp: i32) {
4269    // c:1759
4270    if let Ok(mut tab) = sufaliastab_lock().write() {
4271        // c:1761 — HashNode hd = sufaliastab->removenode(sufaliastab, pm->node.nam);
4272        let _hd = tab.remove(&pm.node.nam);
4273        // c:1763-1764 — if (hd) sufaliastab->freenode(hd); — Rust Drop on scope exit.
4274    }
4275}
4276
4277/// Port of `setaliases()` from `Src/Modules/parameter.c:1769` — C decl
4278/// `static void setaliases(HashTable alht, Param pm, HashTable ht, int flags)`. Implements the
4279/// "replace every alias of the given flag-class with the entries of
4280/// `ht`" semantics that drive `$raliases=(...)`, `$dis_raliases=(...)`,
4281/// `$galiases=(...)`, `$dis_galiases=(...)` assignment.
4282///
4283/// ```c
4284/// static void
4285/// setaliases(HashTable alht, Param pm, HashTable ht, int flags)
4286/// {
4287///     int i;
4288///     HashNode hn, next, hd;
4289///     if (!ht) return;
4290///     for (i = 0; i < alht->hsize; i++)
4291///         for (hn = alht->nodes[i]; hn; hn = next) {
4292///             next = hn->next;
4293///             if (flags == ((Alias)hn)->node.flags &&
4294///                 (hd = alht->removenode(alht, hn->nam)))
4295///                 alht->freenode(hd);
4296///         }
4297///     for (i = 0; i < ht->hsize; i++)
4298///         for (hn = ht->nodes[i]; hn; hn = hn->next) {
4299///             struct value v;
4300///             char *val;
4301///             v.scanflags = v.valflags = v.start = 0;
4302///             v.end = -1;
4303///             v.arr = NULL;
4304///             v.pm = (Param) hn;
4305///             if ((val = getstrvalue(&v)))
4306///                 alht->addnode(alht, ztrdup(hn->nam),
4307///                               createaliasnode(ztrdup(val), flags));
4308///         }
4309///     if (ht != pm->u.hash)
4310///         deleteparamtable(ht);
4311/// }
4312/// ```
4313#[allow(non_snake_case)]
4314#[allow(unused_variables)]
4315/// WARNING: param shape doesn't match C — C passes `alht` (the live
4316/// alias table) plus the temporary HashTable of value-carrying child
4317/// Params; zshrs selects the live table from the ALIAS_SUFFIX bit in
4318/// `flags` and takes the (key, value) pairs directly (see
4319/// setpmcommands for why: zshrs's `hashnode` carries no value slot).
4320pub fn setaliases(
4321    alht: *mut HashTable,
4322    pm: Param, // c:1769
4323    ht: &[(String, String)],
4324    flags: i32,
4325) {
4326    // c:1774-1775 — `if (!ht) return;` — an empty pair list still
4327    // performs the flag-class flush below, same as C's empty-but-
4328    // present temp table.
4329
4330    // c:1777-1789 — drop every alias currently in `alht` whose flags
4331    // exactly match the target flag-class.
4332    //
4333    // C callers select aliastab/sufaliastab via the alht arg:
4334    //   setpmraliases    → alht=aliastab,    flags=0
4335    //   setpmdisraliases → alht=aliastab,    flags=DISABLED
4336    //   setpmgaliases    → alht=aliastab,    flags=ALIAS_GLOBAL
4337    //   setpmdisgaliases → alht=aliastab,    flags=ALIAS_GLOBAL|DISABLED
4338    //   setpmsaliases    → alht=sufaliastab, flags=ALIAS_SUFFIX
4339    //   setpmdissaliases → alht=sufaliastab, flags=ALIAS_SUFFIX|DISABLED
4340    //
4341    // Rust callers pass null `alht`; dispatch on the ALIAS_SUFFIX
4342    // bit (mirrors getalias c:1901 + the scanaliases fix at
4343    // 2d2cdbaa5a). Prior port always operated on aliastab — every
4344    // suffix-alias bulk assignment (\$saliases=(...) etc.) silently
4345    // wrote to the wrong table.
4346    let suffix_table = (flags & ALIAS_SUFFIX) != 0;
4347    let mut keys_to_drop: Vec<String> = Vec::new(); // c:1772 hn iteration
4348    {
4349        let tab = if suffix_table {
4350            sufaliastab_lock()
4351        } else {
4352            aliastab_lock()
4353        }
4354        .read()
4355        .expect("aliastab poisoned");
4356        for (name, alias) in tab.iter() {
4357            // c:1777-1778
4358            if (alias.node.flags as i32) == flags {
4359                // c:1786 flags == hn->node.flags
4360                keys_to_drop.push(name.clone()); // c:1787 removenode(alht, hn->nam)
4361            }
4362        }
4363    }
4364    if !keys_to_drop.is_empty() {
4365        let mut tab = if suffix_table {
4366            sufaliastab_lock()
4367        } else {
4368            aliastab_lock()
4369        }
4370        .write()
4371        .expect("aliastab poisoned");
4372        for name in keys_to_drop {
4373            let _ = tab.remove(&name); // c:1788 freenode(hd)
4374        }
4375    }
4376
4377    // c:1791-1804 — walk every entry in the user-supplied `ht`,
4378    // call createaliasnode(val, flags) and add it to alht:
4379    //   v.pm = (Param)hn; val = getstrvalue(&v);
4380    //   alht->addnode(alht, ztrdup(hn->nam),
4381    //                 createaliasnode(ztrdup(val), flags));
4382    for (nam, val) in ht {
4383        let node = createaliasnode(nam, val, flags as u32); // c:1803
4384        let mut tab = if suffix_table {
4385            sufaliastab_lock()
4386        } else {
4387            aliastab_lock()
4388        }
4389        .write()
4390        .expect("aliastab poisoned");
4391        tab.add(node); // c:1802 addnode
4392    }
4393
4394    // c:1806-1807 — `if (ht != pm->u.hash) deleteparamtable(ht);`
4395    // pm->u.hash discriminator: in C the user-side param node owns the
4396    // table; the post-assignment frees the temporary `ht` when it's
4397    // not the same allocation. Rust port: no separate allocation
4398    // because the typed alias_table is a global singleton; the
4399    // temporary mirror struct gets dropped at end of scope.
4400    let _ = pm; // c:1806
4401    let _ = alht; // c:1769 alht binding (Rust uses aliastab_lock directly)
4402}
4403
4404/// Port of `setpmraliases()` from `Src/Modules/parameter.c:1812` — C decl `setpmraliases(Param pm, HashTable ht)`.
4405#[allow(non_snake_case)]
4406pub fn setpmraliases(pm: Param, ht: &[(String, String)]) {
4407    // c:1812
4408    setaliases(std::ptr::null_mut(), pm, ht, 0) // c:1812
4409}
4410
4411/// Port of `setpmdisraliases()` from `Src/Modules/parameter.c:1819` — C decl `setpmdisraliases(Param pm, HashTable ht)`.
4412#[allow(non_snake_case)]
4413pub fn setpmdisraliases(pm: Param, ht: &[(String, String)]) {
4414    // c:1819
4415    setaliases(std::ptr::null_mut(), pm, ht, DISABLED) // c:1819
4416}
4417
4418/// Port of `setpmgaliases()` from `Src/Modules/parameter.c:1826` — C decl `setpmgaliases(Param pm, HashTable ht)`.
4419#[allow(non_snake_case)]
4420pub fn setpmgaliases(pm: Param, ht: &[(String, String)]) {
4421    // c:1826
4422    setaliases(std::ptr::null_mut(), pm, ht, ALIAS_GLOBAL) // c:1826
4423}
4424
4425/// Port of `setpmdisgaliases()` from `Src/Modules/parameter.c:1833` — C decl `setpmdisgaliases(Param pm, HashTable ht)`.
4426/// C: `setaliases(aliastab, pm, ht, ALIAS_GLOBAL|DISABLED);`
4427#[allow(non_snake_case)]
4428pub fn setpmdisgaliases(pm: Param, ht: &[(String, String)]) {
4429    // c:1833
4430    setaliases(std::ptr::null_mut(), pm, ht, ALIAS_GLOBAL | DISABLED) // c:1819
4431}
4432
4433/// Port of `setpmsaliases()` from `Src/Modules/parameter.c:1840` — C decl `setpmsaliases(Param pm, HashTable ht)`.
4434#[allow(non_snake_case)]
4435pub fn setpmsaliases(pm: Param, ht: &[(String, String)]) {
4436    // c:1840
4437    setaliases(std::ptr::null_mut(), pm, ht, ALIAS_SUFFIX) // c:1840
4438}
4439
4440/// Port of `setpmdissaliases()` from `Src/Modules/parameter.c:1847` — C decl `setpmdissaliases(Param pm, HashTable ht)`.
4441#[allow(non_snake_case)]
4442pub fn setpmdissaliases(pm: Param, ht: &[(String, String)]) {
4443    // c:1847
4444    setaliases(std::ptr::null_mut(), pm, ht, ALIAS_SUFFIX | DISABLED) // c:1847
4445}
4446
4447// (`scan_magic_assoc_keys` moved out of src/ported/ to
4448// src/exec_shims.rs — it has no C counterpart and the
4449// no-non-C-ported-in-src/ported rule applies. The canonical scanpm*
4450// ports below ARE the C dispatch; the aggregator is a
4451// fusevm-bridge convenience that fans the magic-assoc table NAME
4452// out into the right scanpm* call. See exec_shims.rs.)
4453
4454// === auto-generated stubs ===
4455// Direct ports of static helpers from Src/Modules/parameter.c not
4456// yet covered above. zshrs links modules statically; live
4457// state owned by the module's typed struct. Name-parity shims.
4458
4459/// Port of `assignaliasdefs()` from `Src/Modules/parameter.c:1867` — C decl `assignaliasdefs(Param pm, int flags)`.
4460/// C signature: `static void assignaliasdefs(Param pm, int flags)`.
4461/// C body sets `pm->node.flags = PM_SCALAR` (c:1869) then dispatches
4462/// `pm->gsu.s` to one of six static gsu_scalar handler tables based
4463/// on the alias-flavour bits (raw/global/suffix × normal/disabled).
4464/// The `gsu_scalar` struct IS ported at zsh_h.rs:802 (with `GsuScalar`
4465/// = Box<gsu_scalar> alias at c:794), but C uses six C-level statics
4466/// for the per-flavour dispatch tables — `pmralias_gsu`, `pmgalias_gsu`,
4467/// `pmsalias_gsu`, plus the three `pmdis*alias_gsu` variants — that
4468/// can't be const-initialised in Rust because gsu_scalar holds
4469/// `Option<GsuFn>` function pointers. Until the six per-flavour
4470/// statics land as `LazyLock<gsu_scalar>` entries, the flag-to-handler
4471/// mapping is recorded in a name-keyed side-map so future gsu lookups
4472/// resolve the right handler.
4473#[allow(non_snake_case)]
4474pub fn assignaliasdefs(
4475    pm: *mut param, // c:1867
4476    flags: i32,
4477) {
4478    if !pm.is_null() {
4479        unsafe {
4480            (*pm).node.flags = PM_SCALAR as i32;
4481        } // c:1869
4482    }
4483    // c:1871-1893 — switch on flag combination to pick the gsu table.
4484    let handler = match flags {
4485        // c:1873
4486        0 => "pmralias_gsu",                                    // c:1874
4487        f if f == ALIAS_GLOBAL => "pmgalias_gsu",               // c:1877
4488        f if f == ALIAS_SUFFIX => "pmsalias_gsu",               // c:1880
4489        f if f == DISABLED => "pmdisralias_gsu",                // c:1883
4490        f if f == ALIAS_GLOBAL | DISABLED => "pmdisgalias_gsu", // c:1886
4491        f if f == ALIAS_SUFFIX | DISABLED => "pmdissalias_gsu", // c:1889
4492        _ => return,
4493    };
4494    if !pm.is_null() {
4495        let name = unsafe { (*pm).node.nam.clone() };
4496        let m = ALIAS_GSU_HANDLER.get_or_init(|| Mutex::new(HashMap::new()));
4497        if let Ok(mut g) = m.lock() {
4498            g.insert(name, handler.to_string());
4499        }
4500    }
4501}
4502
4503/// Port of `getalias()` from `Src/Modules/parameter.c:1901` — C decl `getalias(HashTable alht, UNUSED(HashTable ht), const char *name, int flags)`.
4504/// C body (c:1906-1919):
4505/// ```c
4506/// pm.node.nam = name;
4507/// assignaliasdefs(pm, flags);
4508/// if (al = alht[name]; flags == al->node.flags)
4509///     pm->u.str = al->text;
4510/// else { pm->u.str = ""; flags |= PM_UNSET|PM_SPECIAL; }
4511/// ```
4512///
4513/// `alht` selects which alias table to query: `aliastab` for
4514/// raw / global aliases, `sufaliastab` for suffix aliases. Static-
4515/// link path: dispatch on the ALIAS_SUFFIX bit in `flags` since the
4516/// ht pointer isn't passed through.
4517#[allow(non_snake_case)]
4518/// WARNING: param names don't match C — Rust=(_alht, _ht, flags) vs C=(alht, ht, name, flags)
4519pub fn getalias(
4520    _alht: *mut HashTable,
4521    _ht: *mut HashTable, // c:1901
4522    name: &str,
4523    flags: i32,
4524) -> Option<Param> {
4525    let table = if (flags & ALIAS_SUFFIX) != 0 {
4526        sufaliastab_lock()
4527    } else {
4528        aliastab_lock()
4529    };
4530    let g = table.read().ok()?;
4531    // c:1911 — `alht->getnode2(alht, name)`. C `getnode2` is
4532    // `gethashnode2` (Src/hashtable.c:255) which returns the entry
4533    // REGARDLESS of the DISABLED flag (unlike `getnode` which masks
4534    // disabled ones). Without `_including_disabled`, `${dis_aliases[k]}`
4535    // would never find a disabled entry because Rust `.get()` filters
4536    // them out.
4537    let entry = g.get_including_disabled(name); // c:1911 alht->getnode2
4538    let (value, found) = if let Some(al) = entry {
4539        // c:1912
4540        // c:1912 — `flags == al->node.flags` strict equality match.
4541        if al.node.flags == flags {
4542            // c:1912 al->node.flags
4543            (al.text.clone(), true) // c:1913 al->text
4544        } else {
4545            (String::new(), false) // c:1916
4546        }
4547    } else {
4548        (String::new(), false) // c:1916
4549    };
4550    let mut pm = Box::new(param {
4551        // c:1906 hcalloc
4552        node: hashnode {
4553            next: None,
4554            nam: name.to_string(), // c:1907
4555            flags: 0,
4556        },
4557        u_data: 0,
4558        u_tied: None,
4559        u_arr: None,
4560        u_str: Some(value), // c:1913 / c:1916
4561        u_val: 0,
4562        u_dval: 0.0,
4563        u_hash: None,
4564        gsu_s: None,
4565        gsu_i: None,
4566        gsu_f: None,
4567        gsu_a: None,
4568        gsu_h: None,
4569        base: 0,
4570        width: 0,
4571        env: None,
4572        ename: None,
4573        old: None,
4574        level: 0,
4575    });
4576    // c:1909 — `assignaliasdefs(pm, flags);` sets PM_SCALAR + selects
4577    // gsu_scalar handler based on alias flavour.
4578    assignaliasdefs(&mut *pm as *mut _, flags); // c:1909
4579    if !found {
4580        pm.node.flags |= (PM_UNSET | PM_SPECIAL) as i32; // c:1917
4581    }
4582    Some(pm) // c:1919
4583}
4584
4585/// Port of `getpmralias()` from `Src/Modules/parameter.c:1923` — C decl `getpmralias(HashTable ht, const char *name)`.
4586/// C: `static HashNode getpmralias(HashTable ht, const char *name)` →
4587///   `return getalias(aliastab, ht, name, 0);`
4588#[allow(non_snake_case)]
4589pub fn getpmralias(ht: *mut HashTable, name: &str) -> Option<Param> {
4590    // c:1923
4591    getalias(std::ptr::null_mut(), ht, name, 0) // c:1923
4592}
4593
4594/// Port of `getpmdisralias()` from `Src/Modules/parameter.c:1930` — C decl `getpmdisralias(HashTable ht, const char *name)`.
4595/// C: `static HashNode getpmdisralias(HashTable ht, const char *name)` →
4596///   `return getalias(aliastab, ht, name, DISABLED);`
4597#[allow(non_snake_case)]
4598pub fn getpmdisralias(ht: *mut HashTable, name: &str) -> Option<Param> {
4599    // c:1930
4600    getalias(std::ptr::null_mut(), ht, name, DISABLED) // c:1930
4601}
4602
4603/// Port of `getpmgalias()` from `Src/Modules/parameter.c:1937` — C decl `getpmgalias(HashTable ht, const char *name)`.
4604/// C: `static HashNode getpmgalias(HashTable ht, const char *name)` →
4605///   `return getalias(aliastab, ht, name, ALIAS_GLOBAL);`
4606#[allow(non_snake_case)]
4607pub fn getpmgalias(ht: *mut HashTable, name: &str) -> Option<Param> {
4608    // c:1937
4609    getalias(std::ptr::null_mut(), ht, name, ALIAS_GLOBAL) // c:1937
4610}
4611
4612/// Port of `getpmdisgalias()` from `Src/Modules/parameter.c:1944` — C decl `getpmdisgalias(HashTable ht, const char *name)`.
4613/// C: `static HashNode getpmdisgalias(HashTable ht, const char *name)` →
4614///   `return getalias(aliastab, ht, name, ALIAS_GLOBAL|DISABLED);`
4615#[allow(non_snake_case)]
4616pub fn getpmdisgalias(ht: *mut HashTable, name: &str) -> Option<Param> {
4617    // c:1944
4618    // Prior port passed `DISABLED` only — matched disabled REGULAR
4619    // aliases (collision with getpmdisralias). C's c:1946 actually
4620    // passes `ALIAS_GLOBAL|DISABLED`; getalias's strict-equality
4621    // check at c:1912 means the wrong constant returned None for
4622    // every disabled global alias since they carry the full
4623    // ALIAS_GLOBAL|DISABLED flag combination.
4624    getalias(std::ptr::null_mut(), ht, name, ALIAS_GLOBAL | DISABLED) // c:1946
4625}
4626
4627/// Port of `getpmsalias()` from `Src/Modules/parameter.c:1951` — C decl `getpmsalias(HashTable ht, const char *name)`.
4628/// C: `static HashNode getpmsalias(HashTable ht, const char *name)` →
4629///   `return getalias(sufaliastab, ht, name, ALIAS_SUFFIX);`
4630#[allow(non_snake_case)]
4631pub fn getpmsalias(ht: *mut HashTable, name: &str) -> Option<Param> {
4632    // c:1953
4633    getalias(std::ptr::null_mut(), ht, name, ALIAS_SUFFIX) // c:1953
4634}
4635
4636/// Port of `getpmdissalias()` from `Src/Modules/parameter.c:1958` — C decl `getpmdissalias(HashTable ht, const char *name)`.
4637/// C: `static HashNode getpmdissalias(HashTable ht, const char *name)` →
4638///   `return getalias(sufaliastab, ht, name, ALIAS_SUFFIX|DISABLED);`
4639#[allow(non_snake_case)]
4640pub fn getpmdissalias(ht: *mut HashTable, name: &str) -> Option<Param> {
4641    // c:1960
4642    getalias(std::ptr::null_mut(), ht, name, ALIAS_SUFFIX | DISABLED) // c:1960
4643}
4644
4645/// Port of `scanaliases()` from `Src/Modules/parameter.c:1965` — C decl `scanaliases(HashTable alht, UNUSED(HashTable ht), ScanFunc func, int pmflags, int alflags)`.
4646/// C: `static void scanaliases(HashTable alht, UNUSED(HashTable ht),
4647///     ScanFunc func, int pmflags, int alflags)` — iterate the alias
4648///     table, synth a Param per matching entry, invoke func.
4649#[allow(non_snake_case)]
4650/// WARNING: param names don't match C — Rust=(_alht, _ht, pmflags, alflags) vs C=(alht, ht, func, pmflags, alflags)
4651pub fn scanaliases(
4652    _alht: *mut HashTable,
4653    _ht: *mut HashTable, // c:1965
4654    func: Option<ParamScanFunc>,
4655    pmflags: i32,
4656    alflags: i32,
4657) {
4658    // c:1968-1988 — `for ((al = (Alias) firstnode(alht)); al;
4659    //                     incnode(al)) { if (!al->node.flags & alflags
4660    //                     && !disabled) emit(al->node.nam) }`.
4661    // Walk the canonical `aliastab` (Src/hashtable.c:1210, ported at
4662    // src/ported/hashtable.rs::aliastab_lock) and emit each alias
4663    // matching the flag filter.
4664    if let Some(f) = func {
4665        // c:1965 — `alht` arg picks the table. C callers:
4666        //   scanpmraliases  → alht=aliastab,     alflags=0
4667        //   scanpmdisraliases → alht=aliastab,     alflags=DISABLED
4668        //   scanpmgaliases  → alht=aliastab,     alflags=ALIAS_GLOBAL
4669        //   scanpmdisgaliases → alht=aliastab,     alflags=ALIAS_GLOBAL|DISABLED
4670        //   scanpmsaliases  → alht=sufaliastab,  alflags=ALIAS_SUFFIX
4671        //   scanpmdissaliases → alht=sufaliastab,  alflags=ALIAS_SUFFIX|DISABLED
4672        //
4673        // Dispatch on the ALIAS_SUFFIX bit (mirrors getalias's
4674        // c:1901 table pick). Prior port used strict equality
4675        // `alflags == ALIAS_SUFFIX`, which routed the
4676        // SUFFIX|DISABLED case to aliastab instead of sufaliastab
4677        // — `${(k)dis_saliases}` returned the wrong table's
4678        // entries when there were no global aliases with DISABLED
4679        // set, but the misdirection breaks the moment a user
4680        // creates a disabled suffix alias.
4681        let lock = if (alflags & ALIAS_SUFFIX) != 0 {
4682            sufaliastab_lock()
4683        } else {
4684            aliastab_lock()
4685        };
4686        // c:1994-1996 — value side only when values were asked for.
4687        let want_val = (pmflags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
4688            || (pmflags as u32 & SCANPM_WANTKEYS) == 0;
4689        // c:1987-1988 — one `struct param pm` for the whole walk
4690        // (`assignaliasdefs` sets the gsu/flags per alias kind).
4691        let mut pm = param::default();
4692        if let Ok(tab) = lock.read() {
4693            for (_, alias) in tab.iter() {
4694                // c:1976 — `for (al = ...; al; ...)`
4695                // c:1977 — `if (alflags == al->node.flags)` strict
4696                // equality: scanpmraliases passes alflags=0 (regular,
4697                // flags==0), scanpmdisraliases passes DISABLED,
4698                // scanpmgaliases ALIAS_GLOBAL, scanpmsaliases
4699                // ALIAS_SUFFIX, scanpmdissaliases ALIAS_SUFFIX|DISABLED
4700                // etc. Anything else is skipped.
4701                if alias.node.flags != alflags {
4702                    continue;
4703                }
4704                pm.node.nam = alias.node.nam.clone(); // c:1993
4705                pm.node.flags = alias.node.flags;
4706                pm.u_str = if want_val {
4707                    Some(alias.text.clone()) // c:1997 pm.u.str = dupstring(al->text)
4708                } else {
4709                    None
4710                };
4711                f(&pm, pmflags); // c:1998
4712            }
4713        }
4714    }
4715}
4716
4717/// Port of `scanpmraliases()` from `Src/Modules/parameter.c:1990` — C decl `scanpmraliases(HashTable ht, ScanFunc func, int flags)`.
4718#[allow(non_snake_case)]
4719/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
4720pub fn scanpmraliases(
4721    ht: *mut HashTable,
4722    func: Option<ParamScanFunc>, // c:1990
4723    flags: i32,
4724) {
4725    scanaliases(std::ptr::null_mut(), ht, func, flags, 0) // c:1993
4726}
4727
4728/// Port of `scanpmdisraliases()` from `Src/Modules/parameter.c:1997` — C decl `scanpmdisraliases(HashTable ht, ScanFunc func, int flags)`.
4729#[allow(non_snake_case)]
4730/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
4731pub fn scanpmdisraliases(
4732    ht: *mut HashTable,
4733    func: Option<ParamScanFunc>, // c:1997
4734    flags: i32,
4735) {
4736    scanaliases(std::ptr::null_mut(), ht, func, flags, DISABLED) // c:2000
4737}
4738
4739/// Port of `scanpmgaliases()` from `Src/Modules/parameter.c:2004` — C decl `scanpmgaliases(HashTable ht, ScanFunc func, int flags)`.
4740#[allow(non_snake_case)]
4741/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
4742pub fn scanpmgaliases(
4743    ht: *mut HashTable,
4744    func: Option<ParamScanFunc>, // c:2004
4745    flags: i32,
4746) {
4747    scanaliases(std::ptr::null_mut(), ht, func, flags, ALIAS_GLOBAL) // c:2007
4748}
4749
4750/// Port of `scanpmdisgaliases()` from `Src/Modules/parameter.c:2011` — C decl `scanpmdisgaliases(HashTable ht, ScanFunc func, int flags)`.
4751#[allow(non_snake_case)]
4752/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
4753pub fn scanpmdisgaliases(
4754    ht: *mut HashTable,
4755    func: Option<ParamScanFunc>, // c:2011
4756    flags: i32,
4757) {
4758    scanaliases(
4759        std::ptr::null_mut(),
4760        ht,
4761        func,
4762        flags, // c:1997
4763        ALIAS_GLOBAL | DISABLED,
4764    )
4765}
4766
4767/// Port of `scanpmsaliases()` from `Src/Modules/parameter.c:2018` — C decl `scanpmsaliases(HashTable ht, ScanFunc func, int flags)`.
4768#[allow(non_snake_case)]
4769/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
4770pub fn scanpmsaliases(
4771    ht: *mut HashTable,
4772    func: Option<ParamScanFunc>, // c:2018
4773    flags: i32,
4774) {
4775    scanaliases(
4776        std::ptr::null_mut(),
4777        ht,
4778        func,
4779        flags, // c:2021
4780        ALIAS_SUFFIX,
4781    )
4782}
4783
4784/// Port of `scanpmdissaliases()` from `Src/Modules/parameter.c:2025` — C decl `scanpmdissaliases(HashTable ht, ScanFunc func, int flags)`.
4785#[allow(non_snake_case)]
4786/// WARNING: param names don't match C — Rust=(ht, func) vs C=(ht, func, flags)
4787pub fn scanpmdissaliases(
4788    ht: *mut HashTable,
4789    func: Option<ParamScanFunc>, // c:2025
4790    flags: i32,
4791) {
4792    scanaliases(
4793        std::ptr::null_mut(),
4794        ht,
4795        func,
4796        flags, // c:2028
4797        ALIAS_SUFFIX | DISABLED,
4798    )
4799}
4800
4801/// Port of `getpmusergroups()` from `Src/Modules/parameter.c:2102` — C decl `getpmusergroups(UNUSED(HashTable ht), const char *name)`.
4802/// C: `static HashNode getpmusergroups(UNUSED(HashTable ht),
4803///     const char *name)` — emit group memberships for `name`.
4804#[allow(non_snake_case)]
4805#[allow(unused_variables)]
4806pub fn getpmusergroups(ht: *mut HashTable, name: &str) -> Option<Param> {
4807    // c:2102
4808    // Faithful port of c:2105-2135:
4809    //   gs = get_all_groups();
4810    //   if (!gs) { zerr('failed to retrieve groups for user: %e', errno);
4811    //              PM_UNSET|PM_SPECIAL; return; }
4812    //   for (gaptr = gs->array; gaptr < gs->array + gs->num; gaptr++) {
4813    //       if (!strcmp(name, gaptr->name)) {
4814    //           sprintf(buf, '%d', gaptr->gid);
4815    //           pm->u.str = dupstring(buf);
4816    //           return &pm->node;
4817    //       }
4818    //   }
4819    //   pm->u.str = dupstring('');
4820    //   pm->node.flags |= (PM_UNSET|PM_SPECIAL);
4821    //
4822    // get_all_groups() returns ONLY the groups the current user
4823    // belongs to (primary + supplementary). Prior Rust port used
4824    // getgrnam(name) which returns ANY group from the system
4825    // database. Semantic divergence:
4826    //
4827    //   - C: \${usergroups[wheel]} = gid_of_wheel iff user in wheel
4828    //   - Rust pre-port: \${usergroups[wheel]} = gid_of_wheel
4829    //     regardless of membership
4830    //
4831    // Now build the per-user group set via getgroups(2) + getgrgid(3)
4832    // resolution, matching C's get_all_groups semantics.
4833    let mut user_groups: Vec<(libc::gid_t, String)> = Vec::new();
4834    // c:2106 part 1 — get_all_groups: getgroups returns supplementary
4835    // gids; the primary gid comes from the current effective gid.
4836    let n_groups = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
4837    if n_groups >= 0 {
4838        let mut gids: Vec<libc::gid_t> = vec![0; n_groups as usize];
4839        let got = unsafe { libc::getgroups(n_groups, gids.as_mut_ptr()) };
4840        if got >= 0 {
4841            // c:2106 includes effective gid in the user's group set
4842            // — get_all_groups c:2081-2083 adds egid if not already
4843            // present.
4844            let egid = unsafe { libc::getegid() };
4845            if !gids.iter().any(|&g| g == egid) {
4846                gids.push(egid);
4847            }
4848            // c:2086-2092 — resolve each gid to a group name.
4849            for gid in gids {
4850                let grp = unsafe { libc::getgrgid(gid) };
4851                if grp.is_null() {
4852                    continue; // c:2088 — failed lookup; skip rather
4853                              // than abort the whole walk (matches the
4854                              // 'return NULL' in C but we're already
4855                              // building a partial set).
4856                }
4857                let gr_name = unsafe { std::ffi::CStr::from_ptr((*grp).gr_name) };
4858                if let Ok(s) = gr_name.to_str() {
4859                    user_groups.push((gid, s.to_string()));
4860                }
4861            }
4862        }
4863    }
4864
4865    let (value, found) = match user_groups.iter().find(|(_, n)| n == name) {
4866        // c:2124-2131 — match on name; emit gid as %d.
4867        Some((gid, _)) => (gid.to_string(), true),
4868        None => (String::new(), false), // c:2134
4869    };
4870    let pm = Box::new(param {
4871        // c:2108 hcalloc
4872        node: hashnode {
4873            next: None,
4874            nam: name.to_string(), // c:2109
4875            flags: if found {
4876                (PM_SCALAR | PM_READONLY) as i32
4877            }
4878            // c:2110
4879            else {
4880                (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32
4881            }, // c:2135
4882        },
4883        u_data: 0,
4884        u_tied: None,
4885        u_arr: None,
4886        u_str: Some(value), // c:2128 / c:2134
4887        u_val: 0,
4888        u_dval: 0.0,
4889        u_hash: None,
4890        gsu_s: None, // c:2111 nullsetscalar_gsu
4891        gsu_i: None,
4892        gsu_f: None,
4893        gsu_a: None,
4894        gsu_h: None,
4895        base: 0,
4896        width: 0,
4897        env: None,
4898        ename: None,
4899        old: None,
4900        level: 0,
4901    });
4902    Some(pm) // c:2136
4903}
4904
4905/// Port of `scanpmusergroups()` from `Src/Modules/parameter.c:2143` — C decl `scanpmusergroups(UNUSED(HashTable ht), ScanFunc func, int flags)`.
4906/// C body (c:2146-2169): get_all_groups() returns Groupset; walk
4907/// gs->array emitting each group name. Static-link path uses
4908/// getgrent(3) — same data source.
4909#[allow(non_snake_case)]
4910/// WARNING: param names don't match C — Rust=(_ht, func) vs C=(ht, func, flags)
4911pub fn scanpmusergroups(
4912    _ht: *mut HashTable,
4913    func: Option<ParamScanFunc>, // c:2143
4914    flags: i32,
4915) {
4916    // c:2143
4917    // Faithful port of c:2146-2167:
4918    //   gs = get_all_groups();
4919    //   if (!gs) { zerr(...); return; }
4920    //   for (gaptr = gs->array; gaptr < gs->array + gs->num; gaptr++) {
4921    //       pm.node.nam = gaptr->name;
4922    //       ... emit gid as %d ...
4923    //       func(&pm.node, flags);
4924    //   }
4925    //
4926    // C iterates ONLY the current user's groups. Prior Rust port
4927    // used getgrent() which walks every group in the system database
4928    // — same semantic divergence as getpmusergroups before the
4929    // 7b5a68a79f fix. \${(k)usergroups} listed every system group,
4930    // breaking scripts that iterate the user's actual memberships.
4931    //
4932    // Build the user group set the same way getpmusergroups now
4933    // does: getgroups(2) + getegid(2) + getgrgid(3).
4934    let f = match func {
4935        Some(f) => f,
4936        None => return,
4937    };
4938
4939    let n_groups = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
4940    if n_groups < 0 {
4941        return; // c:2155 — get_all_groups NULL → zerr + return.
4942    }
4943    let mut gids: Vec<libc::gid_t> = vec![0; n_groups as usize];
4944    let got = unsafe { libc::getgroups(n_groups, gids.as_mut_ptr()) };
4945    if got < 0 {
4946        return;
4947    }
4948    // c:2081-2083 — get_all_groups appends egid if not in the
4949    // supplementary list.
4950    let egid = unsafe { libc::getegid() };
4951    if !gids.iter().any(|&g| g == egid) {
4952        gids.push(egid);
4953    }
4954    // c:2182-2184 — value side only when values were asked for.
4955    let want_val = (flags as u32 & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) != 0
4956        || (flags as u32 & SCANPM_WANTKEYS) == 0;
4957    // c:2176-2177 — one `struct param pm` for the whole walk.
4958    let mut pm = param {
4959        node: hashnode {
4960            next: None,
4961            nam: String::new(),
4962            flags: (PM_SCALAR | PM_READONLY) as i32, // c:2177
4963        },
4964        ..Default::default()
4965    };
4966    for gid in gids {
4967        let grp = unsafe { libc::getgrgid(gid) };
4968        if grp.is_null() {
4969            continue; // c:2088
4970        }
4971        let name = unsafe { std::ffi::CStr::from_ptr((*grp).gr_name) };
4972        pm.node.nam = name.to_string_lossy().into_owned(); // c:2181
4973        pm.u_str = if want_val {
4974            Some(format!("{}", gid)) // c:2187-2188 sprintf(buf, "%d", gaptr->gid)
4975        } else {
4976            None
4977        };
4978        f(&pm, flags); // c:2190
4979    }
4980}
4981
4982/// Port of `struct pardef` from `Src/Modules/parameter.c:2179`. The
4983/// per-magic-assoc parameter spec table — one entry per
4984/// `${parameters}`/`${commands}`/`${functions}`/etc. exposed by the
4985/// `zsh/parameter` module.
4986///
4987/// C definition (c:2179-2187):
4988/// ```c
4989/// struct pardef {
4990///     char *name;
4991///     int flags;
4992///     GetNodeFunc getnfn;
4993///     ScanTabFunc scantfn;
4994///     GsuHash hash_gsu;
4995///     GsuArray array_gsu;
4996///     Param pm;
4997/// };
4998/// ```
4999///
5000/// Rust port keeps the same shape; the GSU function-table fields
5001/// (`hash_gsu`, `array_gsu`) are type-erased via `usize` because the
5002/// `GsuHash`/`GsuArray` types (zsh_h.rs:797-798, `Box<gsu_hash>` /
5003/// `Box<gsu_array>`) own their callback function pointers and can't
5004/// be const-initialised in a Rust static. Consumers cast back at
5005/// dispatch time.
5006#[allow(non_camel_case_types)]
5007#[derive(Debug, Clone, Copy)]
5008pub struct pardef {
5009    // c:2179
5010    /// Parameter name (e.g. "commands", "functions", "options").
5011    pub name: &'static str, // c:2180
5012    /// Flags (PM_* bits — typically PM_HASHED|PM_SPECIAL|PM_HIDE).
5013    pub flags: i32, // c:2181
5014    /// `GetNodeFunc` getnfn — type-erased; the typed callback lives in
5015    /// `PartabHashEntry::getfn` (see `PARTAB`).
5016    pub getnfn: usize, // c:2182
5017    /// `ScanTabFunc` scantfn — type-erased; the typed callback lives in
5018    /// `PartabHashEntry::scanfn` (see `PARTAB`).
5019    pub scantfn: usize, // c:2183
5020    /// `GsuHash` hash_gsu — type-erased.
5021    pub hash_gsu: usize, // c:2184
5022    /// `GsuArray` array_gsu — type-erased.
5023    pub array_gsu: usize, // c:2185
5024    /// `Param pm` — type-erased pointer; populated by createparam.
5025    pub pm: usize, // c:2186
5026}
5027
5028// `partab` — port of `static struct paramdef partab[]` (parameter.c).
5029// 33 SPECIALPMDEF entries — each ties a `${assoc}` magic-assoc name
5030// to its scanpm*/getpm* C callbacks. Rust-side dispatch is wired
5031// through the static `PARTAB` table below: each entry pairs the
5032// name + PM_* flags + getfn/scanfn fn pointers, so paramsubst can
5033// route `${name[key]}` through `getpmX(name=key)` and `${(k)name}`
5034// through `scanpmX(...)` like C does via the GSU `getnfn`/`scantfn`
5035// callbacks (`Src/Modules/parameter.c:2235`+).
5036
5037/// Function-pointer types matching C's `GetNodeFunc` / `ScanTabFunc`
5038/// for the magic-assoc table dispatch.
5039pub type HashGetFn = fn(*mut HashTable, &str) -> Option<Param>;
5040/// `HashScanFn` type alias.
5041pub type HashScanFn = fn(*mut HashTable, Option<crate::ported::zsh_h::ParamScanFunc>, i32);
5042
5043/// Strongly-typed PARTAB entry. C's `paramdef` keeps these as opaque
5044/// pointers; Rust's static-initialization rules make explicit fn
5045/// pointers cleaner. Only the magic-assoc shape (PM_HASHED) is
5046/// populated here; PM_ARRAY entries (`dirstack`, `funcstack`,
5047/// `patchars`, `reswords`, etc.) need a separate ArrayGetFn type.
5048#[allow(non_camel_case_types)]
5049#[derive(Clone, Copy)]
5050pub struct PartabHashEntry {
5051    /// Parameter name — `${name[key]}` triggers dispatch.
5052    pub name: &'static str,
5053    /// PM_* flag bits (PM_HASHED, PM_READONLY_SPECIAL, etc.).
5054    pub flags: i32,
5055    /// Per-key value lookup. Mirrors `getpm*` family in C.
5056    pub getfn: HashGetFn,
5057    /// Owning module when the row is bound by an explicit zmodload
5058    /// (C: the row lives in THAT module's paramdef table —
5059    /// zsh/system's sysparams/errnos at system.c:902-904, mapfile,
5060    /// langinfo). None = zsh/parameter (always available).
5061    pub module: Option<&'static str>,
5062    /// Full-table enumeration. Mirrors `scanpm*` family in C.
5063    pub scanfn: HashScanFn,
5064}
5065
5066/// `static const struct paramdef partab[]` from `Src/Modules/parameter.c:
5067/// 2235-2298`. Each entry binds a magic-assoc name to its
5068/// per-key/full-scan canonical callbacks. The PM_ARRAY entries
5069/// (dirstack/funcstack/patchars/reswords/historywords/etc.) aren't
5070/// included here — they live in a separate `PARTAB_ARRAY` once the
5071/// array-shaped getfn types land.
5072pub static PARTAB: &[PartabHashEntry] = &[
5073    // c:2235 — `aliases`: regular aliases (no ALIAS_GLOBAL bit).
5074    PartabHashEntry {
5075        name: "aliases",
5076        flags: PM_HASHED as i32, // c:2235 SPECIALPMDEF flags
5077        getfn: getpmralias,
5078        module: None,
5079        scanfn: scanpmraliases,
5080    },
5081    // c:2237 — `builtins`: read-only.
5082    PartabHashEntry {
5083        name: "builtins",
5084        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2237 PM_READONLY_SPECIAL
5085        getfn: getpmbuiltin,
5086        module: None,
5087        scanfn: scanpmbuiltins,
5088    },
5089    // c:2238 — `commands`: cmdnamtab lookup + PATH path-build.
5090    PartabHashEntry {
5091        name: "commands",
5092        flags: PM_HASHED as i32, // c:2238
5093        getfn: getpmcommand,
5094        module: None,
5095        scanfn: scanpmcommands,
5096    },
5097    // c:2241 — `dis_aliases`: aliases with DISABLED bit.
5098    PartabHashEntry {
5099        name: "dis_aliases",
5100        flags: PM_HASHED as i32, // c:2241
5101        getfn: getpmdisralias,
5102        module: None,
5103        scanfn: scanpmdisraliases,
5104    },
5105    // c:2243 — `dis_builtins`: read-only disabled.
5106    PartabHashEntry {
5107        name: "dis_builtins",
5108        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2243 PM_READONLY_SPECIAL
5109        getfn: getpmdisbuiltin,
5110        module: None,
5111        scanfn: scanpmdisbuiltins,
5112    },
5113    // c:2245 — `dis_functions`: shfunctab with DISABLED bit.
5114    PartabHashEntry {
5115        name: "dis_functions",
5116        flags: PM_HASHED as i32, // c:2245
5117        getfn: getpmdisfunction,
5118        module: None,
5119        scanfn: scanpmdisfunctions,
5120    },
5121    // c:2249 — `dis_galiases`.
5122    PartabHashEntry {
5123        name: "dis_galiases",
5124        flags: PM_HASHED as i32, // c:2249
5125        getfn: getpmdisgalias,
5126        module: None,
5127        scanfn: scanpmdisgaliases,
5128    },
5129    // c:2255 — `dis_saliases`.
5130    PartabHashEntry {
5131        name: "dis_saliases",
5132        flags: PM_HASHED as i32, // c:2255
5133        getfn: getpmdissalias,
5134        module: None,
5135        scanfn: scanpmdissaliases,
5136    },
5137    // c:2263 — `functions`: shfunctab lookup.
5138    PartabHashEntry {
5139        name: "functions",
5140        flags: PM_HASHED as i32, // c:2263
5141        getfn: getpmfunction,
5142        module: None,
5143        scanfn: scanpmfunctions,
5144    },
5145    // c:2269 — `galiases`: aliases with ALIAS_GLOBAL bit.
5146    PartabHashEntry {
5147        name: "galiases",
5148        flags: PM_HASHED as i32, // c:2269
5149        getfn: getpmgalias,
5150        module: None,
5151        scanfn: scanpmgaliases,
5152    },
5153    // c:2271 — `history`: history-ring entry by event number.
5154    PartabHashEntry {
5155        name: "history",
5156        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2271 PM_READONLY_SPECIAL
5157        getfn: getpmhistory,
5158        module: None,
5159        scanfn: scanpmhistory,
5160    },
5161    // c:2275 — `jobdirs`.
5162    PartabHashEntry {
5163        name: "jobdirs",
5164        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2275 PM_READONLY_SPECIAL
5165        getfn: getpmjobdir,
5166        module: None,
5167        scanfn: scanpmjobdirs,
5168    },
5169    // c:2277 — `jobstates`.
5170    PartabHashEntry {
5171        name: "jobstates",
5172        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2277 PM_READONLY_SPECIAL
5173        getfn: getpmjobstate,
5174        module: None,
5175        scanfn: scanpmjobstates,
5176    },
5177    // c:2279 — `jobtexts`.
5178    PartabHashEntry {
5179        name: "jobtexts",
5180        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2279 PM_READONLY_SPECIAL
5181        getfn: getpmjobtext,
5182        module: None,
5183        scanfn: scanpmjobtexts,
5184    },
5185    // c:2281 — `modules`.
5186    PartabHashEntry {
5187        name: "modules",
5188        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2281 PM_READONLY_SPECIAL
5189        getfn: getpmmodule,
5190        module: None,
5191        scanfn: scanpmmodules,
5192    },
5193    // c:2283 — `nameddirs`.
5194    PartabHashEntry {
5195        name: "nameddirs",
5196        flags: PM_HASHED as i32, // c:2283
5197        getfn: getpmnameddir,
5198        module: None,
5199        scanfn: scanpmnameddirs,
5200    },
5201    // c:2285 — `options`.
5202    PartabHashEntry {
5203        name: "options",
5204        flags: PM_HASHED as i32, // c:2285
5205        getfn: getpmoption,
5206        module: None,
5207        scanfn: scanpmoptions,
5208    },
5209    // c:2287 — `parameters`.
5210    PartabHashEntry {
5211        name: "parameters",
5212        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2287 PM_READONLY_SPECIAL
5213        getfn: getpmparameter,
5214        module: None,
5215        scanfn: scanpmparameters,
5216    },
5217    // c:2293 — `saliases`: suffix aliases.
5218    PartabHashEntry {
5219        name: "saliases",
5220        flags: PM_HASHED as i32, // c:2293
5221        getfn: getpmsalias,
5222        module: None,
5223        scanfn: scanpmsaliases,
5224    },
5225    // c:2295 — `userdirs`.
5226    PartabHashEntry {
5227        name: "userdirs",
5228        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2295 PM_READONLY_SPECIAL
5229        getfn: getpmuserdir,
5230        module: None,
5231        scanfn: scanpmuserdirs,
5232    },
5233    // c:2297 — `usergroups`.
5234    PartabHashEntry {
5235        name: "usergroups",
5236        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2297 PM_READONLY_SPECIAL
5237        getfn: getpmusergroups,
5238        module: None,
5239        scanfn: scanpmusergroups,
5240    },
5241    // c:2247 — `dis_functions_source`: hashed assoc, key=fn name,
5242    // value=source path. Same shape as `functions` but for disabled.
5243    PartabHashEntry {
5244        name: "dis_functions_source",
5245        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2247
5246        getfn: getpmdisfunction_source,
5247        module: None,
5248        scanfn: scanpmdisfunction_source,
5249    },
5250    // c:2265 — `functions_source`.
5251    PartabHashEntry {
5252        name: "functions_source",
5253        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2265
5254        getfn: getpmfunction_source,
5255        module: None,
5256        scanfn: scanpmfunction_source,
5257    },
5258    // Src/Modules/mapfile.c:212 SPECIALPMDEF("mapfile", 0, ...).
5259    // Separate module from parameter.c but same PARTAB shape — both
5260    // register via boot_/enables_ into paramtab.
5261    PartabHashEntry {
5262        name: "mapfile",
5263        flags: PM_HASHED as i32, // mapfile.c:212 SPECIALPMDEF flags=0
5264        getfn: crate::ported::modules::mapfile::getpmmapfile,
5265        module: Some("zsh/mapfile"),
5266        scanfn: crate::ported::modules::mapfile::scanpmmapfile,
5267    },
5268    // Src/Modules/terminfo.c:291 SPECIALPMDEF("terminfo", PM_READONLY, ...).
5269    PartabHashEntry {
5270        name: "terminfo",
5271        // c:Src/Modules/terminfo.c:305 — `SPECIALPMDEF("terminfo", PM_READONLY, …)`.
5272        // PLAIN `PM_READONLY`, NOT `PM_READONLY_SPECIAL`
5273        // (= PM_SPECIAL|PM_READONLY|PM_RO_BY_DESIGN, c:Src/zsh.h:1925), and
5274        // `SPECIALPMDEF` (c:Src/zsh.h:2122) only ORs in
5275        // `PM_SPECIAL|PM_HIDE|PM_HIDEVAL` — it never adds PM_RO_BY_DESIGN.
5276        // Carrying that bit here made printparamnode's c:Src/params.c:6157-6162
5277        // gate (`if (p->level != locallevel || p->level == 0) return;`)
5278        // swallow the row, so `zmodload zsh/system; typeset -p sysparams`
5279        // printed NOTHING where zsh prints `typeset -Ar sysparams`.
5280        // Only the 7 module rows declared outside Src/Modules/parameter.c
5281        // are plain PM_READONLY; the parameter.c rows that really do use
5282        // PM_READONLY_SPECIAL keep the bit.
5283        flags: PM_HASHED as i32 | PM_READONLY as i32,
5284        getfn: crate::ported::modules::terminfo::getterminfo,
5285        module: None,
5286        scanfn: crate::ported::modules::terminfo::scanterminfo,
5287    },
5288    // Src/Modules/termcap.c:299 SPECIALPMDEF("termcap", PM_READONLY, ...).
5289    PartabHashEntry {
5290        name: "termcap",
5291        // c:Src/Modules/termcap.c:312 — `SPECIALPMDEF("termcap", PM_READONLY, …)`.
5292        // PLAIN `PM_READONLY`, NOT `PM_READONLY_SPECIAL`
5293        // (= PM_SPECIAL|PM_READONLY|PM_RO_BY_DESIGN, c:Src/zsh.h:1925), and
5294        // `SPECIALPMDEF` (c:Src/zsh.h:2122) only ORs in
5295        // `PM_SPECIAL|PM_HIDE|PM_HIDEVAL` — it never adds PM_RO_BY_DESIGN.
5296        // Carrying that bit here made printparamnode's c:Src/params.c:6157-6162
5297        // gate (`if (p->level != locallevel || p->level == 0) return;`)
5298        // swallow the row, so `zmodload zsh/system; typeset -p sysparams`
5299        // printed NOTHING where zsh prints `typeset -Ar sysparams`.
5300        // Only the 7 module rows declared outside Src/Modules/parameter.c
5301        // are plain PM_READONLY; the parameter.c rows that really do use
5302        // PM_READONLY_SPECIAL keep the bit.
5303        flags: PM_HASHED as i32 | PM_READONLY as i32,
5304        getfn: crate::ported::modules::termcap::gettermcap,
5305        module: None,
5306        scanfn: crate::ported::modules::termcap::scantermcap,
5307    },
5308    // Src/Zle/zleparameter.c:133 SPECIALPMDEF("widgets", PM_READONLY, ...).
5309    PartabHashEntry {
5310        name: "widgets",
5311        // c:Src/Zle/zleparameter.c:133 — `SPECIALPMDEF("widgets", PM_READONLY, …)`.
5312        // PLAIN `PM_READONLY`, NOT `PM_READONLY_SPECIAL`
5313        // (= PM_SPECIAL|PM_READONLY|PM_RO_BY_DESIGN, c:Src/zsh.h:1925), and
5314        // `SPECIALPMDEF` (c:Src/zsh.h:2122) only ORs in
5315        // `PM_SPECIAL|PM_HIDE|PM_HIDEVAL` — it never adds PM_RO_BY_DESIGN.
5316        // Carrying that bit here made printparamnode's c:Src/params.c:6157-6162
5317        // gate (`if (p->level != locallevel || p->level == 0) return;`)
5318        // swallow the row, so `zmodload zsh/system; typeset -p sysparams`
5319        // printed NOTHING where zsh prints `typeset -Ar sysparams`.
5320        // Only the 7 module rows declared outside Src/Modules/parameter.c
5321        // are plain PM_READONLY; the parameter.c rows that really do use
5322        // PM_READONLY_SPECIAL keep the bit.
5323        flags: PM_HASHED as i32 | PM_READONLY as i32,
5324        getfn: crate::ported::zle::zleparameter::getpmwidgets,
5325        module: None,
5326        scanfn: crate::ported::zle::zleparameter::scanpmwidgets,
5327    },
5328    // Src/Modules/system.c:904 SPECIALPMDEF("sysparams", PM_READONLY, ...).
5329    PartabHashEntry {
5330        name: "sysparams",
5331        // c:Src/Modules/system.c:906 — `SPECIALPMDEF("sysparams", PM_READONLY, …)`.
5332        // PLAIN `PM_READONLY`, NOT `PM_READONLY_SPECIAL`
5333        // (= PM_SPECIAL|PM_READONLY|PM_RO_BY_DESIGN, c:Src/zsh.h:1925), and
5334        // `SPECIALPMDEF` (c:Src/zsh.h:2122) only ORs in
5335        // `PM_SPECIAL|PM_HIDE|PM_HIDEVAL` — it never adds PM_RO_BY_DESIGN.
5336        // Carrying that bit here made printparamnode's c:Src/params.c:6157-6162
5337        // gate (`if (p->level != locallevel || p->level == 0) return;`)
5338        // swallow the row, so `zmodload zsh/system; typeset -p sysparams`
5339        // printed NOTHING where zsh prints `typeset -Ar sysparams`.
5340        // Only the 7 module rows declared outside Src/Modules/parameter.c
5341        // are plain PM_READONLY; the parameter.c rows that really do use
5342        // PM_READONLY_SPECIAL keep the bit.
5343        flags: PM_HASHED as i32 | PM_READONLY as i32,
5344        getfn: crate::ported::modules::system::getpmsysparams,
5345        module: Some("zsh/system"),
5346        scanfn: crate::ported::modules::system::scanpmsysparams,
5347    },
5348    // Src/Modules/langinfo.c:447 SPECIALPMDEF("langinfo", 0, ...).
5349    PartabHashEntry {
5350        name: "langinfo",
5351        // c:Src/Modules/langinfo.c:447 — `SPECIALPMDEF("langinfo", 0,
5352        // NULL, getlanginfo, scanlanginfo)`: the flags argument is 0, so
5353        // the row is NOT PM_READONLY (unlike its zsh/system neighbours
5354        // `sysparams`/`errnos`, which pass PM_READONLY explicitly).
5355        // `${(t)langinfo}` is `association-hide-hideval-special` in zsh.
5356        flags: PM_HASHED as i32, // langinfo.c:447
5357        getfn: crate::ported::modules::langinfo::getlanginfo,
5358        module: Some("zsh/langinfo"),
5359        scanfn: crate::ported::modules::langinfo::scanlanginfo,
5360    },
5361    // Src/Modules/hlgroup.c:166-169 — `SPECIALPMDEF(".zle.esc",
5362    // PM_READONLY_SPECIAL, 0, getpmesc, scanpmesc)` and the `.zle.sgr` twin.
5363    PartabHashEntry {
5364        name: ".zle.esc",
5365        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // hlgroup.c:167 PM_READONLY_SPECIAL
5366        getfn: crate::ported::modules::hlgroup::getpmesc,
5367        module: Some("zsh/hlgroup"),
5368        scanfn: crate::ported::modules::hlgroup::scanpmesc,
5369    },
5370    PartabHashEntry {
5371        name: ".zle.sgr",
5372        flags: PM_HASHED as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // hlgroup.c:168 PM_READONLY_SPECIAL
5373        getfn: crate::ported::modules::hlgroup::getpmsgr,
5374        module: Some("zsh/hlgroup"),
5375        scanfn: crate::ported::modules::hlgroup::scanpmsgr,
5376    },
5377];
5378
5379// scanpmfunction_source / scanpmdisfunction_source already ported
5380// at lines 957/970 — re-used here as PARTAB.scanfn pointers.
5381
5382/// PM_ARRAY entries from `Src/Modules/parameter.c:2239-2291` — single
5383/// whole-array getfn returning `Vec<String>` (no per-key dispatch).
5384/// Mirrors C's `gsu_array.getfn(pm) -> char**`.
5385pub type ArrayGetFn = fn(pm: *mut param) -> Vec<String>;
5386/// Whole-array setter. Mirrors C's `gsu_array.setfn(pm, x)`. Only the
5387/// writable special arrays (`dirstack`) have one; read-only specials
5388/// leave it `None`.
5389pub type ArraySetFn = fn(pm: *mut param, x: Vec<String>);
5390
5391/// Strongly-typed entry for PM_ARRAY-shape magic-assocs.
5392#[allow(non_camel_case_types)]
5393#[derive(Clone, Copy)]
5394pub struct PartabArrayEntry {
5395    /// Parameter name — `${name}` / `${name[N]}` triggers dispatch.
5396    pub name: &'static str,
5397    /// PM_* flag bits — always include PM_ARRAY.
5398    pub flags: i32,
5399    /// Whole-array getter. Mirrors C's `gsu_array.getfn(pm)`.
5400    pub getfn: ArrayGetFn,
5401    /// Whole-array setter. Mirrors C's `gsu_array.setfn(pm, x)`. `None`
5402    /// for read-only specials; `Some` for writable ones (`dirstack`).
5403    pub setfn: Option<ArraySetFn>,
5404    /// Owning module when bound by explicit zmodload; None =
5405    /// zsh/parameter.
5406    pub module: Option<&'static str>,
5407}
5408
5409/// `static const struct paramdef partab[]` PM_ARRAY subset from
5410/// `Src/Modules/parameter.c:2239-2291`. Each entry's `gsu_array.getfn`
5411/// returns the full array (no per-key dispatch).
5412pub static PARTAB_ARRAY: &[PartabArrayEntry] = &[
5413    // c:2273 — `historywords`: words from current line + every history
5414    // entry, newest-first, reverse-by-position within each entry.
5415    PartabArrayEntry {
5416        name: "historywords",
5417        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2273 PM_READONLY_SPECIAL
5418        getfn: histwgetfn,
5419        setfn: None,
5420        module: None,
5421    },
5422    // c:2239 — `dirstack`: $DIRSTACK pushd/popd state.
5423    PartabArrayEntry {
5424        name: "dirstack",
5425        flags: PM_ARRAY as i32, // c:2239
5426        getfn: dirsgetfn,
5427        setfn: Some(dirssetfn), // c:2229 dirs_gsu.setfn = dirssetfn
5428        module: None,
5429    },
5430    // c:2251 — `dis_patchars`: pattern metacharacters when extendedglob off.
5431    PartabArrayEntry {
5432        name: "dis_patchars",
5433        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2251 PM_READONLY_SPECIAL
5434        getfn: dispatcharsgetfn,
5435        setfn: None,
5436        module: None,
5437    },
5438    // c:2253 — `dis_reswords`: reserved words when disabled.
5439    PartabArrayEntry {
5440        name: "dis_reswords",
5441        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2253 PM_READONLY_SPECIAL
5442        getfn: disreswordsgetfn,
5443        setfn: None,
5444        module: None,
5445    },
5446    // c:2257 — `funcfiletrace`: per-frame caller file+lineno.
5447    PartabArrayEntry {
5448        name: "funcfiletrace",
5449        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2257
5450        getfn: funcfiletracegetfn,
5451        setfn: None,
5452        module: None,
5453    },
5454    // c:2259 — `funcsourcetrace`: per-frame def-site file+lineno.
5455    PartabArrayEntry {
5456        name: "funcsourcetrace",
5457        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2259
5458        getfn: funcsourcetracegetfn,
5459        setfn: None,
5460        module: None,
5461    },
5462    // c:2261 — `funcstack`: function-call stack names.
5463    PartabArrayEntry {
5464        name: "funcstack",
5465        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2261
5466        getfn: funcstackgetfn,
5467        setfn: None,
5468        module: None,
5469    },
5470    // c:2267 — `functrace`: per-frame call file+lineno.
5471    PartabArrayEntry {
5472        name: "functrace",
5473        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2267
5474        getfn: functracegetfn,
5475        setfn: None,
5476        module: None,
5477    },
5478    // c:2289 — `patchars`: pattern metacharacters when extendedglob on.
5479    PartabArrayEntry {
5480        name: "patchars",
5481        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2289
5482        getfn: patcharsgetfn,
5483        setfn: None,
5484        module: None,
5485    },
5486    // c:2291 — `reswords`: shell reserved words.
5487    PartabArrayEntry {
5488        name: "reswords",
5489        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2291
5490        getfn: reswordsgetfn,
5491        setfn: None,
5492        module: None,
5493    },
5494    // c:2273 — `historywords`: histwgetfn (parameter.c:1217-1252).
5495    PartabArrayEntry {
5496        name: "historywords",
5497        flags: PM_ARRAY as i32 | PM_READONLY as i32 | PM_RO_BY_DESIGN as i32, // c:2273
5498        getfn: histwgetfn,
5499        setfn: None,
5500        module: None,
5501    },
5502    // Src/Modules/system.c:902 SPECIALPMDEF("errnos", PM_ARRAY|PM_READONLY, ...).
5503    PartabArrayEntry {
5504        name: "errnos",
5505        // c:Src/Modules/system.c:904 — `SPECIALPMDEF("errnos", PM_ARRAY|PM_READONLY, …)`.
5506        // PLAIN `PM_READONLY`, NOT `PM_READONLY_SPECIAL`
5507        // (= PM_SPECIAL|PM_READONLY|PM_RO_BY_DESIGN, c:Src/zsh.h:1925), and
5508        // `SPECIALPMDEF` (c:Src/zsh.h:2122) only ORs in
5509        // `PM_SPECIAL|PM_HIDE|PM_HIDEVAL` — it never adds PM_RO_BY_DESIGN.
5510        // Carrying that bit here made printparamnode's c:Src/params.c:6157-6162
5511        // gate (`if (p->level != locallevel || p->level == 0) return;`)
5512        // swallow the row, so `zmodload zsh/system; typeset -p sysparams`
5513        // printed NOTHING where zsh prints `typeset -Ar sysparams`.
5514        // Only the 7 module rows declared outside Src/Modules/parameter.c
5515        // are plain PM_READONLY; the parameter.c rows that really do use
5516        // PM_READONLY_SPECIAL keep the bit.
5517        flags: PM_ARRAY as i32 | PM_READONLY as i32,
5518        getfn: crate::ported::modules::system::errnosgetfn,
5519        setfn: None,
5520        module: Some("zsh/system"),
5521    },
5522    // Src/Zle/zleparameter.c:132 SPECIALPMDEF("keymaps", PM_ARRAY|PM_READONLY, ...).
5523    PartabArrayEntry {
5524        name: "keymaps",
5525        // c:Src/Zle/zleparameter.c:132 — `SPECIALPMDEF("keymaps", PM_ARRAY|PM_READONLY, …)`.
5526        // PLAIN `PM_READONLY`, NOT `PM_READONLY_SPECIAL`
5527        // (= PM_SPECIAL|PM_READONLY|PM_RO_BY_DESIGN, c:Src/zsh.h:1925), and
5528        // `SPECIALPMDEF` (c:Src/zsh.h:2122) only ORs in
5529        // `PM_SPECIAL|PM_HIDE|PM_HIDEVAL` — it never adds PM_RO_BY_DESIGN.
5530        // Carrying that bit here made printparamnode's c:Src/params.c:6157-6162
5531        // gate (`if (p->level != locallevel || p->level == 0) return;`)
5532        // swallow the row, so `zmodload zsh/system; typeset -p sysparams`
5533        // printed NOTHING where zsh prints `typeset -Ar sysparams`.
5534        // Only the 7 module rows declared outside Src/Modules/parameter.c
5535        // are plain PM_READONLY; the parameter.c rows that really do use
5536        // PM_READONLY_SPECIAL keep the bit.
5537        flags: PM_ARRAY as i32 | PM_READONLY as i32,
5538        getfn: crate::ported::zle::zleparameter::keymapsgetfn,
5539        setfn: None,
5540        module: None,
5541    },
5542    // Src/Builtins/sched.c:382 SPECIALPMDEF("zsh_scheduled_events",
5543    // PM_ARRAY|PM_READONLY, &sched_gsu, NULL, NULL). Registered into
5544    // paramtab by zsh/sched's handlefeatures (via partab[] at c:381).
5545    // zshrs auto-loads zsh/sched and lists it in zsh_default_loaded
5546    // (module.rs:1134), so `$+zsh_scheduled_events` should read 1.
5547    // schedgetfn at sched.rs:582 walks the schedcmds linked list and
5548    // emits `<time>:<flags>:<cmd>` per entry.
5549    PartabArrayEntry {
5550        name: "zsh_scheduled_events",
5551        // c:Src/Builtins/sched.c:383 — `SPECIALPMDEF("zsh_scheduled_events", PM_ARRAY|PM_READONLY, …)`.
5552        // PLAIN `PM_READONLY`, NOT `PM_READONLY_SPECIAL`
5553        // (= PM_SPECIAL|PM_READONLY|PM_RO_BY_DESIGN, c:Src/zsh.h:1925), and
5554        // `SPECIALPMDEF` (c:Src/zsh.h:2122) only ORs in
5555        // `PM_SPECIAL|PM_HIDE|PM_HIDEVAL` — it never adds PM_RO_BY_DESIGN.
5556        // Carrying that bit here made printparamnode's c:Src/params.c:6157-6162
5557        // gate (`if (p->level != locallevel || p->level == 0) return;`)
5558        // swallow the row, so `zmodload zsh/system; typeset -p sysparams`
5559        // printed NOTHING where zsh prints `typeset -Ar sysparams`.
5560        // Only the 7 module rows declared outside Src/Modules/parameter.c
5561        // are plain PM_READONLY; the parameter.c rows that really do use
5562        // PM_READONLY_SPECIAL keep the bit.
5563        flags: PM_ARRAY as i32 | PM_READONLY as i32,
5564        getfn: crate::ported::builtins::sched::schedgetfn,
5565        setfn: None,
5566        module: None,
5567    },
5568    // Src/Modules/ksh93.c:123 — `PARAMDEF(".sh.match", PM_ARRAY|PM_READONLY,
5569    // NULL, &sh_match_gsu)`; sh_match_gsu (c:98-99) reads through
5570    // matchgetfn, which works on the node it is handed.
5571    PartabArrayEntry {
5572        name: ".sh.match",
5573        flags: PM_ARRAY as i32 | PM_READONLY as i32, // ksh93.c:123
5574        getfn: |_pm| crate::ported::modules::ksh93::matchgetfn(&mut crate::ported::zsh_h::param::default()),
5575        setfn: None,
5576        module: Some("zsh/ksh93"),
5577    },
5578];
5579
5580// partab_get / partab_scan_keys / partab_array_get dispatch helpers
5581// live in src/vm_helper.rs (outside src/ported/ — they're Rust-only
5582// convenience wrappers over the typed fn pointers, not C ports).
5583
5584// `module_features` — port of `static struct features module_features`
5585// from parameter.c:2300.
5586
5587/// Port of `setup_()` from `Src/Modules/parameter.c:2311` — C decl `setup_(UNUSED(Module m))`.
5588#[allow(unused_variables)]
5589pub fn setup_(m: *const module) -> i32 {
5590    // c:2311
5591    // C body c:2313-2314 — `return 0`. Faithful empty-body port.
5592    0
5593}
5594
5595/// Port of `features_()` from `Src/Modules/parameter.c:2318` — C decl `features_(Module m, char ***features)`.
5596pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
5597    // c:2318
5598    *features = featuresarray(m, module_features());
5599    0
5600}
5601
5602/// Port of `enables_()` from `Src/Modules/parameter.c:2326` — C decl `enables_(Module m, int **enables)`.
5603/// C body c:2328-2336 — wrap handlefeatures() with incleanup=1/0 so that
5604/// any feature removal does not perturb the main shell's parameter table.
5605pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
5606    // c:2326
5607    INCLEANUP.store(1, std::sync::atomic::Ordering::Relaxed); // c:2341
5608    let ret = handlefeatures(m, module_features(), enables); // c:2341
5609    INCLEANUP.store(0, std::sync::atomic::Ordering::Relaxed); // c:2341
5610    ret // c:2341
5611}
5612
5613/// Port of `boot_()` from `Src/Modules/parameter.c:2341` — C decl `boot_(UNUSED(Module m))`.
5614#[allow(unused_variables)]
5615pub fn boot_(m: *const module) -> i32 {
5616    // c:2341
5617    // C body c:2343-2344 — `return 0`. Faithful empty-body port; the
5618    //                      hash-magic params (parameters, commands,
5619    //                      functions, etc.) are registered via the
5620    //                      partab dispatch in features_/enables_.
5621    //
5622    // zshrs's bin entry skips the canonical handlefeatures chain,
5623    // so `crate::vm_helper::init_partab_params` is called directly
5624    // from ShellExecutor::new() to install PM_SPECIAL placeholder
5625    // Params for every PARTAB / PARTAB_ARRAY entry.
5626    0
5627}
5628
5629/// Port of `cleanup_()` from `Src/Modules/parameter.c:2348` — C decl `cleanup_(Module m)`.
5630/// C body c:2350-2354 — wrap setfeatureenables(NULL) with incleanup=1/0
5631/// matching the same guard enables_ uses.
5632pub fn cleanup_(m: *const module) -> i32 {
5633    // c:2348
5634    INCLEANUP.store(1, std::sync::atomic::Ordering::Relaxed); // c:2359
5635    let ret = setfeatureenables(m, module_features(), None); // c:2359
5636    INCLEANUP.store(0, std::sync::atomic::Ordering::Relaxed); // c:2359
5637    ret // c:2359
5638}
5639
5640/// Port of `finish_()` from `Src/Modules/parameter.c:2359` — C decl `finish_(UNUSED(Module m))`.
5641#[allow(unused_variables)]
5642pub fn finish_(m: *const module) -> i32 {
5643    // c:2359
5644    // C body c:2361-2362 — `return 0`. Faithful empty-body port; the
5645    //                      hash-magic params get unregistered via the
5646    //                      partab dispatch in cleanup_.
5647    0
5648}
5649
5650// =====================================================================
5651// !!! WARNING: RUST-ONLY STATE — NO DIRECT C COUNTERPART !!!
5652// =====================================================================
5653//
5654// `ALIAS_GSU_HANDLER` records which `pm*alias_gsu` static dispatch
5655// table assignaliasdefs() selected for each parameter name. The C
5656// source stores this directly on `Param->gsu.s` as a function-table
5657// pointer (Src/Modules/parameter.c:1842-1860). Until the gsu_scalar
5658// dispatch table machinery is ported in full, this side-map is the
5659// bridge so future gsu lookups can resolve the right handler.
5660//
5661// !!! Remove this side-map once the gsu_scalar dispatch table is
5662// ported in src/ported/params.rs and assignaliasdefs() can write
5663// `pm->gsu.s = &pmralias_gsu` directly. !!!
5664// =====================================================================
5665static ALIAS_GSU_HANDLER: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
5666
5667// File-static globals for parameter.c port — c:38-44, src/init.c.
5668// `dirstack` lives in src/exec.c globals; `funcstack` in src/init.c.
5669// Mirror as Mutex<Vec<...>> for cross-thread safety.
5670/// `DIRSTACK` static.
5671pub static DIRSTACK: Mutex<Vec<String>> = Mutex::new(Vec::new());
5672/// `INCLEANUP` static.
5673pub static INCLEANUP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
5674
5675// `funcstack` global from Src/exec.c:340 — head of the active shell
5676// function call stack. Rust port mirrors the chain as Vec snapshot
5677// (the C source walks `funcstack->prev` to produce array params).
5678/// `FUNCSTACK` static.
5679///
5680/// Storage is [`crate::thread_shell_state::ThreadMutex`], not a plain
5681/// `Mutex`. C's `funcstack` (Src/exec.c:340) is the call chain of the ONE
5682/// thread of execution, and it is what `$funcstack`, `$functrace`,
5683/// `$funcfiletrace` and `$funcsourcetrace` report (c:Src/Modules/
5684/// parameter.c:650/681). zshrs runs `async_precmd` hook functions on a
5685/// pool worker and the completer tree on `compsys::in_editor`'s own
5686/// thread, each of which pushes a `doshfunc` frame (exec.rs, c:6210) — so
5687/// with one shared Vec the shell thread's `$funcstack` grew frames for
5688/// functions it never called. `ThreadMutex` keeps the `lock()` API, so
5689/// every reader below is unchanged.
5690pub static FUNCSTACK: crate::thread_shell_state::ThreadMutex<Vec<crate::ported::zsh_h::funcstack>> =
5691    crate::thread_shell_state::ThreadMutex::new(Vec::new(), Vec::new);
5692
5693// =====================================================================
5694// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART !!!
5695// =====================================================================
5696//
5697// `make_empty_special_pm` is the common Param-construction shape
5698// used by getpmjob{dir,state,text} and getpmmodule when the backing
5699// data isn't reachable from src/ported/. The C source duplicates
5700// this 12-line construct inline at each callsite (c:1387/c:1459/
5701// c:1279/c:1042); Rust pulls it into one helper to avoid the
5702// repetition. NOT a new abstraction — the same struct fields, the
5703// same flag combination, the same "u.str = empty" placeholder that
5704// the executor-side caller overwrites with the live value.
5705//
5706// !!! Do NOT use for getpm* tables whose data IS reachable from
5707// src/ported/ (cmdnamtab, BUILTINS, shfunctab, aliastab, optns,
5708// nameddirtab via passwd) — those compose their value inline. !!!
5709// =====================================================================
5710
5711/// !!! WARNING: RUST-ONLY HELPER !!! — no counterpart in Src/Modules/parameter.c;
5712/// Rust-only `Param` constructor helper; C uses raw struct init
5713/// (equivalent C logic at Src/Modules/parameter.c:1284).
5714/// !!! RUST-ONLY HELPER — see WARNING block above. Synthesises a
5715/// PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL Param with empty
5716/// `u.str`.
5717fn make_empty_special_pm(name: &str) -> Param {
5718    Box::new(param {
5719        node: hashnode {
5720            next: None,
5721            nam: name.to_string(),
5722            flags: (PM_SCALAR | PM_READONLY | PM_UNSET | PM_SPECIAL) as i32,
5723        },
5724        u_data: 0,
5725        u_tied: None,
5726        u_arr: None,
5727        u_str: Some(String::new()),
5728        u_val: 0,
5729        u_dval: 0.0,
5730        u_hash: None,
5731        gsu_s: None,
5732        gsu_i: None,
5733        gsu_f: None,
5734        gsu_a: None,
5735        gsu_h: None,
5736        base: 0,
5737        width: 0,
5738        env: None,
5739        ename: None,
5740        old: None,
5741        level: 0,
5742    })
5743}
5744
5745static MODULE_FEATURES: OnceLock<Mutex<crate::ported::zsh_h::features>> = OnceLock::new();
5746
5747// Local stubs for the per-module entry points. C uses generic
5748// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
5749// 3279/3350/3388) but those take `Builtin` + `Features` pointer
5750// fields the Rust port doesn't carry. The hardcoded descriptor
5751// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
5752// !!! WARNING: RUST-ONLY HELPER !!! — no counterpart in Src/Modules/parameter.c;
5753// Rust-only module-framework shim.
5754// C uses generic featuresarray/handlefeatures/setfeatureenables from
5755// Src/module.c:3279/3350/3388 with C-side Builtin/Features pointers;
5756// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5757fn featuresarray(_m: *const module, _f: &Mutex<crate::ported::zsh_h::features>) -> Vec<String> {
5758    vec![
5759        "p:aliases".to_string(),
5760        "p:builtins".to_string(),
5761        "p:commands".to_string(),
5762        "p:dirstack".to_string(),
5763        "p:dis_aliases".to_string(),
5764        "p:dis_builtins".to_string(),
5765        "p:dis_functions".to_string(),
5766        "p:dis_functions_source".to_string(),
5767        "p:dis_galiases".to_string(),
5768        "p:dis_patchars".to_string(),
5769        "p:dis_reswords".to_string(),
5770        "p:dis_saliases".to_string(),
5771        "p:funcfiletrace".to_string(),
5772        "p:funcsourcetrace".to_string(),
5773        "p:funcstack".to_string(),
5774        "p:functions".to_string(),
5775        "p:functions_source".to_string(),
5776        "p:functrace".to_string(),
5777        "p:galiases".to_string(),
5778        "p:history".to_string(),
5779        "p:historywords".to_string(),
5780        "p:jobdirs".to_string(),
5781        "p:jobstates".to_string(),
5782        "p:jobtexts".to_string(),
5783        "p:modules".to_string(),
5784        "p:nameddirs".to_string(),
5785        "p:options".to_string(),
5786        "p:parameters".to_string(),
5787        "p:patchars".to_string(),
5788        "p:reswords".to_string(),
5789        "p:saliases".to_string(),
5790        "p:userdirs".to_string(),
5791        "p:usergroups".to_string(),
5792    ]
5793}
5794
5795// !!! WARNING: RUST-ONLY HELPER !!! — no counterpart in Src/Modules/parameter.c;
5796// Rust-only module-framework shim.
5797// C uses generic featuresarray/handlefeatures/setfeatureenables from
5798// Src/module.c:3279/3350/3388 with C-side Builtin/Features pointers;
5799// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5800fn handlefeatures(
5801    m: *const module,
5802    f: &Mutex<crate::ported::zsh_h::features>,
5803    enables: &mut Option<Vec<i32>>,
5804) -> i32 {
5805    // c:3392 — the name-keyed variant in src/ported/module.rs; this
5806    // module ships no `Features` descriptor tables for the per-feature
5807    // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
5808    crate::ported::module::handlefeatures("zsh/parameter", &featuresarray(m, f), enables)
5809}
5810
5811// !!! WARNING: RUST-ONLY HELPER !!! — no counterpart in Src/Modules/parameter.c;
5812// Rust-only module-framework shim.
5813// C uses generic featuresarray/handlefeatures/setfeatureenables from
5814// Src/module.c:3279/3350/3388 with C-side Builtin/Features pointers;
5815// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5816fn setfeatureenables(
5817    m: *const module,
5818    f: &Mutex<crate::ported::zsh_h::features>,
5819    e: Option<&[i32]>,
5820) -> i32 {
5821    // c:Src/module.c:3354-3382 — this module has only the partab block
5822    // (c:3376-3378 setparamdefs). cleanup_ (c:2350-2354) passes `e == NULL`,
5823    // which deletes every parameter, so after `zmodload -u zsh/parameter`
5824    // the names fall back to the autoload stubs and the next read reloads
5825    // the module (c:Src/params.c:544-566 loadparamnode).
5826    crate::ported::module::setfeatureenables("zsh/parameter", &featuresarray(m, f), e)
5827}
5828
5829// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5830// ─── RUST-ONLY ACCESSORS ───
5831//
5832// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
5833// RwLock<T>>` globals declared above. C zsh uses direct global
5834// access; Rust needs these wrappers because `OnceLock::get_or_init`
5835// is the only way to lazily construct shared state. These ported sit
5836// here so the body of this file reads in C source order without
5837// the accessor wrappers interleaved between real port ported.
5838// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5839
5840// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5841// ─── RUST-ONLY ACCESSORS ───
5842//
5843// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
5844// RwLock<T>>` globals declared above. C zsh uses direct global
5845// access; Rust needs these wrappers because `OnceLock::get_or_init`
5846// is the only way to lazily construct shared state. These ported sit
5847// here so the body of this file reads in C source order without
5848// the accessor wrappers interleaved between real port ported.
5849// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5850
5851// !!! WARNING: RUST-ONLY HELPER !!! — no counterpart in Src/Modules/parameter.c;
5852// Rust-only module-framework shim.
5853// C uses generic featuresarray/handlefeatures/setfeatureenables from
5854// Src/module.c:3279/3350/3388 with C-side Builtin/Features pointers;
5855// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5856fn module_features() -> &'static Mutex<crate::ported::zsh_h::features> {
5857    MODULE_FEATURES.get_or_init(|| {
5858        Mutex::new(crate::ported::zsh_h::features {
5859            bn_list: None,
5860            bn_size: 0,
5861            cd_list: None,
5862            cd_size: 0,
5863            mf_list: None,
5864            mf_size: 0,
5865            pd_list: None,
5866            pd_size: 33,
5867            n_abstract: 0,
5868        })
5869    })
5870}
5871
5872#[cfg(test)]
5873mod scan_callback_tests {
5874    use super::*;
5875    use std::sync::atomic::{AtomicI32, Ordering};
5876
5877    use crate::ported::zsh_h::param;
5878
5879    // Module-scoped collector statics. Tests are serialised by name +
5880    // each test resets before/after so cross-test bleed is impossible.
5881    static COLLECTED_COUNT: AtomicI32 = AtomicI32::new(0);
5882    static LAST_NAME_LEN: AtomicI32 = AtomicI32::new(0);
5883
5884    fn counting_func(node: &param, _flags: i32) {
5885        COLLECTED_COUNT.fetch_add(1, Ordering::SeqCst);
5886        LAST_NAME_LEN.store(node.node.nam.len() as i32, Ordering::SeqCst);
5887    }
5888
5889    fn reset_counters() {
5890        COLLECTED_COUNT.store(0, Ordering::SeqCst);
5891        LAST_NAME_LEN.store(0, Ordering::SeqCst);
5892    }
5893
5894    /// c:139-145 — scanpmparameters walks realparamtab calling func per
5895    /// non-PM_UNSET entry. Seed paramtab with one entry, verify the
5896    /// callback fires exactly once with the right name.
5897    #[test]
5898    fn scanpmparameters_invokes_func_per_param() {
5899        let _g = crate::test_util::global_state_lock();
5900        reset_counters();
5901        // Seed realparamtab.
5902        let pm = param {
5903            node: hashnode {
5904                next: None,
5905                nam: "ZSHRS_TEST_SP_A".to_string(),
5906                flags: PM_SCALAR as i32,
5907            },
5908            u_data: 0,
5909            u_tied: None,
5910            u_arr: None,
5911            u_str: Some("v".to_string()),
5912            u_val: 0,
5913            u_dval: 0.0,
5914            u_hash: None,
5915            gsu_s: None,
5916            gsu_i: None,
5917            gsu_f: None,
5918            gsu_a: None,
5919            gsu_h: None,
5920            base: 0,
5921            width: 0,
5922            env: None,
5923            ename: None,
5924            old: None,
5925            level: 0,
5926        };
5927        realparamtab()
5928            .write()
5929            .unwrap()
5930            .insert("ZSHRS_TEST_SP_A".to_string(), Box::new(pm));
5931        scanpmparameters(std::ptr::null_mut(), Some(counting_func), 0);
5932        let observed = COLLECTED_COUNT.load(Ordering::SeqCst);
5933        // Cleanup before asserting so failures don't leak state.
5934        realparamtab().write().unwrap().remove("ZSHRS_TEST_SP_A");
5935        assert!(
5936            observed >= 1,
5937            "callback fires at least once for the seeded param (got {})",
5938            observed
5939        );
5940    }
5941
5942    /// c:1199 — scanpmhistory walks hist_ring newest→oldest. With an
5943    /// empty ring the loop body never runs → zero callback invocations.
5944    /// A regression that ran an iter on the sentinel head would emit
5945    /// a spurious extra entry; this test catches it.
5946    #[test]
5947    fn scanpmhistory_empty_ring_invokes_zero_callbacks() {
5948        let _g = crate::test_util::global_state_lock();
5949        reset_counters();
5950        let snapshot: Vec<_> = hist_ring.lock().unwrap().drain(..).collect();
5951        scanpmhistory(std::ptr::null_mut(), Some(counting_func), 0);
5952        let observed = COLLECTED_COUNT.load(Ordering::SeqCst);
5953        hist_ring.lock().unwrap().extend(snapshot);
5954        assert_eq!(observed, 0);
5955    }
5956
5957    /// c:1255 — `pmjobtext` joins each proc's text with " | " (the
5958    /// canonical pipeline-display format). Empty job table → empty
5959    /// string. Regression returning the wrong separator would corrupt
5960    /// every `${jobtexts[1]}` query users hit.
5961    #[test]
5962    fn pmjobtext_empty_jobtab_returns_empty() {
5963        let _g = crate::test_util::global_state_lock();
5964        let s = pmjobtext(std::ptr::null_mut(), 1);
5965        assert_eq!(s, "");
5966    }
5967
5968    /// c:1340 — `pmjobstate` for a job index past the end of jobtab
5969    /// returns empty (defensive). Catches a regression that panics on
5970    /// out-of-range queries.
5971    #[test]
5972    fn pmjobstate_out_of_range_returns_empty() {
5973        let _g = crate::test_util::global_state_lock();
5974        let s = pmjobstate(std::ptr::null_mut(), 9999);
5975        assert_eq!(s, "");
5976    }
5977
5978    /// c:1447 — `pmjobdir` for missing job falls back to global pwd
5979    /// (current_dir). Verify the fallback path returns a non-empty
5980    /// path string.
5981    #[test]
5982    fn pmjobdir_missing_job_falls_back_to_cwd() {
5983        let _g = crate::test_util::global_state_lock();
5984        let s = pmjobdir(std::ptr::null_mut(), 9999);
5985        assert!(
5986            !s.is_empty(),
5987            "fallback to cwd must produce a non-empty path"
5988        );
5989        assert!(
5990            s.starts_with('/') || s == "" || cfg!(not(unix)),
5991            "Unix path must be absolute (got {s:?})"
5992        );
5993    }
5994
5995    /// c:1040 — `getpmmodule(name)` for an unknown module name
5996    /// returns Some with PM_UNSET|PM_SPECIAL flags AND empty u_str
5997    /// per c:1068-1069. Regression returning Some("loaded") would
5998    /// silently lie about which modules are loaded.
5999    #[test]
6000    fn getpmmodule_unknown_module_marks_unset() {
6001        let _g = crate::test_util::global_state_lock();
6002        let pm = getpmmodule(std::ptr::null_mut(), "definitely_not_a_loaded_module_xyz")
6003            .expect("must return Some");
6004        assert_eq!(
6005            pm.u_str.as_deref(),
6006            Some(""),
6007            "unknown module → empty value string"
6008        );
6009        assert_ne!(
6010            pm.node.flags & PM_UNSET as i32,
6011            0,
6012            "unknown module must set PM_UNSET"
6013        );
6014        assert_ne!(
6015            pm.node.flags & PM_SPECIAL as i32,
6016            0,
6017            "unknown module must set PM_SPECIAL"
6018        );
6019    }
6020}
6021
6022#[cfg(test)]
6023mod setalias_tests {
6024    use super::*;
6025    use crate::ported::zsh_h::param;
6026
6027    /// setalias wires `aliastab.add(createaliasnode(name, value, flags))`
6028    /// per c:1701-1702. After call, aliastab should contain the new
6029    /// alias with the given value.
6030    #[test]
6031    fn setalias_inserts_entry_into_aliastab() {
6032        let _g = crate::test_util::global_state_lock();
6033        let pm = param {
6034            node: hashnode {
6035                next: None,
6036                nam: "zshrs_test_alias_x".to_string(),
6037                flags: PM_SCALAR as i32,
6038            },
6039            u_data: 0,
6040            u_tied: None,
6041            u_arr: None,
6042            u_str: None,
6043            u_val: 0,
6044            u_dval: 0.0,
6045            u_hash: None,
6046            gsu_s: None,
6047            gsu_i: None,
6048            gsu_f: None,
6049            gsu_a: None,
6050            gsu_h: None,
6051            base: 0,
6052            width: 0,
6053            env: None,
6054            ename: None,
6055            old: None,
6056            level: 0,
6057        };
6058        setalias(std::ptr::null_mut(), Box::new(pm), "echo hi".to_string(), 0);
6059        let tab = aliastab_lock().read().expect("aliastab poisoned");
6060        let entry = tab.get("zshrs_test_alias_x");
6061        assert!(entry.is_some(), "setalias must add to aliastab");
6062        if let Some(a) = entry {
6063            assert_eq!(a.text, "echo hi", "alias value matches createaliasnode arg");
6064        }
6065    }
6066}
6067
6068#[cfg(test)]
6069mod paramtypestr_table_tests {
6070    use super::*;
6071    use crate::ported::zsh_h::param;
6072
6073    fn pm(flags: u32) -> param {
6074        param {
6075            node: hashnode {
6076                next: None,
6077                nam: String::new(),
6078                flags: flags as i32,
6079            },
6080            u_data: 0,
6081            u_tied: None,
6082            u_arr: None,
6083            u_str: None,
6084            u_val: 0,
6085            u_dval: 0.0,
6086            u_hash: None,
6087            gsu_s: None,
6088            gsu_i: None,
6089            gsu_f: None,
6090            gsu_a: None,
6091            gsu_h: None,
6092            base: 0,
6093            width: 0,
6094            env: None,
6095            ename: None,
6096            old: None,
6097            level: 0,
6098        }
6099    }
6100
6101    /// c:43 — paramtypestr's per-flag dispatch table emits the type
6102    /// name `${(t)foo}` reports. Each PM_TYPE bit pattern maps to
6103    /// a distinct user-visible string. A regression that emits
6104    /// `"scalar"` for every type would break every typeset-introspecting
6105    /// script (many shell scripts grep for "array"/"integer" output).
6106    #[test]
6107    fn integer_param_renders_as_integer() {
6108        let _g = crate::test_util::global_state_lock();
6109        assert_eq!(paramtypestr(&pm(PM_INTEGER)), "integer");
6110    }
6111
6112    #[test]
6113    fn float_e_param_renders_as_float() {
6114        let _g = crate::test_util::global_state_lock();
6115        assert_eq!(paramtypestr(&pm(PM_EFLOAT)), "float");
6116    }
6117
6118    #[test]
6119    fn float_f_param_renders_as_float() {
6120        let _g = crate::test_util::global_state_lock();
6121        assert_eq!(paramtypestr(&pm(PM_FFLOAT)), "float");
6122    }
6123
6124    #[test]
6125    fn array_param_renders_as_array() {
6126        let _g = crate::test_util::global_state_lock();
6127        assert_eq!(paramtypestr(&pm(PM_ARRAY)), "array");
6128    }
6129
6130    #[test]
6131    fn hashed_param_renders_as_association() {
6132        let _g = crate::test_util::global_state_lock();
6133        assert_eq!(paramtypestr(&pm(PM_HASHED)), "association");
6134    }
6135
6136    /// c:43 — `${(t)foo}` includes per-modifier suffixes for readonly /
6137    /// exported / local. They appear after the type name separated by
6138    /// `-`. Regression dropping any modifier breaks typeset-output
6139    /// parsing in user scripts.
6140    #[test]
6141    fn readonly_modifier_appears_after_type_name() {
6142        let _g = crate::test_util::global_state_lock();
6143        let s = paramtypestr(&pm(PM_INTEGER | PM_READONLY));
6144        assert!(
6145            s.contains("readonly"),
6146            "PM_READONLY must appear in type-string (got {s:?})"
6147        );
6148    }
6149
6150    /// c:81-82 — PM_EXPORTED renders as `-export` (note: NOT
6151    /// `-exported`; this is the canonical zsh suffix). Catches a
6152    /// regression where the suffix changes spelling.
6153    #[test]
6154    fn exported_modifier_renders_as_export_suffix() {
6155        let _g = crate::test_util::global_state_lock();
6156        let s = paramtypestr(&pm(PM_INTEGER | PM_EXPORTED));
6157        assert!(
6158            s.contains("-export"),
6159            "PM_EXPORTED must produce '-export' suffix (got {s:?})"
6160        );
6161    }
6162
6163    /// c:63-64 — `-local` suffix is gated on `pm.level != 0`, NOT a
6164    /// PM_LOCAL flag (which is a different concept). Verifies the
6165    /// level-based rendering path; regression flipping the gate would
6166    /// break `local foo` reporting in nested function scopes.
6167    #[test]
6168    fn local_modifier_renders_when_level_nonzero() {
6169        let _g = crate::test_util::global_state_lock();
6170        let mut p = pm(PM_INTEGER);
6171        p.level = 1;
6172        let s = paramtypestr(&p);
6173        assert!(
6174            s.contains("-local"),
6175            "level>0 must add '-local' (got {s:?})"
6176        );
6177    }
6178
6179    /// `Src/Modules/parameter.c:48 + c:91-92` — PM_UNSET short-circuits
6180    /// to empty string BEFORE the type/modifier dispatch. Pin this
6181    /// guard: a regression that drops the PM_UNSET check would emit
6182    /// stale type labels for unset params, leaking PM state through
6183    /// `${(t)varname}` for never-assigned params.
6184    #[test]
6185    fn unset_param_renders_as_empty_string() {
6186        let _g = crate::test_util::global_state_lock();
6187        // Even with type + modifier flags set, PM_UNSET wins.
6188        let s = paramtypestr(&pm(PM_INTEGER | PM_UNSET | PM_READONLY));
6189        assert_eq!(
6190            s, "",
6191            "c:48,91-92 — PM_UNSET wins over every type + modifier"
6192        );
6193    }
6194
6195    /// `Src/Modules/parameter.c:49-50` — PM_AUTOLOAD emits "undefined"
6196    /// regardless of any other type/modifier bits set. Pin both the
6197    /// exact string and the precedence over PM_INTEGER etc.
6198    #[test]
6199    fn autoload_param_renders_as_undefined() {
6200        let _g = crate::test_util::global_state_lock();
6201        assert_eq!(
6202            paramtypestr(&pm(PM_AUTOLOAD)),
6203            "undefined",
6204            "c:49-50 — PM_AUTOLOAD → 'undefined'"
6205        );
6206        // Even with type bits + modifiers set, AUTOLOAD wins.
6207        let s = paramtypestr(&pm(PM_AUTOLOAD | PM_INTEGER | PM_READONLY));
6208        assert_eq!(
6209            s, "undefined",
6210            "c:49-50 — PM_AUTOLOAD precedence over type+modifier"
6211        );
6212    }
6213
6214    /// `Src/Modules/parameter.c:53` — PM_SCALAR has value 0 (all type
6215    /// bits clear). A bare param with no type bits set renders as
6216    /// "scalar". Pin so a regression that emits "" or "unknown" for
6217    /// the zero-type case breaks the most-common `${(t)foo}` path.
6218    #[test]
6219    fn scalar_param_renders_as_scalar() {
6220        let _g = crate::test_util::global_state_lock();
6221        assert_eq!(
6222            paramtypestr(&pm(PM_SCALAR)),
6223            "scalar",
6224            "c:53 — bare PM_SCALAR (zero type bits) → 'scalar'"
6225        );
6226    }
6227
6228    /// `Src/Modules/parameter.c:54` — PM_NAMEREF renders as "nameref".
6229    /// Catches a regression that omits the nameref branch (zsh added
6230    /// nameref support in 5.10+).
6231    #[test]
6232    fn nameref_param_renders_as_nameref() {
6233        let _g = crate::test_util::global_state_lock();
6234        assert_eq!(
6235            paramtypestr(&pm(PM_NAMEREF)),
6236            "nameref",
6237            "c:54 — PM_NAMEREF → 'nameref'"
6238        );
6239    }
6240
6241    /// `Src/Modules/parameter.c:65-90` — Every modifier flag adds a
6242    /// `-suffix`. Sweep all eight modifiers (LEFT/RIGHT_B/RIGHT_Z/
6243    /// LOWER/UPPER/TAGGED/TIED/UNIQUE/HIDE/HIDEVAL/SPECIAL) so a
6244    /// regression silently dropping one breaks `${(t)foo}` typeset
6245    /// output for that flag.
6246    #[test]
6247    fn left_modifier_renders_dash_left_suffix() {
6248        let _g = crate::test_util::global_state_lock();
6249        let s = paramtypestr(&pm(PM_SCALAR | PM_LEFT));
6250        assert!(
6251            s.contains("-left"),
6252            "c:65-66 — PM_LEFT → '-left' (got {s:?})"
6253        );
6254    }
6255
6256    #[test]
6257    fn right_b_modifier_renders_dash_right_blanks_suffix() {
6258        let _g = crate::test_util::global_state_lock();
6259        let s = paramtypestr(&pm(PM_SCALAR | PM_RIGHT_B));
6260        assert!(
6261            s.contains("-right_blanks"),
6262            "c:67-68 — PM_RIGHT_B → '-right_blanks' (got {s:?})"
6263        );
6264    }
6265
6266    #[test]
6267    fn right_z_modifier_renders_dash_right_zeros_suffix() {
6268        let _g = crate::test_util::global_state_lock();
6269        let s = paramtypestr(&pm(PM_SCALAR | PM_RIGHT_Z));
6270        assert!(
6271            s.contains("-right_zeros"),
6272            "c:69-70 — PM_RIGHT_Z → '-right_zeros' (got {s:?})"
6273        );
6274    }
6275
6276    #[test]
6277    fn lower_modifier_renders_dash_lower_suffix() {
6278        let _g = crate::test_util::global_state_lock();
6279        let s = paramtypestr(&pm(PM_SCALAR | PM_LOWER));
6280        assert!(
6281            s.contains("-lower"),
6282            "c:71-72 — PM_LOWER → '-lower' (got {s:?})"
6283        );
6284    }
6285
6286    #[test]
6287    fn upper_modifier_renders_dash_upper_suffix() {
6288        let _g = crate::test_util::global_state_lock();
6289        let s = paramtypestr(&pm(PM_SCALAR | PM_UPPER));
6290        assert!(
6291            s.contains("-upper"),
6292            "c:73-74 — PM_UPPER → '-upper' (got {s:?})"
6293        );
6294    }
6295
6296    #[test]
6297    fn tagged_modifier_renders_dash_tag_suffix() {
6298        let _g = crate::test_util::global_state_lock();
6299        let s = paramtypestr(&pm(PM_SCALAR | PM_TAGGED));
6300        assert!(
6301            s.contains("-tag"),
6302            "c:77-78 — PM_TAGGED → '-tag' (got {s:?})"
6303        );
6304    }
6305
6306    #[test]
6307    fn tied_modifier_renders_dash_tied_suffix() {
6308        let _g = crate::test_util::global_state_lock();
6309        let s = paramtypestr(&pm(PM_SCALAR | PM_TIED));
6310        assert!(
6311            s.contains("-tied"),
6312            "c:79-80 — PM_TIED → '-tied' (got {s:?})"
6313        );
6314    }
6315
6316    #[test]
6317    fn unique_modifier_renders_dash_unique_suffix() {
6318        let _g = crate::test_util::global_state_lock();
6319        let s = paramtypestr(&pm(PM_SCALAR | PM_UNIQUE));
6320        assert!(
6321            s.contains("-unique"),
6322            "c:83-84 — PM_UNIQUE → '-unique' (got {s:?})"
6323        );
6324    }
6325
6326    #[test]
6327    fn hide_modifier_renders_dash_hide_suffix() {
6328        let _g = crate::test_util::global_state_lock();
6329        let s = paramtypestr(&pm(PM_SCALAR | PM_HIDE));
6330        assert!(
6331            s.contains("-hide"),
6332            "c:85-86 — PM_HIDE → '-hide' (got {s:?})"
6333        );
6334    }
6335
6336    #[test]
6337    fn hideval_modifier_renders_dash_hideval_suffix() {
6338        let _g = crate::test_util::global_state_lock();
6339        let s = paramtypestr(&pm(PM_SCALAR | PM_HIDEVAL));
6340        assert!(
6341            s.contains("-hideval"),
6342            "c:87-88 — PM_HIDEVAL → '-hideval' (got {s:?})"
6343        );
6344    }
6345
6346    #[test]
6347    fn special_modifier_renders_dash_special_suffix() {
6348        let _g = crate::test_util::global_state_lock();
6349        let s = paramtypestr(&pm(PM_SCALAR | PM_SPECIAL));
6350        assert!(
6351            s.contains("-special"),
6352            "c:89-90 — PM_SPECIAL → '-special' (got {s:?})"
6353        );
6354    }
6355
6356    /// `Src/Modules/parameter.c:43-94` — Multiple modifiers stack in C
6357    /// source order (level → LEFT → RIGHT_B → RIGHT_Z → LOWER → UPPER
6358    /// → READONLY → TAGGED → TIED → EXPORTED → UNIQUE → HIDE →
6359    /// HIDEVAL → SPECIAL). Pin the order so a regen that reshuffles
6360    /// the branches changes `${(t)foo}` output across the whole zsh
6361    /// ecosystem.
6362    #[test]
6363    fn multiple_modifiers_concatenate_in_c_source_order() {
6364        let _g = crate::test_util::global_state_lock();
6365        let mut p = pm(PM_INTEGER | PM_LEFT | PM_READONLY | PM_EXPORTED);
6366        p.level = 1;
6367        let s = paramtypestr(&p);
6368        // c:43-94 emits left BEFORE readonly BEFORE export.
6369        let i_left = s.find("-left").expect("missing -left");
6370        let i_ro = s.find("-readonly").expect("missing -readonly");
6371        let i_exp = s.find("-export").expect("missing -export");
6372        let i_local = s.find("-local").expect("missing -local");
6373        // c:63-64 — local is FIRST (level check fires before any flag).
6374        assert!(i_local < i_left, "c:63-64 — -local must precede -left");
6375        assert!(i_left < i_ro, "c:65-76 — -left must precede -readonly");
6376        assert!(i_ro < i_exp, "c:75-82 — -readonly must precede -export");
6377    }
6378
6379    // ═══════════════════════════════════════════════════════════════════
6380    // paramtypestr — per-flag combination pinning. Each test builds a
6381    // param with a specific flag set and asserts the exact return string.
6382    // ═══════════════════════════════════════════════════════════════════
6383
6384    fn mk_pm(flags: u32, level: i32) -> param {
6385        param {
6386            node: hashnode {
6387                next: None,
6388                nam: String::new(),
6389                flags: flags as i32,
6390            },
6391            u_data: 0,
6392            u_tied: None,
6393            u_arr: None,
6394            u_str: None,
6395            u_val: 0,
6396            u_dval: 0.0,
6397            u_hash: None,
6398            gsu_s: None,
6399            gsu_i: None,
6400            gsu_f: None,
6401            gsu_a: None,
6402            gsu_h: None,
6403            base: 0,
6404            width: 0,
6405            env: None,
6406            ename: None,
6407            old: None,
6408            level,
6409        }
6410    }
6411
6412    /// PM_SCALAR (flag value 0) → "scalar".
6413    #[test]
6414    fn paramtypestr_scalar_with_no_flags_is_scalar() {
6415        let pm = mk_pm(PM_SCALAR, 0);
6416        assert_eq!(paramtypestr(&pm), "scalar");
6417    }
6418
6419    /// PM_INTEGER → "integer".
6420    #[test]
6421    fn paramtypestr_integer_flag_is_integer() {
6422        let pm = mk_pm(PM_INTEGER, 0);
6423        assert_eq!(paramtypestr(&pm), "integer");
6424    }
6425
6426    /// PM_ARRAY → "array".
6427    #[test]
6428    fn paramtypestr_array_flag_is_array() {
6429        let pm = mk_pm(PM_ARRAY, 0);
6430        assert_eq!(paramtypestr(&pm), "array");
6431    }
6432
6433    /// PM_HASHED → "association".
6434    #[test]
6435    fn paramtypestr_hashed_flag_is_association() {
6436        let pm = mk_pm(PM_HASHED, 0);
6437        assert_eq!(paramtypestr(&pm), "association");
6438    }
6439
6440    /// PM_NAMEREF → "nameref".
6441    #[test]
6442    fn paramtypestr_nameref_flag_is_nameref() {
6443        let pm = mk_pm(PM_NAMEREF, 0);
6444        assert_eq!(paramtypestr(&pm), "nameref");
6445    }
6446
6447    /// PM_EFLOAT → "float".
6448    #[test]
6449    fn paramtypestr_efloat_flag_is_float() {
6450        let pm = mk_pm(PM_EFLOAT, 0);
6451        assert_eq!(paramtypestr(&pm), "float");
6452    }
6453
6454    /// PM_FFLOAT → "float".
6455    #[test]
6456    fn paramtypestr_ffloat_flag_is_float() {
6457        let pm = mk_pm(PM_FFLOAT, 0);
6458        assert_eq!(paramtypestr(&pm), "float");
6459    }
6460
6461    /// PM_UNSET shortcut returns empty string regardless of other flags.
6462    /// c:48-92 — `if (PM_UNSET) return ""`.
6463    #[test]
6464    fn paramtypestr_unset_short_circuits_to_empty() {
6465        let pm = mk_pm(PM_UNSET | PM_INTEGER, 0);
6466        assert_eq!(paramtypestr(&pm), "");
6467    }
6468
6469    /// PM_AUTOLOAD → "undefined" (precedes the type switch).
6470    #[test]
6471    fn paramtypestr_autoload_overrides_type_to_undefined() {
6472        let pm = mk_pm(PM_AUTOLOAD | PM_INTEGER, 0);
6473        assert_eq!(paramtypestr(&pm), "undefined");
6474    }
6475
6476    // ─ Suffix combinations ─────────────────────────────────────────
6477    /// PM_INTEGER + PM_READONLY → "integer-readonly".
6478    #[test]
6479    fn paramtypestr_integer_readonly_combo() {
6480        let pm = mk_pm(PM_INTEGER | PM_READONLY, 0);
6481        assert_eq!(paramtypestr(&pm), "integer-readonly");
6482    }
6483
6484    /// PM_SCALAR + PM_EXPORTED → "scalar-export".
6485    #[test]
6486    fn paramtypestr_scalar_exported_combo() {
6487        let pm = mk_pm(PM_SCALAR | PM_EXPORTED, 0);
6488        assert_eq!(paramtypestr(&pm), "scalar-export");
6489    }
6490
6491    /// PM_ARRAY + PM_UNIQUE → "array-unique".
6492    #[test]
6493    fn paramtypestr_array_unique_combo() {
6494        let pm = mk_pm(PM_ARRAY | PM_UNIQUE, 0);
6495        assert_eq!(paramtypestr(&pm), "array-unique");
6496    }
6497
6498    /// PM_SCALAR + PM_LOWER → "scalar-lower".
6499    #[test]
6500    fn paramtypestr_scalar_lower_combo() {
6501        let pm = mk_pm(PM_SCALAR | PM_LOWER, 0);
6502        assert_eq!(paramtypestr(&pm), "scalar-lower");
6503    }
6504
6505    /// PM_SCALAR + PM_UPPER → "scalar-upper".
6506    #[test]
6507    fn paramtypestr_scalar_upper_combo() {
6508        let pm = mk_pm(PM_SCALAR | PM_UPPER, 0);
6509        assert_eq!(paramtypestr(&pm), "scalar-upper");
6510    }
6511
6512    /// PM_SCALAR + PM_LEFT → "scalar-left".
6513    #[test]
6514    fn paramtypestr_scalar_left_combo() {
6515        let pm = mk_pm(PM_SCALAR | PM_LEFT, 0);
6516        assert_eq!(paramtypestr(&pm), "scalar-left");
6517    }
6518
6519    /// PM_SCALAR + PM_RIGHT_B → "scalar-right_blanks".
6520    #[test]
6521    fn paramtypestr_scalar_right_blanks_combo() {
6522        let pm = mk_pm(PM_SCALAR | PM_RIGHT_B, 0);
6523        assert_eq!(paramtypestr(&pm), "scalar-right_blanks");
6524    }
6525
6526    /// PM_SCALAR + PM_RIGHT_Z → "scalar-right_zeros".
6527    #[test]
6528    fn paramtypestr_scalar_right_zeros_combo() {
6529        let pm = mk_pm(PM_SCALAR | PM_RIGHT_Z, 0);
6530        assert_eq!(paramtypestr(&pm), "scalar-right_zeros");
6531    }
6532
6533    /// PM_SCALAR + PM_HIDE → "scalar-hide".
6534    #[test]
6535    fn paramtypestr_scalar_hide_combo() {
6536        let pm = mk_pm(PM_SCALAR | PM_HIDE, 0);
6537        assert_eq!(paramtypestr(&pm), "scalar-hide");
6538    }
6539
6540    /// PM_SCALAR + PM_HIDEVAL → "scalar-hideval".
6541    #[test]
6542    fn paramtypestr_scalar_hideval_combo() {
6543        let pm = mk_pm(PM_SCALAR | PM_HIDEVAL, 0);
6544        assert_eq!(paramtypestr(&pm), "scalar-hideval");
6545    }
6546
6547    /// PM_SCALAR + PM_SPECIAL → "scalar-special".
6548    #[test]
6549    fn paramtypestr_scalar_special_combo() {
6550        let pm = mk_pm(PM_SCALAR | PM_SPECIAL, 0);
6551        assert_eq!(paramtypestr(&pm), "scalar-special");
6552    }
6553
6554    /// PM_SCALAR + PM_TIED → "scalar-tied".
6555    #[test]
6556    fn paramtypestr_scalar_tied_combo() {
6557        let pm = mk_pm(PM_SCALAR | PM_TIED, 0);
6558        assert_eq!(paramtypestr(&pm), "scalar-tied");
6559    }
6560
6561    /// PM_SCALAR + PM_TAGGED → "scalar-tag".
6562    #[test]
6563    fn paramtypestr_scalar_tagged_combo() {
6564        let pm = mk_pm(PM_SCALAR | PM_TAGGED, 0);
6565        assert_eq!(paramtypestr(&pm), "scalar-tag");
6566    }
6567
6568    /// level > 0 → adds "-local" right after type.
6569    #[test]
6570    fn paramtypestr_level_nonzero_adds_local_suffix() {
6571        let pm = mk_pm(PM_SCALAR, 1);
6572        assert_eq!(paramtypestr(&pm), "scalar-local");
6573    }
6574
6575    /// level=0 → no "-local" suffix.
6576    #[test]
6577    fn paramtypestr_level_zero_no_local_suffix() {
6578        let pm = mk_pm(PM_SCALAR, 0);
6579        assert!(
6580            !paramtypestr(&pm).contains("-local"),
6581            "no -local when level=0"
6582        );
6583    }
6584
6585    /// All flags combined: long suffix chain.
6586    #[test]
6587    fn paramtypestr_many_flags_combined() {
6588        let pm = mk_pm(
6589            PM_INTEGER | PM_READONLY | PM_EXPORTED | PM_TIED | PM_UNIQUE,
6590            2,
6591        );
6592        let s = paramtypestr(&pm);
6593        assert!(s.starts_with("integer"));
6594        assert!(s.contains("-local"));
6595        assert!(s.contains("-readonly"));
6596        assert!(s.contains("-export"));
6597        assert!(s.contains("-tied"));
6598        assert!(s.contains("-unique"));
6599    }
6600
6601    // ─── zsh-corpus pins for paramtypestr ───────────────────────────
6602
6603    /// PM_UNSET → empty string.
6604    #[test]
6605    fn parameter_corpus_paramtypestr_unset_is_empty() {
6606        let pm = mk_pm(PM_UNSET, 0);
6607        assert_eq!(paramtypestr(&pm), "");
6608    }
6609
6610    /// PM_AUTOLOAD → "undefined".
6611    #[test]
6612    fn parameter_corpus_paramtypestr_autoload_is_undefined() {
6613        let pm = mk_pm(PM_AUTOLOAD, 0);
6614        assert_eq!(paramtypestr(&pm), "undefined");
6615    }
6616
6617    /// PM_SCALAR → "scalar".
6618    #[test]
6619    fn parameter_corpus_paramtypestr_scalar() {
6620        let pm = mk_pm(PM_SCALAR, 0);
6621        assert_eq!(paramtypestr(&pm), "scalar");
6622    }
6623
6624    /// PM_ARRAY → "array".
6625    #[test]
6626    fn parameter_corpus_paramtypestr_array() {
6627        let pm = mk_pm(PM_ARRAY, 0);
6628        assert_eq!(paramtypestr(&pm), "array");
6629    }
6630
6631    /// PM_INTEGER → "integer".
6632    #[test]
6633    fn parameter_corpus_paramtypestr_integer() {
6634        let pm = mk_pm(PM_INTEGER, 0);
6635        assert_eq!(paramtypestr(&pm), "integer");
6636    }
6637
6638    /// PM_EFLOAT → "float".
6639    #[test]
6640    fn parameter_corpus_paramtypestr_efloat() {
6641        let pm = mk_pm(PM_EFLOAT, 0);
6642        assert_eq!(paramtypestr(&pm), "float");
6643    }
6644
6645    /// PM_FFLOAT → "float".
6646    #[test]
6647    fn parameter_corpus_paramtypestr_ffloat() {
6648        let pm = mk_pm(PM_FFLOAT, 0);
6649        assert_eq!(paramtypestr(&pm), "float");
6650    }
6651
6652    /// PM_HASHED → "association".
6653    #[test]
6654    fn parameter_corpus_paramtypestr_hashed() {
6655        let pm = mk_pm(PM_HASHED, 0);
6656        assert_eq!(paramtypestr(&pm), "association");
6657    }
6658
6659    /// PM_NAMEREF → "nameref".
6660    #[test]
6661    fn parameter_corpus_paramtypestr_nameref() {
6662        let pm = mk_pm(PM_NAMEREF, 0);
6663        assert_eq!(paramtypestr(&pm), "nameref");
6664    }
6665
6666    /// PM_SCALAR + level=2 → "scalar-local".
6667    #[test]
6668    fn parameter_corpus_paramtypestr_scalar_local() {
6669        let pm = mk_pm(PM_SCALAR, 2);
6670        assert_eq!(paramtypestr(&pm), "scalar-local");
6671    }
6672
6673    /// PM_SCALAR + PM_LEFT → "scalar-left".
6674    #[test]
6675    fn parameter_corpus_paramtypestr_scalar_left() {
6676        let pm = mk_pm(PM_SCALAR | PM_LEFT, 0);
6677        assert_eq!(paramtypestr(&pm), "scalar-left");
6678    }
6679
6680    /// PM_INTEGER + PM_READONLY → "integer-readonly".
6681    #[test]
6682    fn parameter_corpus_paramtypestr_integer_readonly() {
6683        let pm = mk_pm(PM_INTEGER | PM_READONLY, 0);
6684        assert_eq!(paramtypestr(&pm), "integer-readonly");
6685    }
6686
6687    // ═══════════════════════════════════════════════════════════════════
6688    // C-parity tests pinning Src/Modules/parameter.c funcstack helpers.
6689    // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
6690    // ═══════════════════════════════════════════════════════════════════
6691
6692    /// `funcstackgetfn` on empty FUNCSTACK returns empty vec.
6693    /// C `Src/Modules/parameter.c:627` — count is 0, ret has only
6694    /// the terminating NULL (1-element char**), Rust returns empty
6695    /// Vec<String> (matches semantically — caller iterates non-NULL
6696    /// elements).
6697    #[test]
6698    fn funcstackgetfn_empty_stack_returns_empty_vec() {
6699        let _g = crate::test_util::global_state_lock();
6700        // Empty FUNCSTACK (no shell function in progress).
6701        crate::ported::modules::parameter::FUNCSTACK
6702            .lock()
6703            .unwrap()
6704            .clear();
6705        let v = funcstackgetfn(std::ptr::null_mut());
6706        assert!(v.is_empty(), "no shell function in progress → empty stack");
6707    }
6708
6709    /// `functracegetfn` on empty stack returns empty.
6710    #[test]
6711    fn functracegetfn_empty_stack_returns_empty_vec() {
6712        let _g = crate::test_util::global_state_lock();
6713        crate::ported::modules::parameter::FUNCSTACK
6714            .lock()
6715            .unwrap()
6716            .clear();
6717        let v = functracegetfn(std::ptr::null_mut());
6718        assert!(v.is_empty());
6719    }
6720
6721    /// `funcsourcetracegetfn` on empty stack returns empty.
6722    #[test]
6723    fn funcsourcetracegetfn_empty_stack_returns_empty_vec() {
6724        let _g = crate::test_util::global_state_lock();
6725        crate::ported::modules::parameter::FUNCSTACK
6726            .lock()
6727            .unwrap()
6728            .clear();
6729        let v = funcsourcetracegetfn(std::ptr::null_mut());
6730        assert!(v.is_empty());
6731    }
6732
6733    // ═══════════════════════════════════════════════════════════════════
6734    // Additional C-parity tests for Src/Modules/parameter.c accessors.
6735    // ═══════════════════════════════════════════════════════════════════
6736
6737    /// c:911 — `patcharsgetfn` returns Vec (may be empty if getpatchars
6738    /// stub isn't populated). Pin: no panic + return type Vec<String>.
6739    #[test]
6740    fn patcharsgetfn_returns_vec_no_panic() {
6741        let _g = crate::test_util::global_state_lock();
6742        let _v: Vec<String> = patcharsgetfn(std::ptr::null_mut());
6743    }
6744
6745    /// c:917 — `dispatcharsgetfn` returns Vec (no panic).
6746    #[test]
6747    fn dispatcharsgetfn_returns_vec_no_panic() {
6748        let _g = crate::test_util::global_state_lock();
6749        let _v: Vec<String> = dispatcharsgetfn(std::ptr::null_mut());
6750    }
6751
6752    /// c:1255 — `pmjobtext(_, -1)` for out-of-range job returns empty.
6753    #[test]
6754    fn pmjobtext_out_of_range_returns_empty() {
6755        let _g = crate::test_util::global_state_lock();
6756        let s = pmjobtext(std::ptr::null_mut(), 99999);
6757        assert!(s.is_empty(), "out-of-range job → empty");
6758    }
6759
6760    /// c:1340 — `pmjobstate(_, -1)` for invalid job returns empty.
6761    #[test]
6762    fn pmjobstate_out_of_range_returns_empty() {
6763        let _g = crate::test_util::global_state_lock();
6764        let s = pmjobstate(std::ptr::null_mut(), 99999);
6765        assert!(s.is_empty());
6766    }
6767
6768    /// c:1277 — `getpmjobtext` for non-numeric name returns None or
6769    /// PM_UNSET param (no panic).
6770    #[test]
6771    fn getpmjobtext_non_numeric_name_no_panic() {
6772        let _g = crate::test_util::global_state_lock();
6773        let _ = getpmjobtext(std::ptr::null_mut(), "not_a_number");
6774    }
6775
6776    /// c:1277 — `getpmjobtext` empty name no panic.
6777    #[test]
6778    fn getpmjobtext_empty_name_no_panic() {
6779        let _g = crate::test_util::global_state_lock();
6780        let _ = getpmjobtext(std::ptr::null_mut(), "");
6781    }
6782
6783    /// c:1083 — `getpmmodule(_, "")` no panic.
6784    #[test]
6785    fn getpmmodule_empty_name_no_panic() {
6786        let _g = crate::test_util::global_state_lock();
6787        let _ = getpmmodule(std::ptr::null_mut(), "");
6788    }
6789
6790    /// c:1083 — `getpmmodule` unknown module name → returns Some
6791    /// (PM_UNSET) or None per C convention.
6792    #[test]
6793    fn getpmmodule_unknown_name_no_panic() {
6794        let _g = crate::test_util::global_state_lock();
6795        let _ = getpmmodule(std::ptr::null_mut(), "zshrs_never_real_module_xyz");
6796    }
6797
6798    /// c:1677 — `getpmoption(_, "")` no panic.
6799    #[test]
6800    fn getpmoption_empty_name_no_panic() {
6801        let _g = crate::test_util::global_state_lock();
6802        let _ = getpmoption(std::ptr::null_mut(), "");
6803    }
6804
6805    /// c:1947 — `dirsgetfn` returns the dirstack (may be empty).
6806    #[test]
6807    fn dirsgetfn_returns_vec_no_panic() {
6808        let _g = crate::test_util::global_state_lock();
6809        let _ = dirsgetfn(std::ptr::null_mut());
6810    }
6811
6812    /// c:911 — patcharsgetfn deterministic (whatever it returns must
6813    /// be consistent across calls).
6814    #[test]
6815    fn patcharsgetfn_is_deterministic() {
6816        let _g = crate::test_util::global_state_lock();
6817        let first = patcharsgetfn(std::ptr::null_mut());
6818        for _ in 0..3 {
6819            assert_eq!(patcharsgetfn(std::ptr::null_mut()), first);
6820        }
6821    }
6822
6823    // ═══════════════════════════════════════════════════════════════════
6824    // Additional C-parity tests for Src/Modules/parameter.c alias accessors.
6825    // ═══════════════════════════════════════════════════════════════════
6826
6827    /// c:1901 — `getalias(_, _, "missing", 0)` returns Some(PM_UNSET).
6828    /// Per c:1917, missing entries get PM_UNSET|PM_SPECIAL flags set.
6829    #[test]
6830    fn getalias_missing_returns_pm_unset() {
6831        let _g = crate::test_util::global_state_lock();
6832        use crate::ported::zsh_h::PM_UNSET;
6833        let pm = getalias(
6834            std::ptr::null_mut(),
6835            std::ptr::null_mut(),
6836            "zshrs_never_real_alias_xyz",
6837            0,
6838        )
6839        .expect("getalias returns Some even for missing");
6840        assert!(pm.node.flags & PM_UNSET as i32 != 0, "PM_UNSET on miss");
6841    }
6842
6843    /// c:1907 — getalias returns Param with name preserved.
6844    #[test]
6845    fn getalias_returns_param_with_name_round_trip() {
6846        let _g = crate::test_util::global_state_lock();
6847        let pm = getalias(
6848            std::ptr::null_mut(),
6849            std::ptr::null_mut(),
6850            "test_name_xyz",
6851            0,
6852        )
6853        .expect("Some");
6854        assert_eq!(pm.node.nam, "test_name_xyz");
6855    }
6856
6857    /// c:1923 — `getpmralias(_, "missing")` returns Some(PM_UNSET).
6858    #[test]
6859    fn getpmralias_missing_returns_pm_unset() {
6860        let _g = crate::test_util::global_state_lock();
6861        use crate::ported::zsh_h::PM_UNSET;
6862        let pm = getpmralias(std::ptr::null_mut(), "zshrs_never_real_ralias_xyz").expect("Some");
6863        assert!(pm.node.flags & PM_UNSET as i32 != 0);
6864    }
6865
6866    /// c:1930 — `getpmdisralias(_, "missing")` returns Some(PM_UNSET).
6867    #[test]
6868    fn getpmdisralias_missing_returns_pm_unset() {
6869        let _g = crate::test_util::global_state_lock();
6870        use crate::ported::zsh_h::PM_UNSET;
6871        let pm =
6872            getpmdisralias(std::ptr::null_mut(), "zshrs_never_real_disralias_xyz").expect("Some");
6873        assert!(pm.node.flags & PM_UNSET as i32 != 0);
6874    }
6875
6876    /// c:1937 — `getpmgalias(_, "missing")` returns Some(PM_UNSET).
6877    #[test]
6878    fn getpmgalias_missing_returns_pm_unset() {
6879        let _g = crate::test_util::global_state_lock();
6880        use crate::ported::zsh_h::PM_UNSET;
6881        let pm = getpmgalias(std::ptr::null_mut(), "zshrs_never_real_galias_xyz").expect("Some");
6882        assert!(pm.node.flags & PM_UNSET as i32 != 0);
6883    }
6884
6885    /// c:1901 — getalias for empty name returns Some(PM_UNSET).
6886    #[test]
6887    fn getalias_empty_name_returns_some() {
6888        let _g = crate::test_util::global_state_lock();
6889        let _ = getalias(std::ptr::null_mut(), std::ptr::null_mut(), "", 0)
6890            .expect("always Some per C convention");
6891    }
6892
6893    /// c:2844 — `setpmralias` no panic with empty value.
6894    #[test]
6895    fn setpmralias_empty_value_no_panic() {
6896        let _g = crate::test_util::global_state_lock();
6897        use crate::ported::zsh_h::{hashnode, param};
6898        let pm = Box::new(param {
6899            node: hashnode {
6900                next: None,
6901                nam: "test".to_string(),
6902                flags: 0,
6903            },
6904            u_data: 0,
6905            u_tied: None,
6906            u_arr: None,
6907            u_str: None,
6908            u_val: 0,
6909            u_dval: 0.0,
6910            u_hash: None,
6911            gsu_s: None,
6912            gsu_i: None,
6913            gsu_f: None,
6914            gsu_a: None,
6915            gsu_h: None,
6916            base: 0,
6917            width: 0,
6918            env: None,
6919            ename: None,
6920            old: None,
6921            level: 0,
6922        });
6923        setpmralias(pm, String::new());
6924    }
6925
6926    /// c:2891 — `unsetpmalias` no panic.
6927    #[test]
6928    fn unsetpmalias_no_panic() {
6929        let _g = crate::test_util::global_state_lock();
6930        use crate::ported::zsh_h::{hashnode, param};
6931        let pm = Box::new(param {
6932            node: hashnode {
6933                next: None,
6934                nam: "test".to_string(),
6935                flags: 0,
6936            },
6937            u_data: 0,
6938            u_tied: None,
6939            u_arr: None,
6940            u_str: None,
6941            u_val: 0,
6942            u_dval: 0.0,
6943            u_hash: None,
6944            gsu_s: None,
6945            gsu_i: None,
6946            gsu_f: None,
6947            gsu_a: None,
6948            gsu_h: None,
6949            base: 0,
6950            width: 0,
6951            env: None,
6952            ename: None,
6953            old: None,
6954            level: 0,
6955        });
6956        unsetpmalias(pm, 0);
6957    }
6958
6959    /// c:2905 — `unsetpmsalias` no panic.
6960    #[test]
6961    fn unsetpmsalias_no_panic() {
6962        let _g = crate::test_util::global_state_lock();
6963        use crate::ported::zsh_h::{hashnode, param};
6964        let pm = Box::new(param {
6965            node: hashnode {
6966                next: None,
6967                nam: "test".to_string(),
6968                flags: 0,
6969            },
6970            u_data: 0,
6971            u_tied: None,
6972            u_arr: None,
6973            u_str: None,
6974            u_val: 0,
6975            u_dval: 0.0,
6976            u_hash: None,
6977            gsu_s: None,
6978            gsu_i: None,
6979            gsu_f: None,
6980            gsu_a: None,
6981            gsu_h: None,
6982            base: 0,
6983            width: 0,
6984            env: None,
6985            ename: None,
6986            old: None,
6987            level: 0,
6988        });
6989        unsetpmsalias(pm, 0);
6990    }
6991
6992    // ═══════════════════════════════════════════════════════════════════
6993    // Additional C-parity tests for Src/Modules/parameter.c
6994    // c:50 paramtypestr / c:132 getpmparameter / c:606 getpmcommand /
6995    // c:944 getfunction / c:998 getpmfunction / c:1007 getpmdisfunction
6996    // ═══════════════════════════════════════════════════════════════════
6997
6998    /// c:132 — `getpmparameter(null, "")` empty name returns Some(PM_UNSET).
6999    #[test]
7000    fn getpmparameter_empty_name_returns_some_unset() {
7001        use crate::ported::zsh_h::PM_UNSET;
7002        let _g = crate::test_util::global_state_lock();
7003        let pm = getpmparameter(std::ptr::null_mut(), "");
7004        if let Some(p) = pm {
7005            assert_ne!(
7006                p.node.flags & PM_UNSET as i32,
7007                0,
7008                "empty param name → PM_UNSET"
7009            );
7010        }
7011    }
7012
7013    /// c:132 — `getpmparameter` returns Option<Param>.
7014    #[test]
7015    fn getpmparameter_returns_option_param_type() {
7016        use crate::ported::zsh_h::Param;
7017        let _g = crate::test_util::global_state_lock();
7018        let _: Option<Param> = getpmparameter(std::ptr::null_mut(), "anything");
7019    }
7020
7021    /// c:606 — `getpmcommand(null, "")` empty name returns Some.
7022    #[test]
7023    fn getpmcommand_empty_name_returns_some() {
7024        let _g = crate::test_util::global_state_lock();
7025        let pm = getpmcommand(std::ptr::null_mut(), "");
7026        assert!(pm.is_some(), "C convention: always Some for missing");
7027    }
7028
7029    /// c:998 — `getpmfunction(null, "")` empty name returns Some.
7030    #[test]
7031    fn getpmfunction_empty_name_returns_some() {
7032        let _g = crate::test_util::global_state_lock();
7033        let pm = getpmfunction(std::ptr::null_mut(), "");
7034        assert!(pm.is_some(), "C convention: always Some for missing");
7035    }
7036
7037    /// c:1007 — `getpmdisfunction(null, "")` empty name returns Some.
7038    #[test]
7039    fn getpmdisfunction_empty_name_returns_some() {
7040        let _g = crate::test_util::global_state_lock();
7041        let pm = getpmdisfunction(std::ptr::null_mut(), "");
7042        assert!(pm.is_some());
7043    }
7044
7045    /// c:944 — `getfunction(null, "", 0)` returns Option<Param>.
7046    #[test]
7047    fn getfunction_returns_option_param_type() {
7048        use crate::ported::zsh_h::Param;
7049        let _g = crate::test_util::global_state_lock();
7050        let _: Option<Param> = getfunction(std::ptr::null_mut(), "anything", 0);
7051    }
7052
7053    /// c:50 — `paramtypestr` returns String (compile-time type pin).
7054    #[test]
7055    fn paramtypestr_returns_string_type() {
7056        use crate::ported::zsh_h::{hashnode, param};
7057        let pm = param {
7058            node: hashnode {
7059                next: None,
7060                nam: "x".to_string(),
7061                flags: 0,
7062            },
7063            u_data: 0,
7064            u_tied: None,
7065            u_arr: None,
7066            u_str: None,
7067            u_val: 0,
7068            u_dval: 0.0,
7069            u_hash: None,
7070            gsu_s: None,
7071            gsu_i: None,
7072            gsu_f: None,
7073            gsu_a: None,
7074            gsu_h: None,
7075            base: 0,
7076            width: 0,
7077            env: None,
7078            ename: None,
7079            old: None,
7080            level: 0,
7081        };
7082        let _: String = paramtypestr(&pm);
7083    }
7084
7085    /// c:50 — `paramtypestr` for plain scalar returns non-empty string.
7086    #[test]
7087    fn paramtypestr_scalar_returns_nonempty() {
7088        use crate::ported::zsh_h::{hashnode, param};
7089        let pm = param {
7090            node: hashnode {
7091                next: None,
7092                nam: "x".to_string(),
7093                flags: 0,
7094            },
7095            u_data: 0,
7096            u_tied: None,
7097            u_arr: None,
7098            u_str: None,
7099            u_val: 0,
7100            u_dval: 0.0,
7101            u_hash: None,
7102            gsu_s: None,
7103            gsu_i: None,
7104            gsu_f: None,
7105            gsu_a: None,
7106            gsu_h: None,
7107            base: 0,
7108            width: 0,
7109            env: None,
7110            ename: None,
7111            old: None,
7112            level: 0,
7113        };
7114        let s = paramtypestr(&pm);
7115        assert!(!s.is_empty(), "type str must be non-empty");
7116    }
7117
7118    /// c:132 — `getpmparameter` is deterministic for same name.
7119    #[test]
7120    fn getpmparameter_deterministic_for_unknown() {
7121        let _g = crate::test_util::global_state_lock();
7122        let p1 = getpmparameter(std::ptr::null_mut(), "__zshrs_never_param__")
7123            .map(|p| p.node.nam.clone());
7124        let p2 = getpmparameter(std::ptr::null_mut(), "__zshrs_never_param__")
7125            .map(|p| p.node.nam.clone());
7126        assert_eq!(p1, p2, "getpmparameter must be deterministic");
7127    }
7128
7129    /// c:606 — `getpmcommand` returns Option<Param>.
7130    #[test]
7131    fn getpmcommand_returns_option_param_type() {
7132        use crate::ported::zsh_h::Param;
7133        let _g = crate::test_util::global_state_lock();
7134        let _: Option<Param> = getpmcommand(std::ptr::null_mut(), "anything");
7135    }
7136
7137    // ═══════════════════════════════════════════════════════════════════
7138    // Additional C-parity tests for Src/Modules/parameter.c
7139    // c:606 getpmcommand / c:998 getpmfunction / c:1007 getpmdisfunction /
7140    // c:1159 getpmfunction_source / c:1169 getpmdisfunction_source /
7141    // c:1202 funcstackgetfn / c:1239 functracegetfn / c:1273 funcsourcetracegetfn
7142    // ═══════════════════════════════════════════════════════════════════
7143
7144    /// c:606 — `getpmcommand` is deterministic for unknown command.
7145    #[test]
7146    fn getpmcommand_deterministic_for_unknown() {
7147        let _g = crate::test_util::global_state_lock();
7148        let a =
7149            getpmcommand(std::ptr::null_mut(), "__zshrs_never_cmd__").map(|p| p.node.nam.clone());
7150        let b =
7151            getpmcommand(std::ptr::null_mut(), "__zshrs_never_cmd__").map(|p| p.node.nam.clone());
7152        assert_eq!(a, b, "getpmcommand must be deterministic");
7153    }
7154
7155    /// c:998 — `getpmfunction` returns Option<Param>.
7156    #[test]
7157    fn getpmfunction_returns_option_param_type() {
7158        use crate::ported::zsh_h::Param;
7159        let _g = crate::test_util::global_state_lock();
7160        let _: Option<Param> = getpmfunction(std::ptr::null_mut(), "anything");
7161    }
7162
7163    /// c:998 — `getpmfunction` is deterministic for unknown function.
7164    #[test]
7165    fn getpmfunction_deterministic_for_unknown() {
7166        let _g = crate::test_util::global_state_lock();
7167        let a =
7168            getpmfunction(std::ptr::null_mut(), "__zshrs_never_fn__").map(|p| p.node.nam.clone());
7169        let b =
7170            getpmfunction(std::ptr::null_mut(), "__zshrs_never_fn__").map(|p| p.node.nam.clone());
7171        assert_eq!(a, b, "getpmfunction must be deterministic");
7172    }
7173
7174    /// c:1007 — `getpmdisfunction` returns Option<Param>.
7175    #[test]
7176    fn getpmdisfunction_returns_option_param_type() {
7177        use crate::ported::zsh_h::Param;
7178        let _g = crate::test_util::global_state_lock();
7179        let _: Option<Param> = getpmdisfunction(std::ptr::null_mut(), "anything");
7180    }
7181
7182    /// c:1159 — `getpmfunction_source` returns Option<Param>.
7183    #[test]
7184    fn getpmfunction_source_returns_option_param_type() {
7185        use crate::ported::zsh_h::Param;
7186        let _g = crate::test_util::global_state_lock();
7187        let _: Option<Param> = getpmfunction_source(std::ptr::null_mut(), "anything");
7188    }
7189
7190    /// c:1169 — `getpmdisfunction_source` returns Option<Param>.
7191    #[test]
7192    fn getpmdisfunction_source_returns_option_param_type() {
7193        use crate::ported::zsh_h::Param;
7194        let _g = crate::test_util::global_state_lock();
7195        let _: Option<Param> = getpmdisfunction_source(std::ptr::null_mut(), "anything");
7196    }
7197
7198    /// c:1202 — `funcstackgetfn` returns Vec<String> (compile-time type pin).
7199    #[test]
7200    fn funcstackgetfn_returns_vec_string_type() {
7201        let _g = crate::test_util::global_state_lock();
7202        let _: Vec<String> = funcstackgetfn(std::ptr::null_mut());
7203    }
7204
7205    /// c:1239 — `functracegetfn` returns Vec<String>.
7206    #[test]
7207    fn functracegetfn_returns_vec_string_type() {
7208        let _g = crate::test_util::global_state_lock();
7209        let _: Vec<String> = functracegetfn(std::ptr::null_mut());
7210    }
7211
7212    /// c:1273 — `funcsourcetracegetfn` returns Vec<String>.
7213    #[test]
7214    fn funcsourcetracegetfn_returns_vec_string_type() {
7215        let _g = crate::test_util::global_state_lock();
7216        let _: Vec<String> = funcsourcetracegetfn(std::ptr::null_mut());
7217    }
7218
7219    /// c:1202 — `funcstackgetfn(null)` is deterministic across calls.
7220    #[test]
7221    fn funcstackgetfn_null_is_deterministic() {
7222        let _g = crate::test_util::global_state_lock();
7223        let first = funcstackgetfn(std::ptr::null_mut());
7224        for _ in 0..3 {
7225            assert_eq!(
7226                funcstackgetfn(std::ptr::null_mut()),
7227                first,
7228                "funcstackgetfn(null) must be deterministic"
7229            );
7230        }
7231    }
7232}