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