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 `¶m` 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}