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