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