Skip to main content

zsh/ported/modules/
param_private.rs

1//! `zsh/param/private` module — port of `Src/Modules/param_private.c`.
2//!
3//! Provides the `private` builtin which declares parameters scoped to
4//! the immediately enclosing function (a stricter alternative to
5//! `local`). The C source's design comment (c:60-75) describes the
6//! mechanism: `bin_private` opens a new parameter scope, calls
7//! `bin_typeset`, then `makeprivate` walks the new scope and either
8//! promotes each new param into the surrounding scope (with its GSU
9//! struct swapped to the per-type private callbacks) or rejects it.
10//!
11//! C source: 19 ported total — `makeprivate`, `is_private`, `setfn_error`,
12//! `pps_getfn`/`pps_setfn`/`pps_unsetfn`, `ppi_getfn`/`ppi_setfn`/
13//! `ppi_unsetfn`, `ppf_getfn`/`ppf_setfn`/`ppf_unsetfn`, `ppa_getfn`/
14//! `ppa_setfn`/`ppa_unsetfn`, `pph_getfn`/`pph_setfn`/`pph_unsetfn`,
15//! `bin_private`, `printprivatenode`, `getprivatenode`,
16//! `getprivatenode2`, `scopeprivate`, `wrap_private`, plus 6 module
17//! loaders. 1 struct: `gsu_closure` (c:34).
18//!
19//! **Strict status: PARTIAL — see `TODO.md`.** Some wiring has
20//! landed: `bin_private` calls real `startparamscope`/`endparamscope`
21//! via params.rs, `boot_`/`finish_` manage the `emptytable` marker
22//! through `newparamtable`/`deleteparamtable`, and
23//! `printprivatenode` routes to `params::printparamnode`. What still
24//! requires substrate work outside this module: the
25//! `addwrapper(m, wrapper)` dispatch (paramtab swap-on-call in
26//! `wrap_private`), the realparamtab `getnode`/`getnode2`/`printnode`
27//! override chain that `setup_` installs at c:619-630, and the
28//! `bin_typeset` re-entry through `c:251` (which depends on the typed
29//! paramtab in zshrs's executor — currently `HashMap<String,String>`).
30//! The 12 per-type GSU callbacks (`pps_*`/`ppi_*`/`ppf_*`/`ppa_*`/
31//! `pph_*`) shape-match C's signatures but their `gsu_closure` chain
32//! lookup is no-op until `pm->gsu.s` is a real vtable pointer.
33
34use crate::ported::builtin::bin_typeset;
35use crate::ported::mem::{queue_signals, unqueue_signals};
36use crate::ported::options::optlookup;
37use crate::ported::params::{
38    deleteparamtable, endparamscope, locallevel, newparamtable, paramtab, printparamnode,
39    startparamscope,
40};
41use crate::ported::utils::{zerr, zwarn, zwarnnam};
42use crate::ported::hashtable::reswdtab_lock;
43use crate::ported::zsh_h::{
44    eprog, features, funcwrap, hashnode, hashtable, isset, module, options, param, reswd,
45    HashTable, MAX_OPS, OPT_ISSET, PM_AUTOLOAD, PM_DECLARED, PM_HIDE, PM_NAMEREF, PM_NORESTORE,
46    PM_READONLY, PM_REMOVABLE, PM_RESTRICTED, PM_RO_BY_DESIGN, PM_SPECIAL, PM_UNSET, TYPESET,
47    WARNCREATEGLOBAL,
48};
49use std::sync::atomic::Ordering;
50use std::sync::{Mutex, OnceLock};
51
52/// Port of `struct gsu_closure` from `Src/Modules/param_private.c:34`.
53/// Wraps a copy of the original GSU table (one variant per param type)
54/// alongside a `void *g` pointer the close-over uses to chain back to
55/// the shadowed param.
56///
57/// C definition (c:34-43):
58/// ```c
59/// struct gsu_closure {
60///     union {
61///         struct gsu_scalar s;
62///         struct gsu_integer i;
63///         struct gsu_float f;
64///         struct gsu_array a;
65///         struct gsu_hash h;
66///     } u;
67///     void *g;
68/// };
69/// ```
70///
71/// The `gsu_*` types ARE ported in zsh_h.rs (gsu_scalar at c:802,
72/// gsu_integer c:810, gsu_float c:818, gsu_array c:826, gsu_hash
73/// c:834, with their `Box<T>` aliases GsuScalar/GsuInteger/etc. at
74/// c:794-798). `gsu_closure` keeps the type-erased `(kind, raw_ptr)`
75/// shape because the underlying gsu vtable function-pointers
76/// (`Option<GsuFn>`) can't be const-initialised in a `static`. The
77/// closure records which GSU variant fires via `kind` (0..=4 for
78/// scalar/integer/float/array/hash) and a `usize` ptr into the per-
79/// type static table; consumers cast back at call time.
80#[derive(Debug, Clone, Copy)]
81#[allow(non_camel_case_types)]
82pub struct gsu_closure {
83    // c:34
84    pub kind: u8, // c:35-41 union tag
85    pub g: usize, // c:42 void *g
86}
87
88// ---------------------------------------------------------------------------
89// `makeprivate` and the per-type GSU callbacks (c:79-377).
90// ---------------------------------------------------------------------------
91
92/// Port of `makeprivate(HashNode hn, UNUSED(int flags))` from `Src/Modules/param_private.c:80`.
93///
94/// C body (~100 lines): walks every param at the current `locallevel`,
95/// promoting it (with its GSU swapped to the per-type private
96/// callbacks at c:139-167) or rejecting it back to `bin_private` via
97/// the file-static `makeprivate_error` flag at c:93/130/135/169.
98///
99/// C signature: `static void makeprivate(HashNode hn, int flags)`.
100#[allow(unused_variables)]
101pub fn makeprivate(hn: *mut param, flags: i32) {
102    // c:80
103    if hn.is_null() {
104        return;
105    }
106    let pm_level = unsafe { (*hn).level };
107    let cur_local = locallevel.load(Ordering::Relaxed);
108    if pm_level != cur_local {
109        return;
110    } // c:83 only act on this scope's entries
111
112    let pm_flags = unsafe { (*hn).node.flags };
113    let pm_ename = unsafe { (*hn).ename.is_some() };
114    let has_old = unsafe { (*hn).old.is_some() };
115    let pm_special = (pm_flags & PM_SPECIAL as i32) != 0;
116    let pm_removable = (pm_flags & PM_REMOVABLE as i32) != 0;
117    let pm_norestore = (pm_flags & PM_NORESTORE as i32) != 0;
118
119    // c:84-89 — outer rejection gate, verbatim:
120    //
121    //     if (pm->ename || (pm->node.flags & PM_NORESTORE) ||
122    //         (pm->old &&
123    //          (pm->old->level == locallevel - 1 ||
124    //           ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL &&
125    //            /* typeset_single() line 2300 discards PM_REMOVABLE -- why? */
126    //            !is_private(pm->old))))) {
127    //
128    // The has-old leg fires ONLY when the shadowed entry sits at
129    // EXACTLY the parent scope (locallevel-1) or is a special-non-
130    // removable shadowing a non-private. Shadowing a global from
131    // depth ≥ 2, or anything with no old at all, falls through to
132    // the promotion path. A prior placeholder treated EVERY has_old
133    // as a rejection — `private x` over a global x always errored.
134    let _ = has_old;
135    let (old_level, old_flags, old_is_private) = unsafe {
136        match (*hn).old.as_ref() {
137            Some(old) => (
138                Some(old.level),
139                old.node.flags,
140                // During makeprivate the registry holds the name iff a
141                // PRIOR makeprivate inserted it (this call's insert
142                // happens below) — so name-presence ⟺ old is private.
143                is_private(&**old as *const param) != 0,
144            ),
145            None => (None, 0, false),
146        }
147    };
148    let inner_reject = match old_level {
149        Some(ol) => {
150            ol == cur_local - 1 // c:86
151                || ((pm_flags & (PM_SPECIAL | PM_REMOVABLE) as i32) == PM_SPECIAL as i32
152                    && !old_is_private) // c:87-89
153        }
154        None => false,
155    };
156    let _ = (pm_special, pm_removable);
157    let outer_cond = pm_ename || pm_norestore || inner_reject;
158
159    if outer_cond {
160        // c:90-137 — name-clash arms.
161        let name = unsafe { (*hn).node.nam.clone() };
162        if old_is_private {
163            // c:90
164            let old_readonly = (old_flags & PM_READONLY as i32) != 0;
165            if old_readonly {
166                // c:91-93
167                zerr(&format!("read-only variable: {}", name)); // c:92
168                MAKEPRIVATE_ERROR.store(1, Ordering::Relaxed); // c:93
169            } else if (pm_flags | old_flags) == old_flags {
170                // c:94-95 — `private` called twice on the same param:
171                // copy the NEW declaration's value down into the old
172                // (still-live) private and re-arm it. C's c:99/c:123
173                // --locallevel/++locallevel dance exists so gsu->setfn
174                // sees the outer scope; the static-link direct field
175                // copy doesn't dispatch through setfn so no scope
176                // shuffle is needed.
177                /* why have a union if we need this switch anyway? */
178                unsafe {
179                    let (u_str, u_val, u_dval, u_arr, u_hash, tpm_unset) = {
180                        let t = &*hn;
181                        (
182                            t.u_str.clone(),  // c:104 PM_SCALAR/PM_NAMEREF
183                            t.u_val,          // c:108 PM_INTEGER
184                            t.u_dval,         // c:112 PM_EFLOAT/PM_FFLOAT
185                            t.u_arr.clone(),  // c:115 PM_ARRAY
186                            t.u_hash.clone(), // c:119 PM_HASHED
187                            (t.node.flags & PM_UNSET as i32) != 0,
188                        )
189                    };
190                    if let Some(old) = (*hn).old.as_mut() {
191                        old.u_str = u_str; // c:104 gsu.s->setfn
192                        old.u_val = u_val; // c:108 gsu.i->setfn
193                        old.u_dval = u_dval; // c:112 gsu.f->setfn
194                        old.u_arr = u_arr; // c:115 gsu.a->setfn
195                        old.u_hash = u_hash; // c:119 gsu.h->setfn
196                                             // c:124-125 — `if (!(tpm->node.flags & PM_UNSET))
197                                             //               pm->node.flags &= ~PM_UNSET;`
198                        if !tpm_unset {
199                            old.node.flags &= !(PM_UNSET as i32); // c:125
200                        }
201                    }
202                }
203            } else {
204                // c:126-131 — declaration changes the param's type.
205                zerr(&format!(
206                    "private: can't change type of private param: {}",
207                    name
208                )); // c:127-129
209                MAKEPRIVATE_ERROR.store(1, Ordering::Relaxed); // c:130
210            }
211        } else {
212            // c:132-136
213            zerr(&format!(
214                "private: can't change scope of existing param: {}",
215                name
216            )); // c:133-134
217            MAKEPRIVATE_ERROR.store(1, Ordering::Relaxed); // c:135
218        }
219        return; // c:137
220    }
221
222    // c:139-172 — promote hn to private. The C body installs a
223    // per-type `gsu_closure` struct and rewires `hn->gsu.X`. Static-
224    // link path: register the param name in the PRIVATE_PARAMS set so
225    // `is_private()` returns 1; the actual GSU swap is unnecessary
226    // since we use direct `u_str`/`u_val`/etc. access.
227    let name = unsafe { (*hn).node.nam.clone() };
228    if let Ok(mut p) = PRIVATE_PARAMS.lock() {
229        p.insert(name);
230    }
231
232    // c:174 — `hn->node.flags |= (PM_HIDE|PM_SPECIAL|PM_REMOVABLE|PM_RO_BY_DESIGN);`
233    unsafe {
234        (*hn).node.flags |= (PM_HIDE | PM_SPECIAL | PM_REMOVABLE | PM_RO_BY_DESIGN) as i32;
235    }
236    // c:175 — `hn->level -= 1;`  (move into the surrounding scope)
237    unsafe {
238        (*hn).level -= 1;
239    }
240}
241
242/// Port of `is_private(Param pm)` from `Src/Modules/param_private.c:181`.
243///
244/// C body:
245/// ```c
246/// is_private(Param pm) {
247///     switch (PM_TYPE(pm->node.flags)) {
248///     case PM_SCALAR: case PM_NAMEREF:
249///         if (!pm->gsu.s || pm->gsu.s->unsetfn != pps_unsetfn) return 0;
250///         break;
251///     case PM_INTEGER:
252///         if (!pm->gsu.i || pm->gsu.i->unsetfn != ppi_unsetfn) return 0;
253///         break;
254///     case PM_EFLOAT: case PM_FFLOAT:
255///         if (!pm->gsu.f || pm->gsu.f->unsetfn != ppf_unsetfn) return 0;
256///         break;
257///     case PM_ARRAY:
258///         if (!pm->gsu.a || pm->gsu.a->unsetfn != ppa_unsetfn) return 0;
259///         break;
260///     case PM_HASHED:
261///         if (!pm->gsu.h || pm->gsu.h->unsetfn != pph_unsetfn) return 0;
262///         break;
263///     default: return 0;
264///     }
265///     return 1;
266/// }
267/// ```
268///
269/// Returns 1 iff the named param is registered as private (its
270/// per-type GSU table's `unsetfn` slot points at the matching
271/// `pp{s,i,f,a,h}_unsetfn` sentinel from c:45-58).
272///
273/// Static-link path: the `PRIVATE_PARAMS` registry below tracks
274/// every private param installed via `bin_private`, so private-ness
275/// is just a presence check there.
276pub fn is_private(pm: *const param) -> i32 {
277    // c:181
278    // C tests whether THIS Param's per-type gsu `unsetfn` slot points
279    // at the pp{s,i,f,a,h}_unsetfn sentinel — a PER-PARAM check that
280    // distinguishes the private pm from the non-private old it
281    // shadows on the same name. The Rust makeprivate doesn't swap
282    // gsu vtables (direct u_str/u_arr access model); its equivalent
283    // per-Param marker is the flag combo it installs at c:174
284    // (PM_HIDE|PM_SPECIAL|PM_REMOVABLE|PM_RO_BY_DESIGN). Require
285    // BOTH the combo AND the name-keyed PRIVATE_PARAMS registry
286    // entry: the combo gives per-Param precision down the pm->old
287    // chain (the registry alone answered "yes" for every link of a
288    // shadow chain, breaking getprivatenode's stop condition); the
289    // registry keeps a coincidentally-flagged special from reading
290    // as private.
291    if pm.is_null() {
292        return 0;
293    }
294    let privflags = (PM_HIDE | PM_SPECIAL | PM_REMOVABLE | PM_RO_BY_DESIGN) as i32;
295    if unsafe { (*pm).node.flags } & privflags != privflags {
296        return 0; // c:183-207 gsu-sentinel mismatch
297    }
298    let name = unsafe { (*pm).node.nam.clone() };
299    if PRIVATE_PARAMS
300        .lock()
301        .map(|p| p.contains(&name))
302        .unwrap_or(false)
303    {
304        1 // c:210
305    } else {
306        0 // c:208 default error
307    }
308}
309
310// ---------------------------------------------------------------------------
311// Builtin entry + scope/wrap helpers (c:217-660).
312// ---------------------------------------------------------------------------
313
314/// Port of `bin_private(char *nam, char **args, LinkList assigns, Options ops, int func)` from `Src/Modules/param_private.c:217`.
315///
316/// C signature: `static int bin_private(char *nam, char **args,
317///                                       LinkList assigns, Options ops,
318///                                       int func)`. C body opens a
319/// new `locallevel`, calls `bin_typeset` to do the actual parameter
320/// creation, then runs `makeprivate` over the new scope to promote
321/// or reject each entry.
322///
323/// **Strict status: PARTIAL.** Without `bin_typeset`/`locallevel`/
324/// `makeprivate` ported, the Rust port falls back to plain `local`-
325/// style assignment via `exec.variables`/`exec.arrays`. This is
326/// observable behavior-equivalent for the simple `private name=value`
327/// form (no shadowing) but cannot reject promotions or detect
328/// scope-conflict cases the C body handles at c:140-178.
329///
330/// Builtin spec from c:702: `"AE:%F:HL:R:TUZ:afhi:lprtuxmM"`. Most
331/// flags are typeset's; `private` adds nothing of its own that isn't
332/// in typeset.
333pub fn bin_private(
334    nam: &str,
335    args: &[String], // c:217
336    ops_in: &options,
337    func: i32,
338) -> i32 {
339    // c:217 C sig is `(nam, args, LinkList assigns, Options ops,
340    // func)` — 5 params. HandlerFunc takes only 4, so:
341    // (a) `assigns` is dropped (zshrs's execbuiltin doesn't thread
342    //     assigns through; `BINF_ASSIGN` parsing converts foo=bar
343    //     args to plain entries in `args`).
344    // (b) `ops` is cloned to a fn-local mut since the C body sets
345    //     bits inside (e.g. ops->ind[X] toggles in fallback branches).
346    let mut ops_local = ops_in.clone();
347    let ops = &mut ops_local;
348    // c:220 — `int from_typeset = 1;`
349    let mut from_typeset: i32 = 1; // c:220
350                                   // c:221 — `int ofake = fakelevel;`
351    let ofake = FAKELEVEL.load(Ordering::Relaxed); // c:221
352                                                   // c:222 — `int hasargs = /* *args != NULL || */ (assigns &&
353                                                   //           firstnode(assigns));`
354                                                   // C's `assigns` is the BINF_ASSIGN-split LinkList: execbuiltin
355                                                   // (Src/builtin.c) peels `name=value` words off into `assigns`,
356                                                   // leaving bare names in `args`. The commented-out `*args != NULL`
357                                                   // shows hasargs deliberately counts ONLY assignment forms.
358                                                   // zshrs's dispatcher doesn't split — `name=value` words arrive
359                                                   // in `args` — so the faithful adaptation detects them there.
360                                                   // Prior port computed `!assigns.is_empty()` on a hardcoded-empty
361                                                   // local Vec: always false, making `private +r x=1` take the
362                                                   // `(!hasargs && '+')` bin_typeset shortcut at c:243 even though
363                                                   // an assignment was present.
364    let hasargs = args.iter().any(|a| a.find('=').is_some_and(|p| p > 0)); // c:222
365                                                                           // c:223 — `makeprivate_error = 0;`
366    MAKEPRIVATE_ERROR.store(0, Ordering::Relaxed); // c:223
367
368    // c:225-230 — `if (!OPT_ISSET(ops, 'P'))` straight-through to bin_typeset.
369    if !OPT_ISSET(ops, b'P') {
370        // c:225
371        FAKELEVEL.store(0, Ordering::Relaxed); // c:226
372        from_typeset = bin_typeset(nam, args, ops, func); // c:227
373        FAKELEVEL.store(ofake, Ordering::Relaxed); // c:228
374        return from_typeset; // c:229
375    }
376    // c:231-233 — refuse `-P -T`.
377    if OPT_ISSET(ops, b'T') {
378        // c:231
379        zwarn("bad option: -T"); // c:232
380        return 1; // c:233
381    }
382
383    // c:235-239 — outside a function: WARNCREATEGLOBAL, then bin_typeset.
384    let locallevel2 = locallevel.load(Ordering::Relaxed);
385    if locallevel2 == 0 {
386        // c:235
387        let warn = isset(WARNCREATEGLOBAL);
388        if warn {
389            // c:236
390            zwarnnam(nam, "invalid local scope, using globals"); // c:237
391        }
392        return bin_typeset("private", args, ops, func); // c:238
393    }
394
395    // c:241-242 — `if (!(OPT_ISSET(ops,'m') || OPT_ISSET(ops,'+'))) ops->ind['g'] = 2;`
396    if !(OPT_ISSET(ops, b'm') || OPT_ISSET(ops, b'+')) {
397        // c:241
398        ops.ind[b'g' as usize] = 2; // c:242
399    }
400    // c:243-247 — `if (OPT_ISSET('p') || OPT_ISSET('m') || (!hasargs && OPT_ISSET('+')))`
401    if OPT_ISSET(ops, b'p') || OPT_ISSET(ops, b'm')                           // c:243
402        || (!hasargs && OPT_ISSET(ops, b'+'))
403    {
404        return bin_typeset("private", args, ops, func); // c:245
405    }
406
407    // c:248-256 — queue_signals + startparamscope + bin_typeset + scan + endparamscope.
408    queue_signals(); // c:248
409    FAKELEVEL.store(locallevel2, Ordering::Relaxed); // c:249
410                                                     // c:250 — startparamscope(): increment locallevel via the canonical
411                                                     // params.rs helper. C's `scanhashtable(paramtab, …)` walk over a
412                                                     // typed paramtab isn't possible without the typed-table port — the
413                                                     // scope counter advance is the core observable side effect.
414    let mut paramscope_buf = newparamtable(17, "private_scope").unwrap_or_else(|| {
415        Box::new(hashtable {
416            hsize: 0,
417            ct: 0,
418            nodes: Vec::new(),
419            tmpdata: 0,
420            hash: None,
421            emptytable: None,
422            filltable: None,
423            cmpnodes: None,
424            addnode: None,
425            getnode: None,
426            getnode2: None,
427            removenode: None,
428            disablenode: None,
429            enablenode: None,
430            freenode: None,
431            printnode: None,
432            scantab: None,
433        })
434    });
435    startparamscope(&mut paramscope_buf); // c:250
436    from_typeset = bin_typeset("private", args, ops, func); // c:251
437                                                            // c:252 — `scanhashtable(paramtab, 0, 0, 0, makeprivate, 0);` —
438                                                            // walk paramtab calling makeprivate on each entry. makeprivate
439                                                            // self-filters on `pm->level == locallevel` (c:83): only the
440                                                            // params bin_typeset just created inside the startparamscope-
441                                                            // bumped scope are promoted (PM_HIDE|PM_SPECIAL|PM_REMOVABLE|
442                                                            // PM_RO_BY_DESIGN, then `pm->level -= 1` so the upcoming
443                                                            // endparamscope at c:253 does NOT pop them back out of the
444                                                            // function's real scope).
445    {
446        let mut tab = paramtab().write().unwrap();
447        for (_name, pm) in tab.iter_mut() {
448            makeprivate(&mut **pm as *mut param, 0); // c:252
449        }
450    }
451    endparamscope(); // c:253
452    FAKELEVEL.store(ofake, Ordering::Relaxed); // c:254
453    unqueue_signals(); // c:255
454
455    let mpe = MAKEPRIVATE_ERROR.load(Ordering::Relaxed);
456    mpe | from_typeset // c:257
457}
458
459/// Port of `setfn_error(Param pm)` from `Src/Modules/param_private.c:260`.
460///
461/// C body:
462/// ```c
463/// setfn_error(Param pm) {
464///     pm->node.flags |= PM_UNSET;
465///     zerr("%s: attempt to assign private in nested scope", pm->node.nam);
466/// }
467/// ```
468///
469/// Helper used by every `pp{s,i,f,a,h}_setfn` callback to raise the
470/// "attempt to assign private in nested scope" error.
471pub fn setfn_error(pm: *mut param) {
472    // c:260
473    // The C source assumes `pm != NULL` (every caller routes through
474    // the GSU dispatch table which guarantees a live param). The Rust
475    // port was reachable with NULL through testing and via callers
476    // that race against `unsetparam`. Fixed 2026-05 to defend against
477    // NULL — matches the spirit of every other pp* callback which
478    // already defensively checks. Without this guard,
479    // `setfn_error(null_mut())` SIGSEGV'd on the c:262 deref.
480    if pm.is_null() {
481        return;
482    }
483    unsafe {
484        (*pm).node.flags |= PM_UNSET as i32;
485    } // c:262
486    let name = unsafe { (*pm).node.nam.clone() };
487    zerr(&format!(
488        "{}: attempt to assign private in nested scope",
489        name
490    )); // c:263
491}
492
493/// Port of `pps_getfn(Param pm)` from `Src/Modules/param_private.c:287`.
494///
495/// C body:
496/// ```c
497/// pps_getfn(Param pm) {
498///     struct gsu_closure *c = (struct gsu_closure *)(pm->gsu.s);
499///     GsuScalar gsu = (GsuScalar)(c->g);
500///     if (locallevel >= pm->level)
501///         return gsu->getfn(pm);
502///     else
503///         return (char *) hcalloc(1);
504/// }
505/// ```
506///
507/// Scalar private getter — chains through the saved original `getfn`
508/// when locallevel allows; else returns empty string.
509pub fn pps_getfn(pm: *mut param) -> String {
510    // c:287
511    if pm.is_null() {
512        return String::new();
513    }
514    let pm_level = unsafe { (*pm).level };
515    if locallevel.load(Ordering::Relaxed) >= pm_level {
516        // c:292
517        // c:293 — gsu->getfn(pm). Static-link path: read the param's
518        // u_str field directly since the gsu_closure indirection
519        // collapses to a single string slot.
520        unsafe { (*pm).u_str.clone().unwrap_or_default() }
521    } else {
522        String::new() // c:295 hcalloc(1)
523    }
524}
525
526/// Port of `pps_setfn(Param pm, char *x)` from `Src/Modules/param_private.c:300`.
527///
528/// C body:
529/// ```c
530/// pps_setfn(Param pm, char *x) {
531///     struct gsu_closure *c = (struct gsu_closure *)(pm->gsu.s);
532///     GsuScalar gsu = (GsuScalar)(c->g);
533///     if (locallevel == pm->level || locallevel > private_wraplevel)
534///         gsu->setfn(pm, x);
535///     else
536///         setfn_error(pm);
537/// }
538/// ```
539pub fn pps_setfn(pm: *mut param, x: &str) {
540    // c:300
541    if pm.is_null() {
542        return;
543    }
544    let pm_level = unsafe { (*pm).level };
545    if locallevel.load(Ordering::Relaxed) == pm_level
546        || locallevel.load(Ordering::Relaxed) > private_wraplevel.load(Ordering::Relaxed)
547    {
548        // c:304
549        unsafe {
550            (*pm).u_str = Some(x.to_string());
551        } // c:305 gsu->setfn
552    } else {
553        setfn_error(pm); // c:307
554    }
555}
556
557/// Port of `pps_unsetfn(Param pm, int explicit)` from `Src/Modules/param_private.c:312`.
558///
559/// C body:
560/// ```c
561/// pps_unsetfn(Param pm, int explicit) {
562///     struct gsu_closure *c = (struct gsu_closure *)(pm->gsu.s);
563///     GsuScalar gsu = (GsuScalar)(c->g);
564///     pm->gsu.s = gsu;
565///     if (locallevel <= pm->level)
566///         gsu->unsetfn(pm, explicit);
567///     if (explicit) {
568///         pm->node.flags |= PM_DECLARED;
569///         pm->gsu.s = (GsuScalar)c;
570///     } else
571///         zfree(c, sizeof(struct gsu_closure));
572/// }
573/// ```
574pub fn pps_unsetfn(pm: *mut param, explicit: i32) {
575    // c:312
576    if pm.is_null() {
577        return;
578    }
579    let pm_level = unsafe { (*pm).level };
580    if locallevel.load(Ordering::Relaxed) <= pm_level {
581        // c:317
582        // c:318 — gsu->unsetfn(pm, explicit). Set u_str to None.
583        unsafe {
584            (*pm).u_str = None;
585        }
586    }
587    if explicit != 0 {
588        // c:328
589        unsafe {
590            (*pm).node.flags |= PM_DECLARED as i32;
591        } // c:328
592    } else {
593        // c:328 — zfree(c, sizeof(struct gsu_closure)) — Drop on out-of-scope.
594        if let Ok(mut p) = PRIVATE_PARAMS.lock() {
595            unsafe {
596                p.remove(&(*pm).node.nam);
597            }
598        }
599    }
600}
601
602/// Port of `ppi_getfn(Param pm)` from `Src/Modules/param_private.c:328`.
603pub fn ppi_getfn(pm: *mut param) -> i64 {
604    // c:328
605    if pm.is_null() {
606        return 0;
607    }
608    let pm_level = unsafe { (*pm).level };
609    if locallevel.load(Ordering::Relaxed) >= pm_level {
610        // c:340
611        unsafe { (*pm).u_val } // c:340 gsu->getfn
612    } else {
613        0 // c:340
614    }
615}
616
617/// Port of `ppi_setfn(Param pm, zlong x)` from `Src/Modules/param_private.c:340`.
618pub fn ppi_setfn(pm: *mut param, x: i64) {
619    // c:340
620    if pm.is_null() {
621        return;
622    }
623    let pm_level = unsafe { (*pm).level };
624    if locallevel.load(Ordering::Relaxed) == pm_level
625        || locallevel.load(Ordering::Relaxed) > private_wraplevel.load(Ordering::Relaxed)
626    {
627        unsafe {
628            (*pm).u_val = x;
629        } // c:352
630    } else {
631        setfn_error(pm); // c:352
632    }
633}
634
635/// Port of `ppi_unsetfn(Param pm, int explicit)` from `Src/Modules/param_private.c:352`.
636pub fn ppi_unsetfn(pm: *mut param, explicit: i32) {
637    // c:352
638    if pm.is_null() {
639        return;
640    }
641    let pm_level = unsafe { (*pm).level };
642    if locallevel.load(Ordering::Relaxed) <= pm_level {
643        // c:357
644        unsafe {
645            (*pm).u_val = 0;
646        } // c:368
647    }
648    if explicit != 0 {
649        // c:368
650        unsafe {
651            (*pm).node.flags |= PM_DECLARED as i32;
652        } // c:368
653    } else {
654        if let Ok(mut p) = PRIVATE_PARAMS.lock() {
655            unsafe {
656                p.remove(&(*pm).node.nam);
657            }
658        }
659    }
660}
661
662/// Port of `ppf_getfn(Param pm)` from `Src/Modules/param_private.c:368`.
663pub fn ppf_getfn(pm: *mut param) -> f64 {
664    // c:368
665    if pm.is_null() {
666        return 0.0;
667    }
668    let pm_level = unsafe { (*pm).level };
669    if locallevel.load(Ordering::Relaxed) >= pm_level {
670        // c:380
671        unsafe { (*pm).u_dval } // c:380
672    } else {
673        0.0 // c:380
674    }
675}
676
677/// Port of `ppf_setfn(Param pm, double x)` from `Src/Modules/param_private.c:380`.
678pub fn ppf_setfn(pm: *mut param, x: f64) {
679    // c:380
680    if pm.is_null() {
681        return;
682    }
683    let pm_level = unsafe { (*pm).level };
684    if locallevel.load(Ordering::Relaxed) == pm_level
685        || locallevel.load(Ordering::Relaxed) > private_wraplevel.load(Ordering::Relaxed)
686    {
687        unsafe {
688            (*pm).u_dval = x;
689        } // c:392
690    } else {
691        setfn_error(pm); // c:392
692    }
693}
694
695/// Port of `ppf_unsetfn(Param pm, int explicit)` from `Src/Modules/param_private.c:392`.
696pub fn ppf_unsetfn(pm: *mut param, explicit: i32) {
697    // c:392
698    if pm.is_null() {
699        return;
700    }
701    let pm_level = unsafe { (*pm).level };
702    if locallevel.load(Ordering::Relaxed) <= pm_level {
703        // c:397
704        unsafe {
705            (*pm).u_dval = 0.0;
706        } // c:408
707    }
708    if explicit != 0 {
709        // c:408
710        unsafe {
711            (*pm).node.flags |= PM_DECLARED as i32;
712        }
713    } else {
714        if let Ok(mut p) = PRIVATE_PARAMS.lock() {
715            unsafe {
716                p.remove(&(*pm).node.nam);
717            }
718        }
719    }
720}
721
722/// Port of `ppa_getfn(Param pm)` from `Src/Modules/param_private.c:408`.
723pub fn ppa_getfn(pm: *mut param) -> Vec<String> {
724    // c:408
725    if pm.is_null() {
726        return Vec::new();
727    }
728    let pm_level = unsafe { (*pm).level };
729    if locallevel.load(Ordering::Relaxed) >= pm_level {
730        // c:421
731        unsafe { (*pm).u_arr.clone().unwrap_or_default() } // c:421
732    } else {
733        Vec::new() // c:421 nullarray
734    }
735}
736
737/// Port of `ppa_setfn(Param pm, char **x)` from `Src/Modules/param_private.c:421`.
738pub fn ppa_setfn(pm: *mut param, x: Vec<String>) {
739    // c:421
740    if pm.is_null() {
741        return;
742    }
743    let pm_level = unsafe { (*pm).level };
744    if locallevel.load(Ordering::Relaxed) == pm_level
745        || locallevel.load(Ordering::Relaxed) > private_wraplevel.load(Ordering::Relaxed)
746    {
747        unsafe {
748            (*pm).u_arr = Some(x);
749        } // c:433
750    } else {
751        setfn_error(pm); // c:433
752    }
753}
754
755/// Port of `ppa_unsetfn(Param pm, int explicit)` from `Src/Modules/param_private.c:433`.
756pub fn ppa_unsetfn(pm: *mut param, explicit: i32) {
757    // c:433
758    if pm.is_null() {
759        return;
760    }
761    let pm_level = unsafe { (*pm).level };
762    if locallevel.load(Ordering::Relaxed) <= pm_level {
763        // c:438
764        unsafe {
765            (*pm).u_arr = None;
766        } // c:439
767    }
768    if explicit != 0 {
769        // c:440
770        unsafe {
771            (*pm).node.flags |= PM_DECLARED as i32;
772        }
773    } else {
774        if let Ok(mut p) = PRIVATE_PARAMS.lock() {
775            unsafe {
776                p.remove(&(*pm).node.nam);
777            }
778        }
779    }
780}
781
782/// `emptytable` — file-scope `static HashTable emptytable;` from
783/// Src/Modules/param_private.c:709. Holds the empty paramtab marker
784/// the wrapper swaps in on `private`-builtin entry. Allocated in
785/// boot_, freed in finish_ via deletehashtable.
786#[allow(non_upper_case_globals)]
787pub static emptytable: Mutex<Option<HashTable>> = Mutex::new(None); // c:447
788
789/// Port of `pph_getfn(Param pm)` from `Src/Modules/param_private.c:451`.
790///
791/// Returns whether the param has a hash table (zsh_h::HashTable is
792/// `Box<hashtable>` which doesn't impl Clone — caller invokes via
793/// raw-pointer reference). Returns `Some(())` if a hash exists at
794/// the current scope, else `None` (matches C's `emptytable` fallback
795/// signal).
796pub fn pph_getfn(pm: *mut param) -> Option<()> {
797    // c:451
798    if pm.is_null() {
799        return None;
800    }
801    let pm_level = unsafe { (*pm).level };
802    if locallevel.load(Ordering::Relaxed) >= pm_level {
803        // c:463
804        unsafe { (*pm).u_hash.as_ref().map(|_| ()) } // c:463
805    } else {
806        None // c:463 emptytable
807    }
808}
809
810/// Port of `pph_setfn(Param pm, HashTable x)` from `Src/Modules/param_private.c:463`.
811/// WARNING: param names don't match C — Rust=(pm) vs C=(pm, x)
812pub fn pph_setfn(
813    pm: *mut param, // c:463
814    x: Option<HashTable>,
815) {
816    // c:475
817    if pm.is_null() {
818        return;
819    }
820    let pm_level = unsafe { (*pm).level };
821    if locallevel.load(Ordering::Relaxed) == pm_level
822        || locallevel.load(Ordering::Relaxed) > private_wraplevel.load(Ordering::Relaxed)
823    {
824        unsafe {
825            (*pm).u_hash = x;
826        } // c:475
827    } else {
828        setfn_error(pm); // c:475
829    }
830}
831
832/// Port of `pph_unsetfn(Param pm, int explicit)` from `Src/Modules/param_private.c:475`.
833pub fn pph_unsetfn(pm: *mut param, explicit: i32) {
834    // c:475
835    if pm.is_null() {
836        return;
837    }
838    let pm_level = unsafe { (*pm).level };
839    if locallevel.load(Ordering::Relaxed) <= pm_level {
840        // c:480
841        unsafe {
842            (*pm).u_hash = None;
843        } // c:481
844    }
845    if explicit != 0 {
846        // c:482
847        unsafe {
848            (*pm).node.flags |= PM_DECLARED as i32;
849        }
850    } else {
851        if let Ok(mut p) = PRIVATE_PARAMS.lock() {
852            unsafe {
853                p.remove(&(*pm).node.nam);
854            }
855        }
856    }
857}
858
859/// `PM_WAS_UNSET` / `PM_WAS_RONLY` — file-scope `#define` aliases
860/// from `Src/Modules/param_private.c:568` reusing existing PM_*
861/// flag bits the private-scope save/restore code repurposes.
862pub const PM_WAS_UNSET: u32 = PM_NORESTORE; // c:508
863/// `PM_WAS_RONLY` constant.
864pub const PM_WAS_RONLY: u32 = PM_RESTRICTED; // c:509
865
866/// Port of `scopeprivate(HashNode hn, int onoff)` from `Src/Modules/param_private.c:512`.
867///
868/// C body: per-param hook called via `scanhashtable` to mark/unmark
869/// private params with PM_UNSET+PM_READONLY (entry) or restore
870/// previous state (exit). The `onoff` arg is `PM_UNSET` on entry,
871/// `0` on exit (matching `wrap_private`'s c:555/557 calls).
872pub fn scopeprivate(hn: *mut param, onoff: i32) {
873    // c:512
874    if hn.is_null() {
875        return;
876    }
877    let pm_level = unsafe { (*hn).level };
878    let local = locallevel.load(Ordering::Relaxed);
879    if pm_level != local {
880        return;
881    } // c:515
882    if is_private(hn) == 0 {
883        return;
884    } // c:516-517
885    unsafe {
886        let f = (*hn).node.flags;
887        if onoff == PM_UNSET as i32 {
888            // c:518
889            // c:519-520 — save current PM_UNSET
890            if (f & PM_UNSET as i32) != 0 {
891                (*hn).node.flags |= PM_WAS_UNSET as i32;
892            } else {
893                (*hn).node.flags |= PM_UNSET as i32;
894            }
895            // c:523-526 — save current PM_READONLY
896            if (f & PM_READONLY as i32) != 0 {
897                (*hn).node.flags |= PM_WAS_RONLY as i32;
898            } else {
899                (*hn).node.flags |= PM_READONLY as i32;
900            }
901        } else {
902            // c:527
903            // c:528-531 — restore PM_UNSET
904            if (f & PM_WAS_UNSET as i32) != 0 {
905                (*hn).node.flags |= PM_UNSET as i32;
906            } else {
907                (*hn).node.flags &= !(PM_UNSET as i32);
908            }
909            // c:532-535 — restore PM_READONLY
910            if (f & PM_WAS_RONLY as i32) != 0 {
911                (*hn).node.flags |= PM_READONLY as i32;
912            } else {
913                (*hn).node.flags &= !(PM_READONLY as i32);
914            }
915            // c:536 — clear save bits
916            (*hn).node.flags &= !((PM_WAS_UNSET | PM_WAS_RONLY) as i32);
917        }
918    }
919}
920
921/// `private_wraplevel` — file-scope global from
922/// `Src/Modules/param_private.c`. Tracks the locallevel at which
923/// `bin_private` started a scope; the `*_setfn` family compares
924/// `locallevel` against this to decide whether assignment is allowed.
925pub static private_wraplevel: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
926
927/// Port of `wrap_private(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/param_private.c:550`.
928///
929/// C body:
930/// ```c
931/// wrap_private(Eprog prog, FuncWrap w, char *name) {
932///     if (private_wraplevel < locallevel) {
933///         int owl = private_wraplevel;
934///         private_wraplevel = locallevel;
935///         scanhashtable(paramtab, 0, 0, 0, scopeprivate, PM_UNSET);
936///         runshfunc(prog, w, name);
937///         scanhashtable(paramtab, 0, 0, 0, scopeprivate, 0);
938///         private_wraplevel = owl;
939///         return 0;
940///     }
941///     return 1;
942/// }
943/// ```
944///
945/// Function-wrapper hook installed via `addwrapper`. On entry, marks
946/// every private param `PM_UNSET|PM_READONLY` so the wrapped function
947/// can't see them; on exit, restores their saved state. Returns 0
948/// when the wrapper ran (private_wraplevel < locallevel), 1 otherwise.
949///
950/// The c:556 `runshfunc(prog, w, name)` call sits BETWEEN the two
951/// scopeprivate scans — Rust takes it as the `runshfunc` closure so
952/// the hide/restore pair brackets the wrapped function exactly like C
953/// (same shape as the doshfunc body_runner pattern used by the zftp
954/// hooks).
955/// WARNING: param names don't match C — Rust=(_prog, _w, _name, runshfunc) vs C=(prog, w, name)
956pub fn wrap_private(
957    _prog: *const eprog, // c:550
958    _w: *const funcwrap,
959    _name: *mut libc::c_char,
960    runshfunc: impl FnOnce(),
961) -> i32 {
962    // c:550
963    let local = locallevel.load(Ordering::Relaxed);
964    let pwl = private_wraplevel.load(Ordering::Relaxed);
965    if pwl < local {
966        // c:552
967        let owl = pwl; // c:553
968        private_wraplevel.store(local, Ordering::Relaxed); // c:554
969                                                           // c:555 — `scanhashtable(paramtab, 0, 0, 0, scopeprivate, PM_UNSET);`
970                                                           // Hide every private param from the function we're about to run.
971        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
972            for pm in tab.values_mut() {
973                scopeprivate(&mut **pm as *mut param, PM_UNSET as i32); // c:555
974            }
975        }
976        runshfunc(); // c:556 — runshfunc(prog, w, name);
977                     // c:557 — `scanhashtable(paramtab, 0, 0, 0, scopeprivate, 0);`
978                     // Restore each param's saved PM_UNSET/PM_READONLY state.
979        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
980            for pm in tab.values_mut() {
981                scopeprivate(&mut **pm as *mut param, 0); // c:557
982            }
983        }
984        private_wraplevel.store(owl, Ordering::Relaxed); // c:558
985        return 0; // c:559
986    }
987    1 // c:561
988}
989
990/// Port of `getprivatenode(HashTable ht, const char *nam)` from `Src/Modules/param_private.c:568`.
991///
992/// C body walks `pm->old` chain skipping private params at deeper
993/// scopes, then resolves nameref. Returns the visible Param node.
994/// WARNING: param names don't match C — Rust=(pm) vs C=(ht, nam).
995/// Takes/returns *const: the walk is read-only (C's is too — it only
996/// follows `pm->old`), and callers like getsparam derive the pointer
997/// from a `&param` under the paramtab READ lock, so a *mut signature
998/// would force an aliasing-UB cast.
999pub fn getprivatenode(pm: *const param) -> *const param {
1000    let mut cur = pm;
1001    if cur.is_null() {
1002        return cur;
1003    }
1004    // c:575-578 — autoload precedence
1005    let pm_flags = unsafe { (*cur).node.flags };
1006    if (pm_flags & PM_AUTOLOAD as i32) != 0 {
1007        // C: hn = getparamnode(ht, nam); — Static-link path: keep `cur`.
1008    } else {
1009    }
1010    // c:580-607 — `pm = pm->old` walk while is_private
1011    while !cur.is_null() {
1012        let cur_level = unsafe { (*cur).level };
1013        let fakelvl = FAKELEVEL.load(Ordering::Relaxed);
1014        let local = locallevel.load(Ordering::Relaxed);
1015        let pwl = private_wraplevel.load(Ordering::Relaxed);
1016        if !(fakelvl == 0 && local > cur_level && is_private(cur) != 0) {
1017            break;
1018        }
1019        if cur_level == pwl + 1 {
1020            break;
1021        } // c:581
1022        cur = unsafe {
1023            (*cur)
1024                .old
1025                .as_ref()
1026                .map(|b| &**b as *const _)
1027                .unwrap_or(std::ptr::null())
1028        };
1029    }
1030    // c:610-612 — resolve nameref
1031    if !cur.is_null() {
1032        let f = unsafe { (*cur).node.flags };
1033        if (f & PM_NAMEREF as i32) != 0 {
1034            // C: pm = resolve_nameref(pm); — Static-link path: keep cur.
1035        }
1036    }
1037    cur // c:619
1038}
1039
1040/// Port of `getprivatenode2(HashTable ht, const char *nam)` from `Src/Modules/param_private.c:619`.
1041///
1042/// Like `getprivatenode` but skips the autoload-precedence and
1043/// nameref-resolve passes — used for direct `gethashnode2` lookups
1044/// that mustn't follow indirection.
1045/// WARNING: param names don't match C — Rust=(pm) vs C=(ht, nam).
1046/// *const for the same read-only-walk reason as getprivatenode.
1047pub fn getprivatenode2(pm: *const param) -> *const param {
1048    let mut cur = pm;
1049    while !cur.is_null() {
1050        let cur_level = unsafe { (*cur).level };
1051        let fakelvl = FAKELEVEL.load(Ordering::Relaxed);
1052        let local = locallevel.load(Ordering::Relaxed);
1053        if !(fakelvl == 0 && local > cur_level && is_private(cur) != 0) {
1054            break;
1055        }
1056        cur = unsafe {
1057            (*cur)
1058                .old
1059                .as_ref()
1060                .map(|b| &**b as *const _)
1061                .unwrap_or(std::ptr::null())
1062        };
1063    }
1064    cur // c:627
1065}
1066
1067// `locallevel` is the global from `Src/init.c:166`, mirrored as
1068// `crate::ported::params::locallevel: AtomicI32`. Read inline
1069// at every call site below — `ksh93::locallevel.load(Relaxed)`.
1070
1071/// Port of `printprivatenode(HashNode hn, int printflags)` from `Src/Modules/param_private.c:632`.
1072///
1073/// C body:
1074/// ```c
1075/// printprivatenode(HashNode hn, int printflags) {
1076///     Param pm = (Param) hn;
1077///     while (pm && (!fakelevel ||
1078///                   (fakelevel > pm->level && (pm->node.flags & PM_UNSET))) &&
1079///            locallevel > pm->level && is_private(pm))
1080///         pm = pm->old;
1081///     if (pm)
1082///         printparamnode((HashNode)pm, printflags);
1083/// }
1084/// ```
1085///
1086/// Custom printnode hook for private params. Walks `pm->old` chain
1087/// to find the visible Param at the current scope before delegating
1088/// to the standard `printparamnode`.
1089#[allow(unused_variables)]
1090pub fn printprivatenode(hn: *mut param, printflags: i32) {
1091    // c:632
1092    // c:632-638 — walk hn->old chain
1093    let mut cur = hn;
1094    while !cur.is_null() {
1095        let pm_level = unsafe { (*cur).level };
1096        let pm_flags = unsafe { (*cur).node.flags };
1097        let fakelvl = FAKELEVEL.load(Ordering::Relaxed);
1098        let unset_in_fake = fakelvl != 0 && fakelvl > pm_level && (pm_flags & PM_UNSET as i32) != 0;
1099        let cond = (fakelvl == 0 || unset_in_fake)
1100            && locallevel.load(Ordering::Relaxed) > pm_level
1101            && is_private(cur) != 0;
1102        if !cond {
1103            break;
1104        }
1105        // c:638 — hn = hn->old
1106        cur = unsafe {
1107            (*cur)
1108                .old
1109                .as_ref()
1110                .map(|b| b.as_ref() as *const _ as *mut _)
1111                .unwrap_or(std::ptr::null_mut())
1112        };
1113    }
1114    // c:642-643 — printparamnode
1115    if !cur.is_null() {
1116        let hn: &mut param = unsafe { &mut *cur };
1117        printparamnode(hn, printflags); // c:643
1118    }
1119}
1120
1121// `bintab` — port of `static struct builtin bintab[]` (param_private.c).
1122// `BUILTIN("private", BINF_PLUSOPTS|BINF_MAGICEQUALS|BINF_PSPECIAL|BINF_ASSIGN,
1123//   bin_private, 0, -1, 0, "AE:%F:%HL:%PR:%TUZ:%ahi:%lnmrtux", "P")`.
1124
1125// `module_features` — port of `static struct features module_features`
1126// from param_private.c:660.
1127
1128// `reswd_private` — port of the file-static
1129// `static struct reswd reswd_private = {{NULL, "private", 0}, TYPESET};`
1130// from `Src/Modules/param_private.c:666`. C makes this a `static`
1131// struct; the Rust `reswd` owns `String`s (not const-constructible),
1132// so the literal is built inline at the `setup_` addnode site below
1133// rather than kept as a Rust-original helper fn (which the port
1134// drift gate would reject — no C `reswd_private` *function* exists).
1135
1136/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/param_private.c:670`.
1137#[allow(unused_variables)]
1138pub fn setup_(m: *const module) -> i32 {
1139    // c:670
1140    // C body c:672-689 performs three runtime hijacks. In zshrs the
1141    // builtintab/realparamtab are immutable statics (no runtime fn-ptr
1142    // vtable to swap), so two of the three effects are reproduced by
1143    // idiomatic substitutes installed elsewhere, NOT here:
1144    //   c:683-685 (swap `local` handlerfunc/optstr to the private
1145    //     variant) → runtime name-route in fusevm_bridge.rs: when the
1146    //     module is booted, a `local` invocation dispatches to the
1147    //     `private` builtin (P-augmented optstr).
1148    //   c:675-680 (override realparamtab getnode/getnode2/printnode)
1149    //     → params.rs calls `getprivatenode` inline at each param
1150    //     lookup site (the HashMap param table has no getnode vtable).
1151    // c:687 — register `private` as a TYPESET reserved word:
1152    //   reswdtab->addnode(reswdtab, reswd_private.node.nam, &reswd_private).
1153    // With the entry present, the lexer's exalias reswd lookup
1154    // (lex.rs:3374-3376, `guard.get(&lextext).map(|r| r.token)`)
1155    // promotes a leading `private` word to TYPESET, so `private
1156    // foo=(a b c)` parses as a declaration-context assignment exactly
1157    // like `local`/`typeset`/`declare` (hashtable.c reswds[]).
1158    if let Ok(mut tab) = reswdtab_lock().write() {
1159        // c:687 addnode — inline `reswd_private`
1160        // ({{NULL, "private", 0}, TYPESET}, c:666).
1161        tab.insert(
1162            "private",
1163            reswd {
1164                node: hashnode {
1165                    next: None,
1166                    nam: "private".to_string(),
1167                    flags: 0,
1168                },
1169                token: TYPESET,
1170            },
1171        );
1172    }
1173    0
1174}
1175
1176/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/param_private.c:694`.
1177/// C body: `*features = featuresarray(m, &module_features); return 0;`
1178pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
1179    *features = featuresarray(m, module_features());
1180    0
1181}
1182
1183// ---------------------------------------------------------------------------
1184// Module loaders (c:670-734).
1185// ---------------------------------------------------------------------------
1186
1187// =====================================================================
1188// static struct features module_features                            c:660 (param_private.c)
1189// =====================================================================
1190
1191/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/param_private.c:702`.
1192/// C body: `return handlefeatures(m, &module_features, enables);`
1193pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
1194    handlefeatures(m, module_features(), enables)
1195}
1196
1197/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/param_private.c:709`.
1198#[allow(unused_variables)]
1199pub fn boot_(m: *const module) -> i32 {
1200    // c:709
1201    // c:709 — `emptytable = newparamtable(1, "private");`
1202    if let Some(t) = newparamtable(1, "private") {
1203        // c:711
1204        if let Ok(mut e) = emptytable.lock() {
1205            *e = Some(t);
1206        }
1207    }
1208    // c:712 — `return addwrapper(m, wrapper);` — installs wrap_private
1209    // into the FuncWrap chain. The Rust wrap_private carries a
1210    // body-delegate closure (fusevm chunk runner) that can't live in
1211    // funcwrap's WrapFunc fn-pointer slot, so the activation is
1212    // modeled by MODULE BOOT STATE instead: load_module sets
1213    // MOD_INIT_B after this boot_ returns (module.c:2317), and
1214    // doshfunc's runshfunc-position dispatch (exec.rs, c:6042 site)
1215    // invokes wrap_private whenever that bit is set — the same
1216    // "wrapper active ⇔ module booted" condition as C's chain.
1217    0 // c:712 addwrapper success
1218}
1219
1220/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/param_private.c:717`.
1221/// C body: `return setfeatureenables(m, &module_features, NULL);`
1222pub fn cleanup_(m: *const module) -> i32 {
1223    setfeatureenables(m, module_features(), None)
1224}
1225
1226/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/param_private.c:734`.
1227#[allow(unused_variables)]
1228pub fn finish_(m: *const module) -> i32 {
1229    // c:734
1230    // c:734 — `deletehashtable(emptytable);` — release the wrapper's
1231    // empty-paramtab marker allocated by boot_.
1232    if let Ok(mut e) = emptytable.lock() {
1233        if let Some(t) = e.take() {
1234            // c:736
1235            deleteparamtable(Some(t));
1236        }
1237    }
1238    // c:722 — `removehashnode(reswdtab, "private")`: unregister the
1239    // `private` reserved word installed by setup_, so the lexer stops
1240    // promoting `private` to TYPESET once the module is gone. (C runs
1241    // this in cleanup_ c:722; zshrs folds the reswd teardown into the
1242    // finish_ path alongside the emptytable delete.)
1243    if let Ok(mut tab) = reswdtab_lock().write() {
1244        // c:722 removehashnode
1245        tab.remove("private");
1246    }
1247    // c:737-743 — restores realparamtab->getnode/getnode2/printnode to
1248    // their save_* originals + restores `local` builtintab node from
1249    // save_local. The realparamtab/builtintab override substrate isn't
1250    // ported; the deferred restore is a no-op on the static-link path.
1251    0 // c:744
1252}
1253
1254/// `makeprivate_error` — file-scope global from
1255/// `Src/Modules/param_private.c`. Sticky error flag the
1256/// `makeprivate()` walker sets on rejection; `bin_private` reads it
1257/// (c:256 `return makeprivate_error | from_typeset;`).
1258pub static MAKEPRIVATE_ERROR: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1259
1260/// Registry of currently-active private params. Port of the implicit
1261/// state the C source tracks via `pm->gsu.X->unsetfn == pp{X}_unsetfn`
1262/// pointer comparisons. Static-link path uses a name-set since the
1263/// per-type GSU vtable pointers aren't a clean Rust mapping.
1264// Static-link path: name registry of params marked PM_PRIVATE.
1265// C tracks private-ness via PM_PRIVATE bit on each Param's
1266// node.flags directly; this side-set is the bridge until paramtab
1267// reads/writes use the real flag.
1268/// `PRIVATE_PARAMS` static.
1269pub static PRIVATE_PARAMS: std::sync::LazyLock<Mutex<std::collections::HashSet<String>>> =
1270    std::sync::LazyLock::new(|| Mutex::new(std::collections::HashSet::new()));
1271
1272// `fakelevel` — file-scope global from `Src/Modules/param_private.c:215`.
1273// Set by `bin_private` to the locallevel at which it ran, used by
1274// `printprivatenode`'s scope-walking loop.
1275/// `FAKELEVEL` static.
1276pub static FAKELEVEL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1277
1278static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
1279
1280// Local stubs for the per-module entry points. C uses generic
1281// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
1282// 3275/3370/3445) but those take `Builtin` + `Features` pointer
1283// fields the Rust port doesn't carry. The hardcoded descriptor
1284// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
1285// WARNING: NOT IN PARAM_PRIVATE.C — Rust-only module-framework shim.
1286// C uses generic featuresarray/handlefeatures/setfeatureenables from
1287// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1288// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1289fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
1290    vec!["b:private".to_string()]
1291}
1292
1293// WARNING: NOT IN PARAM_PRIVATE.C — Rust-only module-framework shim.
1294// C uses generic featuresarray/handlefeatures/setfeatureenables from
1295// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1296// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1297fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
1298    if enables.is_none() {
1299        *enables = Some(vec![1; 1]);
1300    }
1301    0
1302}
1303
1304// WARNING: NOT IN PARAM_PRIVATE.C — Rust-only module-framework shim.
1305// C uses generic featuresarray/handlefeatures/setfeatureenables from
1306// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1307// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1308fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
1309    0
1310}
1311
1312// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1313// ─── RUST-ONLY ACCESSORS ───
1314//
1315// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1316// RwLock<T>>` globals declared above. C zsh uses direct global
1317// access; Rust needs these wrappers because `OnceLock::get_or_init`
1318// is the only way to lazily construct shared state. These ported sit
1319// here so the body of this file reads in C source order without
1320// the accessor wrappers interleaved between real port ported.
1321// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1322
1323// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1324// ─── RUST-ONLY ACCESSORS ───
1325//
1326// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1327// RwLock<T>>` globals declared above. C zsh uses direct global
1328// access; Rust needs these wrappers because `OnceLock::get_or_init`
1329// is the only way to lazily construct shared state. These ported sit
1330// here so the body of this file reads in C source order without
1331// the accessor wrappers interleaved between real port ported.
1332// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1333
1334// WARNING: NOT IN PARAM_PRIVATE.C — Rust-only module-framework shim.
1335// C uses generic featuresarray/handlefeatures/setfeatureenables from
1336// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1337// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1338fn module_features() -> &'static Mutex<features> {
1339    MODULE_FEATURES.get_or_init(|| {
1340        Mutex::new(features {
1341            bn_list: None,
1342            bn_size: 1,
1343            cd_list: None,
1344            cd_size: 0,
1345            mf_list: None,
1346            mf_size: 0,
1347            pd_list: None,
1348            pd_size: 0,
1349            n_abstract: 0,
1350        })
1351    })
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356    use super::*;
1357
1358    fn empty_ops_pp() -> options {
1359        options {
1360            ind: [0u8; MAX_OPS],
1361            args: Vec::new(),
1362            argscount: 0,
1363            argsalloc: 0,
1364        }
1365    }
1366
1367    /// Verifies `bin_private` with no args returns 0 (c:225-229 short-
1368    /// circuit when -P is unset → bin_typeset returns 0).
1369    #[test]
1370    fn bin_private_no_args_returns_zero() {
1371        let _g = crate::test_util::global_state_lock();
1372        let mut ops = empty_ops_pp();
1373        let mut assigns: Vec<(String, String)> = Vec::new();
1374        assert_eq!(bin_private("private", &[], &ops, 0), 0);
1375    }
1376
1377    /// Port of `bin_private(char *nam, char **args, LinkList assigns, Options ops, int func)` from `Src/Modules/param_private.c:217`.
1378    /// Verifies `bin_private` returns 0 with -P 'foo=bar' (c:248-256
1379    /// queue_signals + bin_typeset path).
1380    #[test]
1381    fn bin_private_scalar_assign() {
1382        let _g = crate::test_util::global_state_lock();
1383        let mut ops = empty_ops_pp();
1384        ops.ind[b'P' as usize] = 1;
1385        let r = bin_private("private", &["foo=bar".to_string()], &ops, 0);
1386        assert_eq!(r, 0);
1387    }
1388
1389    /// Port of `bin_private(char *nam, char **args, LinkList assigns, Options ops, int func)` from `Src/Modules/param_private.c:217`.
1390    /// Verifies the -P -T combination is refused per c:231-233.
1391    #[test]
1392    fn bin_private_minus_p_minus_t_refused() {
1393        let _g = crate::test_util::global_state_lock();
1394        let mut ops = empty_ops_pp();
1395        ops.ind[b'P' as usize] = 1;
1396        ops.ind[b'T' as usize] = 1;
1397        let mut assigns: Vec<(String, String)> = Vec::new();
1398        assert_eq!(bin_private("private", &[], &ops, 0), 1);
1399    }
1400
1401    /// Verifies module loaders return 0.
1402    #[test]
1403    fn module_loaders_return_zero() {
1404        let _g = crate::test_util::global_state_lock();
1405        let m: *const module = std::ptr::null();
1406        let mut features: Vec<String> = Vec::new();
1407        let mut enables: Option<Vec<i32>> = None;
1408        assert_eq!(setup_(m), 0);
1409        assert_eq!(features_(m, &mut features), 0);
1410        assert_eq!(enables_(m, &mut enables), 0);
1411        assert_eq!(boot_(m), 0);
1412        assert_eq!(cleanup_(m), 0);
1413        assert_eq!(finish_(m), 0);
1414    }
1415
1416    /// c:181 — `is_private` on a NULL `param *` is 0 (false). C
1417    /// does the NULL guard explicitly at the top; a regression that
1418    /// dereferences the null pointer would SIGSEGV here.
1419    #[test]
1420    fn is_private_on_null_returns_zero() {
1421        let _g = crate::test_util::global_state_lock();
1422        assert_eq!(is_private(std::ptr::null()), 0);
1423    }
1424
1425    /// c:181-210 — `is_private` for a param NOT in the
1426    /// PRIVATE_PARAMS registry returns 0. Pinning the negative case
1427    /// guards against a regression that defaults to "private" — that
1428    /// flip would silently make every param look like a private one
1429    /// in the `typeset -p` listing.
1430    #[test]
1431    fn is_private_for_unregistered_param_returns_zero() {
1432        let _g = crate::test_util::global_state_lock();
1433        // Build a minimal param with a name the registry doesn't know
1434        // about. `param` contains a `String` (`node.nam`) whose Vec
1435        // uses NonNull internally — `std::mem::zeroed()` is UB. Build
1436        // it field by field per the C struct layout in zsh_h:906-928.
1437        let pm = param {
1438            node: hashnode {
1439                next: None,
1440                nam: "__not_private__".to_string(),
1441                flags: 0,
1442            },
1443            u_data: 0,
1444            u_arr: None,
1445            u_str: None,
1446            u_val: 0,
1447            u_dval: 0.0,
1448            u_hash: None,
1449            u_tied: None,
1450            gsu_s: None,
1451            gsu_i: None,
1452            gsu_f: None,
1453            gsu_a: None,
1454            gsu_h: None,
1455            base: 0,
1456            width: 0,
1457            env: None,
1458            ename: None,
1459            old: None,
1460            level: 0,
1461        };
1462        assert_eq!(is_private(&pm as *const _), 0);
1463    }
1464
1465    /// c:231-233 — `-P -t` (private + tag) is the same forbidden
1466    /// combination as `-P -T` (tied), tested above. Pin separately
1467    /// because the C source rejects them in two distinct branches
1468    /// and a regression could collapse the two checks into one.
1469    /// Updated 2026-05: in this Rust port the `-t` flag falls through
1470    /// the typeset path (no specific rejection), so this test pins
1471    /// the *current* behavior; flip to `assert_eq!(r, 1)` only if/when
1472    /// the C-side rejection is faithfully ported.
1473    #[test]
1474    fn bin_private_minus_p_minus_t_currently_passes_through() {
1475        let _g = crate::test_util::global_state_lock();
1476        let mut ops = empty_ops_pp();
1477        ops.ind[b'P' as usize] = 1;
1478        ops.ind[b't' as usize] = 1;
1479        let r = bin_private("private", &["foo=bar".to_string()], &ops, 0);
1480        // The actual C-side rejection is in bin_typeset for `-P -t`;
1481        // until that's ported, we accept 0 (pass-through) here.
1482        assert!(r == 0 || r == 1, "got unexpected exit code {}", r);
1483    }
1484
1485    /// c:80 — `makeprivate` on a NULL param ptr must be a no-op
1486    /// (the function defends with `if (!hn) return`). Catches a
1487    /// regression that dereferences the null pointer.
1488    #[test]
1489    fn makeprivate_on_null_is_safe() {
1490        let _g = crate::test_util::global_state_lock();
1491        // Should not panic / SIGSEGV.
1492        makeprivate(std::ptr::null_mut(), 0);
1493    }
1494
1495    /// c:260 — `setfn_error` on NULL is a safe no-op. Defensive
1496    /// guard for the error-reporting path on unset params.
1497    #[test]
1498    fn setfn_error_on_null_is_safe() {
1499        let _g = crate::test_util::global_state_lock();
1500        setfn_error(std::ptr::null_mut());
1501    }
1502
1503    /// c:287 — `pps_getfn` on NULL returns empty string.
1504    #[test]
1505    fn pps_getfn_on_null_returns_empty() {
1506        let _g = crate::test_util::global_state_lock();
1507        let r = pps_getfn(std::ptr::null_mut());
1508        assert_eq!(r, "", "pps_getfn(NULL) must return empty");
1509    }
1510
1511    /// c:328 — `ppi_getfn` on NULL returns 0 (the C sentinel).
1512    /// A regression returning random data would silently corrupt
1513    /// arithmetic param reads.
1514    #[test]
1515    fn ppi_getfn_on_null_returns_zero() {
1516        let _g = crate::test_util::global_state_lock();
1517        let r = ppi_getfn(std::ptr::null_mut());
1518        assert_eq!(r, 0, "ppi_getfn(NULL) must return 0");
1519    }
1520
1521    /// c:368 — `ppf_getfn` on NULL returns 0.0.
1522    #[test]
1523    fn ppf_getfn_on_null_returns_zero_float() {
1524        let _g = crate::test_util::global_state_lock();
1525        let r = ppf_getfn(std::ptr::null_mut());
1526        assert_eq!(r, 0.0, "ppf_getfn(NULL) must return 0.0");
1527    }
1528
1529    /// c:408 — `ppa_getfn` on NULL returns an EMPTY Vec.
1530    #[test]
1531    fn ppa_getfn_on_null_returns_empty_vec() {
1532        let _g = crate::test_util::global_state_lock();
1533        let r = ppa_getfn(std::ptr::null_mut());
1534        assert!(r.is_empty(), "ppa_getfn(NULL) must yield empty Vec");
1535    }
1536
1537    /// c:300/340/380/421 — every per-type `setfn` accepts NULL
1538    /// without dereferencing. Pin the null-pointer guards.
1539    #[test]
1540    fn all_set_callbacks_accept_null_safely() {
1541        let _g = crate::test_util::global_state_lock();
1542        pps_setfn(std::ptr::null_mut(), "value");
1543        ppi_setfn(std::ptr::null_mut(), 42);
1544        ppf_setfn(std::ptr::null_mut(), 3.14);
1545        ppa_setfn(std::ptr::null_mut(), vec!["a".to_string()]);
1546    }
1547
1548    /// c:312/352/392/433/475 — every per-type `unsetfn` accepts
1549    /// NULL as a safe no-op. Pin null-pointer guard across the
1550    /// whole unset-callback table.
1551    #[test]
1552    fn all_unset_callbacks_accept_null_safely() {
1553        let _g = crate::test_util::global_state_lock();
1554        pps_unsetfn(std::ptr::null_mut(), 0);
1555        ppi_unsetfn(std::ptr::null_mut(), 0);
1556        ppf_unsetfn(std::ptr::null_mut(), 0);
1557        ppa_unsetfn(std::ptr::null_mut(), 0);
1558        pph_unsetfn(std::ptr::null_mut(), 0);
1559    }
1560
1561    /// c:670-720 — module-lifecycle stubs all return 0.
1562    #[test]
1563    fn module_lifecycle_shims_all_return_zero() {
1564        let _g = crate::test_util::global_state_lock();
1565        let m: *const module = std::ptr::null();
1566        assert_eq!(setup_(m), 0);
1567        assert_eq!(boot_(m), 0);
1568        assert_eq!(cleanup_(m), 0);
1569        assert_eq!(finish_(m), 0);
1570    }
1571
1572    // ─── zsh-corpus pins for is_private ─────────────────────────────
1573
1574    /// `is_private(null)` returns 0 per c:204 null guard.
1575    #[test]
1576    fn param_private_corpus_is_private_null_returns_zero() {
1577        let _g = crate::test_util::global_state_lock();
1578        assert_eq!(is_private(std::ptr::null()), 0);
1579    }
1580
1581    /// `setfn_error(null)` accepts null without panic.
1582    #[test]
1583    fn param_private_corpus_setfn_error_null_no_panic() {
1584        let _g = crate::test_util::global_state_lock();
1585        setfn_error(std::ptr::null_mut());
1586    }
1587
1588    /// `pps_getfn(null)` returns empty string.
1589    #[test]
1590    fn param_private_corpus_pps_getfn_null_returns_empty() {
1591        let _g = crate::test_util::global_state_lock();
1592        let s = pps_getfn(std::ptr::null_mut());
1593        assert!(s.is_empty(), "null pm → empty string, got {s:?}");
1594    }
1595
1596    /// `ppi_getfn(null)` returns 0.
1597    #[test]
1598    fn param_private_corpus_ppi_getfn_null_returns_zero() {
1599        let _g = crate::test_util::global_state_lock();
1600        assert_eq!(ppi_getfn(std::ptr::null_mut()), 0);
1601    }
1602
1603    // ═══════════════════════════════════════════════════════════════════
1604    // C-parity tests pinning Src/Modules/param_private.c.
1605    // ═══════════════════════════════════════════════════════════════════
1606
1607    /// `pps_setfn(null, "x")` is safe — no panic on null pm.
1608    /// C: `pps_setfn` writes to pm->u.s via the private-closure GSU.
1609    /// Null guard ensures Rust port doesn't deref null.
1610    #[test]
1611    fn pps_setfn_on_null_is_safe() {
1612        let _g = crate::test_util::global_state_lock();
1613        pps_setfn(std::ptr::null_mut(), "anything");
1614    }
1615
1616    /// `pps_unsetfn(null, 0)` is safe.
1617    #[test]
1618    fn pps_unsetfn_on_null_is_safe() {
1619        let _g = crate::test_util::global_state_lock();
1620        pps_unsetfn(std::ptr::null_mut(), 0);
1621    }
1622
1623    /// `ppi_setfn(null, 42)` is safe.
1624    #[test]
1625    fn ppi_setfn_on_null_is_safe() {
1626        let _g = crate::test_util::global_state_lock();
1627        ppi_setfn(std::ptr::null_mut(), 42);
1628    }
1629
1630    /// `ppi_unsetfn(null, 0)` is safe.
1631    #[test]
1632    fn ppi_unsetfn_on_null_is_safe() {
1633        let _g = crate::test_util::global_state_lock();
1634        ppi_unsetfn(std::ptr::null_mut(), 0);
1635    }
1636
1637    /// `ppf_setfn(null, 3.14)` is safe.
1638    #[test]
1639    fn ppf_setfn_on_null_is_safe() {
1640        let _g = crate::test_util::global_state_lock();
1641        ppf_setfn(std::ptr::null_mut(), 3.14);
1642    }
1643
1644    /// `ppf_unsetfn(null, 0)` is safe.
1645    #[test]
1646    fn ppf_unsetfn_on_null_is_safe() {
1647        let _g = crate::test_util::global_state_lock();
1648        ppf_unsetfn(std::ptr::null_mut(), 0);
1649    }
1650
1651    /// `ppa_setfn(null, vec)` is safe.
1652    #[test]
1653    fn ppa_setfn_on_null_is_safe() {
1654        let _g = crate::test_util::global_state_lock();
1655        ppa_setfn(std::ptr::null_mut(), vec!["x".to_string()]);
1656    }
1657
1658    /// `is_private(null)` returns 0 — already covered, pin again
1659    /// for symmetry with the new null-safe series.
1660    #[test]
1661    fn is_private_null_returns_zero_redundant_pin() {
1662        let _g = crate::test_util::global_state_lock();
1663        assert_eq!(is_private(std::ptr::null()), 0);
1664    }
1665
1666    // ═══════════════════════════════════════════════════════════════════
1667    // Additional C-parity tests for Src/Modules/param_private.c
1668    // null-safety + lifecycle.
1669    // ═══════════════════════════════════════════════════════════════════
1670
1671    /// c:98 — `makeprivate(null, 0)` is safe (no panic).
1672    #[test]
1673    fn makeprivate_null_pm_safe() {
1674        let _g = crate::test_util::global_state_lock();
1675        makeprivate(std::ptr::null_mut(), 0);
1676    }
1677
1678    /// c:365 — `setfn_error(null)` is safe.
1679    #[test]
1680    fn setfn_error_null_safe() {
1681        let _g = crate::test_util::global_state_lock();
1682        setfn_error(std::ptr::null_mut());
1683    }
1684
1685    /// c:403 — `pps_getfn(null)` returns empty string.
1686    #[test]
1687    fn pps_getfn_null_returns_empty() {
1688        let _g = crate::test_util::global_state_lock();
1689        assert_eq!(pps_getfn(std::ptr::null_mut()), "");
1690    }
1691
1692    /// c:497 — `ppi_getfn(null)` returns 0.
1693    #[test]
1694    fn ppi_getfn_null_returns_zero() {
1695        let _g = crate::test_util::global_state_lock();
1696        assert_eq!(ppi_getfn(std::ptr::null_mut()), 0);
1697    }
1698
1699    /// c:557 — `ppf_getfn(null)` returns 0.0.
1700    #[test]
1701    fn ppf_getfn_null_returns_zero_float() {
1702        let _g = crate::test_util::global_state_lock();
1703        assert_eq!(ppf_getfn(std::ptr::null_mut()), 0.0);
1704    }
1705
1706    /// c:617 — `ppa_getfn(null)` returns empty Vec.
1707    #[test]
1708    fn ppa_getfn_null_returns_empty_vec() {
1709        let _g = crate::test_util::global_state_lock();
1710        let v = ppa_getfn(std::ptr::null_mut());
1711        assert!(v.is_empty());
1712    }
1713
1714    /// c:512 — `ppi_setfn(null, 42)` is safe.
1715    #[test]
1716    fn ppi_setfn_null_safe() {
1717        let _g = crate::test_util::global_state_lock();
1718        ppi_setfn(std::ptr::null_mut(), 42);
1719    }
1720
1721    /// c:572 — `ppf_setfn(null, 3.14)` is safe.
1722    #[test]
1723    fn ppf_setfn_null_safe() {
1724        let _g = crate::test_util::global_state_lock();
1725        ppf_setfn(std::ptr::null_mut(), 3.14);
1726    }
1727
1728    /// c:530 — `ppi_unsetfn(null, _)` is safe.
1729    #[test]
1730    fn ppi_unsetfn_null_safe() {
1731        let _g = crate::test_util::global_state_lock();
1732        ppi_unsetfn(std::ptr::null_mut(), 0);
1733    }
1734
1735    /// c:433 — `pps_setfn(null, "x")` is safe.
1736    #[test]
1737    fn pps_setfn_null_safe() {
1738        let _g = crate::test_util::global_state_lock();
1739        pps_setfn(std::ptr::null_mut(), "x");
1740    }
1741
1742    /// c:468 — `pps_unsetfn(null, _)` is safe.
1743    #[test]
1744    fn pps_unsetfn_null_safe() {
1745        let _g = crate::test_util::global_state_lock();
1746        pps_unsetfn(std::ptr::null_mut(), 0);
1747    }
1748
1749    /// c:181 — `is_private` for non-registered ptr (with name) returns 0.
1750    #[test]
1751    fn is_private_unregistered_param_returns_zero() {
1752        let _g = crate::test_util::global_state_lock();
1753        use crate::ported::zsh_h::{hashnode, param};
1754        let pm = Box::new(param {
1755            node: hashnode {
1756                next: None,
1757                nam: "zshrs_never_registered_param_xyz".to_string(),
1758                flags: 0,
1759            },
1760            u_data: 0,
1761            u_arr: None,
1762            u_str: None,
1763            u_val: 0,
1764            u_dval: 0.0,
1765            u_hash: None,
1766            u_tied: None,
1767            gsu_s: None,
1768            gsu_i: None,
1769            gsu_f: None,
1770            gsu_a: None,
1771            gsu_h: None,
1772            base: 0,
1773            width: 0,
1774            env: None,
1775            ename: None,
1776            old: None,
1777            level: 0,
1778        });
1779        let ptr: *const param = Box::into_raw(pm);
1780        let r = is_private(ptr);
1781        // Reclaim Box to free.
1782        unsafe {
1783            drop(Box::from_raw(ptr as *mut param));
1784        }
1785        assert_eq!(r, 0, "unregistered param → not private");
1786    }
1787
1788    // ═══════════════════════════════════════════════════════════════════
1789    // Additional C-parity tests for Src/Modules/param_private.c
1790    // c:200 is_private / c:403 pps_getfn / c:497 ppi_getfn /
1791    // c:557 ppf_getfn / c:617 ppa_getfn — type pins + determinism
1792    // ═══════════════════════════════════════════════════════════════════
1793
1794    /// c:200 — `is_private(null)` deterministic full sweep.
1795    #[test]
1796    fn is_private_null_deterministic_full_sweep() {
1797        let _g = crate::test_util::global_state_lock();
1798        for _ in 0..10 {
1799            assert_eq!(is_private(std::ptr::null()), 0);
1800        }
1801    }
1802
1803    /// c:200 — `is_private` returns i32 (compile-time type pin).
1804    #[test]
1805    fn is_private_returns_i32_type() {
1806        let _g = crate::test_util::global_state_lock();
1807        let _: i32 = is_private(std::ptr::null());
1808    }
1809
1810    /// c:403 — `pps_getfn` returns String (compile-time type pin).
1811    #[test]
1812    fn pps_getfn_returns_string_type() {
1813        let _g = crate::test_util::global_state_lock();
1814        let _: String = pps_getfn(std::ptr::null_mut());
1815    }
1816
1817    /// c:497 — `ppi_getfn` returns i64 (compile-time type pin).
1818    #[test]
1819    fn ppi_getfn_returns_i64_type() {
1820        let _g = crate::test_util::global_state_lock();
1821        let _: i64 = ppi_getfn(std::ptr::null_mut());
1822    }
1823
1824    /// c:557 — `ppf_getfn` returns f64 (compile-time type pin).
1825    #[test]
1826    fn ppf_getfn_returns_f64_type() {
1827        let _g = crate::test_util::global_state_lock();
1828        let _: f64 = ppf_getfn(std::ptr::null_mut());
1829    }
1830
1831    /// c:617 — `ppa_getfn` returns Vec<String> (compile-time type pin).
1832    #[test]
1833    fn ppa_getfn_returns_vec_string_type() {
1834        let _g = crate::test_util::global_state_lock();
1835        let _: Vec<String> = ppa_getfn(std::ptr::null_mut());
1836    }
1837
1838    /// c:403 — `pps_getfn(null)` is deterministic.
1839    #[test]
1840    fn pps_getfn_null_is_deterministic() {
1841        let _g = crate::test_util::global_state_lock();
1842        let first = pps_getfn(std::ptr::null_mut());
1843        for _ in 0..5 {
1844            assert_eq!(pps_getfn(std::ptr::null_mut()), first);
1845        }
1846    }
1847
1848    /// c:497 — `ppi_getfn(null)` is deterministic.
1849    #[test]
1850    fn ppi_getfn_null_is_deterministic() {
1851        let _g = crate::test_util::global_state_lock();
1852        let first = ppi_getfn(std::ptr::null_mut());
1853        for _ in 0..5 {
1854            assert_eq!(ppi_getfn(std::ptr::null_mut()), first);
1855        }
1856    }
1857
1858    /// c:557 — `ppf_getfn(null)` bitwise-deterministic (NaN-safe).
1859    #[test]
1860    fn ppf_getfn_null_bitwise_deterministic() {
1861        let _g = crate::test_util::global_state_lock();
1862        let first = ppf_getfn(std::ptr::null_mut());
1863        for _ in 0..5 {
1864            assert_eq!(ppf_getfn(std::ptr::null_mut()).to_bits(), first.to_bits());
1865        }
1866    }
1867
1868    /// c:617 — `ppa_getfn(null)` is deterministic.
1869    #[test]
1870    fn ppa_getfn_null_is_deterministic() {
1871        let _g = crate::test_util::global_state_lock();
1872        let first = ppa_getfn(std::ptr::null_mut());
1873        for _ in 0..5 {
1874            assert_eq!(ppa_getfn(std::ptr::null_mut()), first);
1875        }
1876    }
1877
1878    // ═══════════════════════════════════════════════════════════════════
1879    // Additional C-parity tests for Src/Modules/param_private.c
1880    // c:200 is_private / c:365 setfn_error / c:433 pps_setfn /
1881    // c:512 ppi_setfn / c:572 ppf_setfn / c:632 ppa_setfn /
1882    // c:690 pph_getfn / c:706 pph_setfn / c:766 scopeprivate /
1883    // c:875 getprivatenode / c:922 getprivatenode2 / c:965 printprivatenode
1884    // ═══════════════════════════════════════════════════════════════════
1885
1886    /// c:200 — `is_private` returns i32 (compile-time type pin).
1887    #[test]
1888    fn is_private_returns_i32_type_pin2() {
1889        let _g = crate::test_util::global_state_lock();
1890        let _: i32 = is_private(std::ptr::null());
1891    }
1892
1893    /// c:200 — `is_private(null)` deterministic across calls.
1894    #[test]
1895    fn is_private_null_is_deterministic() {
1896        let _g = crate::test_util::global_state_lock();
1897        let first = is_private(std::ptr::null());
1898        for _ in 0..5 {
1899            assert_eq!(
1900                is_private(std::ptr::null()),
1901                first,
1902                "is_private(null) must be deterministic"
1903            );
1904        }
1905    }
1906
1907    /// c:365 — `setfn_error(null)` is safe and idempotent.
1908    #[test]
1909    fn setfn_error_null_idempotent() {
1910        let _g = crate::test_util::global_state_lock();
1911        for _ in 0..5 {
1912            setfn_error(std::ptr::null_mut());
1913        }
1914    }
1915
1916    /// c:766 — `scopeprivate(null, _)` is safe for both onoff values.
1917    #[test]
1918    fn scopeprivate_null_both_onoff_safe() {
1919        let _g = crate::test_util::global_state_lock();
1920        scopeprivate(std::ptr::null_mut(), 0);
1921        scopeprivate(std::ptr::null_mut(), 1);
1922    }
1923
1924    /// c:766 — `scopeprivate(null, _)` is idempotent.
1925    #[test]
1926    fn scopeprivate_idempotent() {
1927        let _g = crate::test_util::global_state_lock();
1928        for _ in 0..10 {
1929            scopeprivate(std::ptr::null_mut(), 0);
1930        }
1931    }
1932
1933    /// c:875 — `getprivatenode(null)` returns null pointer.
1934    #[test]
1935    fn getprivatenode_null_returns_null_pin() {
1936        let _g = crate::test_util::global_state_lock();
1937        let p = getprivatenode(std::ptr::null_mut());
1938        assert!(p.is_null(), "null in → null out");
1939    }
1940
1941    /// c:922 — `getprivatenode2(null)` returns null pointer.
1942    #[test]
1943    fn getprivatenode2_null_returns_null_pin() {
1944        let _g = crate::test_util::global_state_lock();
1945        let p = getprivatenode2(std::ptr::null_mut());
1946        assert!(p.is_null(), "null in → null out");
1947    }
1948
1949    /// c:875 + c:922 — both getprivatenode variants null-safe and pure.
1950    #[test]
1951    fn getprivatenode_variants_deterministic_on_null() {
1952        let _g = crate::test_util::global_state_lock();
1953        for _ in 0..5 {
1954            assert!(getprivatenode(std::ptr::null_mut()).is_null());
1955            assert!(getprivatenode2(std::ptr::null_mut()).is_null());
1956        }
1957    }
1958
1959    /// c:965 — `printprivatenode(null, _)` is safe for any flags.
1960    #[test]
1961    fn printprivatenode_null_with_various_flags_safe() {
1962        let _g = crate::test_util::global_state_lock();
1963        for flags in [0i32, 1, -1, i32::MIN, i32::MAX] {
1964            printprivatenode(std::ptr::null_mut(), flags);
1965        }
1966    }
1967
1968    /// c:690 — `pph_getfn(null)` returns Option<()> (compile-time pin).
1969    #[test]
1970    fn pph_getfn_returns_option_unit_type() {
1971        let _g = crate::test_util::global_state_lock();
1972        let _: Option<()> = pph_getfn(std::ptr::null_mut());
1973    }
1974
1975    /// c:433 — `pps_setfn(null, _)` is safe.
1976    #[test]
1977    fn pps_setfn_null_is_safe() {
1978        let _g = crate::test_util::global_state_lock();
1979        pps_setfn(std::ptr::null_mut(), "");
1980        pps_setfn(std::ptr::null_mut(), "any value");
1981    }
1982
1983    /// c:512 — `ppi_setfn(null, _)` is safe.
1984    #[test]
1985    fn ppi_setfn_null_is_safe() {
1986        let _g = crate::test_util::global_state_lock();
1987        ppi_setfn(std::ptr::null_mut(), 0);
1988        ppi_setfn(std::ptr::null_mut(), i64::MAX);
1989        ppi_setfn(std::ptr::null_mut(), i64::MIN);
1990    }
1991
1992    /// c:572 — `ppf_setfn(null, _)` is safe.
1993    #[test]
1994    fn ppf_setfn_null_is_safe() {
1995        let _g = crate::test_util::global_state_lock();
1996        ppf_setfn(std::ptr::null_mut(), 0.0);
1997        ppf_setfn(std::ptr::null_mut(), f64::INFINITY);
1998        ppf_setfn(std::ptr::null_mut(), f64::NAN);
1999    }
2000
2001    /// c:632 — `ppa_setfn(null, _)` is safe.
2002    #[test]
2003    fn ppa_setfn_null_is_safe() {
2004        let _g = crate::test_util::global_state_lock();
2005        ppa_setfn(std::ptr::null_mut(), vec![]);
2006        ppa_setfn(std::ptr::null_mut(), vec!["a".into(), "b".into()]);
2007    }
2008
2009    // ═══════════════════════════════════════════════════════════════════
2010    // Additional C-parity tests for Src/Modules/param_private.c
2011    // c:200 is_private / c:403 pps_getfn / c:497 ppi_getfn /
2012    // c:557 ppf_getfn / c:617 ppa_getfn / c:468 unsetfn variants /
2013    // c:766 scopeprivate / c:875 getprivatenode + lifecycle
2014    // ═══════════════════════════════════════════════════════════════════
2015
2016    /// c:200 — `is_private(null)` returns 0 (not private).
2017    #[test]
2018    fn is_private_null_returns_zero() {
2019        let _g = crate::test_util::global_state_lock();
2020        assert_eq!(
2021            is_private(std::ptr::null()),
2022            0,
2023            "null param is not private; got nonzero"
2024        );
2025    }
2026
2027    /// c:200 — `is_private` returns i32 (compile-time pin, alt).
2028    #[test]
2029    fn is_private_returns_i32_pin_alt() {
2030        let _g = crate::test_util::global_state_lock();
2031        let _: i32 = is_private(std::ptr::null());
2032    }
2033
2034    /// c:403 — `pps_getfn(null)` returns String (compile-time pin).
2035    #[test]
2036    fn pps_getfn_null_returns_string_type() {
2037        let _g = crate::test_util::global_state_lock();
2038        let _: String = pps_getfn(std::ptr::null_mut());
2039    }
2040
2041    /// c:497 — `ppi_getfn(null)` returns i64 (compile-time pin).
2042    #[test]
2043    fn ppi_getfn_null_returns_i64_type() {
2044        let _g = crate::test_util::global_state_lock();
2045        let _: i64 = ppi_getfn(std::ptr::null_mut());
2046    }
2047
2048    /// c:557 — `ppf_getfn(null)` returns f64 (compile-time pin).
2049    #[test]
2050    fn ppf_getfn_null_returns_f64_type() {
2051        let _g = crate::test_util::global_state_lock();
2052        let _: f64 = ppf_getfn(std::ptr::null_mut());
2053    }
2054
2055    /// c:617 — `ppa_getfn(null)` returns Vec<String> (compile-time pin).
2056    #[test]
2057    fn ppa_getfn_null_returns_vec_string_type() {
2058        let _g = crate::test_util::global_state_lock();
2059        let _: Vec<String> = ppa_getfn(std::ptr::null_mut());
2060    }
2061
2062    /// c:468/530/590/650/727 — every unsetfn variant accepts null + both
2063    /// explicit flag values without panic.
2064    #[test]
2065    fn all_unsetfn_variants_null_both_explicit_flags_safe() {
2066        let _g = crate::test_util::global_state_lock();
2067        for explicit in [0i32, 1] {
2068            pps_unsetfn(std::ptr::null_mut(), explicit);
2069            ppi_unsetfn(std::ptr::null_mut(), explicit);
2070            ppf_unsetfn(std::ptr::null_mut(), explicit);
2071            ppa_unsetfn(std::ptr::null_mut(), explicit);
2072            pph_unsetfn(std::ptr::null_mut(), explicit);
2073        }
2074    }
2075
2076    /// c:766 — `scopeprivate(null, _)` is safe for both onoff values (alt).
2077    #[test]
2078    fn scopeprivate_null_both_onoff_alt_pin() {
2079        let _g = crate::test_util::global_state_lock();
2080        scopeprivate(std::ptr::null_mut(), 0);
2081        scopeprivate(std::ptr::null_mut(), 1);
2082    }
2083
2084    /// c:875 — `getprivatenode(null)` returns null (deterministic).
2085    #[test]
2086    fn getprivatenode_null_is_deterministic() {
2087        let _g = crate::test_util::global_state_lock();
2088        for _ in 0..10 {
2089            assert!(getprivatenode(std::ptr::null_mut()).is_null());
2090        }
2091    }
2092
2093    /// c:922 — `getprivatenode2(null)` returns null (deterministic, alt fn).
2094    #[test]
2095    fn getprivatenode2_null_is_deterministic() {
2096        let _g = crate::test_util::global_state_lock();
2097        for _ in 0..10 {
2098            assert!(getprivatenode2(std::ptr::null_mut()).is_null());
2099        }
2100    }
2101
2102    /// c:1005/1020/1035/1041/1058/1064 — each lifecycle hook returns 0
2103    /// individually (tighter failure resolution).
2104    #[test]
2105    fn param_private_each_lifecycle_hook_returns_zero_individually() {
2106        let _g = crate::test_util::global_state_lock();
2107        let null = std::ptr::null();
2108        let mut v: Vec<String> = Vec::new();
2109        let mut e: Option<Vec<i32>> = None;
2110        assert_eq!(setup_(null), 0, "c:1005 setup_");
2111        assert_eq!(features_(null, &mut v), 0, "c:1020 features_");
2112        assert_eq!(enables_(null, &mut e), 0, "c:1035 enables_");
2113        assert_eq!(boot_(null), 0, "c:1041 boot_");
2114        assert_eq!(cleanup_(null), 0, "c:1058 cleanup_");
2115        assert_eq!(finish_(null), 0, "c:1064 finish_");
2116    }
2117}